Files
massive-cloud/server/analysisJobControl.ts

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);
}