Sfoglia il codice sorgente

fix(e2b): preserve signals after output drain

Tianyi Cui 2 mesi fa
parent
commit
e9bef2967d

+ 3 - 1
packages/e2b/subprocess-e2b/src/process.ts

@@ -512,7 +512,9 @@ export class E2BSubprocessHandle implements SubprocessHandle {
         this.stdoutReader?.invalidateSpill()
         this.stderrReader?.invalidateSpill()
         await handle.disconnect()
-        return { exitCode, signal: null }
+        return this.terminationSignal === null
+          ? { exitCode, signal: null }
+          : { exitCode: null, signal: this.terminationSignal }
       }
       const completed = await Promise.race([settlement, waitTick().then(() => undefined)])
       if (completed !== undefined) return this.commandOutcome(completed)

+ 21 - 1
packages/e2b/subprocess-e2b/tests/subprocess.spec.ts

@@ -103,6 +103,7 @@ class FakeSandbox {
   readonly signalErrors: unknown[] = []
   trapsTerm = false
   delaysKill = false
+  delaysKillCompletion = false
   sdkKillStops = true
   alive = true
   ambient = 'PATH=/ambient/bin\0KEEP=safe\0NPM_TOKEN=secret\0DSH_STALE=old\0BROKEN\0=bad\0'
@@ -247,7 +248,7 @@ class FakeSandbox {
             throw error
           }
           if (!this.delaysKill) this.alive = false
-          this.handle.fail(137)
+          if (!this.delaysKillCompletion) this.handle.fail(137)
           return { exitCode: 0, stdout: '', stderr: '' }
         }
         if ((options as StartOptions | undefined)?.background === true) {
@@ -473,6 +474,25 @@ describe('E2BSubprocessHandle', () => {
     expect(fake.handle.disconnects).toBe(0)
   })
 
+  it('preserves a requested signal when output draining expires', async () => {
+    const fake = new FakeSandbox()
+    fake.trapsTerm = true
+    fake.delaysKill = true
+    fake.delaysKillCompletion = true
+    fake.sdkKillStops = false
+    const handle = new E2BSubprocessHandle(runtime(fake), spec({ graceMs: 5 }), '/runtime/drain-signal')
+    await flush()
+
+    handle.terminate()
+    await vi.waitFor(() => { expect(fake.commandsSeen).toContain('kill -KILL -- -4242') })
+    fake.exitStatus = '143\n'
+
+    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
+    expect(fake.handle.disconnects).toBe(1)
+    fake.alive = false
+    await expect(handle.waitForExit()).resolves.toBe(true)
+  })
+
   it('rejects an invalid direct-command exit status', async () => {
     const fake = new FakeSandbox()
     const handle = new E2BSubprocessHandle(runtime(fake), spec(), '/runtime/invalid-status')