Implement background process monitoring and inspection tools (#23799)

This commit is contained in:
Coco Sheng
2026-04-02 11:01:00 -04:00
committed by GitHub
parent 811a383d50
commit f510394721
13 changed files with 1181 additions and 12 deletions
@@ -103,6 +103,8 @@ export interface ShellExecutionConfig {
maxSerializedLines?: number;
sandboxConfig?: SandboxConfig;
backgroundCompletionBehavior?: 'inject' | 'notify' | 'silent';
originalCommand?: string;
sessionId?: string;
}
/**
@@ -114,6 +116,8 @@ interface ActivePty {
ptyProcess: IPty;
headlessTerminal: pkg.Terminal;
maxSerializedLines?: number;
command: string;
sessionId?: string;
}
interface ActiveChildProcess {
@@ -124,6 +128,8 @@ interface ActiveChildProcess {
sniffChunks: Buffer[];
binaryBytesReceived: number;
};
command: string;
sessionId?: string;
}
const findLastContentLine = (
@@ -230,11 +236,28 @@ const writeBufferToLogStream = (
*
*/
export type BackgroundProcess = {
pid: number;
command: string;
status: 'running' | 'exited';
exitCode?: number | null;
signal?: number | null;
};
export type BackgroundProcessRecord = Omit<BackgroundProcess, 'pid'> & {
startTime: number;
endTime?: number;
};
export class ShellExecutionService {
private static activePtys = new Map<number, ActivePty>();
private static activeChildProcesses = new Map<number, ActiveChildProcess>();
private static backgroundLogPids = new Set<number>();
private static backgroundLogStreams = new Map<number, fs.WriteStream>();
private static backgroundProcessHistory = new Map<
string, // sessionId
Map<number, BackgroundProcessRecord>
>();
static getLogDir(): string {
return path.join(Storage.getGlobalTempDir(), 'background-processes');
@@ -519,10 +542,12 @@ export class ShellExecutionService {
binaryBytesReceived: 0,
};
if (child.pid) {
if (child.pid !== undefined) {
this.activeChildProcesses.set(child.pid, {
process: child,
state,
command: shellExecutionConfig.originalCommand ?? commandToExecute,
sessionId: shellExecutionConfig.sessionId,
});
}
@@ -696,6 +721,17 @@ export class ShellExecutionService {
exitCode,
signal: exitSignal,
};
const sessionId = shellExecutionConfig.sessionId ?? 'default';
const history =
ShellExecutionService.backgroundProcessHistory.get(sessionId);
const historyItem = history?.get(pid);
if (historyItem) {
historyItem.status = 'exited';
historyItem.exitCode = exitCode ?? undefined;
historyItem.signal = exitSignal ?? undefined;
historyItem.endTime = Date.now();
}
onOutputEvent(event);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -849,6 +885,8 @@ export class ShellExecutionService {
ptyProcess,
headlessTerminal,
maxSerializedLines: shellExecutionConfig.maxSerializedLines,
command: shellExecutionConfig.originalCommand ?? commandToExecute,
sessionId: shellExecutionConfig.sessionId,
});
const result = ExecutionLifecycleService.attachExecution(ptyPid, {
@@ -1116,6 +1154,17 @@ export class ShellExecutionService {
exitCode,
signal: signal ?? null,
};
const sessionId = shellExecutionConfig.sessionId ?? 'default';
const history =
ShellExecutionService.backgroundProcessHistory.get(sessionId);
const historyItem = history?.get(ptyPid);
if (historyItem) {
historyItem.status = 'exited';
historyItem.exitCode = exitCode;
historyItem.signal = signal ?? null;
historyItem.endTime = Date.now();
}
onOutputEvent(event);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -1269,16 +1318,57 @@ export class ShellExecutionService {
*
* @param pid The process ID of the target PTY.
*/
static background(pid: number): void {
static background(pid: number, sessionId?: string, command?: string): void {
const activePty = this.activePtys.get(pid);
const activeChild = this.activeChildProcesses.get(pid);
const resolvedSessionId =
sessionId ?? activePty?.sessionId ?? activeChild?.sessionId;
const resolvedCommand =
command ??
activePty?.command ??
activeChild?.command ??
'unknown command';
if (!resolvedSessionId) {
throw new Error('Session ID is required for background operations');
}
const MAX_BACKGROUND_PROCESS_HISTORY_SIZE = 100;
const history =
this.backgroundProcessHistory.get(resolvedSessionId) ??
new Map<
number,
{
command: string;
status: 'running' | 'exited';
exitCode?: number | null;
signal?: number | null;
startTime: number;
endTime?: number;
}
>();
if (history.size >= MAX_BACKGROUND_PROCESS_HISTORY_SIZE) {
const oldestPid = history.keys().next().value;
if (oldestPid !== undefined) {
history.delete(oldestPid);
}
}
history.set(pid, {
command: resolvedCommand,
status: 'running',
startTime: Date.now(),
});
this.backgroundProcessHistory.set(resolvedSessionId, history);
// Set up background logging
const logPath = this.getLogFilePath(pid);
const logDir = this.getLogDir();
try {
mkdirSync(logDir, { recursive: true });
const stream = fs.createWriteStream(logPath, { flags: 'w' });
mkdirSync(logDir, { recursive: true, mode: 0o700 });
const stream = fs.createWriteStream(logPath, { flags: 'wx' });
stream.on('error', (err) => {
debugLogger.warn('Background log stream error:', err);
});
@@ -1391,4 +1481,20 @@ export class ShellExecutionService {
}
}
}
static listBackgroundProcesses(sessionId: string): BackgroundProcess[] {
if (!sessionId) {
throw new Error('Session ID is required');
}
const history = this.backgroundProcessHistory.get(sessionId);
if (!history) return [];
return Array.from(history.entries()).map(([pid, info]) => ({
pid,
command: info.command,
status: info.status,
exitCode: info.exitCode,
signal: info.signal,
}));
}
}