mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-31 00:11:11 -07:00
Refine execution lifecycle facade follow-ups
This commit is contained in:
@@ -80,7 +80,7 @@ export const useShellCommandProcessor = (
|
||||
setShellInputFocused: (value: boolean) => void,
|
||||
terminalWidth?: number,
|
||||
terminalHeight?: number,
|
||||
activeToolPtyId?: number,
|
||||
activeBackgroundExecutionId?: number,
|
||||
isWaitingForConfirmation?: boolean,
|
||||
) => {
|
||||
const [state, dispatch] = useReducer(shellReducer, initialState);
|
||||
@@ -103,7 +103,8 @@ export const useShellCommandProcessor = (
|
||||
}
|
||||
const m = manager.current;
|
||||
|
||||
const activePtyId = state.activeShellPtyId || activeToolPtyId;
|
||||
const activePtyId =
|
||||
state.activeShellPtyId ?? activeBackgroundExecutionId ?? undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const isForegroundActive = !!activePtyId || !!isWaitingForConfirmation;
|
||||
@@ -191,7 +192,8 @@ export const useShellCommandProcessor = (
|
||||
]);
|
||||
|
||||
const backgroundCurrentShell = useCallback(() => {
|
||||
const pidToBackground = state.activeShellPtyId || activeToolPtyId;
|
||||
const pidToBackground =
|
||||
state.activeShellPtyId ?? activeBackgroundExecutionId;
|
||||
if (pidToBackground) {
|
||||
ShellExecutionService.background(pidToBackground);
|
||||
m.backgroundedPids.add(pidToBackground);
|
||||
@@ -202,7 +204,7 @@ export const useShellCommandProcessor = (
|
||||
m.restoreTimeout = null;
|
||||
}
|
||||
}
|
||||
}, [state.activeShellPtyId, activeToolPtyId, m]);
|
||||
}, [state.activeShellPtyId, activeBackgroundExecutionId, m]);
|
||||
|
||||
const dismissBackgroundShell = useCallback(
|
||||
(pid: number) => {
|
||||
|
||||
@@ -96,6 +96,31 @@ const MockedUserPromptEvent = vi.hoisted(() =>
|
||||
vi.fn().mockImplementation(() => {}),
|
||||
);
|
||||
const mockParseAndFormatApiError = vi.hoisted(() => vi.fn());
|
||||
const mockIsBackgroundExecutionData = vi.hoisted(
|
||||
() => (data: unknown): data is { executionId?: number; pid?: number } => {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false;
|
||||
}
|
||||
const value = data as {
|
||||
executionId?: unknown;
|
||||
pid?: unknown;
|
||||
command?: unknown;
|
||||
initialOutput?: unknown;
|
||||
};
|
||||
return (
|
||||
(value.executionId === undefined || typeof value.executionId === 'number') &&
|
||||
(value.pid === undefined || typeof value.pid === 'number') &&
|
||||
(value.command === undefined || typeof value.command === 'string') &&
|
||||
(value.initialOutput === undefined ||
|
||||
typeof value.initialOutput === 'string')
|
||||
);
|
||||
},
|
||||
);
|
||||
const mockGetBackgroundExecutionId = vi.hoisted(
|
||||
() =>
|
||||
(data: { executionId?: number; pid?: number }): number | undefined =>
|
||||
data.executionId ?? data.pid,
|
||||
);
|
||||
|
||||
const MockValidationRequiredError = vi.hoisted(
|
||||
() =>
|
||||
@@ -121,6 +146,11 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actualCoreModule = (await importOriginal()) as any;
|
||||
return {
|
||||
...actualCoreModule,
|
||||
isBackgroundExecutionData:
|
||||
actualCoreModule.isBackgroundExecutionData ??
|
||||
mockIsBackgroundExecutionData,
|
||||
getBackgroundExecutionId:
|
||||
actualCoreModule.getBackgroundExecutionId ?? mockGetBackgroundExecutionId,
|
||||
GitService: vi.fn(),
|
||||
GeminiClient: MockedGeminiClientClass,
|
||||
UserPromptEvent: MockedUserPromptEvent,
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
buildUserSteeringHintPrompt,
|
||||
GeminiCliOperation,
|
||||
getPlanModeExitMessage,
|
||||
getBackgroundExecutionId,
|
||||
isBackgroundExecutionData,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Config,
|
||||
@@ -100,12 +102,12 @@ interface BackgroundedToolInfo {
|
||||
initialOutput: string;
|
||||
}
|
||||
|
||||
interface BackgroundExecutionData {
|
||||
type BackgroundExecutionDataLike = {
|
||||
executionId?: number;
|
||||
pid?: number;
|
||||
command?: string;
|
||||
initialOutput?: string;
|
||||
}
|
||||
} & Record<string, unknown>;
|
||||
|
||||
enum StreamProcessingStatus {
|
||||
Completed,
|
||||
@@ -118,55 +120,58 @@ const SUPPRESSED_TOOL_ERRORS_NOTE =
|
||||
const LOW_VERBOSITY_FAILURE_NOTE =
|
||||
'This request failed. Press F12 for diagnostics, or run /settings and change "Error Verbosity" to full for full details.';
|
||||
|
||||
function isBackgroundExecutionData(
|
||||
data: unknown,
|
||||
): data is BackgroundExecutionData {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const executionId = 'executionId' in data ? data.executionId : undefined;
|
||||
const pid = 'pid' in data ? data.pid : undefined;
|
||||
const command = 'command' in data ? data.command : undefined;
|
||||
const initialOutput =
|
||||
'initialOutput' in data ? data.initialOutput : undefined;
|
||||
|
||||
return (
|
||||
(executionId === undefined || typeof executionId === 'number') &&
|
||||
(pid === undefined || typeof pid === 'number') &&
|
||||
(command === undefined || typeof command === 'string') &&
|
||||
(initialOutput === undefined || typeof initialOutput === 'string')
|
||||
);
|
||||
function isBackgroundExecutionDataValidator(
|
||||
candidate: unknown,
|
||||
): candidate is (data: unknown) => data is BackgroundExecutionDataLike {
|
||||
return typeof candidate === 'function';
|
||||
}
|
||||
|
||||
function getBackgroundExecutionId(
|
||||
data: BackgroundExecutionData,
|
||||
function isBackgroundExecutionIdGetter(
|
||||
candidate: unknown,
|
||||
): candidate is (data: BackgroundExecutionDataLike) => number | undefined {
|
||||
return typeof candidate === 'function';
|
||||
}
|
||||
|
||||
function isBackgroundExecutionDataFromCore(
|
||||
data: unknown,
|
||||
): data is BackgroundExecutionDataLike {
|
||||
const candidate: unknown = isBackgroundExecutionData;
|
||||
if (isBackgroundExecutionDataValidator(candidate)) {
|
||||
return candidate(data);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getBackgroundExecutionIdFromCore(
|
||||
data: BackgroundExecutionDataLike,
|
||||
): number | undefined {
|
||||
if (typeof data.executionId === 'number') {
|
||||
return data.executionId;
|
||||
const candidate: unknown = getBackgroundExecutionId;
|
||||
if (isBackgroundExecutionIdGetter(candidate)) {
|
||||
return candidate(data);
|
||||
}
|
||||
if (typeof data.pid === 'number') {
|
||||
return data.pid;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return data.executionId ?? data.pid;
|
||||
}
|
||||
|
||||
function getBackgroundedToolInfo(
|
||||
toolCall: TrackedCompletedToolCall | TrackedCancelledToolCall,
|
||||
): BackgroundedToolInfo | undefined {
|
||||
const response = toolCall.response as ToolResponseWithParts;
|
||||
const rawData = response?.data;
|
||||
const data = isBackgroundExecutionData(rawData) ? rawData : undefined;
|
||||
const executionId = data ? getBackgroundExecutionId(data) : undefined;
|
||||
const rawData: unknown = response?.data;
|
||||
if (!isBackgroundExecutionDataFromCore(rawData)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const executionId = getBackgroundExecutionIdFromCore(rawData);
|
||||
if (executionId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
executionId,
|
||||
command: data.command ?? toolCall.request.name,
|
||||
initialOutput: data.initialOutput ?? '',
|
||||
command: rawData.command ?? toolCall.request.name,
|
||||
initialOutput: rawData.initialOutput ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,21 @@ describe('ExecutionLifecycleService', () => {
|
||||
expect(result.error?.message).toContain('Operation cancelled by user');
|
||||
});
|
||||
|
||||
it('does not probe OS process state for completed non-process execution IDs', async () => {
|
||||
const handle = ExecutionLifecycleService.createExecution();
|
||||
if (handle.pid === undefined) {
|
||||
throw new Error('Expected execution ID.');
|
||||
}
|
||||
|
||||
ExecutionLifecycleService.completeExecution(handle.pid, { exitCode: 0 });
|
||||
await handle.result;
|
||||
|
||||
const processKillSpy = vi.spyOn(process, 'kill');
|
||||
expect(ExecutionLifecycleService.isActive(handle.pid)).toBe(false);
|
||||
expect(processKillSpy).not.toHaveBeenCalled();
|
||||
processKillSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('manages external executions through registration hooks', async () => {
|
||||
const writeInput = vi.fn();
|
||||
const isActive = vi.fn().mockReturnValue(true);
|
||||
|
||||
@@ -312,16 +312,6 @@ export class ExecutionLifecycleService {
|
||||
this.settleExecution(executionId, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use completeWithResult() for new call sites.
|
||||
*/
|
||||
static finalizeExecution(
|
||||
executionId: number,
|
||||
result: ExecutionResult,
|
||||
): void {
|
||||
this.completeWithResult(executionId, result);
|
||||
}
|
||||
|
||||
static background(executionId: number): void {
|
||||
const resolve = this.activeResolvers.get(executionId);
|
||||
if (!resolve) {
|
||||
@@ -423,6 +413,9 @@ export class ExecutionLifecycleService {
|
||||
static isActive(executionId: number): boolean {
|
||||
const execution = this.activeExecutions.get(executionId);
|
||||
if (!execution) {
|
||||
if (executionId >= NON_PROCESS_EXECUTION_ID_START) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return process.kill(executionId, 0);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user