上篇《把 Electron 应用搬进鸿蒙的 8 个坑》结尾,我留了个「绕不过去的坎」:鸿蒙应用域里,spawn 一个 node / pnpm 子进程,会被 seccomp 直接 SIGSYS 杀掉。

当时我把它归为「平台级无解」。今天这篇要打自己的脸:那个坎不是无解,换个思路就过去了。 插件市场的一键安装,现在已经在鸿蒙真机上跑通。

这篇文章讲清三件事:为什么 spawn 必死、怎么用「进程内 pnpm」绕过去、真机上又踩了哪三个坑。如果你也在鸿蒙上折腾 Node 生态的包管理,这套思路可以直接抄。

一、问题:为什么 spawn 一个 node 就死

先把这个「坎」的硬度说清楚。

我在鸿蒙 2in1 真机上、应用进程里、同一个 SELinux 域下,实测了一组命令:

命令结果
node -v✅ v24.13.0
node -e "console.log(1)"❌ Signal 31(SIGSYS)
node --jitless -e "…"❌ SIGSYS
npm -v❌ SIGSYS
python3 -c "print('ok')"✅

看懂这组数据,关键在于三个「不是」:

  • 不是「不能执行代码」——python3 连同多线程都正常跑;
  • 不是「node 坏了」——node -v 能正常打印版本;
  • 也不是文件沙箱、权限、路径问题——放宽文件策略后结果不变。

真正的原因是:应用域里 fork 出的子进程,只要尝试分配「JIT 可执行内存」,就会被内核 seccomp 策略投递 SIGSYS。V8 一创建 isolate(真正开始编译执行 JS),必死。

而 pnpm、npm、corepack——全都是 node 脚本。所以「把 pnpm 装到 PATH 上让市场调用」这条路,跟 pnpm 装在哪、PATH 怎么配,一点关系都没有,天生走不通。

二、排查:三个假象,一个真凶

排查过程我踩了三个坑,前两个都是假象,差点把我带沟里。

假象一:pnpm@12 装不上。

报错 pnpm does not ship a prebuilt binary for openharmony-arm64。因为 pnpm 从 v12 起改成了 Rust 原生分发壳,平台白名单里压根没有 openharmony。解法是退回纯 JS 的 pnpm@11 / 10——但这只是「装上了」,离「能用」还差十万八千里。

假象二:PATH 不生效。

写 ~/.profile 没用,因为鸿蒙终端是 zsh,只读 ~/.zshenv。这个坑花了不少时间,解决后 pnpm 在终端里能跑了,应用里还是不行。

真凶:就是上面那组 SIGSYS 实测。

终端(真实 shell)能跑 node/pnpm,应用域里的子进程却全崩——差别就在 seccomp 只针对应用域 fork 出的子进程。

三、洞察:SIGSYS 只杀子进程,不杀主进程

卡住我的那堵墙,其实有个缺口:

SIGSYS 只杀「fork 出的子进程」。而 Electron 主进程自身,就是 Node 22.17,能正常跑 JS。

既然主进程能跑 JS,那我不 spawn 子进程,把 pnpm 的 JS 直接 import 进主进程里跑,不就把 seccomp 绕开了?

这就是整个方案的立足点——「进程内 pnpm」。思路一换,墙就没了。

四、方案:把 pnpm 塞进主进程,用 worker 线程跑

方案定了,落地有三个关键决策。

决策一:版本退回 pnpm@10.34.6。

pnpm@11 是纯 ESM,但它依赖 node:sqlite,而 Electron/Node 运行时没有这个 binding,import 直接报 No such binding: sqlite。所以退回 pnpm@10.34.6(纯 CJS、不碰 node:sqlite)。

决策二:丢进 worker 线程,而不是直接 import。

直接 import pnpm 有个坑:pnpm 的 main 是「火忘式」异步、从不调 process.exit,你根本没法知道它装完了没;更危险的是,它内部可能调 process.exit,在进程内跑会直接杀掉 Electron 主进程。

解法是把它丢进 worker_threads.Worker,一次解决三件事:

  • 完成检测:worker 事件循环排空 → 退出,退出码就是 pnpm 的退出码;
  • process.exit 隔离:只会退出 worker,不杀主进程;
  • ESM 缓存:每次 new Worker 都是全新模块图,不用管缓存击穿。

决策三:扁平布局,绕开 symlink / hardlink 禁令。

pnpm 默认 node-linker=isolated,会在 node_modules 里建 symlink、在 store 里建 hardlink——鸿蒙沙箱两者都禁。所以必须:

  • nodeLinker: hoisted:生成扁平 node_modules,无 symlink、无 .pnpm 虚拟 store;
  • packageImportMethod: copy:从 store 拷贝文件而非 hardlink;
  • storeDir 指向沙箱内可写目录。

五、真机上的三个修复

设计想得再周全,真机一跑还是崩出三个问题,逐个修:

#问题现象修复
P1pnpm@11 依赖 node:sqliteimport 抛 No such binding: sqlite引擎退回 pnpm@10.34.6
P2Electron 的 process.execPath 只读pnpm 配置里无条件 process.execPath = node → 抛 Cannot assign to read only propertyworker import 前把 process.execPath 设成 no-op setter(保留真实值)
P3鸿蒙禁 symlink,pnpm 仍建 .bin 软链EACCES: symlink … -> .bin/… → 安装失败worker 里把 fs.symlink 在 EACCES/EPERM 时回退为 copy,.bin 落成真实文件

三个修复都不复杂,但每一个都是「真机才暴露、设计时想不到」的坑。

六、结果:一键安装,真机跑通

修完这三个,端到端通了。市场一键安装 / 卸载插件,全程:

  • 无 node / pnpm 子进程——日志里的 pid 就是应用主进程;
  • 无 symlink——profile 的 node_modules 递归扫描 symlinkCount = 0;
  • 零 ELF、零二进制证书、零额外 ACL。

装了个插件试水,exit=0, hot=true, live——装上即热加载。

写在最后

回头看,这个「绕不过去的坎」其实不是坎,是个思路问题:

你以为「跑 pnpm」只能 spawn 子进程,但鸿蒙的 seccomp 只杀子进程、不杀主进程——换个「进程内」的角度,墙就没了。

如果你也在鸿蒙上折腾 Node 生态的包管理,这套「worker 线程进程内跑 pnpm + hoisted + symlink→copy」的思路,可以直接抄。

代码已开源,欢迎 star:

  • 鸿蒙工程:https://github.com/fellow99/dsh-desktop-hos
  • 桌面工程:https://github.com/fellow99/dsh-desktop
  • 工程总览:https://github.com/fellow99/deepseek-harness-workspace
  • 上游 DeepSeek Harness:https://github.com/deepseek-ai/deepseek-harness
  • 插件市场:https://github.com/dsh-market/dsh-market
  • 鸿蒙 Electron 运行时:https://atomgit.com/jianguoxu/harmonypc-electron

下一篇我写 App 上架合规踩坑:被 AppGallery 打回 12 条整改,从商标撞名到 ArkWeb 合规。关注我,别错过。

感谢各位关注,欢迎访问我的 GitHub 主页:https://fellow99.github.io/

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