mirror of https://github.com/garrytan/gstack.git
Merge 00303ebf86 into a3259400a3
This commit is contained in:
commit
22e26d6ec0
|
|
@ -7,8 +7,6 @@
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32';
|
|
||||||
|
|
||||||
// ─── Filesystem ────────────────────────────────────────────────
|
// ─── Filesystem ────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Remove a file, ignoring ENOENT (already gone). Rethrows other errors. */
|
/** Remove a file, ignoring ENOENT (already gone). Rethrows other errors. */
|
||||||
|
|
@ -36,23 +34,24 @@ export function safeKill(pid: number, signal: NodeJS.Signals | number): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a PID is alive. Pure boolean probe — returns false for ALL errors. */
|
/**
|
||||||
|
* Check if a PID is alive. Pure boolean probe — never throws.
|
||||||
|
*
|
||||||
|
* Uses signal-0 on every platform. Node/Bun implement `process.kill(pid, 0)`
|
||||||
|
* on Windows via `OpenProcess` — a pure existence check that spawns no child
|
||||||
|
* process and opens no console window. The old Windows branch shelled out to
|
||||||
|
* `tasklist`, which Windows gives its own console window; on the terminal-agent
|
||||||
|
* watchdog's per-tick existence check that flashed a `conhost.exe` window every
|
||||||
|
* 60s for the whole session (#1952). The parent watchdog already uses signal-0
|
||||||
|
* directly; this unifies on it.
|
||||||
|
*/
|
||||||
export function isProcessAlive(pid: number): boolean {
|
export function isProcessAlive(pid: number): boolean {
|
||||||
if (IS_WINDOWS) {
|
|
||||||
try {
|
|
||||||
const result = Bun.spawnSync(
|
|
||||||
['tasklist', '/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
|
|
||||||
{ stdout: 'pipe', stderr: 'pipe', timeout: 3000 }
|
|
||||||
);
|
|
||||||
return result.stdout.toString().includes(`"${pid}"`);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
process.kill(pid, 0);
|
process.kill(pid, 0);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
return false;
|
// ESRCH → gone (false); EPERM → exists but not signallable → still alive.
|
||||||
|
// The old Unix branch collapsed every error to false, missing the EPERM edge.
|
||||||
|
return err?.code === 'EPERM';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, test, expect } from 'bun:test';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
import { safeUnlink, safeKill, isProcessAlive } from '../src/error-handling';
|
import { safeUnlink, safeKill, isProcessAlive } from '../src/error-handling';
|
||||||
|
|
||||||
describe('safeUnlink', () => {
|
describe('safeUnlink', () => {
|
||||||
|
|
@ -44,4 +45,36 @@ describe('isProcessAlive', () => {
|
||||||
test('returns false for non-existent process', () => {
|
test('returns false for non-existent process', () => {
|
||||||
expect(isProcessAlive(99999999)).toBe(false);
|
expect(isProcessAlive(99999999)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('treats a live-but-EPERM process as alive (#1952)', () => {
|
||||||
|
// PID 1 (init/launchd) exists but a non-root user can't signal it →
|
||||||
|
// process.kill(1, 0) throws EPERM, which means alive, not dead.
|
||||||
|
// (Skip when running as root, where signalling PID 1 is permitted.)
|
||||||
|
if (typeof process.getuid === 'function' && process.getuid() === 0) return;
|
||||||
|
let threwEPERM = false;
|
||||||
|
try {
|
||||||
|
process.kill(1, 0);
|
||||||
|
} catch (err: any) {
|
||||||
|
threwEPERM = err?.code === 'EPERM';
|
||||||
|
}
|
||||||
|
if (threwEPERM) expect(isProcessAlive(1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Regression tripwire for #1952: isProcessAlive must never spawn a child
|
||||||
|
// process to probe a PID. The Windows branch used to shell out to `tasklist`,
|
||||||
|
// which Windows gives its own console window — flashing a conhost.exe window
|
||||||
|
// every watchdog tick (default 60s) for the whole session. signal-0 is a pure
|
||||||
|
// syscall on all platforms (OpenProcess on Windows), so the source must not
|
||||||
|
// reintroduce tasklist or a spawn in this probe.
|
||||||
|
test('isProcessAlive source spawns nothing (no Windows console flash, #1952)', () => {
|
||||||
|
const src = fs.readFileSync(
|
||||||
|
path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'error-handling.ts'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const start = src.indexOf('export function isProcessAlive');
|
||||||
|
expect(start).toBeGreaterThan(-1);
|
||||||
|
const body = src.slice(start, src.indexOf('\n}', start));
|
||||||
|
expect(body).not.toMatch(/tasklist/i);
|
||||||
|
expect(body).not.toMatch(/spawnSync|spawn\(/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue