feat: add inspection task lifecycle controls
This commit is contained in:
154
server/analysisJobControl.ts
Normal file
154
server/analysisJobControl.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { ChildProcess, execFileSync } from 'node:child_process';
|
||||
|
||||
export type LegacyAnalysisKind = 'smooth_heatmap' | 'assembly_inspection' | 'obj_deviation';
|
||||
|
||||
interface ActiveLegacyJob {
|
||||
resultId: number;
|
||||
taskId: number;
|
||||
kind: LegacyAnalysisKind;
|
||||
child: ChildProcess;
|
||||
}
|
||||
|
||||
interface ProcessRow {
|
||||
pid: number;
|
||||
ppid: number;
|
||||
command: string;
|
||||
}
|
||||
|
||||
const activeLegacyJobs = new Map<number, ActiveLegacyJob>();
|
||||
|
||||
export function registerLegacyAnalysisChild(
|
||||
resultId: number,
|
||||
taskId: number,
|
||||
kind: LegacyAnalysisKind,
|
||||
child: ChildProcess,
|
||||
): void {
|
||||
activeLegacyJobs.set(resultId, { resultId, taskId, kind, child });
|
||||
child.once('exit', () => {
|
||||
activeLegacyJobs.delete(resultId);
|
||||
});
|
||||
}
|
||||
|
||||
export function cancelLegacyAnalysisJob(resultId: number): boolean {
|
||||
const job = activeLegacyJobs.get(resultId);
|
||||
let cancelled = false;
|
||||
if (job) {
|
||||
activeLegacyJobs.delete(resultId);
|
||||
try {
|
||||
job.child.kill('SIGKILL');
|
||||
cancelled = true;
|
||||
} catch {
|
||||
// Process may already have exited.
|
||||
}
|
||||
}
|
||||
return killOrphanLegacyAnalysisProcesses({ resultId }) || cancelled;
|
||||
}
|
||||
|
||||
export function cancelLegacyAnalysisJobsByTask(taskId: number): number[] {
|
||||
const cancelled = new Set<number>();
|
||||
for (const [resultId, job] of Array.from(activeLegacyJobs.entries())) {
|
||||
if (job.taskId !== taskId) continue;
|
||||
activeLegacyJobs.delete(resultId);
|
||||
try {
|
||||
job.child.kill('SIGKILL');
|
||||
cancelled.add(resultId);
|
||||
} catch {
|
||||
// Process may already have exited.
|
||||
}
|
||||
}
|
||||
for (const resultId of killOrphanLegacyAnalysisProcesses({ taskId })) {
|
||||
cancelled.add(resultId);
|
||||
}
|
||||
return Array.from(cancelled);
|
||||
}
|
||||
|
||||
function listProcesses(): ProcessRow[] {
|
||||
try {
|
||||
const output = execFileSync('ps', ['-eo', 'pid=,ppid=,command='], {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
pid: Number(match[1]),
|
||||
ppid: Number(match[2]),
|
||||
command: match[3],
|
||||
};
|
||||
})
|
||||
.filter((row): row is ProcessRow => row != null && Number.isFinite(row.pid) && Number.isFinite(row.ppid));
|
||||
} catch (err) {
|
||||
console.warn('[AnalysisJobControl] 无法列出进程,跳过 legacy 孤儿进程清理:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function collectDescendants(processes: ProcessRow[], rootPids: Set<number>): Set<number> {
|
||||
const all = new Set(rootPids);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const proc of processes) {
|
||||
if (!all.has(proc.pid) && all.has(proc.ppid)) {
|
||||
all.add(proc.pid);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
function killPids(pids: number[]): number[] {
|
||||
const killed: number[] = [];
|
||||
for (const pid of pids) {
|
||||
if (pid <= 1 || pid === process.pid) continue;
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
killed.push(pid);
|
||||
} catch {
|
||||
// Process may already have exited.
|
||||
}
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
|
||||
function runnerRegex(): RegExp {
|
||||
return /(smoothHeatmapJobRunner|assemblyInspectionJobRunner|deviationHeatmapJobRunner)\.(?:ts|js)\s+(\d+)\s+(\d+)\b/;
|
||||
}
|
||||
|
||||
function killOrphanLegacyAnalysisProcesses(input: { resultId: number }): boolean;
|
||||
function killOrphanLegacyAnalysisProcesses(input: { taskId: number }): number[];
|
||||
function killOrphanLegacyAnalysisProcesses(input: { resultId?: number; taskId?: number }): boolean | number[] {
|
||||
const processes = listProcesses();
|
||||
const roots = new Set<number>();
|
||||
const resultIds = new Set<number>();
|
||||
|
||||
for (const proc of processes) {
|
||||
const match = proc.command.match(runnerRegex());
|
||||
if (!match) continue;
|
||||
const resultId = Number(match[2]);
|
||||
const taskId = Number(match[3]);
|
||||
if (input.resultId != null && resultId === input.resultId) {
|
||||
roots.add(proc.pid);
|
||||
resultIds.add(resultId);
|
||||
}
|
||||
if (input.taskId != null && taskId === input.taskId) {
|
||||
roots.add(proc.pid);
|
||||
resultIds.add(resultId);
|
||||
}
|
||||
}
|
||||
|
||||
const targets = Array.from(collectDescendants(processes, roots)).sort((a, b) => b - a);
|
||||
const killed = killPids(targets);
|
||||
if (killed.length > 0) {
|
||||
console.warn(`[AnalysisJobControl] 已清理 legacy 分析进程 pid=${killed.join(', ')}`);
|
||||
}
|
||||
|
||||
if (input.resultId != null) return killed.length > 0;
|
||||
return Array.from(resultIds);
|
||||
}
|
||||
Reference in New Issue
Block a user