Skip to content

Commit 7e06559

Browse files
authored
Consistently guard restarts against concurrent auto updates (#21016)
1 parent bbcfff5 commit 7e06559

10 files changed

Lines changed: 48 additions & 31 deletions

packages/cli/src/ui/AppContainer.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
129129
import { type UpdateObject } from './utils/updateCheck.js';
130130
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
131131
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
132-
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
132+
import { relaunchApp } from '../utils/processUtils.js';
133133
import type { SessionInfo } from '../utils/sessionUtils.js';
134134
import { useMessageQueue } from './hooks/useMessageQueue.js';
135135
import { useMcpStatus } from './hooks/useMcpStatus.js';
@@ -781,13 +781,12 @@ export const AppContainer = (props: AppContainerProps) => {
781781
authType === AuthType.LOGIN_WITH_GOOGLE &&
782782
config.isBrowserLaunchSuppressed()
783783
) {
784-
await runExitCleanup();
785784
writeToStdout(`
786785
----------------------------------------------------------------
787786
Logging in with Google... Restarting Gemini CLI to continue.
788787
----------------------------------------------------------------
789788
`);
790-
process.exit(RELAUNCH_EXIT_CODE);
789+
await relaunchApp();
791790
}
792791
}
793792
setAuthState(AuthState.Authenticated);
@@ -2497,8 +2496,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
24972496
});
24982497
}
24992498
}
2500-
await runExitCleanup();
2501-
process.exit(RELAUNCH_EXIT_CODE);
2499+
await relaunchApp();
25022500
},
25032501
handleNewAgentsSelect: async (choice: NewAgentsChoice) => {
25042502
if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) {

packages/cli/src/ui/auth/AuthDialog.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ import {
2121
} from '@google/gemini-cli-core';
2222
import { useKeypress } from '../hooks/useKeypress.js';
2323
import { AuthState } from '../types.js';
24-
import { runExitCleanup } from '../../utils/cleanup.js';
2524
import { validateAuthMethodWithSettings } from './useAuth.js';
26-
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
25+
import { relaunchApp } from '../../utils/processUtils.js';
2726

2827
interface AuthDialogProps {
2928
config: Config;
@@ -133,10 +132,7 @@ export function AuthDialog({
133132
config.isBrowserLaunchSuppressed()
134133
) {
135134
setExiting(true);
136-
setTimeout(async () => {
137-
await runExitCleanup();
138-
process.exit(RELAUNCH_EXIT_CODE);
139-
}, 100);
135+
setTimeout(relaunchApp, 100);
140136
return;
141137
}
142138

packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
99
import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js';
1010
import { useKeypress } from '../hooks/useKeypress.js';
1111
import { runExitCleanup } from '../../utils/cleanup.js';
12-
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
12+
import {
13+
RELAUNCH_EXIT_CODE,
14+
_resetRelaunchStateForTesting,
15+
} from '../../utils/processUtils.js';
1316
import { type Config } from '@google/gemini-cli-core';
1417

1518
// Mocks
@@ -38,6 +41,7 @@ describe('LoginWithGoogleRestartDialog', () => {
3841
vi.clearAllMocks();
3942
exitSpy.mockClear();
4043
vi.useRealTimers();
44+
_resetRelaunchStateForTesting();
4145
});
4246

4347
it('renders correctly', async () => {

packages/cli/src/ui/auth/LoginWithGoogleRestartDialog.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { type Config } from '@google/gemini-cli-core';
88
import { Box, Text } from 'ink';
99
import { theme } from '../semantic-colors.js';
1010
import { useKeypress } from '../hooks/useKeypress.js';
11-
import { runExitCleanup } from '../../utils/cleanup.js';
12-
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
11+
import { relaunchApp } from '../../utils/processUtils.js';
1312

1413
interface LoginWithGoogleRestartDialogProps {
1514
onDismiss: () => void;
@@ -36,8 +35,7 @@ export const LoginWithGoogleRestartDialog = ({
3635
});
3736
}
3837
}
39-
await runExitCleanup();
40-
process.exit(RELAUNCH_EXIT_CODE);
38+
await relaunchApp();
4139
}, 100);
4240
return true;
4341
}

packages/cli/src/ui/components/DialogManager.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ import { ProQuotaDialog } from './ProQuotaDialog.js';
2121
import { ValidationDialog } from './ValidationDialog.js';
2222
import { OverageMenuDialog } from './OverageMenuDialog.js';
2323
import { EmptyWalletDialog } from './EmptyWalletDialog.js';
24-
import { runExitCleanup } from '../../utils/cleanup.js';
25-
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
24+
import { relaunchApp } from '../../utils/processUtils.js';
2625
import { SessionBrowser } from './SessionBrowser.js';
2726
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
2827
import { ModelDialog } from './ModelDialog.js';
@@ -231,10 +230,7 @@ export const DialogManager = ({
231230
<Box flexDirection="column">
232231
<SettingsDialog
233232
onSelect={() => uiActions.closeSettingsDialog()}
234-
onRestartRequest={async () => {
235-
await runExitCleanup();
236-
process.exit(RELAUNCH_EXIT_CODE);
237-
}}
233+
onRestartRequest={relaunchApp}
238234
availableTerminalHeight={terminalHeight - staticExtraHeight}
239235
/>
240236
</Box>

packages/cli/src/ui/components/FolderTrustDialog.test.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ describe('FolderTrustDialog', () => {
246246

247247
it('should call relaunchApp when isRestarting is true', async () => {
248248
vi.useFakeTimers();
249-
const relaunchApp = vi.spyOn(processUtils, 'relaunchApp');
249+
const relaunchApp = vi
250+
.spyOn(processUtils, 'relaunchApp')
251+
.mockResolvedValue(undefined);
250252
const { waitUntilReady, unmount } = renderWithProviders(
251253
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
252254
);
@@ -259,7 +261,9 @@ describe('FolderTrustDialog', () => {
259261

260262
it('should not call relaunchApp if unmounted before timeout', async () => {
261263
vi.useFakeTimers();
262-
const relaunchApp = vi.spyOn(processUtils, 'relaunchApp');
264+
const relaunchApp = vi
265+
.spyOn(processUtils, 'relaunchApp')
266+
.mockResolvedValue(undefined);
263267
const { waitUntilReady, unmount } = renderWithProviders(
264268
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
265269
);

packages/cli/src/ui/components/FolderTrustDialog.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
5454
useEffect(() => {
5555
let timer: ReturnType<typeof setTimeout>;
5656
if (isRestarting) {
57-
timer = setTimeout(async () => {
58-
await relaunchApp();
59-
}, 250);
57+
timer = setTimeout(relaunchApp, 250);
6058
}
6159
return () => {
6260
if (timer) clearTimeout(timer);

packages/cli/src/ui/components/IdeTrustChangeDialog.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ describe('IdeTrustChangeDialog', () => {
6262
});
6363

6464
it('calls relaunchApp when "r" is pressed', async () => {
65-
const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp');
65+
const relaunchAppSpy = vi
66+
.spyOn(processUtils, 'relaunchApp')
67+
.mockResolvedValue(undefined);
6668
const { stdin, waitUntilReady, unmount } = renderWithProviders(
6769
<IdeTrustChangeDialog reason="NONE" />,
6870
);
@@ -78,7 +80,9 @@ describe('IdeTrustChangeDialog', () => {
7880
});
7981

8082
it('calls relaunchApp when "R" is pressed', async () => {
81-
const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp');
83+
const relaunchAppSpy = vi
84+
.spyOn(processUtils, 'relaunchApp')
85+
.mockResolvedValue(undefined);
8286
const { stdin, waitUntilReady, unmount } = renderWithProviders(
8387
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
8488
);
@@ -94,7 +98,9 @@ describe('IdeTrustChangeDialog', () => {
9498
});
9599

96100
it('does not call relaunchApp when another key is pressed', async () => {
97-
const relaunchAppSpy = vi.spyOn(processUtils, 'relaunchApp');
101+
const relaunchAppSpy = vi
102+
.spyOn(processUtils, 'relaunchApp')
103+
.mockResolvedValue(undefined);
98104
const { stdin, waitUntilReady, unmount } = renderWithProviders(
99105
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
100106
);

packages/cli/src/utils/processUtils.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
*/
66

77
import { vi } from 'vitest';
8-
import { RELAUNCH_EXIT_CODE, relaunchApp } from './processUtils.js';
8+
import {
9+
RELAUNCH_EXIT_CODE,
10+
relaunchApp,
11+
_resetRelaunchStateForTesting,
12+
} from './processUtils.js';
913
import * as cleanup from './cleanup.js';
1014
import * as handleAutoUpdate from './handleAutoUpdate.js';
1115

@@ -19,6 +23,10 @@ describe('processUtils', () => {
1923
.mockReturnValue(undefined as never);
2024
const runExitCleanup = vi.spyOn(cleanup, 'runExitCleanup');
2125

26+
beforeEach(() => {
27+
_resetRelaunchStateForTesting();
28+
});
29+
2230
afterEach(() => vi.clearAllMocks());
2331

2432
it('should wait for updates, run cleanup, and exit with the relaunch code', async () => {

packages/cli/src/utils/processUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ export const RELAUNCH_EXIT_CODE = 199;
1515
/**
1616
* Exits the process with a special code to signal that the parent process should relaunch it.
1717
*/
18+
let isRelaunching = false;
19+
20+
/** @internal only for testing */
21+
export function _resetRelaunchStateForTesting(): void {
22+
isRelaunching = false;
23+
}
24+
1825
export async function relaunchApp(): Promise<void> {
26+
if (isRelaunching) return;
27+
isRelaunching = true;
1928
await waitForUpdateCompletion();
2029
await runExitCleanup();
2130
process.exit(RELAUNCH_EXIT_CODE);

0 commit comments

Comments
 (0)