Files
LocalNetMsg/src/main/remote/shell.ts
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

64 lines
2.2 KiB
TypeScript

// 跨平台默认 shell 选择
import { platform } from 'node:process'
import { execSync } from 'node:child_process'
export interface ShellSpec {
file: string
args: string[]
label: string
}
function which(bin: string): string | null {
try {
const cmd = platform === 'win32' ? `where ${bin}` : `command -v ${bin}`
const out = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split(/\r?\n/)[0]
return out || null
} catch {
return null
}
}
export function defaultShell(): ShellSpec {
switch (platform) {
case 'win32': {
// 优先 PowerShell 7 (pwsh), 否则 Windows PowerShell 5 (powershell)
const pwsh = which('pwsh.exe') || which('pwsh')
if (pwsh) return { file: pwsh, args: ['-NoLogo'], label: 'PowerShell 7' }
const ps5 = which('powershell.exe') || which('powershell')
if (ps5) return { file: ps5, args: ['-NoLogo'], label: 'Windows PowerShell' }
return { file: 'powershell.exe', args: ['-NoLogo'], label: 'PowerShell' }
}
case 'darwin':
case 'linux': {
const envShell = process.env.SHELL
if (envShell) {
const name = envShell.split('/').pop() || envShell
return { file: envShell, args: [], label: name }
}
if (platform === 'darwin') {
const zsh = which('zsh')
if (zsh) return { file: zsh, args: [], label: 'zsh' }
const bash = which('bash')
if (bash) return { file: bash, args: ['-l'], label: 'bash' }
} else {
const bash = which('bash')
if (bash) return { file: bash, args: ['-l'], label: 'bash' }
const sh = which('sh')
if (sh) return { file: sh, args: [], label: 'sh' }
}
return { file: '/bin/sh', args: [], label: 'sh' }
}
default:
return { file: '/bin/sh', args: [], label: 'sh' }
}
}
// 终端的初始工作目录
// Windows 上通常没有 HOME, 但装了 Git Bash / MSYS 之后 HOME 可能被设成 POSIX 路径
// (比如 /c/Users/xxx), 那个路径在 Win32 下不存在 -> pty 起不来, 所以 win32 优先 USERPROFILE。
export function defaultCwd(): string {
const home = platform === 'win32'
? (process.env.USERPROFILE || process.env.HOME)
: (process.env.HOME || process.env.USERPROFILE)
return home || process.cwd()
}