Skip to content

Commit 91b18ef

Browse files
committed
fix(cli): optimize app relaunch process and fix restart functionality
1 parent 7311e24 commit 91b18ef

File tree

2 files changed

+138
-46
lines changed

2 files changed

+138
-46
lines changed

packages/cli/index.ts

Lines changed: 131 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* SPDX-License-Identifier: Apache-2.0
77
*/
88

9-
import { main } from './src/gemini.js';
10-
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
11-
import { runExitCleanup } from './src/utils/cleanup.js';
9+
import { spawn } from 'node:child_process';
10+
import os from 'node:os';
11+
import v8 from 'node:v8';
1212

1313
// --- Global Entry Point ---
1414

@@ -20,52 +20,145 @@ process.on('uncaughtException', (error) => {
2020
error instanceof Error &&
2121
error.message === 'Cannot resize a pty that has already exited'
2222
) {
23-
// This error happens on Windows with node-pty when resizing a pty that has just exited.
24-
// It is a race condition in node-pty that we cannot prevent, so we silence it.
2523
return;
2624
}
2725

28-
// For other errors, we rely on the default behavior, but since we attached a listener,
29-
// we must manually replicate it.
3026
if (error instanceof Error) {
31-
writeToStderr(error.stack + '\n');
27+
process.stderr.write(error.stack + '\n');
3228
} else {
33-
writeToStderr(String(error) + '\n');
29+
process.stderr.write(String(error) + '\n');
3430
}
3531
process.exit(1);
3632
});
3733

