Files
LocalNetMsg/docs/windows-compat.md
T
xiaoshuai f158377742 fix(compat): Windows 兼容性修复 + 跨平台兼容分析
终端在 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 条用例通过
2026-10-20 16:59:26 +08:00

173 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 里写的是
```json
"**/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`
的脚本路径也必须是真实文件
**修复**:
```json
"**/node_modules/@lydell/node-pty-*/**/*" // ✅ 递归
```
自测 S7 会守住这条规则(防止以后又被改回单层)。
### 3.2 文件名:Windows 保留设备名 + 尾部点空格
Win32 的两个坑,Linux 上完全不存在:
- **保留设备名**:`CON` `PRN` `AUX` `NUL` `COM1-9` `LPT1-9` `CLOCK$`,
**带扩展名也算**(`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. 已知限制与注意事项
1. **ConPTY 需要 Windows 10 1809+**。更老的 Windows(Server 2016 及以前)
没有 ConPTY,node-pty 1.2 的 Windows 实现直接依赖它 → 终端不可用。
2. **Windows 防火墙**首次运行会弹窗问是否允许 Electron 访问网络。
用户若选了"取消",UDP 发现和 WS 都连不上 —— 这是最常见的"装完连不上"原因。
3. **USB 真透传需要 WinUSB 驱动**。node-usb 走 libusb/WinUSB,
Windows 上必须用 Zadig 把设备驱动换成 WinUSB,否则 `loadDevices()` 枚举不到
(代码里已能自动下载 Zadig 并提权安装)。
4. **虚拟串口要 admin**。com0com 装驱动必须管理员,代码走 `sudo-prompt` 提权。
5. **`i.family` 的取值**。`discovery.ts` 里判断 `i.family !== 'IPv4'`,
Node 18+ 才把 `family` 从数字 4 改成字符串 `'IPv4'`。Electron 32 = Node 20 ✓,
但别把这行改成数字比较。
6. **长路径**。Windows 默认 260 字符上限,`safeFileName` 把文件名截到 200 留余量,
但"深目录 + 长文件名"仍可能超限,需要系统开启 LongPathsEnabled。
7. **路径大小写不敏感**。`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) | 原生模块解包规则,**必须递归** |