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(); 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(); 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): Set { 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(); const resultIds = new Set(); 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); }