155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
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);
|
|
}
|