description: "CPython 子进程代码 runtime:为 Python 模型代码实现 dsh-code-runtime seam,及其使用的 fd-3 wire 协议。"
English | 中文
dsh-code-runtime-python 交付 PythonCodeRuntime——dsh-code-runtime seam 的 CPython 子进程实现:它以 language: 'python'、isolation: 'process' 注册为 codeRuntime,每次 run() 启动一个全新的 python3 -I 子进程,把程序作为 async 函数体执行,通过子进程 fd 3 上的无版本 JSON-lines 协议通信(stdout/stderr 留给程序自己的输出)。宿主侧(src/protocol.ts)把每条入站帧都视为敌意并逐字段重建后才读取;Python 侧(py/protocol.py)镜像消息词汇。隔离(不是安全边界——模型代码与 bash 同等的信任)来自空环境、RLIMIT_CPU/RLIMIT_AS、墙钟上限与 SIGTERM→宽限→SIGKILL 进程组拆卸,所有上限都在插件加载期校验。
在需要通过 code-runtime seam 运行 Python 模型代码时选择本包:向 dsh-tools 注册 PythonCodeRuntime,run() 就在全新的 python3 -I 子进程中执行每个程序,成功时以 result.value resolve、失败时以 result.error resolve(正交的 CodeRunFailure.kind 分类涵盖解析失败、抛出异常、无效完成值、输出溢出、预算到期、中止与执行基底终止);只有 seam 误用才 reject——绑定命名空间畸形,或已释放后仍调用。配置在加载期被拒绝:非 Unix 平台、非正或非整数的预算、低于截断标记下限(64)的 maxLogBytes、setTimeout 会收敛的定时器值、超过单个 fd-3 帧可承载的预算,以及最坏峰值会突破 RLIMIT_AS 的 addressSpaceMb/输出预算组合。
包的默认导出是 PythonCodeRuntime 插件。其公开面还重新导出宿主侧协议词汇:validateChildFrame(重建每条入站帧)、无损 JSON codec 与计量器(encodeJsonPlain、checkDoneValue、hasUnsafeIntegerToken、hasNonLosslessNumber)、logTruncationMarker(共享截断标记文本),以及 resolvePythonBin(对照当前 PATH 的解释器查找)、readProcessStart(供测试用的进程启动统计)和 detachResidual(已结算运行的资源清理测试 seam)。每个上限都是带默认值并经校验的 Config 字段:cpuSeconds(60)、maxWallMs(600000)、addressSpaceMb(512,Darwin 上不生效)、maxLogBytes(65536)、maxValueBytes(32768)、graceMs(3000)与 pythonBin(python3,在子进程以空环境启动前对照 PATH 解析;在 PATH 上无命中的裸名会在加载期被拒绝,而不是静默回退到平台默认 PATH)。
帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:boot-ack、call、log、done。宿主 → 子进程:boot(首帧,携带全部上限与命名空间声明)、run(boot-ack 之后,只携带程序体)与每个 call 一个 reply。伪造帧可在 done 上同时携带 value 与 error,因此消费方必须先检查 error,在它存在时忽略 value。log 帧的 open 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧追加到同一条目,因此 print('a', end='', flush=True); print('b') 读回为一条 'ab' 条目而不是假换行。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。
宿主侧校验在不抛异常的情况下丢弃垃圾,因此畸形或伪造帧永远不会让宿主进程崩溃:validateChildFrame 对任何不能干净重建的内容返回 undefined,非数字的 call id 永远不会被回显进 reply,伪造的额外字段永远不会被带走。非无损 JSON 或超过配置字节预算的完成值会被显式拒绝(non-lossless/over-budget),而不是被静默取整或截断。原始长度超过 64 MiB 的 fd-3 帧会让本次运行以 worker-exit 结算(接收路径在 toString/JSON.parse 之前限制原始帧,紧凑宽帧不能解码出远超其线上字节的宿主内存)。
当 runtime 契约不够时阅读这些。它们从 seam 定义走向设计记录与配套后端。
间接地,通过 dsh-tools 中的 Code Mode,它把程序的完成值或失败渲染成保留的 run_code 结果。
无直接失效;指定的消费方拥有任何请求前缀变化。
这些限制定义本包覆盖与不覆盖的内容;它们是当前包约束,不是任务积压。
cpuSeconds 在两侧是否都是 int;类型级漂移由评审加后端的真实子进程套件捕获。setsid() 逃出子进程组后代不被组拆卸回收——kill(-pid) 够不到它;运行仍按 done 帧决定的值结算,若该孤儿持有管道,close 截止兜底会强制结算,但孤儿本身在自行退出前一直存活到 fiber 之外。log 帧被丢弃——运行一旦结算,宿主侧捕获即关闭;迟到的 fd-3 log 帧(来自比 done 帧存活更久的线程)会被丢弃,而不是追加到 logs。maxValueBytes 只计量 done 帧的完成值;宽 binding 回复在宿主侧重建(snapshotJsonValue 遍历)并整帧编码,两侧都只受进程内存约束(与没有子进程侧预算的 binding 实参一样)。run() 是一次性的——logs 只有在 CodeRunResult resolve 后才能获得;没有为运行中程序产生的输出提供流式日志或进度接口。maxLogBytes/maxValueBytes 在加载期被限制到同一解析器上限,因此诚实子进程的帧总能放得下;模型构造的超过 64 MiB 的 binding 实参(一个在 seam 层没有预算的值)会触发同一上限——这是该 OOM 防护的已接受残余。worker-exit 告终,隔离成立,只有失败分类降级。ulimit -t 1 CPU 超限被报告为 worker-exit 而非 timeout——当宿主在一个与软限相等的硬 CPU 限下启动且该限为 1 时,_clamped 无法下调软限,内核在同一 tick SIGKILL 忙循环,SIGXCPU 永远不会送达;隔离成立,只有分类降级。ctx.plugin(...) 与真实子进程测试得到验证;完整的 dsh 应用组合(codeRuntime 经真实 Loader 注册)由该层一个受跟踪的装配测试覆盖,不由本包的测试套件承担。