终端在 Windows 上起不来 (打包规则漏解包)
- 根因: build.asarUnpack 写的是 "**/node_modules/@lydell/node-pty-*/*", 只匹配一层,
于是 prebuilds/<plat>/ 下的文件没被解包, 留在 app.asar 里
- Windows 上这些必须是磁盘上的真实文件: conpty.dll (OS loader 加载)、
OpenConsole.exe (CreateProcess)、以及 node-pty 用 worker_threads.Worker 起的
conoutSocketWorker.js —— node-pty 本身就写了
__dirname.replace('node_modules.asar', 'node_modules.asar.unpacked') 来适配
- Linux 上看不出来: 只需要一个 pty.node, Electron 的 asar 层能 dlopen 归档里的 .node
- 修复: 规则递归化为 "**/node_modules/@lydell/node-pty-*/**/*"
- 验证: 重新打包后 pty.node 已落到 app.asar.unpacked, 安装版真实 spawn 成功
Windows 保留设备名 / 尾部点空格导致落盘写错
- 新增 src/main/safe-name.ts 的 safeFileName()
- Win32 把 CON/PRN/AUX/NUL/COM1-9/LPT1-9 (带扩展名也算) 当设备, open() 打开的不是文件;
还会悄悄吃掉文件名结尾的点和空格, 导致 existsSync 判断和实际落盘对不上
- 两处落盘都接上: file-server.ts 的 targetPath()、drive.ts 的 downloadDriveFile()
(后者名字来自对端共享目录, 属不可信输入)
- 策略是"最小必要": 非法字符全平台都清, 保留名和尾点空格只在 win32 上改写,
避免动到 Linux 上合法的文件名
其他
- 终端 cwd: Windows 优先用 USERPROFILE (装了 Git Bash 时 HOME 可能是 POSIX 路径, Win32 下不存在)
- USB 面板空列表提示按平台给, 不再一律说"Linux 用户需要在 dialout 组"
分析文档与自测
- 新增 docs/windows-compat.md: 各原生模块的 N-API 情况、已有 win32 分支的位置、
已知限制 (ConPTY 需 Win10 1809+、防火墙首次弹窗、USB 需 WinUSB 驱动等)、
以及一份 Windows 真机验证清单
- 自测新增 S1-S8 组: 保留名改写、尾部点空格、非法字符、跨平台不过度清洗、
超长截断、默认 shell/cwd、asarUnpack 规则回归护栏、上传落盘 + 路径穿越
- 全部 38 条用例通过
8.2 KiB
Windows 兼容性分析与注意事项
LocalNetMsg 是跨平台应用 (Electron 32 / Node 20 / Chromium 128)。这份文档记录 Windows 上能跑什么、哪些地方有坑、以及已经修过的地方,避免以后改代码时把 Windows 弄坏。
测试环境:Ubuntu 22.04 (x64) + Electron 32.3.3。Windows 侧结论基于 依赖包源码与打包产物的静态验证,未在真实 Windows 机器上跑过(见文末"待验证")。
1. 结论速览
| 功能 | Windows | 说明 |
|---|---|---|
| 聊天 / 文件传输 / 网盘 | ✅ | 纯 Node HTTP/WS,无平台依赖 |
| 局域网发现 | ✅ | UDP 广播,多网卡 + 255.255.255.255 兜底 |
| 远程终端 | ✅(修过) | 需 ConPTY (Win10 1809+),打包规则曾有坑,见 §3.1 |
| 串口转发 | ✅ | serialport 有 win32 预编译;虚拟串口走 com0com |
| USB (libusb 字节桥) | ⚠️ | node-usb 可用,但需 WinUSB 驱动 (Zadig) |
| USB/IP 真透传 | ❌ | Linux 内核特性,代码里已明确拒绝 |
| 开机自启 | ✅ | app.setLoginItemSettings |
| 托盘 / 通知 | ✅ | 托盘图标走 resources/icon.png |
2. 原生模块:全是 N-API 预编译,不需要针对 Electron 重编
这是 Windows 打包能不能成的第一道关。build.npmRebuild 是 false
(原生模块已在 Electron ABI 上,重编反而坏事),所以所有原生模块必须自带
跨 ABI 的预编译二进制。现状:
| 模块 | Windows 产物 | ABI | 是否需要编译 |
|---|---|---|---|
@lydell/node-pty |
@lydell/node-pty-win32-x64 |
N-API (napi_register_module_v1) |
否 |
node-hid |
prebuilds/HID-win32-x64/node-napi-v4.node |
N-API | 否 |
@serialport/bindings-cpp |
prebuilds/win32-x64/*.node |
N-API | 否 |
usb (node-usb 3) |
@node-usb/usb-win32-x64-msvc |
napi-rs | 否 |
better-sqlite3 |
源码编译 | 非 N-API | 是,靠 postinstall 的 electron-builder install-app-deps |
坑:
better-sqlite3是唯一需要编译的。npmRebuild: false只影响打包期,postinstall仍然会重编它。如果用npm ci --ignore-scripts,应用会启动即崩 (sqlite 打开失败)。
node-pty 1.2 换成了"主包 + 平台包"拆包架构:主包只是 loader,
真正的二进制在 @lydell/node-pty-<platform>-<arch> 里。换平台时别忘了
平台包也要在 asarUnpack 名单里(见 §3.1)。
3. 已修复的 Windows 阻塞项
3.1 打包:node-pty 平台包没被解包 → Windows 终端起不来
现象:Linux 上终端正常,Windows 上一点就报 spawn 失败。
根因:build.asarUnpack 里写的是
"**/node_modules/@lydell/node-pty-*/*" // ❌ 只匹配一层
/* 只匹配直接子项,于是 prebuilds/ 目录本身匹配上了,但
prebuilds/win32-x64/conpty.node 这种两层深的文件没匹配上 → 留在 asar 里。
为什么 Linux 上没暴露:Linux 的 PTY 只需要一个 pty.node,
Electron 的 asar 层能 dlopen 归档里的 .node(实测可用)。
Windows 就不行了:
conpty.node要加载同目录的conpty.dll(OS loader 从真实路径加载,asar 读不了)- 还要
OpenConsole.exe(CreateProcess必须是磁盘上的真文件) - node-pty 自己就写了
__dirname.replace('node_modules.asar', 'node_modules.asar.unpacked')—— 它明确假设平台包在 unpacked 目录里,因为worker_threads.Worker的脚本路径也必须是真实文件
修复:
"**/node_modules/@lydell/node-pty-*/**/*" // ✅ 递归
自测 S7 会守住这条规则(防止以后又被改回单层)。
3.2 文件名:Windows 保留设备名 + 尾部点空格
Win32 的两个坑,Linux 上完全不存在:
- 保留设备名:
CONPRNAUXNULCOM1-9LPT1-9CLOCK$, 带扩展名也算(CON.txt依然是控制台设备)。对这类名字open()打开的是设备, 数据会写进控制台或者直接失败。 - 尾部点/空格被悄悄吞掉:
report.实际落到report, 于是existsSync('report.')为真,而我们以为写的是另一个文件 —— 重名判断和实际落盘对不上。
修复:新增 src/main/safe-name.ts 的 safeFileName(),
在两处落盘前调用:
file-server.ts的targetPath()—— 接收方保存上传文件drive.ts的downloadDriveFile()—— 名字来自对端共享目录,属于不可信输入
清洗策略是"最小必要":非法字符 \ / : * ? " < > | 全平台都清;
保留名和尾点尾空格只在 win32 上处理,避免无谓改掉 Linux 上合法的文件名。
3.3 终端工作目录
process.env.HOME 在 Windows 上通常不存在,但装了 Git Bash / MSYS 之后
可能被设成 POSIX 路径(/c/Users/xxx)—— 那个路径在 Win32 下不存在,pty 起不来。
修复:shell.ts 新增 defaultCwd(),win32 优先 USERPROFILE。
4. 已有 Windows 分支的地方(别改坏)
| 文件 | Windows 处理 |
|---|---|
remote/shell.ts |
defaultShell() 优先 PowerShell 7 (pwsh),其次 Windows PowerShell 5;用 where 而不是 command -v |
remote/vcom.ts |
虚拟串口走 com0com(自动下载安装器 + setupc.exe 配对),Linux/macOS 走 socat |
permissions.ts |
fixWindowsHID() 用 Zadig 装 WinUSB 驱动;diagnoseUsb() 给 Windows 提示 |
notify.ts |
win32 分支单独处理通知 |
discovery.ts |
UDP setBroadcast(true),同时发 255.255.255.255 / 127.0.0.1 / 每接口子网广播 |
Linux 专属逻辑(udev 规则、dialout 组、usbip、/sys/bus/usb 读取)都在
platform === 'linux' 判断内,Windows 不会走到。
5. 已知限制与注意事项
- ConPTY 需要 Windows 10 1809+。更老的 Windows(Server 2016 及以前) 没有 ConPTY,node-pty 1.2 的 Windows 实现直接依赖它 → 终端不可用。
- Windows 防火墙首次运行会弹窗问是否允许 Electron 访问网络。 用户若选了"取消",UDP 发现和 WS 都连不上 —— 这是最常见的"装完连不上"原因。
- USB 真透传需要 WinUSB 驱动。node-usb 走 libusb/WinUSB,
Windows 上必须用 Zadig 把设备驱动换成 WinUSB,否则
loadDevices()枚举不到 (代码里已能自动下载 Zadig 并提权安装)。 - 虚拟串口要 admin。com0com 装驱动必须管理员,代码走
sudo-prompt提权。 i.family的取值。discovery.ts里判断i.family !== 'IPv4', Node 18+ 才把family从数字 4 改成字符串'IPv4'。Electron 32 = Node 20 ✓, 但别把这行改成数字比较。- 长路径。Windows 默认 260 字符上限,
safeFileName把文件名截到 200 留余量, 但"深目录 + 长文件名"仍可能超限,需要系统开启 LongPathsEnabled。 - 路径大小写不敏感。
existsSync在 Windows 上大小写不敏感, 所以A.apk和a.apk会被当成同一个文件 —— 重名自增逻辑能正确处理, 但 Linux/macOS 上会存成两个文件。两端行为不一致,属预期。
6. 在 Windows 上验证的清单
代码改完,Windows 侧至少要过这一遍(本机无法替代):
- 安装后能启动、图标正常(见
docs/connection-status.md里的图标坑) - 侧边栏能发现到对端设备(UDP 广播 + 防火墙)
- 收发文字消息正常
- 收发文件正常,文件名带中文/特殊字符/
CON也不出错 - 网盘能浏览对方目录、能下载(含整目录保持层级)
- 远程终端能打开、能看到回显、能输入
- 串口列表能看到 COM 口(设备管理器里有)
- USB 模式能枚举到设备(装了 WinUSB 的话)
- 托盘图标正常、右键菜单能唤出窗口
- 开机自启开关生效
7. 相关文件
| 文件 | 作用 |
|---|---|
src/main/safe-name.ts |
跨平台文件名清洗(Windows 保留名 / 尾点空格) |
src/main/remote/shell.ts |
跨平台 shell 选择 + 默认工作目录 |
src/main/remote/vcom.ts |
虚拟串口(socat / com0com) |
src/main/permissions.ts |
权限诊断与一键修复(udev / dialout / WinUSB) |
build.asarUnpack (package.json) |
原生模块解包规则,必须递归 |