electron

electron在国产操作系统打包问题

# 排障记录:npm run dev 启动报 ERR_DLOPEN_FAILED(窗口创建失败)

项目 内容
发生日期 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)开发机启动;打包产物取决于打包机构建环境
状态 ✅ 已解决

# 1. 现象

执行 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 不可枚举,真正的原因(缺哪个库)被吞掉了。这是本次排障走弯路的主要原因。

# 2. 根因

一句话: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-ciphersdataSource.ts:44,加密库驱动,先炸的是它)
  • better-sqlite3

不受影响的模块packages/ 下三个 Rust 模块(hyt-exam-native / hyt-exam-network / hyt-exam-zip,源码在 /home/ynd/code/ies_security)——ldd 依赖齐全、Electron 实测加载正常,本次问题与它们无关。

# 3. 排查过程(方法论留档)

  1. 复现:本机跑 npm run dev,确认主/preload 构建成功、应用进程存活 → 排除构建问题。
  2. 拿真实报错
    • 终端日志只有 {"code":"ERR_DLOPEN_FAILED"},无 message;
    • 临时改进 src/main/index.ts 的 catch,把 error.message 打出来(该改进已保留合入,见 §5);
    • 为避开正在运行实例的 Electron 单例锁,用隔离配置目录起独立实例: XDG_CONFIG_HOME=/tmp/hyt-debug-config npm run dev
    • 拿到关键信息:libm.so.6: version GLIBC_2.29 not found (required by .../better_sqlite3.node)
  3. 验证二进制要求objdump -T xxx.node | grep -oE "GLIBC_2\.[0-9]+" | sort -Vu,两个 sqlite 模块最高要求 2.29;本机 ldd --version = 2.28。文件 mtime 停留在 2023/2024 → 证实是下载的预编译包而非本机编译产物。
  4. 排除干扰项:三个 hyt Rust 模块 ldd 全通过、ELECTRON_RUN_AS_NODE 实测加载成功 → 排除。
  5. 坑:ELECTRON_RUN_AS_NODE 假阳性:直接 require('better-sqlite3') 显示"加载成功"是假象——JS 包装层是懒加载,真正实例化 Database 才触发 dlopen。验证必须 new Database(':memory:') 真正跑一遍。
  6. 坑:@electron/rebuild 静默跳过npx @electron/rebuild --only better-sqlite3,better-sqlite3-multiple-ciphers 输出 "Rebuild Complete" 但根本没编译(文件 mtime 未变)。勿信它的成功输出,重编后务必 objdump + mtime 复核。
  7. 坑:头文件下载 404
    • cdn.npmmirror.com/binaries/electron/npmmirror.com/mirrors/electron/ 上均无 node-v22.3.x-headers.tar.gz
    • 可用源:https://electronjs.org/headers/(跟随重定向后 200)。
  8. 坑:node-gyp 版本 vs Python:node-gyp@12 代码用了海象运算符,UOS 20 的 Python 3.7.3 直接 SyntaxError必须用 node-gyp@9

# 4. 解决方案(完整操作手册)

# 4.0 方式一:一键修复(推荐)

npm run rebuild:linux

脚本自动读取当前实际安装的 Electron 版本并对两个 sqlite 模块重编,完整代码见 §5.2。 每次在本机执行 npm install / npm ci 后建议跑一次(install 可能重新拉回坏的预编译包)。

# 4.1 方式二:手动修复命令(逐步,留档备查)

# 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++)。

# 4.2 验证(三层,缺一不可)

# 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 次)。

# 4.3 诊断命令(下次同类问题直接用)

# 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('<模块>')"

# 5. 随修复合入的代码变更(完整代码)

# 5.1 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,否则排障时看不到真实原因。

# 5.2 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)')

# 5.3 package.json —— 新增 scripts 条目

"rebuild:linux": "node ./build/rebuild-linux-sqlite.cjs",

(加在 rebuild:all 之后,与其余 rebuild 系列脚本放在一起。)

# 6. 环境速查(UOS 20 开发机)

依赖 状态 备注
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 稳定,本次无责

# 7. 遗留事项 / 建议

  1. 复发条件仍在.npmrcbetter_sqlite3_binary_host 指向 npmmirror 预编译源,对 Windows 开发机是加速,对 UOS 20 是坑。暂未改动(避免影响其他同事环境),靠 npm run rebuild:linux 兜底。若后续 UOS 开发机增多,可考虑把该配置移到各平台局部配置中。
  2. 项目的 rebuild:64 脚本用 electron-rebuild,在本机同样存在"静默成功"风险(见 §3 第 6 条),Linux 下优先用 rebuild:linux
  3. 目前架构上 dataBasePool.init() 失败会阻断窗口创建(考试机无数据库无法工作,属合理设计)。如需"数据库失败时弹窗提示而非无响应",需另行评审。
上次更新: