Rework windows start and stop methods so that command windows dont appear. Stop with signal since we cannot signint.

This commit is contained in:
Jaret Burkett
2026-07-27 17:43:01 -06:00
parent 6b0449c326
commit 461e798708
8 changed files with 389 additions and 77 deletions

View File

@@ -6,6 +6,43 @@ import { promisify } from 'util';
const execAsync = promisify(exec);
const isWindows = process.platform === 'win32';
// How long a stopping job gets to run its graceful shutdown (KeyboardInterrupt
// -> on_error -> final DB write) before we assume it is hung and force-kill it.
const GRACEFUL_STOP_TIMEOUT_MS = 60_000;
const BACKSTOP_POLL_MS = 2_000;
const isProcessAlive = (pid: number): boolean => {
try {
process.kill(pid, 0);
return true;
} catch (e: any) {
// EPERM means it exists but belongs to someone else, which still counts.
return e?.code === 'EPERM';
}
};
// Windows: the trainer's stop watcher sees the `stop` flag and raises SIGINT
// inside the process, so the graceful path runs without us sending anything.
// This backstop only exists for a job that is too hung to notice the flag.
// Poll (rather than one long timer) so we stop watching the moment the pid
// dies and never touch a recycled pid.
const scheduleForceKillBackstop = (pid: number, jobID: string) => {
const startedAt = Date.now();
const timer = setInterval(() => {
if (!isProcessAlive(pid)) {
clearInterval(timer);
return;
}
if (Date.now() - startedAt < GRACEFUL_STOP_TIMEOUT_MS) return;
clearInterval(timer);
console.warn(`Job ${jobID} (pid ${pid}) still alive ${GRACEFUL_STOP_TIMEOUT_MS / 1000}s after stop request, force killing`);
execAsync(`taskkill /PID ${pid} /T /F`, { windowsHide: true }).catch(() => {
// already gone
});
}, BACKSTOP_POLL_MS);
timer.unref?.();
};
export async function GET(request: NextRequest, { params }: { params: { jobID: string } }) {
const { jobID } = await params;
@@ -30,9 +67,12 @@ export async function GET(request: NextRequest, { params }: { params: { jobID: s
console.log(`Attempting to stop job ${jobID} with PID ${job.pid}`);
try {
if (isWindows) {
// Windows doesn't support SIGINT for arbitrary processes.
// Use taskkill with /T (tree) to send a CTRL+C-like termination.
await execAsync(`taskkill /PID ${job.pid} /T /F`, { windowsHide: true });
// No external SIGINT possible on Windows (the job runs under pythonw
// with no console), and none is needed: the `stop` flag written above
// is the signal. The trainer's stop watcher polls it and raises
// SIGINT in-process, which runs the same KeyboardInterrupt shutdown
// as Linux -- final DB write and the closing log lines.
scheduleForceKillBackstop(job.pid, jobID);
} else {
process.kill(job.pid, 'SIGINT');
}