| 项目 | 内容 |
|---|---|
| 发生日期 | 2026-09-03 |
| 影响版本 | hyt-client 4.26.108(dev-4.26.108-linux 分支) |
| 发生环境 | UOS 20(Debian 10 基座,内核 4.19)/ glibc 2.28 / Python 3.7.3 / Node v20.19.5 / Electron 22.3.27 |
| 影响范围 | 仅 Linux(UOS 20)开发机启动;打包产物取决于打包机构建环境 |
| 状态 | ✅ 已解决 |
执行 npm run dev,主进程/preload 构建均成功、Electron 能启动,但主窗口不出来,终端日志中出现:
error: createWindow error: - {"code":"ERR_DLOPEN_FAILED"}
info: 防火墙检测完成
info: 防火墙已关闭
...
注意 {"code":"ERR_DLOPEN_FAILED"} 只有 code 没有 message——因为当时的 catch 用 JSON.stringify(error) 记录,而 Error 的 message/stack 不可枚举,真正的原因(缺哪个库)被吞掉了。这是本次排障走弯路的主要原因。
一句话:npm install 从 npmmirror 拉到了新版 glibc 环境编译的 sqlite 预编译二进制,其引用的 GLIBC_2.29 版本符号(log/pow/exp/log2)在 UOS 20 的 glibc 2.28 上不存在,dlopen 失败,进而数据库初始化失败、窗口创建失败。
完整因果链:
.npmrc 配置了 better_sqlite3_binary_host → npmmirror 预编译源
↓
npm install 时 prebuild-install 直接下载预编译 .node(未在本机编译)
↓
预编译包在新 glibc 环境构建,引用了 GLIBC_2.29 版本的 log/pow/exp/log2 符号
↓
本机 UOS 20 glibc 只有 2.28 → process.dlopen 抛
/lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found
(required by node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node)
↓
dataSource.ts 里 require('better-sqlite3-multiple-ciphers') 失败 (ERR_DLOPEN_FAILED)
↓
dataBasePool.init() 失败 → createWindow() 进 catch → 返回 null,无主窗口
受影响模块(两个都中招,objdump -T 均可见 GLIBC_2.29 引用):
better-sqlite3-multiple-ciphers(dataSource.ts:44,加密库驱动,先炸的是它)better-sqlite3不受影响的模块:packages/ 下三个 Rust 模块(hyt-exam-native / hyt-exam-network / hyt-exam-zip,源码在 /home/ynd/code/ies_security)——ldd 依赖齐全、Electron 实测加载正常,本次问题与它们无关。
npm run dev,确认主/preload 构建成功、应用进程存活 → 排除构建问题。{"code":"ERR_DLOPEN_FAILED"},无 message;src/main/index.ts 的 catch,把 error.message 打出来(该改进已保留合入,见 §5);XDG_CONFIG_HOME=/tmp/hyt-debug-config npm run devlibm.so.6: version GLIBC_2.29 not found (required by .../better_sqlite3.node)。objdump -T xxx.node | grep -oE "GLIBC_2\.[0-9]+" | sort -Vu,两个 sqlite 模块最高要求 2.29;本机 ldd --version = 2.28。文件 mtime 停留在 2023/2024 → 证实是下载的预编译包而非本机编译产物。ldd 全通过、ELECTRON_RUN_AS_NODE 实测加载成功 → 排除。ELECTRON_RUN_AS_NODE 假阳性:直接 require('better-sqlite3') 显示"加载成功"是假象——JS 包装层是懒加载,真正实例化 Database 才触发 dlopen。验证必须 new Database(':memory:') 真正跑一遍。@electron/rebuild 静默跳过:npx @electron/rebuild --only better-sqlite3,better-sqlite3-multiple-ciphers 输出 "Rebuild Complete" 但根本没编译(文件 mtime 未变)。勿信它的成功输出,重编后务必 objdump + mtime 复核。cdn.npmmirror.com/binaries/electron/ 与 npmmirror.com/mirrors/electron/ 上均无 node-v22.3.x-headers.tar.gz;https://electronjs.org/headers/(跟随重定向后 200)。SyntaxError。必须用 node-gyp@9。npm run rebuild:linux
脚本自动读取当前实际安装的 Electron 版本并对两个 sqlite 模块重编,完整代码见 §5.2。
每次在本机执行 npm install / npm ci 后建议跑一次(install 可能重新拉回坏的预编译包)。
# 0) 前置:确认实际安装的 Electron 版本(--target 必须精确匹配它,本次为 22.3.27)
node -p "require('./node_modules/electron/package.json').version"
# 1) 重编 better-sqlite3-multiple-ciphers(加密库驱动,dataSource.ts 用的是它)
cd node_modules/better-sqlite3-multiple-ciphers
npx --yes node-gyp@9 rebuild --build-from-source \
--target=22.3.27 --arch=x64 \
--dist-url=https://electronjs.org/headers
# 2) 重编 better-sqlite3(同样中招,一并修复)
cd ../better-sqlite3
npx --yes node-gyp@9 rebuild --build-from-source \
--target=22.3.27 --arch=x64 \
--dist-url=https://electronjs.org/headers
# 3) 回到项目根目录
cd ../..
命令要点:
--target 用实际安装的 Electron 版本。这两个模块走 V8 API(非 N-API),必须针对 Electron ABI 编译,编成 Node ABI 会因 NODE_MODULE_VERSION 不匹配无法加载;--dist-url=https://electronjs.org/headers:Electron 头文件官方源。npmmirror / cdn.npmmirror 上没有 22.3.x 的 headers(404),勿用;--build-from-source 强制源码编译,链上本机 glibc 2.28;node-gyp@9:UOS 20 的 Python 3.7.3 跑不了 node-gyp@12(海象运算符 SyntaxError);python3 / make / g++(本机已具备,缺了先 sudo apt install python3 make g++)。# 1) 符号层:产物最高 glibc 要求应 ≤ 2.28(修复前是 2.29)
objdump -T node_modules/better-sqlite3/build/Release/better_sqlite3.node \
| grep -oE "GLIBC_2\.[0-9]+" | sort -Vu
objdump -T node_modules/better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node \
| grep -oE "GLIBC_2\.[0-9]+" | sort -Vu
# 预期输出末尾:GLIBC_2.14 / GLIBC_2.28(不能再出现 GLIBC_2.29)
# 顺便核对文件 mtime 已更新(防止"假编译",见 §3 第 6 条)
ls -la node_modules/better-sqlite3*/build/Release/better_sqlite3.node
# 2) 模块层:ELECTRON_RUN_AS_NODE 下真实建库读写(不能只 require,见 §3 第 5 条!)
ELECTRON_RUN_AS_NODE=1 ./node_modules/.bin/electron -e "
const D = require('better-sqlite3-multiple-ciphers');
const db = new D(':memory:');
db.exec('create table t(a int); insert into t values (42)');
console.log('OK:', db.prepare('select a from t').get()); db.close();"
# 预期输出:OK: { a: 42 } (better-sqlite3 同样测一遍)
# 3) 应用层:隔离单例锁跑完整应用,确认无 createWindow error
XDG_CONFIG_HOME=/tmp/hyt-debug-config npm run dev
# 预期:日志中 createWindow error / ERR_DLOPEN_FAILED 出现 0 次,
# drivesData → 防火墙检测 等启动流程正常走完,主窗口出现
本次三层全部通过(应用层 createWindow error 0 次)。
# A) 看 .node 到底缺什么库(比报错日志更直接)
ldd node_modules/<模块>/build/Release/xxx.node | grep "not found"
# B) 看 .node 的 glibc 符号版本要求(本机 2.28,产物必须 ≤ 2.28)
objdump -T node_modules/<模块>/build/Release/xxx.node | grep -oE "GLIBC_2\.[0-9]+" | sort -Vu
# C) 本机 glibc 版本
ldd --version | head -1
# D) 绕过单例锁起独立调试实例(不干扰正在运行的 dev 实例)
XDG_CONFIG_HOME=/tmp/hyt-debug-config npm run dev
# E) 在 Electron 环境里试加载/试实例化某个原生模块
ELECTRON_RUN_AS_NODE=1 ./node_modules/.bin/electron -e "require('<模块>')"
src/main/index.ts —— createWindow 的 catch 日志增强改前(Error 的 message/stack 不可枚举,JSON.stringify 会全部丢掉,只剩 code):
} catch (error) {
logger.error(`createWindow error: - ${JSON.stringify(error)}`)
return null
}
改后(能打出缺哪个库/符号的完整信息):
} catch (error) {
// JSON.stringify(Error) 只会输出 code 等可枚举属性,会丢失 message/stack(如 dlopen 失败的具体原因)
logger.error(
`createWindow error: - ${
error instanceof Error
? `${error.message} | code=${(error as NodeJS.ErrnoException).code ?? ''} | stack=${error.stack ?? ''}`
: JSON.stringify(error)
}`
)
return null
}
教训:所有
catch里的错误日志都不要只用JSON.stringify(error),至少补上error.message,否则排障时看不到真实原因。
build/rebuild-linux-sqlite.cjs —— 新增,防复发一键修复脚本(完整代码)// UOS 20 (Debian 10, glibc 2.28) 专用:
// .npmrc 配置的 better_sqlite3_binary_host 会让 npm install 下载新版 glibc 环境编译的
// 预编译二进制(要求 GLIBC_2.29),在 UOS 20 上 dlopen 失败(ERR_DLOPEN_FAILED),
// 导致 dataBasePool.init() 失败、createWindow 失败。
// 本脚本用本机工具链 + Electron 头文件从源码重编,产物仅依赖本机 glibc。
// 用法:npm install 之后执行 npm run rebuild:linux
// 注意:需用 node-gyp@9(node-gyp@12 需要 Python>=3.8,UOS 20 自带 3.7)。
const { execSync } = require('child_process')
const path = require('path')
const fs = require('fs')
const root = path.join(__dirname, '..')
const electronVersion = JSON.parse(
fs.readFileSync(path.join(root, 'node_modules/electron/package.json'), 'utf8')
).version
const modules = ['better-sqlite3-multiple-ciphers', 'better-sqlite3']
const arch = process.arch === 'arm64' ? 'arm64' : 'x64'
for (const name of modules) {
const dir = path.join(root, 'node_modules', name)
if (!fs.existsSync(dir)) {
console.log(`[skip] ${name} 未安装`)
continue
}
console.log(`[build] ${name} (electron v${electronVersion}, ${arch})`)
execSync(
`npx --yes node-gyp@9 rebuild --build-from-source ` +
`--target=${electronVersion} --arch=${arch} ` +
`--dist-url=https://electronjs.org/headers`,
{ cwd: dir, stdio: 'inherit' }
)
}
console.log('完成:sqlite 原生模块已按本机 glibc 重编(Electron ABI)')
package.json —— 新增 scripts 条目"rebuild:linux": "node ./build/rebuild-linux-sqlite.cjs",
(加在 rebuild:all 之后,与其余 rebuild 系列脚本放在一起。)
| 依赖 | 状态 | 备注 |
|---|---|---|
| glibc | 2.28 | 任何预编译 .node 都要 objdump 核对 ≤2.28 |
| Python | 3.7.3 | 编译原生模块用 node-gyp@9(@12 需 Python≥3.8) |
| g++ / make / python3 | 已具备 | 源码编译 OK |
| Electron 头文件 | 用 https://electronjs.org/headers | npmmirror 上没有 22.3.x 的 headers,勿用 |
| Electron | 22.3.27(^22.3.9 实际解析) | 编原生模块 --target 要精确匹配此版本 |
| Rust 模块源码 | /home/ynd/code/ies_security | hyt-exam-native / network / zip;N-API 风格命名,ABI 稳定,本次无责 |
.npmrc 的 better_sqlite3_binary_host 指向 npmmirror 预编译源,对 Windows 开发机是加速,对 UOS 20 是坑。暂未改动(避免影响其他同事环境),靠 npm run rebuild:linux 兜底。若后续 UOS 开发机增多,可考虑把该配置移到各平台局部配置中。rebuild:64 脚本用 electron-rebuild,在本机同样存在"静默成功"风险(见 §3 第 6 条),Linux 下优先用 rebuild:linux。dataBasePool.init() 失败会阻断窗口创建(考试机无数据库无法工作,属合理设计)。如需"数据库失败时弹窗提示而非无响应",需另行评审。