Refactor to defer initialization. (#8925)

This commit is contained in:
Jacob Richman
2025-09-22 19:48:25 -07:00
committed by GitHub
parent 40db029887
commit 7e1705274c
5 changed files with 572 additions and 106 deletions

View File

@@ -396,6 +396,15 @@ export async function loadHierarchicalGeminiMemory(
);
}
export function isDebugMode(argv: CliArgs): boolean {
return (
argv.debug ||
[process.env['DEBUG'], process.env['DEBUG_MODE']].some(
(v) => v === 'true' || v === '1',
)
);
}
export async function loadCliConfig(
settings: Settings,
extensions: Extension[],
@@ -403,12 +412,8 @@ export async function loadCliConfig(
argv: CliArgs,
cwd: string = process.cwd(),
): Promise<Config> {
const debugMode =
argv.debug ||
[process.env['DEBUG'], process.env['DEBUG_MODE']].some(
(v) => v === 'true' || v === '1',
) ||
false;
const debugMode = isDebugMode(argv);
const memoryImportFormat = settings.context?.importFormat || 'tree';
const ideMode = settings.ide?.enabled ?? false;

View File

@@ -44,8 +44,10 @@ vi.mock('./config/config.js', () => ({
loadCliConfig: vi.fn().mockResolvedValue({
getSandbox: vi.fn(() => false),
getQuestion: vi.fn(() => ''),
isInteractive: () => false,
} as unknown as Config),
parseArguments: vi.fn().mockResolvedValue({}),
isDebugMode: vi.fn(() => false),
}));
vi.mock('read-package-up', () => ({
@@ -76,18 +78,20 @@ vi.mock('./utils/sandbox.js', () => ({
start_sandbox: vi.fn(() => Promise.resolve()), // Mock as an async function that resolves
}));
vi.mock('./utils/relaunch.js', () => ({
relaunchAppInChildProcess: vi.fn(),
}));
vi.mock('./config/sandboxConfig.js', () => ({
loadSandboxConfig: vi.fn(),
}));
describe('gemini.tsx main function', () => {
let originalEnvGeminiSandbox: string | undefined;
let originalEnvSandbox: string | undefined;
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
[];
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
beforeEach(() => {
// Store and clear sandbox-related env variables to ensure a consistent test environment
originalEnvGeminiSandbox = process.env['GEMINI_SANDBOX'];
@@ -123,7 +127,73 @@ describe('gemini.tsx main function', () => {
vi.restoreAllMocks();
});
it('verifies that we dont load the config before relaunchAppInChildProcess', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
const { relaunchAppInChildProcess } = await import('./utils/relaunch.js');
const { loadCliConfig } = await import('./config/config.js');
const { loadSettings } = await import('./config/settings.js');
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
vi.mocked(loadSandboxConfig).mockResolvedValue(undefined);
const callOrder: string[] = [];
vi.mocked(relaunchAppInChildProcess).mockImplementation(async () => {
callOrder.push('relaunch');
});
vi.mocked(loadCliConfig).mockImplementation(async () => {
callOrder.push('loadCliConfig');
return {
isInteractive: () => false,
getQuestion: () => '',
getSandbox: () => false,
getDebugMode: () => false,
getListExtensions: () => false,
getMcpServers: () => ({}),
initialize: vi.fn(),
getIdeMode: () => false,
getExperimentalZedIntegration: () => false,
getScreenReader: () => false,
getGeminiMdFileCount: () => 0,
getProjectRoot: () => '/',
} as unknown as Config;
});
vi.mocked(loadSettings).mockReturnValue({
errors: [],
merged: {
advanced: { autoConfigureMemory: true },
security: { auth: {} },
ui: {},
},
setValue: vi.fn(),
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
} as never);
try {
await main();
} catch (e) {
// Mocked process exit throws an error.
if (!(e instanceof MockProcessExitError)) throw e;
}
// It is critical that we call relaunch before loadCliConfig to avoid
// loading config in the outer process when we are going to relaunch.
// By ensuring we don't load the config we also ensure we don't trigger any
// operations that might require loading the config such as such as
// initializing mcp servers.
// For the sandbox case we still have to load a partial cli config.
// we can authorize outside the sandbox.
expect(callOrder).toEqual(['relaunch', 'loadCliConfig']);
processExitSpy.mockRestore();
});
it('should log unhandled promise rejections and open debug console on first error', async () => {
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
const appEventsMock = vi.mocked(appEvents);
const rejectionError = new Error('Test unhandled rejection');

View File

@@ -8,15 +8,14 @@ import React from 'react';
import { render } from 'ink';
import { AppContainer } from './ui/AppContainer.js';
import { loadCliConfig, parseArguments } from './config/config.js';
import * as cliConfig from './config/config.js';
import { readStdin } from './utils/readStdin.js';
import { basename } from 'node:path';
import v8 from 'node:v8';
import os from 'node:os';
import dns from 'node:dns';
import { spawn } from 'node:child_process';
import { start_sandbox } from './utils/sandbox.js';
import type { DnsResolutionOrder, LoadedSettings } from './config/settings.js';
import { RELAUNCH_EXIT_CODE } from './utils/processUtils.js';
import {
loadSettings,
migrateDeprecatedSettings,
@@ -58,6 +57,10 @@ import { SessionStatsProvider } from './ui/contexts/SessionContext.js';
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
import { KeypressProvider } from './ui/contexts/KeypressContext.js';
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
import {
relaunchAppInChildProcess,
relaunchOnExitCode,
} from './utils/relaunch.js';
export function validateDnsResolutionOrder(
order: string | undefined,
@@ -76,7 +79,7 @@ export function validateDnsResolutionOrder(
return defaultValue;
}
function getNodeMemoryArgs(config: Config): string[] {
function getNodeMemoryArgs(isDebugMode: boolean): string[] {
const totalMemoryMB = os.totalmem() / (1024 * 1024);
const heapStats = v8.getHeapStatistics();
const currentMaxOldSpaceSizeMb = Math.floor(
@@ -85,7 +88,7 @@ function getNodeMemoryArgs(config: Config): string[] {
// Set target to 50% of total memory
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
if (config.getDebugMode()) {
if (isDebugMode) {
console.debug(
`Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`,
);
@@ -96,7 +99,7 @@ function getNodeMemoryArgs(config: Config): string[] {
}
if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
if (config.getDebugMode()) {
if (isDebugMode) {
console.debug(
`Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`,
);
@@ -107,53 +110,8 @@ function getNodeMemoryArgs(config: Config): string[] {
return [];
}
async function relaunchOnExitCode(runner: () => Promise<number>) {
while (true) {
try {
const exitCode = await runner();
if (exitCode !== RELAUNCH_EXIT_CODE) {
process.exit(exitCode);
}
} catch (error) {
process.stdin.resume();
console.error('Fatal error: Failed to relaunch the CLI process.', error);
process.exit(1);
}
}
}
async function relaunchAppInChildProcess(additionalArgs: string[]) {
if (process.env['GEMINI_CLI_NO_RELAUNCH']) {
return;
}
const runner = () => {
const nodeArgs = [...additionalArgs, ...process.argv.slice(1)];
const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
// The parent process should not be reading from stdin while the child is running.
process.stdin.pause();
const child = spawn(process.execPath, nodeArgs, {
stdio: 'inherit',
env: newEnv,
});
return new Promise<number>((resolve, reject) => {
child.on('error', reject);
child.on('close', (code) => {
// Resume stdin before the parent process exits.
process.stdin.resume();
resolve(code ?? 1);
});
});
};
await relaunchOnExitCode(runner);
}
import { runZedIntegration } from './zed-integration/zedIntegration.js';
import { loadSandboxConfig } from './config/sandboxConfig.js';
export function setupUnhandledRejectionHandler() {
let unhandledRejectionOccurred = false;
@@ -248,13 +206,6 @@ export async function main() {
await cleanupCheckpoints();
const argv = await parseArguments(settings.merged);
const extensions = loadExtensions();
const config = await loadCliConfig(
settings.merged,
extensions,
sessionId,
argv,
);
// Check for invalid input combinations early to prevent crashes
if (argv.promptInteractive && !process.stdin.isTTY) {
@@ -264,28 +215,10 @@ export async function main() {
process.exit(1);
}
const wasRaw = process.stdin.isRaw;
let kittyProtocolDetectionComplete: Promise<boolean> | undefined;
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
// Set this as early as possible to avoid spurious characters from
// input showing up in the output.
process.stdin.setRawMode(true);
// This cleanup isn't strictly needed but may help in certain situations.
process.on('SIGTERM', () => {
process.stdin.setRawMode(wasRaw);
});
process.on('SIGINT', () => {
process.stdin.setRawMode(wasRaw);
});
// Detect and enable Kitty keyboard protocol once at startup.
kittyProtocolDetectionComplete = detectAndEnableKittyProtocol();
}
const isDebugMode = cliConfig.isDebugMode(argv);
const consolePatcher = new ConsolePatcher({
stderr: true,
debugMode: config.getDebugMode(),
debugMode: isDebugMode,
});
consolePatcher.patch();
registerCleanup(consolePatcher.cleanup);
@@ -294,14 +227,6 @@ export async function main() {
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
);
if (config.getListExtensions()) {
console.log('Installed extensions:');
for (const extension of extensions) {
console.log(`- ${extension.config.name}`);
}
process.exit(0);
}
// Set a default auth type if one isn't set.
if (!settings.merged.security?.auth?.selectedType) {
if (process.env['CLOUD_SHELL'] === 'true') {
@@ -313,8 +238,6 @@ export async function main() {
}
}
setMaxSizedBoxDebugging(config.getDebugMode());
// Load custom themes from settings
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
@@ -326,14 +249,13 @@ export async function main() {
}
}
const initializationResult = await initializeApp(config, settings);
// hop into sandbox if we are outside and sandboxing is enabled
if (!process.env['SANDBOX']) {
const memoryArgs = settings.merged.advanced?.autoConfigureMemory
? getNodeMemoryArgs(config)
? getNodeMemoryArgs(isDebugMode)
: [];
const sandboxConfig = config.getSandbox();
const sandboxConfig = await loadSandboxConfig(settings.merged, argv);
if (sandboxConfig) {
if (
settings.merged.security?.auth?.selectedType &&
@@ -347,7 +269,21 @@ export async function main() {
if (err) {
throw new Error(err);
}
await config.refreshAuth(settings.merged.security.auth.selectedType);
// We intentially omit the list of extensions here because extensions
// should not impact auth.
// TODO(jacobr): refactor loadCliConfig so there is a minimal version
// that only initializes enough config to enable refreshAuth or find
// another way to decouple refreshAuth from requiring a config.
const partialConfig = await loadCliConfig(
settings.merged,
[],
sessionId,
argv,
);
await partialConfig.refreshAuth(
settings.merged.security.auth.selectedType,
);
} catch (err) {
console.error('Error authenticating:', err);
process.exit(1);
@@ -390,10 +326,52 @@ export async function main() {
} else {
// Relaunch app so we always have a child process that can be internally
// restarted if needed.
await relaunchAppInChildProcess(memoryArgs);
await relaunchAppInChildProcess(memoryArgs, []);
}
}
// We are now past the logic handling potentially launching a child process
// to run Gemini CLI. It is now safe to perform expensive initialization that
// may have side effects.
const extensions = loadExtensions();
const config = await loadCliConfig(
settings.merged,
extensions,
sessionId,
argv,
);
if (config.getListExtensions()) {
console.log('Installed extensions:');
for (const extension of extensions) {
console.log(`- ${extension.config.name}`);
}
process.exit(0);
}
const wasRaw = process.stdin.isRaw;
let kittyProtocolDetectionComplete: Promise<boolean> | undefined;
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
// Set this as early as possible to avoid spurious characters from
// input showing up in the output.
process.stdin.setRawMode(true);
// This cleanup isn't strictly needed but may help in certain situations.
process.on('SIGTERM', () => {
process.stdin.setRawMode(wasRaw);
});
process.on('SIGINT', () => {
process.stdin.setRawMode(wasRaw);
});
// Detect and enable Kitty keyboard protocol once at startup.
kittyProtocolDetectionComplete = detectAndEnableKittyProtocol();
}
setMaxSizedBoxDebugging(isDebugMode);
const initializationResult = await initializeApp(config, settings);
if (
settings.merged.security?.auth?.selectedType ===
AuthType.LOGIN_WITH_GOOGLE &&

View File

@@ -0,0 +1,345 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import { EventEmitter } from 'node:events';
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:child_process')>();
return {
...actual,
spawn: vi.fn(),
};
});
const mockedSpawn = vi.mocked(spawn);
// Import the functions initially
import { relaunchAppInChildProcess, relaunchOnExitCode } from './relaunch.js';
describe('relaunchOnExitCode', () => {
let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let stdinResumeSpy: MockInstance;
beforeEach(() => {
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('PROCESS_EXIT_CALLED');
});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
stdinResumeSpy = vi
.spyOn(process.stdin, 'resume')
.mockImplementation(() => process.stdin);
vi.clearAllMocks();
});
afterEach(() => {
processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
stdinResumeSpy.mockRestore();
});
it('should exit with non-RELAUNCH_EXIT_CODE', async () => {
const runner = vi.fn().mockResolvedValue(0);
await expect(relaunchOnExitCode(runner)).rejects.toThrow(
'PROCESS_EXIT_CALLED',
);
expect(runner).toHaveBeenCalledTimes(1);
expect(processExitSpy).toHaveBeenCalledWith(0);
});
it('should continue running when RELAUNCH_EXIT_CODE is returned', async () => {
let callCount = 0;
const runner = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount === 1) return RELAUNCH_EXIT_CODE;
if (callCount === 2) return RELAUNCH_EXIT_CODE;
return 0; // Exit on third call
});
await expect(relaunchOnExitCode(runner)).rejects.toThrow(
'PROCESS_EXIT_CALLED',
);
expect(runner).toHaveBeenCalledTimes(3);
expect(processExitSpy).toHaveBeenCalledWith(0);
});
it('should handle runner errors', async () => {
const error = new Error('Runner failed');
const runner = vi.fn().mockRejectedValue(error);
await expect(relaunchOnExitCode(runner)).rejects.toThrow(
'PROCESS_EXIT_CALLED',
);
expect(runner).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Fatal error: Failed to relaunch the CLI process.',
error,
);
expect(stdinResumeSpy).toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
describe('relaunchAppInChildProcess', () => {
let processExitSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let stdinPauseSpy: MockInstance;
let stdinResumeSpy: MockInstance;
// Store original values to restore later
const originalEnv = { ...process.env };
const originalExecArgv = [...process.execArgv];
const originalArgv = [...process.argv];
const originalExecPath = process.execPath;
beforeEach(() => {
vi.clearAllMocks();
process.env = { ...originalEnv };
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
process.execArgv = [...originalExecArgv];
process.argv = [...originalArgv];
process.execPath = '/usr/bin/node';
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('PROCESS_EXIT_CALLED');
});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
stdinPauseSpy = vi
.spyOn(process.stdin, 'pause')
.mockImplementation(() => process.stdin);
stdinResumeSpy = vi
.spyOn(process.stdin, 'resume')
.mockImplementation(() => process.stdin);
});
afterEach(() => {
process.env = { ...originalEnv };
process.execArgv = [...originalExecArgv];
process.argv = [...originalArgv];
process.execPath = originalExecPath;
processExitSpy.mockRestore();
consoleErrorSpy.mockRestore();
stdinPauseSpy.mockRestore();
stdinResumeSpy.mockRestore();
});
describe('when GEMINI_CLI_NO_RELAUNCH is set', () => {
it('should return early without spawning a child process', async () => {
process.env['GEMINI_CLI_NO_RELAUNCH'] = 'true';
await relaunchAppInChildProcess(['--test'], ['--verbose']);
expect(mockedSpawn).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
});
describe('when GEMINI_CLI_NO_RELAUNCH is not set', () => {
beforeEach(() => {
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
});
it('should construct correct node arguments from execArgv, additionalNodeArgs, script, additionalScriptArgs, and argv', () => {
// Test the argument construction logic directly by extracting it into a testable function
// This tests the same logic that's used in relaunchAppInChildProcess
// Setup test data to verify argument ordering
const mockExecArgv = ['--inspect=9229', '--trace-warnings'];
const mockArgv = [
'/usr/bin/node',
'/path/to/cli.js',
'command',
'--flag=value',
'--verbose',
];
const additionalNodeArgs = [
'--max-old-space-size=4096',
'--experimental-modules',
];
const additionalScriptArgs = ['--model', 'gemini-1.5-pro', '--debug'];
// Extract the argument construction logic from relaunchAppInChildProcess
const script = mockArgv[1];
const scriptArgs = mockArgv.slice(2);
const nodeArgs = [
...mockExecArgv,
...additionalNodeArgs,
script,
...additionalScriptArgs,
...scriptArgs,
];
// Verify the argument construction follows the expected pattern:
// [...process.execArgv, ...additionalNodeArgs, script, ...additionalScriptArgs, ...scriptArgs]
const expectedArgs = [
// Original node execution arguments
'--inspect=9229',
'--trace-warnings',
// Additional node arguments passed to function
'--max-old-space-size=4096',
'--experimental-modules',
// The script path
'/path/to/cli.js',
// Additional script arguments passed to function
'--model',
'gemini-1.5-pro',
'--debug',
// Original script arguments (everything after the script in process.argv)
'command',
'--flag=value',
'--verbose',
];
expect(nodeArgs).toEqual(expectedArgs);
});
it('should handle empty additional arguments correctly', () => {
// Test edge cases with empty arrays
const mockExecArgv = ['--trace-warnings'];
const mockArgv = ['/usr/bin/node', '/app/cli.js', 'start'];
const additionalNodeArgs: string[] = [];
const additionalScriptArgs: string[] = [];
// Extract the argument construction logic
const script = mockArgv[1];
const scriptArgs = mockArgv.slice(2);
const nodeArgs = [
...mockExecArgv,
...additionalNodeArgs,
script,
...additionalScriptArgs,
...scriptArgs,
];
const expectedArgs = ['--trace-warnings', '/app/cli.js', 'start'];
expect(nodeArgs).toEqual(expectedArgs);
});
it('should handle complex argument patterns', () => {
// Test with various argument types including flags with values, boolean flags, etc.
const mockExecArgv = ['--max-old-space-size=8192'];
const mockArgv = [
'/usr/bin/node',
'/cli.js',
'--config=/path/to/config.json',
'--verbose',
'subcommand',
'--output',
'file.txt',
];
const additionalNodeArgs = ['--inspect-brk=9230'];
const additionalScriptArgs = ['--model=gpt-4', '--temperature=0.7'];
const script = mockArgv[1];
const scriptArgs = mockArgv.slice(2);
const nodeArgs = [
...mockExecArgv,
...additionalNodeArgs,
script,
...additionalScriptArgs,
...scriptArgs,
];
const expectedArgs = [
'--max-old-space-size=8192',
'--inspect-brk=9230',
'/cli.js',
'--model=gpt-4',
'--temperature=0.7',
'--config=/path/to/config.json',
'--verbose',
'subcommand',
'--output',
'file.txt',
];
expect(nodeArgs).toEqual(expectedArgs);
});
// Note: Additional integration tests for spawn behavior are complex due to module mocking
// limitations with ES modules. The core logic is tested in relaunchOnExitCode tests.
it('should handle null exit code from child process', async () => {
process.argv = ['/usr/bin/node', '/app/cli.js'];
const mockChild = createMockChildProcess(0, false); // Don't auto-close
mockedSpawn.mockImplementation(() => {
// Emit close with null code immediately
setImmediate(() => {
mockChild.emit('close', null);
});
return mockChild;
});
// Start the relaunch process
const promise = relaunchAppInChildProcess([], []);
await expect(promise).rejects.toThrow('PROCESS_EXIT_CALLED');
// Should default to exit code 1
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
});
/**
* Creates a mock child process that emits events asynchronously
*/
function createMockChildProcess(
exitCode: number = 0,
autoClose: boolean = false,
): ChildProcess {
const mockChild = new EventEmitter() as ChildProcess;
Object.assign(mockChild, {
stdin: null,
stdout: null,
stderr: null,
stdio: [null, null, null],
pid: 12345,
killed: false,
exitCode: null,
signalCode: null,
spawnargs: [],
spawnfile: '',
kill: vi.fn(),
send: vi.fn(),
disconnect: vi.fn(),
unref: vi.fn(),
ref: vi.fn(),
});
if (autoClose) {
setImmediate(() => {
mockChild.emit('close', exitCode);
});
}
return mockChild;
}

View File

@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawn } from 'node:child_process';
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
export async function relaunchOnExitCode(runner: () => Promise<number>) {
while (true) {
try {
const exitCode = await runner();
if (exitCode !== RELAUNCH_EXIT_CODE) {
process.exit(exitCode);
}
} catch (error) {
process.stdin.resume();
console.error('Fatal error: Failed to relaunch the CLI process.', error);
process.exit(1);
}
}
}
export async function relaunchAppInChildProcess(
additionalNodeArgs: string[],
additionalScriptArgs: string[],
) {
if (process.env['GEMINI_CLI_NO_RELAUNCH']) {
return;
}
const runner = () => {
// process.argv is [node, script, ...args]
// We want to construct [ ...nodeArgs, script, ...scriptArgs]
const script = process.argv[1];
const scriptArgs = process.argv.slice(2);
const nodeArgs = [
...process.execArgv,
...additionalNodeArgs,
script,
...additionalScriptArgs,
...scriptArgs,
];
const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
// The parent process should not be reading from stdin while the child is running.
process.stdin.pause();
const child = spawn(process.execPath, nodeArgs, {
stdio: 'inherit',
env: newEnv,
});
return new Promise<number>((resolve, reject) => {
child.on('error', reject);
child.on('close', (code) => {
// Resume stdin before the parent process exits.
process.stdin.resume();
resolve(code ?? 1);
});
});
};
await relaunchOnExitCode(runner);
}