chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import v8 from 'node:v8';
|
||||
import {
|
||||
RELAUNCH_EXIT_CODE,
|
||||
getSpawnConfig,
|
||||
getScriptArgs,
|
||||
} from './src/utils/processUtils.js';
|
||||
|
||||
// --- Global Entry Point ---
|
||||
|
||||
// Suppress known race condition error in node-pty on Windows and Linux
|
||||
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
|
||||
process.on('uncaughtException', (error) => {
|
||||
if (error instanceof Error) {
|
||||
const message = error.message || '';
|
||||
const isPtyResizeError =
|
||||
message === 'Cannot resize a pty that has already exited';
|
||||
const isEbadfError =
|
||||
message.includes('EBADF') ||
|
||||
(error as { code?: string }).code === 'EBADF';
|
||||
const isFromNodePty =
|
||||
error.stack?.includes('node-pty') || error.stack?.includes('PtyResize');
|
||||
|
||||
if ((isPtyResizeError || isEbadfError) && isFromNodePty) {
|
||||
// This error happens with node-pty when resizing a pty that has just exited.
|
||||
// It is a race condition in node-pty that we cannot prevent, so we silence it.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For other errors, we rely on the default behavior, but since we attached a listener,
|
||||
// we must manually replicate it.
|
||||
if (error instanceof Error) {
|
||||
process.stderr.write(error.stack + '\n');
|
||||
} else {
|
||||
process.stderr.write(String(error) + '\n');
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function getMemoryNodeArgs(): Promise<string[]> {
|
||||
let autoConfigureMemory = true;
|
||||
try {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const { join } = await import('node:path');
|
||||
// Respect GEMINI_CLI_HOME environment variable, falling back to os.homedir()
|
||||
const baseDir =
|
||||
process.env['GEMINI_CLI_HOME'] || join(os.homedir(), '.gemini');
|
||||
const settingsPath = join(baseDir, 'settings.json');
|
||||
const rawSettings = readFileSync(settingsPath, 'utf8');
|
||||
const settings = JSON.parse(rawSettings);
|
||||
if (settings?.advanced?.autoConfigureMemory === false) {
|
||||
autoConfigureMemory = false;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (autoConfigureMemory) {
|
||||
const totalMemoryMB = os.totalmem() / (1024 * 1024);
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const currentMaxOldSpaceSizeMb = Math.floor(
|
||||
heapStats.heap_size_limit / 1024 / 1024,
|
||||
);
|
||||
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
|
||||
|
||||
if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
|
||||
return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (!process.env['GEMINI_CLI_NO_RELAUNCH'] && !process.env['SANDBOX']) {
|
||||
// --- Lightweight Parent Process / Daemon ---
|
||||
// We avoid importing heavy dependencies here to save ~1.5s of startup time.
|
||||
|
||||
const scriptArgs = getScriptArgs();
|
||||
const memoryArgs = await getMemoryNodeArgs();
|
||||
const { spawnArgs, env: newEnv } = getSpawnConfig(memoryArgs, scriptArgs);
|
||||
|
||||
let latestAdminSettings: unknown = undefined;
|
||||
|
||||
// Prevent the parent process from exiting prematurely on signals.
|
||||
// The child process will receive the same signals and handle its own cleanup.
|
||||
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
||||
process.on(sig as NodeJS.Signals, () => {});
|
||||
}
|
||||
|
||||
const runner = () => {
|
||||
process.stdin.pause();
|
||||
|
||||
const child = spawn(process.execPath, spawnArgs, {
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
env: newEnv,
|
||||
});
|
||||
|
||||
if (latestAdminSettings) {
|
||||
child.send({ type: 'admin-settings', settings: latestAdminSettings });
|
||||
}
|
||||
|
||||
child.on('message', (msg: { type?: string; settings?: unknown }) => {
|
||||
if (msg.type === 'admin-settings-update' && msg.settings) {
|
||||
latestAdminSettings = msg.settings;
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<number>((resolve) => {
|
||||
child.on('error', (err) => {
|
||||
process.stderr.write(
|
||||
'Error: Failed to start child process: ' + err.message + '\n',
|
||||
);
|
||||
resolve(1);
|
||||
});
|
||||
child.on('close', (code) => {
|
||||
process.stdin.resume();
|
||||
resolve(code ?? 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const exitCode = await runner();
|
||||
if (process.platform === 'android' || exitCode !== RELAUNCH_EXIT_CODE) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
process.stdin.resume();
|
||||
process.stderr.write(
|
||||
`Fatal error: Failed to relaunch the CLI process.\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// --- Heavy Child Process ---
|
||||
// Now we can safely import everything.
|
||||
const { main } = await import('./src/gemini.js');
|
||||
const { FatalError, writeToStderr } = await import(
|
||||
'@google/gemini-cli-core'
|
||||
);
|
||||
const { runExitCleanup } = await import('./src/utils/cleanup.js');
|
||||
|
||||
main().catch(async (error: unknown) => {
|
||||
// Set a timeout to force exit if cleanup hangs
|
||||
const cleanupTimeout = setTimeout(() => {
|
||||
writeToStderr('Cleanup timed out, forcing exit...\n');
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
await runExitCleanup();
|
||||
} catch (cleanupError: unknown) {
|
||||
writeToStderr(
|
||||
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(cleanupTimeout);
|
||||
}
|
||||
|
||||
if (error instanceof FatalError) {
|
||||
let errorMessage = error.message;
|
||||
if (!process.env['NO_COLOR']) {
|
||||
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
||||
}
|
||||
writeToStderr(errorMessage + '\n');
|
||||
process.exit(error.exitCode);
|
||||
}
|
||||
|
||||
writeToStderr('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
writeToStderr(error.stack + '\n');
|
||||
} else {
|
||||
writeToStderr(String(error) + '\n');
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user