fix: detect stale PID in runner state after abnormal shutdown (#931)

* fix: verify PID belongs to hapi before treating runner as alive

After OS upgrade, stale runner.state.json PID can be reused by unrelated
processes. The old kill(pid, 0) check passes for any process, causing
start-sync to loop with 'Runner already running' indefinitely.

Now uses ps/wmic to confirm the process command line contains 'hapi'
before considering the runner alive. Falls back to alive-only check if
ps/wmic fails.

* fix: precise runner process detection and wmic fallback

* fix: add fallback for ps failure in non-Windows branch
This commit is contained in:
KorenKrita
2026-06-18 10:11:02 +08:00
committed by GitHub
parent 26d3c2eb34
commit b3add07ad7
2 changed files with 30 additions and 4 deletions
+4 -4
View File
@@ -10,7 +10,7 @@ import packageJson from '../../package.json';
import { existsSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { isBunCompiled, projectPath } from '@/projectPath';
import { isProcessAlive, killProcess } from '@/utils/process';
import { isProcessAlive, isHapiRunnerProcess, killProcess } from '@/utils/process';
import { configuration } from '@/configuration';
import { hashRunnerCliApiToken, isRunnerStateCompatibleWithIdentity } from './runnerIdentity';
@@ -143,12 +143,12 @@ export async function checkIfRunnerRunningAndCleanupStaleState(): Promise<boolea
return false;
}
// Check if the runner is running
if (isProcessAlive(state.pid)) {
// Verify PID is alive AND belongs to hapi (not a reused PID from another process)
if (isHapiRunnerProcess(state.pid)) {
return true;
}
logger.debug('[RUNNER RUN] Runner PID not running, cleaning up state');
logger.debug('[RUNNER RUN] Runner PID not running or not a hapi process, cleaning up state');
await cleanupRunnerState();
return false;
}
+26
View File
@@ -16,6 +16,32 @@ export function isProcessAlive(pid: number): boolean {
}
}
// ponytail: ps -p is cheap and avoids PID-reuse false positives after OS upgrades/reboots
function isRunnerCommand(commandLine: string): boolean {
return /(?:^|\s)runner(?:\s|$)/.test(commandLine) && /(?:^|\s)start-sync(?:\s|$)/.test(commandLine);
}
export function isHapiRunnerProcess(pid: number): boolean {
if (!isProcessAlive(pid)) {
return false;
}
if (isWindows()) {
const result = spawn.sync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'CommandLine'], { stdio: 'pipe' });
if (result.error) {
return true;
}
if (result.status !== 0) {
return isProcessAlive(pid);
}
return isRunnerCommand(result.stdout?.toString() ?? '');
}
const result = spawn.sync('ps', ['-p', String(pid), '-o', 'command='], { stdio: 'pipe' });
if (result.error || result.status !== 0) {
return isProcessAlive(pid);
}
return isRunnerCommand(result.stdout?.toString() ?? '');
}
function killProcessWindows(pid: number, force: boolean): boolean {
if (!isProcessAlive(pid)) {
return true;