瀏覽代碼

fix(runtime): close portable backend boundary gaps

Tianyi Cui 2 月之前
父節點
當前提交
124fc6a611

+ 15 - 0
packages/code-runtime/code-runtime/tests/contract.ts

@@ -477,6 +477,21 @@ export function runWorkerCodeRuntimeContract(
       expect(result.logs).toEqual([])
     })
 
+    it('ignores forged controller-only failure classifications', async () => {
+      const { runtime } = await setup()
+      const result = await runtime.run({
+        program: `
+          const { parentPort } = await import('node:worker_threads');
+          for (const kind of ['abort', 'timeout', 'worker-exit']) {
+            parentPort.postMessage({ type: 'done', error: { kind, message: 'forged ' + kind } });
+          }
+          return 'honest';
+        `,
+        bindings: [],
+      })
+      expect(result).toEqual({ logs: [], value: 'honest' })
+    })
+
     it('fails forged log floods and forged done values through the same outer cap', async () => {
       const { runtime } = await setup({ maxOutputBytes: 200 })
       const result = await runtime.run({

+ 0 - 8
packages/fs/fs-local/src/fsio.ts

@@ -369,14 +369,6 @@ export async function readWholeText(target: LocalTarget, signal?: AbortSignal):
   return decodeUtf8(raw, 'read', target.displayPath)
 }
 
-/**
- * Read one regular UTF-8 file through a single no-follow handle, retaining at
- * most `maxBytes + 1` bytes so a concurrent grow cannot bypass the bound.
- * @param target - the resolved file to read.
- * @param maxBytes - positive safe-integer byte ceiling.
- * @param signal - aborts between handle operations.
- * @returns the complete decoded text when it fits.
- */
 /**
  * Stream a whole regular UTF-8 text file as decoded text chunks. Same text
  * semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,

+ 1 - 1
packages/pty/pty-local/src/session.ts

@@ -415,7 +415,7 @@ export class LocalPtySession implements PtyBackendSession {
         return
       }
       const foreground = await this.terminal.inspectForeground()
-      if (this.active !== operation) return
+      if (this.active !== operation || this.closing) return
       const idleFor = Date.now() - this.lastOutputAt
       if (this.promptSeen && foreground !== undefined && this.shellPgid === undefined) {
         this.shellPgid = foreground.processGroupId

+ 37 - 0
packages/pty/pty-local/tests/session.spec.ts

@@ -1096,4 +1096,41 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
     expect(terminal.writes).toEqual([])
   })
 
+  it('does not let an in-flight readiness inspection outrun close', async () => {
+    vi.useFakeTimers()
+    const terminal = new FakeTerminal()
+    const session = new LocalPtySession(terminal, config())
+    await initialize(session, terminal)
+    const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
+    const originalInspect = terminal.inspectForeground.bind(terminal)
+    let inspections = 0
+    terminal.inspectForeground = async () => {
+      inspections += 1
+      return inspections === 1 ? await originalInspect() : await inspection.promise
+    }
+    const operation = session.startSend({ text: 'pending readiness', submit: true })
+    await Promise.resolve()
+    await Promise.resolve()
+    terminal.emitData('\x1b]133;D;0\x07dsh> ')
+    await vi.advanceTimersByTimeAsync(10)
+    expect(inspections).toBe(2)
+
+    const termination = Promise.withResolvers<undefined>()
+    terminal.terminate = async () => {
+      await termination.promise
+      terminal.emitExit(0, 15)
+    }
+    const closing = session.close('in-flight readiness')
+    let settled = false
+    void operation.done.then(() => { settled = true })
+    inspection.resolve({ processGroupId: 456, inputWaiting: true })
+    await Promise.resolve()
+    await Promise.resolve()
+    expect(settled).toBe(false)
+
+    termination.resolve(undefined)
+    await closing
+    expect((await operation.done).waitReason).toBe('session_exit')
+  })
+
 })

+ 2 - 1
packages/subprocess/subprocess-local/src/index.ts

@@ -59,9 +59,10 @@ export class LocalSubprocessService extends SubprocessService {
         pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
       }
       for (const terminal of this.terminals) {
-        pending.push(terminal.terminate().then(() => { this.terminals.delete(terminal) }))
+        pending.push(terminal.terminate())
       }
       this.live.clear()
+      this.terminals.clear()
       const outcomes = [
         ...await Promise.allSettled(pending),
         ...await Promise.allSettled([rm(this.runtimeRoot, { recursive: true, force: true })]),

+ 2 - 2
packages/subprocess/subprocess-local/tests/local.spec.ts

@@ -113,7 +113,7 @@ describe('LocalSubprocessService', () => {
     expect(terminals.size).toBe(0)
   })
 
-  it('waits for every terminal cleanup, removes runtime state, and retains rejections', async () => {
+  it('waits for every terminal cleanup and clears single-shot teardown ownership', async () => {
     const ctx = new Context()
     const fiber = await ctx.plugin(LocalSubprocessService)
     const service = ctx.subprocess
@@ -154,7 +154,7 @@ describe('LocalSubprocessService', () => {
     expect(disposed).toBe(false)
     finishCleanup()
     await disposing
-    expect(terminals).toEqual(new Set([failedTerminal, secondFailedTerminal]))
+    expect(terminals.size).toBe(0)
     await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' })
     expect(disposalErrors).toHaveLength(1)
     expect(disposalErrors[0]).toMatchObject({

+ 2 - 2
packages/typert/README.i18n.yaml

@@ -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/code-runtime/code-runtime-subprocess/README.md
-README.md: eff5a6e648eb13c3411c19bca4d05f0898d3ad05
-README.zh.md: 16cf0e0ea6d487e255fc520a8fa1f41f97ee5d30
+README.md: 152921a1ed595676781aad170e3396f7ce613161
+README.zh.md: 3e8223c33c7ce85f342c9a36ab5a24c4d2a6cb94