description: "CPython 子进程代码 runtime:为 Python 模型代码实现 dsh-code-runtime seam,及其使用的 fd-3 wire 协议。"
English | 中文
dsh-experimental-code-runtime-python 提供私有的源码 checkout PythonCodeRuntime,即 dsh-code-runtime seam 的 CPython 子进程实现。它以 language: 'python'、isolation: 'process' 注册为 codeRuntime,每次 run() 启动一个全新的 CPython 3.10+ 子进程,把程序作为 async 函数体执行,通过子进程 fd 3 上的无版本 JSON-lines 协议通信(stdout/stderr 留给程序自己的输出)。宿主侧(src/protocol.ts)把每条入站帧都视为敌意并逐字段重建后才读取;Python 侧(py/protocol.py)镜像消息词汇。隔离(不是安全边界——模型代码与 bash 同等的信任)来自仅含临时目录的环境、RLIMIT_CPU/RLIMIT_AS、墙钟上限与 SIGTERM→宽限→SIGKILL 进程组拆卸,所有上限都在插件加载期校验。
仅在显式源码检出组合中选择这个私有实验包。将 PythonCodeRuntime 与 dsh-tools 一起注册后,run() 会在全新的 CPython 3.10+ 子进程中执行每个程序;成功时以 result.value resolve,失败时以 result.error resolve(正交的 CodeRunFailure.kind 分类涵盖解析失败、抛出异常、无效完成值、输出溢出、预算到期、中止与执行基底终止)。仅有 seam 误用会 reject——binding 命名空间不合法,或在 dispose 后调用。配置在加载期拒绝:非 Unix 平台;不是可执行普通文件的显式 pythonBin,或无法在 PATH 上解析的裸名;非 CPython、低于 3.10 或探测失败的解释器;非正或非整数预算;低于截断标记下限(64)的 maxLogBytes;会被 setTimeout 截断的定时器值;超过有效 fd-3 帧上限的预算(宿主堆无法安全解析接近上限的帧时,该上限会降低);或最坏峰值会突破 RLIMIT_AS 的 addressSpaceMb/输出预算组合。
包的默认导出是 PythonCodeRuntime 插件。其公开面还重新导出宿主侧协议词汇:validateChildFrame(重建每条入站帧)、无损 JSON codec 与计量器(encodeJsonPlain、checkDoneValue、hasUnsafeIntegerToken、hasNonLosslessNumber)、logTruncationMarker(共享截断标记文本),以及 resolvePythonBin(对照当前 PATH 的解释器查找)、readProcessStart(供测试用的进程启动统计)、detachResidual(已结算运行的资源清理测试 seam)与 hostFrameParseCeiling(给定堆上限可容纳的堆推导帧解析上限)。每个上限都是带默认值并经校验的 Config 字段:cpuSeconds(60)、maxWallMs(600000)、addressSpaceMb(512,Darwin 上不生效)、maxLogBytes(65536)、maxValueBytes(32768)、graceMs(3000)与 pythonBin(python3,在加载期解析、检查可执行性,在五秒强制终止期限内探测版本并固定)。每个子进程只接收 TMPDIR;环境中的凭证、PATH、HOME 与其他宿主状态均不可见。
帧在子进程 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,或当宿主的配置堆无法安全解析接近上限的帧时更低——见 hostFrameParseCeiling)的 fd-3 帧会让本次运行以 worker-exit 结算(接收路径在 toString/JSON.parse 之前限制原始帧,紧凑宽帧不能解码出远超其线上字节的宿主内存)。
当 runtime 契约不够时阅读这些。它们从 seam 定义走向设计记录与配套后端。
间接地,通过 dsh-tools 中的 PTC mode;当显式的源码 checkout 组合挂载本提供方时,它会把程序的完成值或失败渲染成保留的 run_code 结果,且已发布 profile 均不挂载这个私有包。
无直接失效;指定的消费方拥有任何请求前缀变化。
这些限制定义本包覆盖与不覆盖的内容;它们是当前包约束,不是任务积压。
cpuSeconds 在两侧是否都是 int;类型级漂移由评审加后端的真实子进程套件捕获。setsid() 逃出子进程组后代不被组拆卸回收——kill(-pid) 够不到它;运行仍按 done 帧决定的值结算,若该孤儿持有管道,close 截止兜底会强制结算,但孤儿本身在自行退出前一直存活到 fiber 之外。log 帧被丢弃——运行一旦结算,宿主侧捕获即关闭;迟到的 fd-3 log 帧(来自比 done 帧存活更久的线程)会被丢弃,而不是追加到 logs。maxValueBytes 只计量 done 帧的完成值;宽 binding 回复在宿主侧重建(snapshotJsonValue 遍历)并整帧编码,两侧都只受进程内存约束(与没有子进程侧预算的 binding 实参一样)。ptc-python-turn 快照通过真实 Loader 替换 headless PTC 运行时;已发布 profile 继续使用 Worker 线程后端。result.logs 中的总顺序可能不同。ctx.codeRuntime 注册前失败。run() 是一次性的——logs 只有在 CodeRunResult resolve 后才能获得;没有为运行中程序产生的输出提供流式日志或进度接口。hostFrameParseCeiling);maxLogBytes/maxValueBytes 在加载期被限制到同一上限,因此诚实子进程的帧总能放得下;模型构造的超过该上限的 binding 实参(一个在 seam 层没有预算的值)会触发同一上限——这是该 OOM 防护的已接受残余。drain;只持续发送调用而不消费回复的子进程会让保留的积压(及其钉住的 binding 结果)一直增长到墙钟,因此积压上限让运行提前失败。binding 结果在 seam 层没有字节上限,所以这是计数上限而非字节上限。worker-exit 告终,隔离成立,只有失败分类降级。ulimit -t 1 CPU 超限被报告为 worker-exit 而非 timeout——当宿主在一个与软限相等的硬 CPU 限下启动且该限为 1 时,_clamped 无法下调软限,内核在同一 tick SIGKILL 忙循环,SIGXCPU 永远不会送达;隔离成立,只有分类降级。[dsh-code-runtime-python] log capture truncated at <N> bytes 与 dsh-code-runtime-python- 临时目录前缀被测试逐字节锚定,且独立于 npm 包名;promotion(去掉 experimental- 前缀)不会重命名它们。