update
This commit is contained in:
167
src/main/utils/path-check.ts
Normal file
167
src/main/utils/path-check.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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 {};
|
||||
Reference in New Issue
Block a user