Files
gstack/browse/src/error-handling.ts
T
270f1a038a fix(browse): windowsHide on every Windows-reachable spawn (#1835)
Console windows flashed (and stole focus) on every daemon relaunch,
taskkill, tasklist poll, and powershell DPAPI call — node-level spawns
default windowsHide to false. Covered: the node -e launcher (outer spawnSync
AND the inner detached daemon spawn inside the launcher string), the
dev-mode bun fallback, killServer's taskkill, isProcessAlive's tasklist,
and cookie-import's powershell + tasklist. The Bun-polyfill shims were
covered by absorbed PRs #2523 + #2539 (thanks @jwilk-hrep,
@jerrynicholsai); this closes the sites those PRs didn't reach. The icacls
sites land with the #1605 DACL commit alongside the static tripwire that
pins all of them. R8's planned spawnHidden() helper is deliberately NOT
built: the polyfill default plus the tripwire achieve the no-drift goal
without indirection over seven heterogeneous call shapes. The polyfill +
spawn-hide tests join the Windows CI shard.

Ported from time-attack/gstack (GStack 2).

Co-authored-by: Sina Matian <sina@time-attack.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:53:33 -07:00

59 lines
1.9 KiB
TypeScript

/**
* Shared error-handling utilities for browse server and CLI.
*
* Each wrapper uses selective catches (checks err.code) to avoid masking
* unexpected errors. Empty catches would be flagged by slop-scan.
*/
import * as fs from 'fs';
const IS_WINDOWS = process.platform === 'win32';
// ─── Filesystem ────────────────────────────────────────────────
/** Remove a file, ignoring ENOENT (already gone). Rethrows other errors. */
export function safeUnlink(filePath: string): void {
try {
fs.unlinkSync(filePath);
} catch (err: any) {
if (err?.code !== 'ENOENT') throw err;
}
}
/** Remove a file, ignoring ALL errors. Use only in best-effort cleanup (shutdown, emergency). */
export function safeUnlinkQuiet(filePath: string): void {
try { fs.unlinkSync(filePath); } catch {}
}
// ─── Process ───────────────────────────────────────────────────
/** Send a signal to a process, ignoring ESRCH (already dead). Rethrows other errors. */
export function safeKill(pid: number, signal: NodeJS.Signals | number): void {
try {
process.kill(pid, signal);
} catch (err: any) {
if (err?.code !== 'ESRCH') throw err;
}
}
/** Check if a PID is alive. Pure boolean probe — returns false for ALL errors. */
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, windowsHide: true }
);
return result.stdout.toString().includes(`"${pid}"`);
} catch {
return false;
}
}
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}