fix(code-runtime): bound interpreter version probe

This commit is contained in:
Tianyi Cui
2026-08-31 15:55:05 +08:00
parent 711ec7ffac
commit d6bd5eb973
11 changed files with 42 additions and 20 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/experimental/code-runtime-python/README.md
README.md: 8b596ec5e8bcb7a0d458fe11e61c3742b6efc823
README.zh.md: a0035beec49a531d7ff37363921ecefa869877a6
README.md: 117daeac38c329521e5334b41106b71c629a0efc
README.zh.md: 0727c705a8eb024f068804c0a1af6e81c2e02d6b
@@ -29,7 +29,7 @@ Choose this private experimental package only in an explicit source-checkout com
### What you get
The package's default export is the `PythonCodeRuntime` plugin. Its public surface also re-exports the host-side protocol vocabulary: `validateChildFrame` (rebuilds every inbound frame), the lossless-JSON codec and meters (`encodeJsonPlain`, `checkDoneValue`, `hasUnsafeIntegerToken`, `hasNonLosslessNumber`), `logTruncationMarker` (the shared truncation-marker text), plus `resolvePythonBin` (interpreter lookup against the current `PATH`), `readProcessStart` (process-start statistics for tests), and `detachResidual` (a test seam for the settled run's resource cleanup). Every cap is a validated `Config` field with a default: `cpuSeconds` (60), `maxWallMs` (600000), `addressSpaceMb` (512, not applied on Darwin), `maxLogBytes` (65536), `maxValueBytes` (32768), `graceMs` (3000), and `pythonBin` (`python3`, resolved, executable-checked, version-probed, and frozen at load). Each child receives only `TMPDIR`; ambient credentials, `PATH`, `HOME`, and other host state stay unavailable.
The package's default export is the `PythonCodeRuntime` plugin. Its public surface also re-exports the host-side protocol vocabulary: `validateChildFrame` (rebuilds every inbound frame), the lossless-JSON codec and meters (`encodeJsonPlain`, `checkDoneValue`, `hasUnsafeIntegerToken`, `hasNonLosslessNumber`), `logTruncationMarker` (the shared truncation-marker text), plus `resolvePythonBin` (interpreter lookup against the current `PATH`), `readProcessStart` (process-start statistics for tests), and `detachResidual` (a test seam for the settled run's resource cleanup). Every cap is a validated `Config` field with a default: `cpuSeconds` (60), `maxWallMs` (600000), `addressSpaceMb` (512, not applied on Darwin), `maxLogBytes` (65536), `maxValueBytes` (32768), `graceMs` (3000), and `pythonBin` (`python3`, resolved, executable-checked, version-probed under a five-second force-kill deadline, and frozen at load). Each child receives only `TMPDIR`; ambient credentials, `PATH`, `HOME`, and other host state stay unavailable.
### The wire
@@ -29,7 +29,7 @@ kind: "package-reference"
### 你得到什么
包的默认导出是 `PythonCodeRuntime` 插件。其公开面还重新导出宿主侧协议词汇:`validateChildFrame`(重建每条入站帧)、无损 JSON codec 与计量器(`encodeJsonPlain``checkDoneValue``hasUnsafeIntegerToken``hasNonLosslessNumber`)、`logTruncationMarker`(共享截断标记文本),以及 `resolvePythonBin`(对照当前 `PATH` 的解释器查找)、`readProcessStart`(供测试用的进程启动统计)和 `detachResidual`(已结算运行的资源清理测试 seam)。每个上限都是带默认值并经校验的 `Config` 字段:`cpuSeconds`60)、`maxWallMs`600000)、`addressSpaceMb`512Darwin 上不生效)、`maxLogBytes`65536)、`maxValueBytes`32768)、`graceMs`3000)与 `pythonBin``python3`,在加载期解析、检查可执行性探测版本并固定)。每个子进程只接收 `TMPDIR`;环境中的凭证、`PATH``HOME` 与其他宿主状态均不可见。
包的默认导出是 `PythonCodeRuntime` 插件。其公开面还重新导出宿主侧协议词汇:`validateChildFrame`(重建每条入站帧)、无损 JSON codec 与计量器(`encodeJsonPlain``checkDoneValue``hasUnsafeIntegerToken``hasNonLosslessNumber`)、`logTruncationMarker`(共享截断标记文本),以及 `resolvePythonBin`(对照当前 `PATH` 的解释器查找)、`readProcessStart`(供测试用的进程启动统计)和 `detachResidual`(已结算运行的资源清理测试 seam)。每个上限都是带默认值并经校验的 `Config` 字段:`cpuSeconds`60)、`maxWallMs`600000)、`addressSpaceMb`512Darwin 上不生效)、`maxLogBytes`65536)、`maxValueBytes`32768)、`graceMs`3000)与 `pythonBin``python3`,在加载期解析、检查可执行性,在五秒强制终止期限内探测版本并固定)。每个子进程只接收 `TMPDIR`;环境中的凭证、`PATH``HOME` 与其他宿主状态均不可见。
### wire
@@ -85,7 +85,8 @@ export interface Config {
graceMs?: number
/**
* Absolute path, relative path, or basename of a CPython 3.10+ interpreter.
* Resolved and validated once at plugin load; a basename searches `PATH`.
* Resolved and validated once at plugin load under a five-second force-kill
* deadline; a basename searches `PATH`.
*/
pythonBin?: string
}
@@ -413,6 +414,8 @@ export function resolvePythonBin(bin: string): string | undefined {
accessSync(candidate, fsConstants.X_OK)
return statSync(candidate).isFile() ? candidate : undefined
} catch {
// Missing, inaccessible, and non-stat-able candidates are ordinary
// lookup misses; the constructor reports the final load error.
return undefined
}
}
@@ -457,6 +460,9 @@ function validatePythonBin(bin: string): void {
encoding: 'utf8',
env: pythonEnvironment(),
timeout: PYTHON_PROBE_TIMEOUT_MS,
// The configured executable is outside our control. Force-kill it at the
// deadline so a wrapper that ignores SIGTERM cannot block plugin load.
killSignal: 'SIGKILL',
maxBuffer: 1_024,
}).trim()
} catch (error: unknown) {
@@ -13,11 +13,12 @@ import { Context } from 'cordis'
* which is exactly the branch that regressed. The mock is confined to this file
* so the real-subprocess suite in runtime.spec.ts is untouched.
*/
const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }))
vi.mock('node:child_process', async importOriginal => ({
...(await importOriginal<typeof import('node:child_process')>()),
spawn: spawnMock,
}))
const { execFileSyncMock, spawnMock } = vi.hoisted(() => ({ execFileSyncMock: vi.fn(), spawnMock: vi.fn() }))
vi.mock('node:child_process', async (importOriginal) => {
const original = await importOriginal<typeof import('node:child_process')>()
execFileSyncMock.mockImplementation(original.execFileSync)
return { ...original, execFileSync: execFileSyncMock, spawn: spawnMock }
})
const { PythonCodeRuntime } = await import('../src/index.ts')
@@ -44,6 +45,7 @@ function fakeChildWithThrowingFd3(): EventEmitter {
}
afterEach(() => {
execFileSyncMock.mockClear()
spawnMock.mockReset()
})
@@ -127,6 +129,18 @@ function fakeChildBackpressuredThenDestroyed(): { child: EventEmitter; proto: Pa
}
describe('PythonCodeRuntime — boot-write failure', () => {
it('force-kills a version probe that exceeds its load-time deadline', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(PythonCodeRuntime)
expect(execFileSyncMock).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining(['-I', '-c']),
expect.objectContaining({ timeout: 5_000, killSignal: 'SIGKILL' }),
)
await fiber.dispose()
})
it('resolves a worker-exit when the fd-3 boot write throws (no TDZ ReferenceError)', async () => {
// Before the fix, the boot-write block ran BEFORE `wallTimer`, `onAbort`,
// and `live` were initialized, so its `finish()` (which clears `wallTimer`,