codebee 0.1.62 → 0.1.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +499 -490
- package/LICENSE +21 -21
- package/README.md +484 -486
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/aiflavor.py +197 -197
- package/app/core/attachments.py +532 -532
- package/app/core/automation.py +758 -758
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/board.py +209 -209
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1191 -1191
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +19 -14
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +186 -186
- package/app/core/covergen.py +356 -356
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +173 -173
- package/app/core/dispatch_log.py +113 -113
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +64 -64
- package/app/core/errorlog.py +137 -137
- package/app/core/flows.py +355 -355
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +341 -341
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +156 -156
- package/app/core/jobs.py +1022 -1022
- package/app/core/manager.py +1996 -1996
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +66 -66
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +3266 -3266
- package/app/core/notify.py +106 -106
- package/app/core/paihang.py +109 -109
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +4444 -4426
- package/app/core/planner.py +27 -27
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +533 -533
- package/app/core/publish/fanqie.py +128 -128
- package/app/core/publish/flow.py +406 -406
- package/app/core/publish/ledger.py +198 -198
- package/app/core/publish/manager.py +590 -590
- package/app/core/publish/qimao.py +141 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/repeat_guard.py +124 -124
- package/app/core/router.py +20 -3
- package/app/core/runner.py +2014 -1991
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +149 -149
- package/app/core/settings_schema.py +262 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +106 -106
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/step_runner.py +132 -132
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/volumes.py +355 -355
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2090 -2090
- package/app/main.py +2826 -2826
- package/app/pick_dialog.py +110 -110
- package/app/ui/app.js +13977 -13977
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +2711 -2711
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +1 -1
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +6125 -6125
- package/bin/tutti.js +147 -147
- package/package.json +41 -41
- package/tools/wx_reader.py +261 -261
package/app/core/beekeeper.py
CHANGED
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""蜂巢保全:服务无论以何种方式退出,本服务拉起的蜜蜂 CLI 一起带走。
|
|
3
|
-
|
|
4
|
-
背景(2026-09-23 用户定案):CodeBee 关闭后——无论正常退出、Ctrl+C、崩溃
|
|
5
|
-
还是被 taskkill 硬杀——在跑的 CLI/蜜蜂(codex/claude/kimi 等)必须一起杀
|
|
6
|
-
掉,否则孤儿进程把内存吃满。当天 16:50 一次服务重启就留过一只孤儿
|
|
7
|
-
claude.exe(直到管道断裂才饿死)。
|
|
8
|
-
|
|
9
|
-
Windows:Job Object(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)。蜜蜂 Popen 一出生
|
|
10
|
-
就 AssignProcessToJobObject 挂进蜂巢,整棵子进程树自动继承巢内身份;服务进
|
|
11
|
-
程死亡(哪怕 os._exit 跳过 atexit、被 TerminateProcess 断头)时内核关闭巢柄,
|
|
12
|
-
负责把巢内进程全部收走——这是唯一能覆盖「任意死法」的机制。
|
|
13
|
-
- 升级重启的新实例(selfupdate.relaunch)不经 run_process、不挂巢,得以
|
|
14
|
-
存活——否则就地重启等于自杀。桌面蜜蜂、explorer、cmd /k 交互终端同理豁免。
|
|
15
|
-
POSIX(含 Mac):没有 Job Object 也没有 Linux 的 prctl PDEATHSIG,靠登记 +
|
|
16
|
-
atexit 清扫兜底:run_process 的子进程本就 start_new_session(自成进程组,
|
|
17
|
-
pgid==pid),sweep 用 killpg 连树杀。正常退出 / Ctrl+C / SIGTERM/SIGHUP
|
|
18
|
-
(main.py 已把它们转成 SystemExit,atexit 照跑)全覆盖;仅 kill -9 无解
|
|
19
|
-
(任何机制都救不了,属已知边界)。
|
|
20
|
-
|
|
21
|
-
本模块绝不抛异常:挂巢失败只降级为旧行为(取消/超时收尸仍在),绝不影响
|
|
22
|
-
蜜蜂正常起跑。不依赖 pywin32,ctypes 直调 kernel32。
|
|
23
|
-
"""
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import atexit
|
|
27
|
-
import collections
|
|
28
|
-
import os
|
|
29
|
-
import signal
|
|
30
|
-
import weakref
|
|
31
|
-
|
|
32
|
-
# 近期蜜蜂登记:只保最近若干条防无限增长。Windows 上 Job 是主机制、登记只是
|
|
33
|
-
# atexit 兜底的补充;POSIX 上登记是主机制(sweep 全靠它)。
|
|
34
|
-
_RECENT = collections.deque(maxlen=256)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# ---------------------------------------------------------------- Windows 巢
|
|
38
|
-
if os.name == "nt":
|
|
39
|
-
import ctypes
|
|
40
|
-
from ctypes import wintypes
|
|
41
|
-
|
|
42
|
-
_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
|
43
|
-
_INFO_JOB_OBJECT_EXTENDED_LIMIT = 9
|
|
44
|
-
_STILL_ACTIVE = 259
|
|
45
|
-
|
|
46
|
-
class _IO_COUNTERS(ctypes.Structure):
|
|
47
|
-
_fields_ = [(n, ctypes.c_ulonglong) for n in (
|
|
48
|
-
"ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
|
|
49
|
-
"ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
|
|
50
|
-
|
|
51
|
-
class _JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
|
|
52
|
-
_fields_ = [
|
|
53
|
-
("PerProcessUserTimeLimit", ctypes.c_longlong),
|
|
54
|
-
("PerJobUserTimeLimit", ctypes.c_longlong),
|
|
55
|
-
("LimitFlags", wintypes.DWORD),
|
|
56
|
-
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
57
|
-
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
58
|
-
("ActiveProcessLimit", wintypes.DWORD),
|
|
59
|
-
("Affinity", ctypes.c_size_t), # ULONG_PTR
|
|
60
|
-
("PriorityClass", wintypes.DWORD),
|
|
61
|
-
("SchedulingClass", wintypes.DWORD),
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
class _JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
|
|
65
|
-
_fields_ = [
|
|
66
|
-
("BasicLimitInformation", _JOBOBJECT_BASIC_LIMIT_INFORMATION),
|
|
67
|
-
("IoInfo", _IO_COUNTERS),
|
|
68
|
-
("ProcessMemoryLimit", ctypes.c_size_t),
|
|
69
|
-
("JobMemoryLimit", ctypes.c_size_t),
|
|
70
|
-
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
|
71
|
-
("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
_kernel32 = ctypes.windll.kernel32
|
|
75
|
-
|
|
76
|
-
class BeeNest:
|
|
77
|
-
"""一个 KILL_ON_JOB_CLOSE 巢。生产用模块单例 NEST;测试自建私有巢。"""
|
|
78
|
-
|
|
79
|
-
def __init__(self):
|
|
80
|
-
self._handle = None
|
|
81
|
-
self.assigned = 0
|
|
82
|
-
|
|
83
|
-
def _ensure_job(self):
|
|
84
|
-
if self._handle:
|
|
85
|
-
return self._handle
|
|
86
|
-
h = _kernel32.CreateJobObjectW(None, None)
|
|
87
|
-
if not h:
|
|
88
|
-
return None
|
|
89
|
-
info = _JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
|
|
90
|
-
info.BasicLimitInformation.LimitFlags = _LIMIT_KILL_ON_JOB_CLOSE
|
|
91
|
-
if not _kernel32.SetInformationJobObject(
|
|
92
|
-
h, _INFO_JOB_OBJECT_EXTENDED_LIMIT,
|
|
93
|
-
ctypes.byref(info), ctypes.sizeof(info)):
|
|
94
|
-
_kernel32.CloseHandle(h)
|
|
95
|
-
return None
|
|
96
|
-
self._handle = h
|
|
97
|
-
return h
|
|
98
|
-
|
|
99
|
-
def adopt(self, proc):
|
|
100
|
-
h = self._ensure_job()
|
|
101
|
-
if not h:
|
|
102
|
-
return
|
|
103
|
-
handle = getattr(proc, "_handle", None)
|
|
104
|
-
if not handle:
|
|
105
|
-
return
|
|
106
|
-
# 子进程树(cmd /c 垫片 → claude.exe → 其孙)自动继承巢内身份,
|
|
107
|
-
# 只需挂直系子进程。Win8+ 支持嵌套 Job,外层有别的 Job 也不碍事。
|
|
108
|
-
if _kernel32.AssignProcessToJobObject(h, int(handle)):
|
|
109
|
-
self.assigned += 1
|
|
110
|
-
|
|
111
|
-
def close(self):
|
|
112
|
-
"""主动关巢:巢内进程全部被内核收走(等价于服务进程死亡)。"""
|
|
113
|
-
if self._handle:
|
|
114
|
-
try:
|
|
115
|
-
_kernel32.CloseHandle(self._handle)
|
|
116
|
-
except Exception:
|
|
117
|
-
pass
|
|
118
|
-
self._handle = None
|
|
119
|
-
|
|
120
|
-
else:
|
|
121
|
-
class BeeNest:
|
|
122
|
-
"""POSIX 占位巢:杀全靠 sweep()(登记 + killpg),close 无事可做。"""
|
|
123
|
-
|
|
124
|
-
def __init__(self):
|
|
125
|
-
self.assigned = 0
|
|
126
|
-
|
|
127
|
-
def adopt(self, proc):
|
|
128
|
-
self.assigned += 1
|
|
129
|
-
|
|
130
|
-
def close(self):
|
|
131
|
-
pass
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
NEST = BeeNest()
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def adopt(proc):
|
|
138
|
-
"""把蜜蜂进程挂巢:服务死则蜜蜂亡。绝不抛异常、绝不拖慢起跑。"""
|
|
139
|
-
if proc is None:
|
|
140
|
-
return
|
|
141
|
-
try:
|
|
142
|
-
_RECENT.append((proc.pid, weakref.ref(proc)))
|
|
143
|
-
NEST.adopt(proc)
|
|
144
|
-
except Exception:
|
|
145
|
-
pass
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def sweep():
|
|
149
|
-
"""退出清扫:登记在册且仍存活的蜜蜂连树杀。
|
|
150
|
-
|
|
151
|
-
POSIX 主机制(atexit 在正常退出/Ctrl+C/SIGTERM→SystemExit 时都会跑);
|
|
152
|
-
Windows 只是 Job 的兜底(Job 建不出来或句柄异常时仍能杀到登记的直系
|
|
153
|
-
进程,孙进程交给既有 taskkill /T 收尸路径)。Windows 端拿不到 Popen
|
|
154
|
-
存活证据的条目不盲杀——防 PID 复用误伤无辜进程(反正有 Job 兜底)。
|
|
155
|
-
"""
|
|
156
|
-
while _RECENT:
|
|
157
|
-
pid, ref = _RECENT.popleft()
|
|
158
|
-
proc = ref() if ref else None
|
|
159
|
-
if proc is not None:
|
|
160
|
-
try:
|
|
161
|
-
if proc.poll() is not None:
|
|
162
|
-
continue # 已退出,跳过
|
|
163
|
-
except Exception:
|
|
164
|
-
pass
|
|
165
|
-
if os.name == "nt":
|
|
166
|
-
if proc is None:
|
|
167
|
-
continue
|
|
168
|
-
try:
|
|
169
|
-
os.kill(pid, signal.SIGTERM) # Windows 语义 = TerminateProcess
|
|
170
|
-
except Exception:
|
|
171
|
-
pass
|
|
172
|
-
else:
|
|
173
|
-
# run_process 子进程 start_new_session:pgid==pid,killpg 连树杀
|
|
174
|
-
try:
|
|
175
|
-
os.killpg(pid, signal.SIGKILL)
|
|
176
|
-
except Exception:
|
|
177
|
-
try:
|
|
178
|
-
os.kill(pid, signal.SIGKILL)
|
|
179
|
-
except Exception:
|
|
180
|
-
pass
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
atexit.register(sweep)
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""蜂巢保全:服务无论以何种方式退出,本服务拉起的蜜蜂 CLI 一起带走。
|
|
3
|
+
|
|
4
|
+
背景(2026-09-23 用户定案):CodeBee 关闭后——无论正常退出、Ctrl+C、崩溃
|
|
5
|
+
还是被 taskkill 硬杀——在跑的 CLI/蜜蜂(codex/claude/kimi 等)必须一起杀
|
|
6
|
+
掉,否则孤儿进程把内存吃满。当天 16:50 一次服务重启就留过一只孤儿
|
|
7
|
+
claude.exe(直到管道断裂才饿死)。
|
|
8
|
+
|
|
9
|
+
Windows:Job Object(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)。蜜蜂 Popen 一出生
|
|
10
|
+
就 AssignProcessToJobObject 挂进蜂巢,整棵子进程树自动继承巢内身份;服务进
|
|
11
|
+
程死亡(哪怕 os._exit 跳过 atexit、被 TerminateProcess 断头)时内核关闭巢柄,
|
|
12
|
+
负责把巢内进程全部收走——这是唯一能覆盖「任意死法」的机制。
|
|
13
|
+
- 升级重启的新实例(selfupdate.relaunch)不经 run_process、不挂巢,得以
|
|
14
|
+
存活——否则就地重启等于自杀。桌面蜜蜂、explorer、cmd /k 交互终端同理豁免。
|
|
15
|
+
POSIX(含 Mac):没有 Job Object 也没有 Linux 的 prctl PDEATHSIG,靠登记 +
|
|
16
|
+
atexit 清扫兜底:run_process 的子进程本就 start_new_session(自成进程组,
|
|
17
|
+
pgid==pid),sweep 用 killpg 连树杀。正常退出 / Ctrl+C / SIGTERM/SIGHUP
|
|
18
|
+
(main.py 已把它们转成 SystemExit,atexit 照跑)全覆盖;仅 kill -9 无解
|
|
19
|
+
(任何机制都救不了,属已知边界)。
|
|
20
|
+
|
|
21
|
+
本模块绝不抛异常:挂巢失败只降级为旧行为(取消/超时收尸仍在),绝不影响
|
|
22
|
+
蜜蜂正常起跑。不依赖 pywin32,ctypes 直调 kernel32。
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import atexit
|
|
27
|
+
import collections
|
|
28
|
+
import os
|
|
29
|
+
import signal
|
|
30
|
+
import weakref
|
|
31
|
+
|
|
32
|
+
# 近期蜜蜂登记:只保最近若干条防无限增长。Windows 上 Job 是主机制、登记只是
|
|
33
|
+
# atexit 兜底的补充;POSIX 上登记是主机制(sweep 全靠它)。
|
|
34
|
+
_RECENT = collections.deque(maxlen=256)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------- Windows 巢
|
|
38
|
+
if os.name == "nt":
|
|
39
|
+
import ctypes
|
|
40
|
+
from ctypes import wintypes
|
|
41
|
+
|
|
42
|
+
_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
|
43
|
+
_INFO_JOB_OBJECT_EXTENDED_LIMIT = 9
|
|
44
|
+
_STILL_ACTIVE = 259
|
|
45
|
+
|
|
46
|
+
class _IO_COUNTERS(ctypes.Structure):
|
|
47
|
+
_fields_ = [(n, ctypes.c_ulonglong) for n in (
|
|
48
|
+
"ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
|
|
49
|
+
"ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
|
|
50
|
+
|
|
51
|
+
class _JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
|
|
52
|
+
_fields_ = [
|
|
53
|
+
("PerProcessUserTimeLimit", ctypes.c_longlong),
|
|
54
|
+
("PerJobUserTimeLimit", ctypes.c_longlong),
|
|
55
|
+
("LimitFlags", wintypes.DWORD),
|
|
56
|
+
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
57
|
+
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
58
|
+
("ActiveProcessLimit", wintypes.DWORD),
|
|
59
|
+
("Affinity", ctypes.c_size_t), # ULONG_PTR
|
|
60
|
+
("PriorityClass", wintypes.DWORD),
|
|
61
|
+
("SchedulingClass", wintypes.DWORD),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
class _JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
|
|
65
|
+
_fields_ = [
|
|
66
|
+
("BasicLimitInformation", _JOBOBJECT_BASIC_LIMIT_INFORMATION),
|
|
67
|
+
("IoInfo", _IO_COUNTERS),
|
|
68
|
+
("ProcessMemoryLimit", ctypes.c_size_t),
|
|
69
|
+
("JobMemoryLimit", ctypes.c_size_t),
|
|
70
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
|
71
|
+
("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
_kernel32 = ctypes.windll.kernel32
|
|
75
|
+
|
|
76
|
+
class BeeNest:
|
|
77
|
+
"""一个 KILL_ON_JOB_CLOSE 巢。生产用模块单例 NEST;测试自建私有巢。"""
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
self._handle = None
|
|
81
|
+
self.assigned = 0
|
|
82
|
+
|
|
83
|
+
def _ensure_job(self):
|
|
84
|
+
if self._handle:
|
|
85
|
+
return self._handle
|
|
86
|
+
h = _kernel32.CreateJobObjectW(None, None)
|
|
87
|
+
if not h:
|
|
88
|
+
return None
|
|
89
|
+
info = _JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
|
|
90
|
+
info.BasicLimitInformation.LimitFlags = _LIMIT_KILL_ON_JOB_CLOSE
|
|
91
|
+
if not _kernel32.SetInformationJobObject(
|
|
92
|
+
h, _INFO_JOB_OBJECT_EXTENDED_LIMIT,
|
|
93
|
+
ctypes.byref(info), ctypes.sizeof(info)):
|
|
94
|
+
_kernel32.CloseHandle(h)
|
|
95
|
+
return None
|
|
96
|
+
self._handle = h
|
|
97
|
+
return h
|
|
98
|
+
|
|
99
|
+
def adopt(self, proc):
|
|
100
|
+
h = self._ensure_job()
|
|
101
|
+
if not h:
|
|
102
|
+
return
|
|
103
|
+
handle = getattr(proc, "_handle", None)
|
|
104
|
+
if not handle:
|
|
105
|
+
return
|
|
106
|
+
# 子进程树(cmd /c 垫片 → claude.exe → 其孙)自动继承巢内身份,
|
|
107
|
+
# 只需挂直系子进程。Win8+ 支持嵌套 Job,外层有别的 Job 也不碍事。
|
|
108
|
+
if _kernel32.AssignProcessToJobObject(h, int(handle)):
|
|
109
|
+
self.assigned += 1
|
|
110
|
+
|
|
111
|
+
def close(self):
|
|
112
|
+
"""主动关巢:巢内进程全部被内核收走(等价于服务进程死亡)。"""
|
|
113
|
+
if self._handle:
|
|
114
|
+
try:
|
|
115
|
+
_kernel32.CloseHandle(self._handle)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
self._handle = None
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
class BeeNest:
|
|
122
|
+
"""POSIX 占位巢:杀全靠 sweep()(登记 + killpg),close 无事可做。"""
|
|
123
|
+
|
|
124
|
+
def __init__(self):
|
|
125
|
+
self.assigned = 0
|
|
126
|
+
|
|
127
|
+
def adopt(self, proc):
|
|
128
|
+
self.assigned += 1
|
|
129
|
+
|
|
130
|
+
def close(self):
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
NEST = BeeNest()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def adopt(proc):
|
|
138
|
+
"""把蜜蜂进程挂巢:服务死则蜜蜂亡。绝不抛异常、绝不拖慢起跑。"""
|
|
139
|
+
if proc is None:
|
|
140
|
+
return
|
|
141
|
+
try:
|
|
142
|
+
_RECENT.append((proc.pid, weakref.ref(proc)))
|
|
143
|
+
NEST.adopt(proc)
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def sweep():
|
|
149
|
+
"""退出清扫:登记在册且仍存活的蜜蜂连树杀。
|
|
150
|
+
|
|
151
|
+
POSIX 主机制(atexit 在正常退出/Ctrl+C/SIGTERM→SystemExit 时都会跑);
|
|
152
|
+
Windows 只是 Job 的兜底(Job 建不出来或句柄异常时仍能杀到登记的直系
|
|
153
|
+
进程,孙进程交给既有 taskkill /T 收尸路径)。Windows 端拿不到 Popen
|
|
154
|
+
存活证据的条目不盲杀——防 PID 复用误伤无辜进程(反正有 Job 兜底)。
|
|
155
|
+
"""
|
|
156
|
+
while _RECENT:
|
|
157
|
+
pid, ref = _RECENT.popleft()
|
|
158
|
+
proc = ref() if ref else None
|
|
159
|
+
if proc is not None:
|
|
160
|
+
try:
|
|
161
|
+
if proc.poll() is not None:
|
|
162
|
+
continue # 已退出,跳过
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
if os.name == "nt":
|
|
166
|
+
if proc is None:
|
|
167
|
+
continue
|
|
168
|
+
try:
|
|
169
|
+
os.kill(pid, signal.SIGTERM) # Windows 语义 = TerminateProcess
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
else:
|
|
173
|
+
# run_process 子进程 start_new_session:pgid==pid,killpg 连树杀
|
|
174
|
+
try:
|
|
175
|
+
os.killpg(pid, signal.SIGKILL)
|
|
176
|
+
except Exception:
|
|
177
|
+
try:
|
|
178
|
+
os.kill(pid, signal.SIGKILL)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
atexit.register(sweep)
|