Electron 发布进程被回收 用脱离进程组托住

自动化发一条内容,链路里最脆的一环往往不是平台改版,也不是登录态过期,而是你拉起来的那个 GUI 进程根本活不到发布结束。这篇文章讲一个踩了很久、代价很高的坑:Electron 发布进程被上游任务回收连坐杀掉,以及怎么用脱离进程组的方式把它托住。

一、现象:进程起来了,发布却断在半路

这个坑有两种长相,都很容易被误判成别的问题。

第一种:前台跑。 直接在终端里拉起应用,一开始全对------接口探活返回 <h1>MatrixMedia API</h1>,说明服务活着;等几分钟再探,返回连接被拒,进程没了。中间什么都没报。

第二种:丢到后台跑。 用 & 丢到后台,以为父进程退场就不影响了。结果任务被回收的那一刻,进程树一起死,发布正好做到一半被杀。翻应用自己的记录,那条 pushData 落的是「应用退出,已中断发布」。

json 复制代码
{ "pt": "掘金", "publishStatus": "publishing", "lastPublishMessage": "应用退出,已中断发布" }

关键判断在这里:不是 Electron 崩了,不是 puppeteer 报错,也不是登录态失效------是它被上游的「任务回收」连坐带走。分不清这一层,就会一直在错的层里排查:换浏览器参数、重装依赖、重新登录,全是白费。

二、两个根因:控制台连带和 Job 对象

2.1 根因一:控制台事件按进程组广播

Windows 上 CreateProcess 默认让子进程继承父进程的控制台 。控制台收到 CTRL_CLOSE_EVENT(窗口被关、宿主任务退场)时,这个信号是发给同一个进程组里的所有进程的,不是只发给发起方。Electron 本身是 GUI 子系统程序,但被 CLI 以控制台方式拉起之后,它还是挂在父控制台上,照样收得到。

所以 nohup + disown 这套东西救不了场------它们是 POSIX 的作业控制概念,只改 shell 自己的作业表,不改变进程在 Windows 上的 Console / Job 归属:

bash 复制代码
# 看起来"脱离了",其实一点没脱:进程仍在同一个 Console 与 Job 里
nohup ./mmcli_env.cmd > /dev/null 2>&1 &
disown

2.2 根因二:Job Object 的 kill-on-close

更硬的一层是 Job Object。调度器、宿主环境、IDE 常常把子进程塞进一个 Job,并设上 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE(0x2000)。它的语义是:Job 里最后一个句柄被关闭时,Job 内的所有进程(含孙子进程)被强制终止。

这才是「父进程一退出,GUI 子进程立刻被回收」的真正机制。注意它和「进程崩了」的区别:进程没有崩,是有人按下了一键清场,而且清的是整棵树。

要真正脱身,得用 CREATE_BREAKAWAY_FROM_JOB(0x01000000)把子进程从 Job 里摘出去。但它有个前置条件:父 Job 必须设了 JOB_OBJECT_LIMIT_BREAKAWAY_OK ,没设就直接 Access is denied------这时只能退回到「让发起方活到发布结束」这条路。

三、解法一:用脱离进程的方式起 GUI

Python 侧起进程时把三个创建标志说清楚:

python 复制代码
import os, subprocess

DETACHED_PROCESS        = 0x00000008   # 不分配控制台,从根上收不到控制台关闭事件
CREATE_NEW_PROCESS_GROUP = 0x00000200  # 自成进程组,父级 Ctrl 事件打不进来
CREATE_BREAKAWAY_FROM_JOB = 0x01000000 # 父进程在 Job 里时尝试脱离(需 Job 允许)

# 必须剔除 ELECTRON_RUN_AS_NODE,否则 Electron 会退化成 node
env = {k: v for k, v in os.environ.items() if k != "ELECTRON_RUN_AS_NODE"}

p = subprocess.Popen(
    [exe],
    creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
    close_fds=True,
    stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    env=env,
    cwd=os.path.dirname(exe),
)
print("pid", p.pid)   # 记下 pid 就够了,别再去 wait() 它

三个标志各管一件事,别混:

标志 值 解决什么
DETACHED_PROCESS 0x00000008 新进程不分配控制台,拿不到父控制台的关闭事件
CREATE_NEW_PROCESS_GROUP 0x00000200 自成进程组,GenerateConsoleCtrlEvent 打不到它
CREATE_BREAKAWAY_FROM_JOB 0x01000000 把子进程从父 Job 摘出去;父 Job 没开 breakaway 会报 Access denied

还有一个必改项:env 里必须删掉 ELECTRON_RUN_AS_NODE 。宿主环境会把它带进子进程,Electron 一旦看到这个变量就退化成纯 node,启动即 Cannot find module '...\cli'。这个报错看起来像「CLI 坏了」,其实是环境变量把它的身份改了,排查方向完全跑偏。

