mirror of
https://github.com/wassname/pi-lgtm.git
synced 2026-07-10 01:23:39 +08:00
141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
/**
|
|
* process-tracker.ts — Background process management for tasks.
|
|
*
|
|
* Tracks spawned child processes, buffers their output, and supports
|
|
* blocking wait and graceful stop (SIGTERM → 5s → SIGKILL).
|
|
*/
|
|
|
|
import type { ChildProcess } from "node:child_process";
|
|
import type { BackgroundProcess } from "./types.js";
|
|
|
|
export interface ProcessOutput {
|
|
output: string;
|
|
status: BackgroundProcess["status"];
|
|
exitCode?: number;
|
|
startedAt: number;
|
|
completedAt?: number;
|
|
command?: string;
|
|
}
|
|
|
|
export class ProcessTracker {
|
|
private processes = new Map<string, BackgroundProcess>();
|
|
|
|
/** Register a spawned process for a task. */
|
|
track(taskId: string, proc: ChildProcess, command?: string): void {
|
|
const bp: BackgroundProcess = {
|
|
taskId,
|
|
pid: proc.pid!,
|
|
command,
|
|
output: [],
|
|
status: "running",
|
|
startedAt: Date.now(),
|
|
proc,
|
|
abortController: new AbortController(),
|
|
waiters: [],
|
|
};
|
|
|
|
// Buffer stdout
|
|
proc.stdout?.on("data", (data: Buffer) => {
|
|
bp.output.push(data.toString());
|
|
});
|
|
|
|
// Buffer stderr
|
|
proc.stderr?.on("data", (data: Buffer) => {
|
|
bp.output.push(data.toString());
|
|
});
|
|
|
|
// Handle process exit
|
|
proc.on("close", (code, _signal) => {
|
|
if (bp.status === "running") {
|
|
bp.status = code === 0 ? "completed" : "error";
|
|
}
|
|
bp.exitCode = code ?? undefined;
|
|
bp.completedAt = Date.now();
|
|
// Notify all waiters
|
|
for (const resolve of bp.waiters) resolve();
|
|
bp.waiters = [];
|
|
});
|
|
|
|
proc.on("error", (err) => {
|
|
if (bp.status === "running") {
|
|
bp.status = "error";
|
|
bp.output.push(`Process error: ${err.message}`);
|
|
bp.completedAt = Date.now();
|
|
for (const resolve of bp.waiters) resolve();
|
|
bp.waiters = [];
|
|
}
|
|
});
|
|
|
|
this.processes.set(taskId, bp);
|
|
}
|
|
|
|
/** Get current output and status for a task's process. */
|
|
getOutput(taskId: string): ProcessOutput | undefined {
|
|
const bp = this.processes.get(taskId);
|
|
if (!bp) return undefined;
|
|
return {
|
|
output: bp.output.join(""),
|
|
status: bp.status,
|
|
exitCode: bp.exitCode,
|
|
startedAt: bp.startedAt,
|
|
completedAt: bp.completedAt,
|
|
command: bp.command,
|
|
};
|
|
}
|
|
|
|
/** Wait for a task's process to complete, with timeout. */
|
|
waitForCompletion(taskId: string, timeout: number, signal?: AbortSignal): Promise<ProcessOutput | undefined> {
|
|
const bp = this.processes.get(taskId);
|
|
if (!bp) return Promise.resolve(undefined);
|
|
if (bp.status !== "running") return Promise.resolve(this.getOutput(taskId));
|
|
|
|
return new Promise<ProcessOutput | undefined>((resolve) => {
|
|
let settled = false;
|
|
const timer = setTimeout(finish, timeout);
|
|
|
|
function finish() {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolve(self.getOutput(taskId));
|
|
}
|
|
|
|
const self = this;
|
|
bp.waiters.push(finish);
|
|
signal?.addEventListener("abort", finish, { once: true });
|
|
});
|
|
}
|
|
|
|
/** Stop a task's background process. SIGTERM → 5s → SIGKILL. */
|
|
async stop(taskId: string): Promise<boolean> {
|
|
const bp = this.processes.get(taskId);
|
|
if (!bp || bp.status !== "running") return false;
|
|
|
|
bp.status = "stopped";
|
|
bp.proc.kill("SIGTERM");
|
|
|
|
// Wait up to 5s for graceful exit
|
|
await new Promise<void>((resolve) => {
|
|
const timer = setTimeout(() => {
|
|
try { bp.proc.kill("SIGKILL"); } catch { /* already dead */ }
|
|
resolve();
|
|
}, 5000);
|
|
|
|
bp.proc.on("close", () => {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
bp.completedAt = Date.now();
|
|
for (const resolve of bp.waiters) resolve();
|
|
bp.waiters = [];
|
|
return true;
|
|
}
|
|
|
|
/** Get the process record for a task. */
|
|
getProcess(taskId: string): BackgroundProcess | undefined {
|
|
return this.processes.get(taskId);
|
|
}
|
|
}
|