38-
main().catch(async (error) => {
39-
// Set a timeout to force exit if cleanup hangs
40-
const cleanupTimeout = setTimeout(() => {
41-
writeToStderr('Cleanup timed out, forcing exit...\n');
42-
process.exit(1);
43-
}, 5000);
44-
45-
try {
46-
await runExitCleanup();
47-
} catch (cleanupError) {
48-
writeToStderr(
49-
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
50-
);
51-
} finally {
52-
clearTimeout(cleanupTimeout);
53-
}
34+
async function run() {
35+
if (!process.env['GEMINI_CLI_NO_RELAUNCH'] && !process.env['SANDBOX']) {
36+
// --- Lightweight Parent Process / Daemon ---
37+
// We avoid importing heavy dependencies here to save ~1.5s of startup time.
38+
39+
// Check memory arguments (lightweight version)
40+
const nodeArgs: string[] = [...process.execArgv];
41+
const scriptArgs = process.argv.slice(2);
42+
const isCommand =
43+
scriptArgs.length > 0 &&
44+
[
45+
'mcp',
46+
'extensions',
47+
'extension',
48+
'skills',
49+
'skill',
50+
'hooks',
51+
'hook',
52+
].includes(scriptArgs[0]);
5453

55-
if (error instanceof FatalError) {
56-
let errorMessage = error.message;
57-
if (!process.env['NO_COLOR']) {
58-
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
54+
if (!isCommand) {
55+
// Very rudimentary check for memory args without parsing full settings.
56+
// If we need more memory, we just provide it.
57+
const totalMemoryMB = os.totalmem() / (1024 * 1024);
58+
const heapStats = v8.getHeapStatistics();
59+
const currentMaxOldSpaceSizeMb = Math.floor(
60+
heapStats.heap_size_limit / 1024 / 1024,
61+
);
62+
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
63+
64+
if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
65+
nodeArgs.push(`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`);
66+
}
5967
}
60-
writeToStderr(errorMessage + '\n');
61-
process.exit(error.exitCode);
62-
}
6368

64-
writeToStderr('An unexpected critical error occurred:');
65-
if (error instanceof Error) {
66-
writeToStderr(error.stack + '\n');
69+
const script = process.argv[1];
70+
nodeArgs.push(script);
71+
nodeArgs.push(...scriptArgs);
72+
73+
const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
74+
const RELAUNCH_EXIT_CODE = 199;
75+
let latestAdminSettings: unknown = undefined;
76+
77+
const runner = () => {
78+
process.stdin.pause();
79+
80+
const child = spawn(process.execPath, nodeArgs, {
81+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
82+
env: newEnv,
83+
});
84+
85+
if (latestAdminSettings) {
86+
child.send({ type: 'admin-settings', settings: latestAdminSettings });
87+
}
88+
89+
child.on('message', (msg: { type?: string; settings?: unknown }) => {
90+
if (msg.type === 'admin-settings-update' && msg.settings) {
91+
latestAdminSettings = msg.settings;
92+
}
93+
});
94+
95+
return new Promise<number>((resolve) => {
96+
child.on('error', () => resolve(1));
97+
child.on('close', (code) => {
98+
process.stdin.resume();
99+
resolve(code ?? 1);
100+
});
101+
});
102+
};
103+
104+
while (true) {
105+
try {
106+
const exitCode = await runner();
107+
if (exitCode !== RELAUNCH_EXIT_CODE) {
108+
process.exit(exitCode);
109+
}
110+
} catch (error: unknown) {
111+
process.stdin.resume();
112+
process.stderr.write(
113+
`Fatal error: Failed to relaunch the CLI process.\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
114+
);
115+
process.exit(1);
116+
}
117+
}
67118
} else {
68-
writeToStderr(String(error) + '\n');
119+
// --- Heavy Child Process ---
120+
// Now we can safely import everything.
121+
const { main } = await import('./src/gemini.js');
122+
const { FatalError, writeToStderr } = await import(
123+
'@google/gemini-cli-core'
124+
);
125+
const { runExitCleanup } = await import('./src/utils/cleanup.js');
126+
127+
main().catch(async (error: unknown) => {
128+
// Set a timeout to force exit if cleanup hangs
129+
const cleanupTimeout = setTimeout(() => {
130+
writeToStderr('Cleanup timed out, forcing exit...\n');
131+
process.exit(1);
132+
}, 5000);
133+
134+
try {
135+
await runExitCleanup();
136+
} catch (cleanupError: unknown) {
137+
writeToStderr(
138+
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
139+
);
140+
} finally {
141+
clearTimeout(cleanupTimeout);
142+
}
143+
144+
if (error instanceof FatalError) {
145+
let errorMessage = error.message;
146+
if (!process.env['NO_COLOR']) {
147+
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
148+
}
149+
writeToStderr(errorMessage + '\n');
150+
process.exit(error.exitCode);
151+
}
152+
153+
writeToStderr('An unexpected critical error occurred:');
154+
if (error instanceof Error) {
155+
writeToStderr(error.stack + '\n');
156+
} else {
157+
writeToStderr(String(error) + '\n');
158+
}
159+
process.exit(1);
160+
});
69161
}
70-
process.exit(1);
71-
});
162+
}
163+
164+
run();

packages/cli/src/gemini.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
8080
import { appEvents, AppEvent } from './utils/events.js';
8181
import { SessionError, SessionSelector } from './utils/sessionUtils.js';
8282

83-
import {
84-
relaunchAppInChildProcess,
85-
relaunchOnExitCode,
86-
} from './utils/relaunch.js';
83+
import { relaunchOnExitCode } from './utils/relaunch.js';
8784
import { loadSandboxConfig } from './config/sandboxConfig.js';
8885
import { deleteSession, listSessions } from './utils/sessions.js';
8986
import { createPolicyUpdater } from './config/policy.js';
@@ -382,6 +379,12 @@ export async function main() {
382379
// Set remote admin settings if returned from CCPA.
383380
if (remoteAdminSettings) {
384381
settings.setRemoteAdminSettings(remoteAdminSettings);
382+
if (process.send) {
383+
process.send({
384+
type: 'admin-settings-update',
385+
settings: remoteAdminSettings,
386+
});
387+
}
385388
}
386389

387390
// Run deferred command now that we have admin settings.
@@ -439,10 +442,6 @@ export async function main() {
439442
);
440443
await runExitCleanup();
441444
process.exit(ExitCodes.SUCCESS);
442-
} else {
443-
// Relaunch app so we always have a child process that can be internally
444-
// restarted if needed.
445-
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
446445
}
447446
}
448447

0 commit comments

Comments
 (0)