四、解法二:让发起方活到发布结束

脱离进程只解决了「不被连坐」,没解决「谁来等结果」。发布是分钟级的长任务,一次 30~50 秒,平台弹窗多的时候更久,发起方必须先活到它结束,而且不能靠 wait() 子进程句柄来等------那个句柄一旦失效,你连它什么时候没的都不知道。

正确姿势是轮询输出文件:

python 复制代码
import pathlib, time

out = pathlib.Path(r"C:\Users\molan\AppData\Local\Temp\mmcli-juejin-2026-10-14.txt")
deadline = time.time() + 300          # 给足 5 分钟
while time.time() < deadline:
    if out.exists():
        txt = out.read_text(encoding="utf-8", errors="ignore")
        if "puppeteerFile-done" in txt or "文章发布成功" in txt:
            break
    time.sleep(3)

两条硬规矩:

  1. 发布必须放进「自带轮询循环的常驻任务」里跑,父进程持续存活到发布结束。发完就返回的写法一定失败------父进程一退,进程树就没了,输出文件根本不会生成。
  2. puppeteerFile-done status:true 只代表提交成功,不代表真上架。落没落到平台侧要另查一遍,别把提交回执当成最终结论。

顺便说一个并发信号,很实用:如果准备发布时发现目标输出文件被别的进程占着(删不掉、报句柄冲突),说明已经有一个发布在跑了。这时候正确动作是立刻停手,而不是清文件重试------清完只会让两个实例撞同一个草稿。

五、为什么这个坑特别贵:僵记录会拦路

被杀掉留下的不只是「这次没发成」,还有一条赖着不走的僵尸记录:

  • pushData 那条的状态停在 publishing,永远不翻转;
  • 之后所有新提交都被 isDuplicate是否存在相同的数据 true 静默拒绝------不报错、不抛异常、退出码还是 0,你只会觉得「怎么今天不发了」。

恢复办法是把那条僵记录判死,队列才会重新接收提交:

json 复制代码
POST /changeData
{
  "fileName": "pushData",
  "type": "update",
  "item": {
    "id": 1791234567890123,
    "date": "2026-10-14",
    "publishStatus": "failed",
    "lastPublishMessage": "应用退出,已中断发布(进程被回收)",
    "publishFailCount": 1,
    "publishSuccessCount": 0,
    "publishAbnormalCount": 0,
    "lastPublishAt": 1791999999999
  }
}

改判成 failed 之后,再做单平台重发,不要整批重来。

判定口诀 :某条 publishing 超过 10 分钟没翻 success,且主日志里完全没有对应平台的自动化日志 → 不是这个平台自己坏了,而是它前面有个平台卡住了串行的发布 worker,把队列堵死在后头了。

六、小结

把这件事收敛成一张清单:

  1. 前台跑 GUI 会被沙箱回收,探活通过不代表活得过几分钟。
  2. nohup + disown 治不了 Windows 的 Console / Job 连坐,别在这上面浪费时间。
  3. 起进程用 DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP ,父 Job 允许时再加 CREATE_BREAKAWAY_FROM_JOB。
  4. env 里剔除 ELECTRON_RUN_AS_NODE,否则 Electron 退化成 node,报错方向会完全误导你。
  5. 发起方要活到发布结束,靠轮询输出文件判完成,不靠等进程句柄。
  6. 发现输出文件被占用 = 有并发发布在进行,停手,别清文件重试。
  7. 僵在 publishing 的记录要主动判死,否则后续提交会被静默拒绝。

「进程没崩」不等于「进程还在」。在一个会被回收的宿主里做自动化,进程生命周期和业务逻辑一样是必修课------它出问题的时候不报错,只是安静地把你的发布做掉一半。

相关推荐
kaixin_learn_qt_ing1 小时前
hexdump -C /path/to/binary | less 看十六进制内容
前端·css·less
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 链接与图片
前端·html·html5
sun_weitao1 小时前
Chrome 扩展插件开发指南
前端·javascript·chrome
小马不起床1 小时前
零基础玩转 FastAPI_12:前端详情增强与角色权限分配
前端·状态模式·fastapi
前端snow2 小时前
ai agent --- 切割文件 RecursiveCharacterTextSplitter
前端
松就是我902982 小时前
可编辑 PPT:调研、设计与实施方案
前端
前端snow2 小时前
ai agent --- Elasticsearch 混合检索
前端
java1234_小锋2 小时前
【技术专题】HTML5 网页技术 - HTML5 文本与排版标签
前端·html·html5
Wokoo72 小时前
Selenium:元素定位、动态适配与性能优化(二)
前端·selenium·自动化