fix proxy on cloudtops/linux and for older versions of docker, more robust start/stop and error reporting (#945)

This commit is contained in:
Olcan 2025-06-11 10:50:31 -07:00 committed by GitHub
parent d96af8bacd
commit 9237e95f11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 80 additions and 50 deletions

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { execSync, spawn, type ChildProcess } from 'node:child_process'; import { exec, execSync, spawn, type ChildProcess } from 'node:child_process';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
@ -16,6 +16,9 @@ import {
USER_SETTINGS_DIR, USER_SETTINGS_DIR,
SETTINGS_DIRECTORY_NAME, SETTINGS_DIRECTORY_NAME,
} from '../config/settings.js'; } from '../config/settings.js';
import { promisify } from 'util';
const execAsync = promisify(exec);
function getContainerPath(hostPath: string): string { function getContainerPath(hostPath: string): string {
if (os.platform() !== 'win32') { if (os.platform() !== 'win32') {
@ -283,7 +286,8 @@ export async function start_sandbox(sandbox: string) {
]; ];
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND; const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
let proxyProcess: ChildProcess | undefined; let proxyProcess: ChildProcess | undefined = undefined;
let sandboxProcess: ChildProcess | undefined = undefined;
const sandboxEnv = { ...process.env }; const sandboxEnv = { ...process.env };
if (proxyCommand) { if (proxyCommand) {
const proxy = const proxy =
@ -306,6 +310,17 @@ export async function start_sandbox(sandbox: string) {
shell: true, shell: true,
detached: true, detached: true,
}); });
// install handlers to stop proxy on exit/signal
const stopProxy = () => {
console.log('stopping proxy ...');
if (proxyProcess?.pid) {
process.kill(-proxyProcess.pid, 'SIGTERM');
}
};
process.on('exit', stopProxy);
process.on('SIGINT', stopProxy);
process.on('SIGTERM', stopProxy);
// commented out as it disrupts ink rendering // commented out as it disrupts ink rendering
// proxyProcess.stdout?.on('data', (data) => { // proxyProcess.stdout?.on('data', (data) => {
// console.info(data.toString()); // console.info(data.toString());
@ -313,31 +328,26 @@ export async function start_sandbox(sandbox: string) {
proxyProcess.stderr?.on('data', (data) => { proxyProcess.stderr?.on('data', (data) => {
console.error(data.toString()); console.error(data.toString());
}); });
console.log('waiting for proxy to start ...'); proxyProcess.on('close', (code, signal) => {
execSync(`until lsof -i :8877 | grep -q "LISTEN"; do sleep 0.1; done`); console.error(
} `ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
try { );
// spawn child and let it inherit stdio if (sandboxProcess?.pid) {
const child = spawn(sandbox, args, { process.kill(-sandboxProcess.pid, 'SIGTERM');
stdio: 'inherit', }
env: sandboxEnv, process.exit(1);
}); });
if (proxyProcess) { console.log('waiting for proxy to start ...');
proxyProcess.on('close', (code, signal) => { await execAsync(
console.error( `until curl -s http://localhost:8877; do sleep 0.25; done`,
`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`, );
);
if (child.pid) {
process.kill(-child.pid, 'SIGTERM');
}
});
}
await new Promise((resolve) => child.on('close', resolve));
} finally {
if (proxyProcess?.pid) {
process.kill(-proxyProcess.pid, 'SIGTERM');
}
} }
// spawn child and let it inherit stdio
sandboxProcess = spawn(sandbox, args, {
stdio: 'inherit',
env: sandboxEnv,
});
await new Promise((resolve) => sandboxProcess?.on('close', resolve));
return; return;
} }
@ -598,10 +608,12 @@ export async function start_sandbox(sandbox: string) {
// Determine if the current user's UID/GID should be passed to the sandbox. // Determine if the current user's UID/GID should be passed to the sandbox.
// See shouldUseCurrentUserInSandbox for more details. // See shouldUseCurrentUserInSandbox for more details.
let userFlag = '';
if (await shouldUseCurrentUserInSandbox()) { if (await shouldUseCurrentUserInSandbox()) {
const uid = execSync('id -u').toString().trim(); const uid = execSync('id -u').toString().trim();
const gid = execSync('id -g').toString().trim(); const gid = execSync('id -g').toString().trim();
args.push('--user', `${uid}:${gid}`); args.push('--user', `${uid}:${gid}`);
userFlag = `--user ${uid}:${gid}`;
// when forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well // when forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well
args.push('--env', `HOME=${os.homedir()}`); args.push('--env', `HOME=${os.homedir()}`);
} }
@ -613,15 +625,25 @@ export async function start_sandbox(sandbox: string) {
args.push(...entrypoint(workdir)); args.push(...entrypoint(workdir));
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
let proxyProcess: ChildProcess | undefined; let proxyProcess: ChildProcess | undefined = undefined;
let sandboxProcess: ChildProcess | undefined = undefined;
if (proxyCommand) { if (proxyCommand) {
// run proxyCommand in its own container // run proxyCommand in its own container
const proxyContainerCommand = `${sandbox} run --rm --init --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} --network ${SANDBOX_NETWORK_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`; const proxyContainerCommand = `${sandbox} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
proxyProcess = spawn(proxyContainerCommand, { proxyProcess = spawn(proxyContainerCommand, {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
shell: true, shell: true,
detached: true, detached: true,
}); });
// install handlers to stop proxy on exit/signal
const stopProxy = () => {
console.log('stopping proxy container ...');
execSync(`${sandbox} rm -f ${SANDBOX_PROXY_NAME}`);
};
process.on('exit', stopProxy);
process.on('SIGINT', stopProxy);
process.on('SIGTERM', stopProxy);
// commented out as it disrupts ink rendering // commented out as it disrupts ink rendering
// proxyProcess.stdout?.on('data', (data) => { // proxyProcess.stdout?.on('data', (data) => {
// console.info(data.toString()); // console.info(data.toString());
@ -629,35 +651,43 @@ export async function start_sandbox(sandbox: string) {
proxyProcess.stderr?.on('data', (data) => { proxyProcess.stderr?.on('data', (data) => {
console.error(data.toString().trim()); console.error(data.toString().trim());
}); });
proxyProcess.on('close', (code, signal) => {
console.error(
`ERROR: proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
);
if (sandboxProcess?.pid) {
process.kill(-sandboxProcess.pid, 'SIGTERM');
}
process.exit(1);
});
console.log('waiting for proxy to start ...'); console.log('waiting for proxy to start ...');
execSync(`until lsof -i :8877 | grep -q "LISTEN"; do sleep 0.1; done`); await execAsync(`until curl -s http://localhost:8877; do sleep 0.25; done`);
// connect proxy container to sandbox network
// (workaround for older versions of docker that don't support multiple --network args)
await execAsync(
`${sandbox} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`,
);
} }
try { // spawn child and let it inherit stdio
// spawn child and let it inherit stdio sandboxProcess = spawn(sandbox, args, {
const child = spawn(sandbox, args, { stdio: 'inherit',
stdio: 'inherit', });
});
child.on('error', (err) => { sandboxProcess.on('error', (err) => {
console.error('Sandbox process error:', err); console.error('Sandbox process error:', err);
}); });
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
child.on('close', (code, signal) => { sandboxProcess?.on('close', (code, signal) => {
if (code !== 0) { if (code !== 0) {
console.log( console.log(
`Sandbox process exited with code: ${code}, signal: ${signal}`, `Sandbox process exited with code: ${code}, signal: ${signal}`,
); );
} }
resolve(); resolve();
});
}); });
} finally { });
if (proxyProcess?.pid) {
process.kill(-proxyProcess.pid, 'SIGTERM');
}
}
} }
// Helper functions to ensure sandbox image is present // Helper functions to ensure sandbox image is present