dynworkflow 0.1.3__tar.gz → 0.1.4__tar.gz
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.
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/PKG-INFO +4 -4
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/README.md +3 -3
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/pyproject.toml +1 -1
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/cache.py +38 -9
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/flow.py +157 -47
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/__init__.py +0 -0
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/agent.py +0 -0
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/py.typed +0 -0
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/reporter.py +0 -0
- {dynworkflow-0.1.3 → dynworkflow-0.1.4}/src/dynworkflow/schemas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dynworkflow
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Dynamic Workflow Library for Alkaid0
|
|
5
5
|
Author: cxykevin
|
|
6
6
|
Author-email: cxykevin <cxykevin@yeah.net>
|
|
@@ -40,18 +40,18 @@ def summarize() -> d.TaskGenerator:
|
|
|
40
40
|
flow.execute(summarize())
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`
|
|
43
|
+
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`、一个或多个下游 `Execute` 任务,或它们的组合(例如 `return d.Result(value), next_node(x=value)`)。`Result` 只记录本节点结果,不会阻止下游调度。生成器节点可以 `yield Agent` 或 `MultiAgent`,接收 Agent 结果后返回节点结果。
|
|
44
44
|
|
|
45
45
|
## 公共 API
|
|
46
46
|
|
|
47
47
|
- `Flow`:创建并执行工作流。
|
|
48
48
|
- `Flow.node(name)`:注册节点函数。
|
|
49
49
|
- `Flow.execute(*tasks)`:在当前进程中执行任务,并在所有节点结束后抛出节点异常。
|
|
50
|
-
- `Flow.run(*tasks)
|
|
50
|
+
- `Flow.run(*tasks)`:运行工作流,并监听 stdin 中的 JSONL 控制命令。支持 fork 的平台在 fork 子进程中运行,其他平台(如 Windows)在当前进程中运行。
|
|
51
51
|
- `Agent`:描述一次 Agent 请求。
|
|
52
52
|
- `MultiAgent`:并发运行多个 Agent 请求。
|
|
53
53
|
- `AgentClient`:连接 ACP WebSocket 端点。
|
|
54
|
-
- `Result
|
|
54
|
+
- `Result`:保存节点结果;可与下游任务同时返回。
|
|
55
55
|
|
|
56
56
|
## 执行语义
|
|
57
57
|
|
|
@@ -30,18 +30,18 @@ def summarize() -> d.TaskGenerator:
|
|
|
30
30
|
flow.execute(summarize())
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`
|
|
33
|
+
使用 `@flow.node(name)` 声明节点。普通节点可以返回 `None`、`Result`、一个或多个下游 `Execute` 任务,或它们的组合(例如 `return d.Result(value), next_node(x=value)`)。`Result` 只记录本节点结果,不会阻止下游调度。生成器节点可以 `yield Agent` 或 `MultiAgent`,接收 Agent 结果后返回节点结果。
|
|
34
34
|
|
|
35
35
|
## 公共 API
|
|
36
36
|
|
|
37
37
|
- `Flow`:创建并执行工作流。
|
|
38
38
|
- `Flow.node(name)`:注册节点函数。
|
|
39
39
|
- `Flow.execute(*tasks)`:在当前进程中执行任务,并在所有节点结束后抛出节点异常。
|
|
40
|
-
- `Flow.run(*tasks)
|
|
40
|
+
- `Flow.run(*tasks)`:运行工作流,并监听 stdin 中的 JSONL 控制命令。支持 fork 的平台在 fork 子进程中运行,其他平台(如 Windows)在当前进程中运行。
|
|
41
41
|
- `Agent`:描述一次 Agent 请求。
|
|
42
42
|
- `MultiAgent`:并发运行多个 Agent 请求。
|
|
43
43
|
- `AgentClient`:连接 ACP WebSocket 端点。
|
|
44
|
-
- `Result
|
|
44
|
+
- `Result`:保存节点结果;可与下游任务同时返回。
|
|
45
45
|
|
|
46
46
|
## 执行语义
|
|
47
47
|
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
缓存内容分两类:
|
|
5
5
|
- ``agent_cache``:Agent 运行结果(MultiAgent 按单个 agent 分别缓存),
|
|
6
6
|
以及未正常结束会话的 ACP sessionId(供下次按需 ACP load 续跑);
|
|
7
|
-
- ``node_cache``:节点执行结果——终值(Result/None
|
|
8
|
-
(Execute 链序列化为 [目标节点 id + 调用时参数]
|
|
7
|
+
- ``node_cache``:节点执行结果——终值(Result/None)、下游调度
|
|
8
|
+
(Execute 链序列化为 [目标节点 id + 调用时参数]),或两者的组合;
|
|
9
|
+
命中时重建重放。
|
|
9
10
|
|
|
10
11
|
联合键统一为 [会话作用域 + 工作流名 + 节点 ID + 载荷 hash];节点载荷
|
|
11
12
|
包含函数源码体 hash(代码变更即失效)与全部有效参数。
|
|
@@ -204,11 +205,24 @@ class WorkflowCache:
|
|
|
204
205
|
return False
|
|
205
206
|
return self._put("agent_cache", info, serialized, extra_kind=kind)
|
|
206
207
|
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _valid_execute_items(items: Any) -> bool:
|
|
210
|
+
"""_valid_execute_items() 校验下游调度条目列表是否合法"""
|
|
211
|
+
if not isinstance(items, list):
|
|
212
|
+
return False
|
|
213
|
+
for item in items:
|
|
214
|
+
if (not isinstance(item, dict)
|
|
215
|
+
or not isinstance(item.get("node_id"), str)
|
|
216
|
+
or not isinstance(item.get("args"), dict)):
|
|
217
|
+
return False
|
|
218
|
+
return True
|
|
219
|
+
|
|
207
220
|
def get_node(self, info: CacheKey) -> dict[str, Any] | None:
|
|
208
221
|
"""get_node() 查询节点条目。
|
|
209
222
|
|
|
210
|
-
返回形如 ``{"kind": "none"}``、``{"kind": "result", "value": ...}
|
|
211
|
-
|
|
223
|
+
返回形如 ``{"kind": "none"}``、``{"kind": "result", "value": ...}``、
|
|
224
|
+
``{"kind": "executes", "items": [{"node_id": ..., "args": ...}]}``
|
|
225
|
+
或 ``{"kind": "items", "items": [...]}``(Result 与下游调度组合);
|
|
212
226
|
格式非法视为未命中。
|
|
213
227
|
"""
|
|
214
228
|
conn = self._conn
|
|
@@ -232,17 +246,32 @@ class WorkflowCache:
|
|
|
232
246
|
if not isinstance(parsed, dict):
|
|
233
247
|
return None
|
|
234
248
|
kind = parsed.get("kind")
|
|
235
|
-
if kind not in {"none", "result", "executes"}:
|
|
249
|
+
if kind not in {"none", "result", "executes", "items"}:
|
|
236
250
|
return None
|
|
237
251
|
if kind == "executes":
|
|
252
|
+
if not self._valid_execute_items(parsed.get("items")):
|
|
253
|
+
return None
|
|
254
|
+
elif kind == "items":
|
|
238
255
|
items = parsed.get("items")
|
|
239
|
-
if not isinstance(items, list):
|
|
256
|
+
if not isinstance(items, list) or not items:
|
|
240
257
|
return None
|
|
258
|
+
result_count = 0
|
|
241
259
|
for item in items:
|
|
242
|
-
if
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
if not isinstance(item, dict):
|
|
261
|
+
return None
|
|
262
|
+
item_kind = item.get("kind")
|
|
263
|
+
if item_kind == "result":
|
|
264
|
+
if "value" not in item:
|
|
265
|
+
return None
|
|
266
|
+
result_count += 1
|
|
267
|
+
elif item_kind == "execute":
|
|
268
|
+
if (not isinstance(item.get("node_id"), str)
|
|
269
|
+
or not isinstance(item.get("args"), dict)):
|
|
270
|
+
return None
|
|
271
|
+
else:
|
|
245
272
|
return None
|
|
273
|
+
if result_count > 1:
|
|
274
|
+
return None
|
|
246
275
|
return parsed
|
|
247
276
|
|
|
248
277
|
def put_node(self, info: CacheKey, descriptor: dict[str, Any]) -> bool:
|
|
@@ -11,6 +11,7 @@ import threading
|
|
|
11
11
|
import time
|
|
12
12
|
import ast
|
|
13
13
|
import textwrap
|
|
14
|
+
import traceback
|
|
14
15
|
from enum import Enum
|
|
15
16
|
|
|
16
17
|
from .agent import (Agent, AgentClientInterface, MultiAgent, AgentStatus,
|
|
@@ -43,7 +44,7 @@ class Graph(TypedDict):
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class Result:
|
|
46
|
-
"Result
|
|
47
|
+
"Result 保存节点结果;节点可同时继续调度下游任务"
|
|
47
48
|
|
|
48
49
|
def __init__(self, result: Any) -> None:
|
|
49
50
|
self.result_val = result
|
|
@@ -52,7 +53,7 @@ class Result:
|
|
|
52
53
|
return f"Result({self.result_val})"
|
|
53
54
|
|
|
54
55
|
def __str__(self) -> str:
|
|
55
|
-
return str(self.
|
|
56
|
+
return str(self.result_val)
|
|
56
57
|
|
|
57
58
|
def result(self) -> Any:
|
|
58
59
|
"Result.result() 获取结果"
|
|
@@ -76,20 +77,54 @@ class NodeStatus(Enum):
|
|
|
76
77
|
ERROR = "error" # 完成
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
def _split_node_result(
|
|
81
|
+
ret: 'NodeResult') -> "tuple[list[Any], list[Execute]]":
|
|
82
|
+
'"_split_node_result() 拆分节点返回值为 (Result 值列表, 下游任务列表)"'
|
|
83
|
+
|
|
84
|
+
if ret is None:
|
|
85
|
+
return [], []
|
|
86
|
+
if isinstance(ret, (Result, Execute)):
|
|
87
|
+
items: list[Any] = [ret]
|
|
88
|
+
elif isinstance(ret, (list, tuple)):
|
|
89
|
+
items = list(ret)
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"Invalid node result: {ret!r}")
|
|
92
|
+
results: list[Any] = []
|
|
93
|
+
executes: list[Execute] = []
|
|
94
|
+
for item in items:
|
|
95
|
+
if isinstance(item, Result):
|
|
96
|
+
results.append(item.result())
|
|
97
|
+
elif isinstance(item, Execute):
|
|
98
|
+
executes.append(item)
|
|
99
|
+
else:
|
|
100
|
+
raise ValueError(f"Invalid node result item: {item!r}")
|
|
101
|
+
if len(results) > 1:
|
|
102
|
+
raise ValueError("A node may return at most one Result")
|
|
103
|
+
return results, executes
|
|
104
|
+
|
|
105
|
+
|
|
79
106
|
def _describe_node_result(ret: 'NodeResult') -> "dict[str, Any] | None":
|
|
80
107
|
'"_describe_node_result() 把节点返回值序列化为缓存描述符;不可缓存时返回 None'
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
results, executes = _split_node_result(ret)
|
|
110
|
+
exec_items: list[dict[str, Any]] = [
|
|
111
|
+
{"node_id": item._node._node_id, "args": dict(item._args)}
|
|
112
|
+
for item in executes
|
|
113
|
+
]
|
|
114
|
+
if not results:
|
|
115
|
+
if not exec_items:
|
|
116
|
+
return {"kind": "none"}
|
|
117
|
+
return {"kind": "executes", "items": exec_items}
|
|
118
|
+
if not exec_items:
|
|
85
119
|
# value 含不可序列化对象时由 put_node 整体放弃写入
|
|
86
|
-
return {"kind": "result", "value":
|
|
87
|
-
items
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
return {"kind": "result", "value": results[0]}
|
|
121
|
+
items: list[dict[str, Any]] = [
|
|
122
|
+
{"kind": "result", "value": results[0]}]
|
|
123
|
+
items.extend(
|
|
124
|
+
{"kind": "execute", "node_id": item._node._node_id,
|
|
125
|
+
"args": dict(item._args)}
|
|
126
|
+
for item in executes)
|
|
127
|
+
return {"kind": "items", "items": items}
|
|
93
128
|
|
|
94
129
|
|
|
95
130
|
class Node():
|
|
@@ -307,6 +342,9 @@ class Node():
|
|
|
307
342
|
self._report_status(flow, NodeStatus.ERROR, dic)
|
|
308
343
|
return
|
|
309
344
|
|
|
345
|
+
# 先校验返回值:多个 Result 或非法元素在此失败并转为节点错误
|
|
346
|
+
results, executes = _split_node_result(ret)
|
|
347
|
+
|
|
310
348
|
self.status = NodeStatus.DONE
|
|
311
349
|
# 推送:节点执行完成
|
|
312
350
|
self._report_status(flow, NodeStatus.DONE, dic)
|
|
@@ -317,55 +355,70 @@ class Node():
|
|
|
317
355
|
if descriptor is not None:
|
|
318
356
|
flow.cache.put_node(node_info, descriptor)
|
|
319
357
|
|
|
320
|
-
#
|
|
321
|
-
if
|
|
322
|
-
|
|
323
|
-
# 返回值处理
|
|
324
|
-
if isinstance(ret, Result):
|
|
325
|
-
value = ret.result()
|
|
358
|
+
# 记录 Result;与下游调度互不排斥,可同时生效
|
|
359
|
+
if results:
|
|
360
|
+
value = results[0]
|
|
326
361
|
flow._result[self._node_id] = value
|
|
327
362
|
# 推送:节点终值(宿主据此在 workflow 视图里展示 Result)
|
|
328
363
|
self._report_result(flow, value)
|
|
329
|
-
return
|
|
330
364
|
|
|
331
365
|
# 执行流程下一步
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
for i in ret:
|
|
335
|
-
i.run(flow)
|
|
366
|
+
for item in executes:
|
|
367
|
+
item.run(flow)
|
|
336
368
|
|
|
337
369
|
def _replay_cached(self, flow: 'Flow', cached: dict[str, Any]) -> None:
|
|
338
|
-
"Node._replay_cached()
|
|
370
|
+
"Node._replay_cached() 重放缓存的节点结果(终值/下游调度/两者组合)"
|
|
339
371
|
kind = cached.get("kind")
|
|
340
372
|
if kind == "none":
|
|
341
373
|
return
|
|
342
374
|
if kind == "result":
|
|
343
|
-
|
|
344
|
-
flow._result[self._node_id] = value
|
|
345
|
-
# 缓存命中同样上报终值,宿主视图与真实执行保持一致
|
|
346
|
-
self._report_result(flow, value)
|
|
375
|
+
self._replay_result(flow, cached.get("value"))
|
|
347
376
|
return
|
|
377
|
+
known = {node._node_id: node for node in flow._nodes}
|
|
348
378
|
if kind == "executes":
|
|
349
|
-
known = {node._node_id: node for node in flow._nodes}
|
|
350
379
|
items = cached.get("items")
|
|
351
380
|
if not isinstance(items, list):
|
|
352
381
|
items = []
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
382
|
+
self._replay_executes(flow, known,
|
|
383
|
+
cast(list[dict[str, Any]], items))
|
|
384
|
+
return
|
|
385
|
+
if kind == "items":
|
|
386
|
+
raw_items = cached.get("items")
|
|
387
|
+
if not isinstance(raw_items, list):
|
|
388
|
+
raw_items = []
|
|
389
|
+
for item in cast(list[dict[str, Any]], raw_items):
|
|
390
|
+
item_kind = item.get("kind")
|
|
391
|
+
if item_kind == "result":
|
|
392
|
+
self._replay_result(flow, item.get("value"))
|
|
393
|
+
elif item_kind == "execute":
|
|
394
|
+
self._replay_executes(flow, known, [item])
|
|
395
|
+
else:
|
|
358
396
|
raise ValueError(
|
|
359
|
-
f"
|
|
360
|
-
# 重建调用点参数并走正常填充/调度路径,
|
|
361
|
-
# 完整保留跨节点部分参数累积语义。
|
|
362
|
-
args: Any = item.get("args")
|
|
363
|
-
args_value: dict[str, Any] = dict(cast(dict[str, Any], args)) if isinstance(
|
|
364
|
-
args, dict) else {}
|
|
365
|
-
Execute(target, args_value).run(flow)
|
|
397
|
+
f"Invalid cached node item kind: {item_kind!r}")
|
|
366
398
|
return
|
|
367
399
|
raise ValueError(f"Invalid cached node result kind: {kind!r}")
|
|
368
400
|
|
|
401
|
+
def _replay_result(self, flow: 'Flow', value: Any) -> None:
|
|
402
|
+
"_replay_result() 写入并上报缓存中的节点终值"
|
|
403
|
+
flow._result[self._node_id] = value
|
|
404
|
+
# 缓存命中同样上报终值,宿主视图与真实执行保持一致
|
|
405
|
+
self._report_result(flow, value)
|
|
406
|
+
|
|
407
|
+
def _replay_executes(self, flow: 'Flow', known: "dict[str, Node]",
|
|
408
|
+
items: list[dict[str, Any]]) -> None:
|
|
409
|
+
"_replay_executes() 重放缓存中的下游调度"
|
|
410
|
+
for item in items:
|
|
411
|
+
target_id = str(item.get("node_id"))
|
|
412
|
+
target = known.get(target_id)
|
|
413
|
+
if target is None:
|
|
414
|
+
raise ValueError(f"Cached target node not found: {target_id}")
|
|
415
|
+
# 重建调用点参数并走正常填充/调度路径,
|
|
416
|
+
# 完整保留跨节点部分参数累积语义。
|
|
417
|
+
args: Any = item.get("args")
|
|
418
|
+
args_value: dict[str, Any] = dict(
|
|
419
|
+
cast(dict[str, Any], args)) if isinstance(args, dict) else {}
|
|
420
|
+
Execute(target, args_value).run(flow)
|
|
421
|
+
|
|
369
422
|
def _create_thread_task(self, flow: 'Flow') -> threading.Thread:
|
|
370
423
|
"Node.thread_tast() 启动线程"
|
|
371
424
|
if (self._thread is not None and self._thread.is_alive()):
|
|
@@ -440,11 +493,14 @@ type Agents = (
|
|
|
440
493
|
MultiAgent |
|
|
441
494
|
Agent
|
|
442
495
|
) # Agents 子 Agent 对象
|
|
443
|
-
type
|
|
496
|
+
type NodeResultItem = (
|
|
444
497
|
Execute |
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
498
|
+
Result
|
|
499
|
+
) # NodeResultItem 节点返回的单个元素
|
|
500
|
+
type NodeResult = (
|
|
501
|
+
NodeResultItem |
|
|
502
|
+
list[NodeResultItem] |
|
|
503
|
+
tuple[NodeResultItem, ...] |
|
|
448
504
|
None
|
|
449
505
|
) # Nodes 节点返回对象
|
|
450
506
|
type TaskGenerator = (Generator[
|
|
@@ -627,7 +683,11 @@ class Flow:
|
|
|
627
683
|
self._raise_exceptions()
|
|
628
684
|
|
|
629
685
|
def run(self, *tasks: Execute) -> NoReturn:
|
|
630
|
-
"""
|
|
686
|
+
"""Flow.run() 运行工作流,同时监听 stdin JSONL 控制命令。
|
|
687
|
+
|
|
688
|
+
支持 fork 的平台(Linux/macOS)在 fork 子进程内执行工作流;
|
|
689
|
+
没有 fork 的平台(如 Windows)在当前进程内执行工作流。
|
|
690
|
+
"""
|
|
631
691
|
sys.stdout.write("\x1edynworkflow\x1f\n")
|
|
632
692
|
sys.stdout.flush()
|
|
633
693
|
self._report = True
|
|
@@ -635,6 +695,26 @@ class Flow:
|
|
|
635
695
|
self._reporter = optional_reporter(True)
|
|
636
696
|
if self._agent is None:
|
|
637
697
|
raise ValueError("No availble agent found!")
|
|
698
|
+
if "fork" in multiprocessing.get_all_start_methods():
|
|
699
|
+
self._run_forked(tasks)
|
|
700
|
+
else:
|
|
701
|
+
# Windows 等平台既没有 fork,spawn 又无法序列化 Flow 状态与
|
|
702
|
+
# 节点函数,因此退化为在当前进程内执行工作流。
|
|
703
|
+
self._run_in_current_process(tasks)
|
|
704
|
+
|
|
705
|
+
def _exit(self, code: int) -> NoReturn:
|
|
706
|
+
"_exit() 强制刷新输出并短暂等待后退出,避免宿主漏读最后一批事件"
|
|
707
|
+
try:
|
|
708
|
+
sys.stdout.flush()
|
|
709
|
+
sys.stderr.flush()
|
|
710
|
+
except Exception:
|
|
711
|
+
# 管道已关闭等情况不影响退出流程。
|
|
712
|
+
pass
|
|
713
|
+
time.sleep(0.2)
|
|
714
|
+
sys.exit(code)
|
|
715
|
+
|
|
716
|
+
def _run_forked(self, tasks: tuple[Execute, ...]) -> NoReturn:
|
|
717
|
+
"_run_forked() 在 fork 子进程执行工作流,当前进程监听 stdin JSONL。"
|
|
638
718
|
control: Any = multiprocessing.get_context("fork").Queue()
|
|
639
719
|
process = multiprocessing.get_context("fork").Process(
|
|
640
720
|
target=self._execute_child, args=(tasks, control))
|
|
@@ -670,7 +750,37 @@ class Flow:
|
|
|
670
750
|
process.terminate()
|
|
671
751
|
process.join(2)
|
|
672
752
|
# Preserve a non-zero exit status when the child failed unexpectedly.
|
|
673
|
-
|
|
753
|
+
self._exit(process.exitcode if process.exitcode is not None else 0)
|
|
754
|
+
|
|
755
|
+
def _run_in_current_process(self, tasks: tuple[Execute, ...]) -> NoReturn:
|
|
756
|
+
"_run_in_current_process() 在没有 fork 的平台于当前进程内执行工作流,"
|
|
757
|
+
"stdin 由 daemon 线程转发到控制队列。"
|
|
758
|
+
control: Any = queue.Queue()
|
|
759
|
+
|
|
760
|
+
def read_stdin() -> None:
|
|
761
|
+
try:
|
|
762
|
+
for line in sys.stdin:
|
|
763
|
+
try:
|
|
764
|
+
command = json.loads(line)
|
|
765
|
+
except json.JSONDecodeError:
|
|
766
|
+
continue
|
|
767
|
+
if isinstance(command, dict):
|
|
768
|
+
command = cast(dict[str, str], command)
|
|
769
|
+
control.put(command)
|
|
770
|
+
if command.get("cmd") == "shutdown":
|
|
771
|
+
return
|
|
772
|
+
finally:
|
|
773
|
+
# stdin 结束视为关闭请求,与 fork 版本父进程的清理行为一致。
|
|
774
|
+
control.put({"cmd": "shutdown"})
|
|
775
|
+
|
|
776
|
+
threading.Thread(target=read_stdin, daemon=True).start()
|
|
777
|
+
try:
|
|
778
|
+
self._execute_child(tasks, control)
|
|
779
|
+
except BaseException:
|
|
780
|
+
# 保留非零退出码,行为与 fork 子进程异常退出保持一致。
|
|
781
|
+
traceback.print_exc()
|
|
782
|
+
self._exit(1)
|
|
783
|
+
self._exit(0)
|
|
674
784
|
|
|
675
785
|
def _execute_child(self, tasks: tuple[Execute, ...], control: Any) -> None:
|
|
676
786
|
def listen() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|