Files
Literature-Management/src/main/utils/path-check.ts
2026-10-21 14:42:59 +08:00

168 lines
7.5 KiB
TypeScript
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.
// D2-020: 主进程启动路径安全检查,作为 scripts/start.cjs 的兜底防御。
//
// 必须在 main/index.ts 的最顶部 import,以便在 better-sqlite3 dlopen 之前
// 截断路径确实不安全的进程。
//
// 策略(2026-08-28 修订):
// - cwd 超长 (>240) → 致命,直接退出。真实的 Windows MAX_PATH 限制。
// - cwd 含 CJK → 仅提示。实测 Electron 30 + Win11 能正常 dlopen CJK 路径,
// 原来的致命判定反而逼出了 ASCII junction,把 vite dev server 搞崩。
// 详见 scripts/start.cjs 顶部的分析。
//
// 另:作为第二道兜底,如果 .node 文件的 ABI 已经过期(mtime 早于 electron.exe),
// 直接报清晰错误而不是让 dlopen 抛乱码堆栈。start.cjs 在正常 npm run dev 下
// 会先一步自动 rebuild,这里只处理"用户绕过 npm"或"自动 rebuild 失败"的场景。
import process from 'node:process';
import { existsSync, statSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
// 必须用 createRequire 而不是 ESM `import` —— ESM import 会被 hoist 到模块顶部,
// 导致 `import('electron')` 在本检查函数之前就执行,破坏"先检查再 dlopen"的时序保证。
// 实际上这里只读 .node 路径不真正 dlopen,但用 require 形式是为了显式表达意图,
// 避免未来重构时被 hoist 规则坑到。
const requireFromHere = createRequire(import.meta.url);
function checkWindowsPath(): void {
if (process.platform !== 'win32') return;
// 仅在 dev/源码运行模式下生效:此时 better-sqlite3 从 cwd/node_modules 加载,
// cwd 的 CJK 会让 dlopen 失败。打包模式下 .node 从 app.asar.unpacked 加载
// (位于 %LOCALAPPDATA%),cwd 是否含 CJK 与加载路径无关,跳过。
const cwd = process.cwd();
const devNativeMarker = path.join(cwd, 'node_modules', 'better-sqlite3');
if (!existsSync(devNativeMarker)) return;
// 匹配非 ASCII(含 CJK)字符。使用 \x00-\x7F 边界是检测非 ASCII 的
// 标准写法。\x00 控制字符是有意的,排除注释 eslint。
// eslint-disable-next-line no-control-regex
const hasCJK = /[^\x00-\x7F]/.test(cwd);
const tooLong = cwd.length > 240;
if (!hasCJK && !tooLong) return;
// CJK 不再是致命错误(2026-08-28 修订)。
//
// 原假设:CJK cwd → Windows 给 .node 加 \\?\ 前缀时 Node 按 CP936 解码 UTF-8 → dlopen 失败。
// 实测在 Electron 30 + Win11 下不成立:scripts/start.cjs 的 verify-abi 探针就是在真实 CJK
// cwd 下 spawn electron.exe 跑 require('better-sqlite3') + new Database(':memory:') 成功的。
//
// 而把 CJK 判成致命错误的代价很实在:它逼着 start.cjs 去建 ASCII junction,而 junction 会让
// process.cwd() 与 vite 的 cacheDir 不同源,直接崩掉 dev server(见 start.cjs 顶部注释)。
// 所以 CJK 降级为一次性提示,真正 dlopen 失败时下面的 ABI 检查和 Node 自己的报错会接管。
if (hasCJK && !tooLong) {
process.stderr.write(
`[path-check] 注意: 项目路径含非 ASCII 字符 (${cwd.length} chars): ${cwd}\n` +
`[path-check] 已知在 Electron 30 + Win11 下可正常加载 native module。\n` +
`[path-check] 若 better-sqlite3 dlopen 失败,试 RKMS_FORCE_JUNCTION=1 npm run dev。\n`,
);
return;
}
// 超长路径仍然致命 —— 这是真实的 Windows MAX_PATH 限制,与字符集无关。
const reason = `path too long (${cwd.length} chars)`;
const lines = [
'',
'=====================================================',
'[FATAL] Project path is not safe for native module loading.',
` cwd: ${cwd}`,
` reason: ${reason}`,
'',
' Windows 的 MAX_PATH 会让 node_modules 深层文件路径被截断。',
'',
' Manual fix: move the project to a shorter path, e.g.',
' D:\\dev\\research-knowledge-management-system',
'',
' Or create a junction and launch from it:',
` mklink /J C:\\rkms "${cwd}"`,
' cd C:\\rkms && npm install && npm run dev',
'=====================================================',
'',
];
// logger 尚未初始化,直接写 stderr
process.stderr.write(lines.join('\n'));
process.exit(1);
}
/**
* 检查 better_sqlite3.node 的 mtime 是否晚于 electron.exe。
* mtime 早于 electron → 升级 Electron 后没重跑 install,ABI 漂移。
* 此时直接给清晰错误并退出 —— 比让 Node dlopen 抛
* "The module '...\\?\D:\data\我的工作...' was compiled against a different
* Node.js version using NODE_MODULE_VERSION XXX" 这种带乱码路径的错友好得多。
*
* 这一层是 scripts/start.cjs 自动 rebuild 的**兜底**:如果用户绕过 npm 直接
* 跑 electron(IDE / 双击 / 手敲),start.cjs 的检测和自动修复都不会发生,
* 这里就接管报错。
*/
function checkBetterSqliteAbi(): void {
if (process.platform !== 'win32') return;
const cwd = process.cwd();
const sqlitePath = path.join(
cwd,
'node_modules',
'better-sqlite3',
'build',
'Release',
'better_sqlite3.node',
);
if (!existsSync(sqlitePath)) return; // 第一次 install,交给 postinstall / 下次 npm run dev 的 start.cjs
let electronPath: unknown;
try {
electronPath = requireFromHere('electron');
} catch {
return; // electron 模块找不到(非 dev 模式或打包后),跳过
}
if (typeof electronPath !== 'string' || !existsSync(electronPath)) return;
let sqliteStat, electronStat;
try {
sqliteStat = statSync(sqlitePath);
electronStat = statSync(electronPath);
} catch {
return;
}
// .node 比 electron 新 → 已经对齐,无事可做
if (sqliteStat.mtimeMs >= electronStat.mtimeMs) return;
// 用 writeSync 直写 fd=2 (stderr) 而不是 console.error:
// console.error 会把 string 通过 tty stream 写出,Windows PowerShell cp936 终端
// 会按 GBK 解码,把 UTF-8 字节再次变成乱码 —— 反而掩盖真正的根因。
// writeSync 直接发字节流,配合终端 UTF-8 codepage (start.cjs 已 chcp 65001)
// 一定能正确显示中文路径。
try {
process.stderr.write(`\r\n`);
process.stderr.write(`=====================================================\r\n`);
process.stderr.write(`[FATAL] better-sqlite3 ABI mismatch.\r\n`);
process.stderr.write(` better_sqlite3.node mtime: ${sqliteStat.mtime.toISOString()}\r\n`);
process.stderr.write(` electron.exe mtime: ${electronStat.mtime.toISOString()}\r\n`);
process.stderr.write(`\r\n`);
process.stderr.write(
` 说明: 升级 Electron 后没重跑 npm install,导致 .node 是用旧的 ABI 编译的。\r\n`,
);
process.stderr.write(` 启动主进程时 dlopen 会抛 'NODE_MODULE_VERSION XXX' 错误。\r\n`);
process.stderr.write(`\r\n`);
process.stderr.write(` 修复(在项目根目录执行任一命令):\r\n`);
process.stderr.write(` npm run rebuild:native\r\n`);
process.stderr.write(` npm rebuild better-sqlite3\r\n`);
process.stderr.write(` npx electron-rebuild -f -w better-sqlite3\r\n`);
process.stderr.write(`\r\n`);
process.stderr.write(
` 之后正常 npm run dev 即可。如果是通过 IDE 启动,请改用 npm run dev 让 start.cjs 自动处理。\r\n`,
);
process.stderr.write(`=====================================================\r\n`);
} catch {
// 写不出来就退,反正 process.exit(1) 也会被 IDE 看到
}
process.exit(1);
}
checkWindowsPath();
checkBetterSqliteAbi();
export {};