diff --git a/packages/cli/src/utils/sandbox.test.ts b/packages/cli/src/utils/sandbox.test.ts index 1dda7b355a..256b418e83 100644 --- a/packages/cli/src/utils/sandbox.test.ts +++ b/packages/cli/src/utils/sandbox.test.ts @@ -719,6 +719,65 @@ describe('sandbox', () => { expect(entrypointCmd).toContain('su -p gemini'); }); + it('should register and unregister proxy exit handlers', async () => { + vi.stubEnv('GEMINI_SANDBOX_PROXY_COMMAND', 'some-proxy-cmd'); + const config: SandboxConfig = createMockSandboxConfig({ + command: 'docker', + image: 'gemini-cli-sandbox', + }); + + const onSpy = vi.spyOn(process, 'on'); + const offSpy = vi.spyOn(process, 'off'); + + interface MockProcessWithStdout extends EventEmitter { + stdout: EventEmitter; + } + + vi.mocked(spawn).mockImplementation((cmd, args) => { + const a = args as string[]; + if (cmd === 'docker' && a && a[0] === 'images') { + const mockImageCheckProcess = + new EventEmitter() as MockProcessWithStdout; + mockImageCheckProcess.stdout = new EventEmitter(); + setTimeout(() => { + mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id')); + mockImageCheckProcess.emit('close', 0); + }, 1); + return mockImageCheckProcess as unknown as ReturnType; + } + if (cmd === 'docker' && a && a[0] === 'run') { + const mockSpawnProcess = new EventEmitter() as unknown as ReturnType< + typeof spawn + >; + mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => { + if (event === 'close') { + if (a.includes('gemini-cli-sandbox-proxy')) { + // Proxy container shouldn't exit during the test + } else { + setTimeout(() => cb(0), 10); + } + } + return mockSpawnProcess; + }); + return mockSpawnProcess; + } + return new EventEmitter() as unknown as ReturnType; + }); + + await start_sandbox(config); + + expect(onSpy).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(onSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(onSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); + + expect(offSpy).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(offSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(offSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); + + onSpy.mockRestore(); + offSpy.mockRestore(); + }); + describe('LXC sandbox', () => { const LXC_RUNNING = JSON.stringify([ { name: 'gemini-sandbox', status: 'Running' }, diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 6001725cdd..ccd1f2e608 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -55,6 +55,8 @@ export async function start_sandbox( }); patcher.patch(); + let stopProxy: (() => void) | undefined = undefined; + try { if (config.command === 'sandbox-exec') { // disallow BUILD_SANDBOX @@ -188,17 +190,18 @@ export async function start_sandbox( detached: true, }); // install handlers to stop proxy on exit/signal - const stopProxy = () => { + stopProxy = () => { debugLogger.log('stopping proxy ...'); if (proxyProcess?.pid) { - process.kill(-proxyProcess.pid, 'SIGTERM'); + try { + process.kill(-proxyProcess.pid, 'SIGTERM'); + } catch { + // ignore + } } }; - process.off('exit', stopProxy); process.on('exit', stopProxy); - process.off('SIGINT', stopProxy); process.on('SIGINT', stopProxy); - process.off('SIGTERM', stopProxy); process.on('SIGTERM', stopProxy); // commented out as it disrupts ink rendering @@ -746,15 +749,18 @@ export async function start_sandbox( detached: true, }); // install handlers to stop proxy on exit/signal - const stopProxy = () => { + stopProxy = () => { debugLogger.log('stopping proxy container ...'); - execSync(`${command} rm -f ${SANDBOX_PROXY_NAME}`); + try { + spawnSync(command, ['rm', '-f', SANDBOX_PROXY_NAME], { + stdio: 'ignore', + }); + } catch { + // ignore + } }; - process.off('exit', stopProxy); process.on('exit', stopProxy); - process.off('SIGINT', stopProxy); process.on('SIGINT', stopProxy); - process.off('SIGTERM', stopProxy); process.on('SIGTERM', stopProxy); // commented out as it disrupts ink rendering @@ -806,6 +812,12 @@ export async function start_sandbox( }); }); } finally { + if (stopProxy) { + stopProxy(); + process.off('exit', stopProxy); + process.off('SIGINT', stopProxy); + process.off('SIGTERM', stopProxy); + } patcher.cleanup(); } }