diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 0f6210a2..f5129547 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -11,6 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; +import { safeUnlink, safeKill, isProcessAlive } from './error-handling'; import { resolveConfig, ensureStateDir, readVersionHash } from './config'; const config = resolveConfig(); @@ -103,27 +104,7 @@ function readState(): ServerState | null { } } -function isProcessAlive(pid: number): boolean { - if (IS_WINDOWS) { - // Bun's compiled binary can't signal Windows PIDs (always throws ESRCH). - // Use tasklist as a fallback. Only for one-shot calls — too slow for polling loops. - 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 { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} +// isProcessAlive is imported from ./error-handling /** * HTTP health check — definitive proof the server is alive and responsive. @@ -153,7 +134,9 @@ async function killServer(pid: number): Promise { ['taskkill', '/PID', String(pid), '/T', '/F'], { stdout: 'pipe', stderr: 'pipe', timeout: 5000 } ); - } catch {} + } catch (err: any) { + if (err?.code !== 'ENOENT') throw err; + } const deadline = Date.now() + 2000; while (Date.now() < deadline && isProcessAlive(pid)) { await Bun.sleep(100); @@ -161,7 +144,7 @@ async function killServer(pid: number): Promise { return; } - try { process.kill(pid, 'SIGTERM'); } catch { return; } + safeKill(pid, 'SIGTERM'); // Wait up to 2s for graceful shutdown const deadline = Date.now() + 2000; @@ -171,7 +154,7 @@ async function killServer(pid: number): Promise { // Force kill if still alive if (isProcessAlive(pid)) { - try { process.kill(pid, 'SIGKILL'); } catch {} + safeKill(pid, 'SIGKILL'); } } @@ -197,10 +180,10 @@ function cleanupLegacyState(): void { }); const cmd = check.stdout.toString().trim(); if (cmd.includes('bun') || cmd.includes('server.ts')) { - try { process.kill(data.pid, 'SIGTERM'); } catch {} + safeKill(data.pid, 'SIGTERM'); } } - fs.unlinkSync(fullPath); + safeUnlink(fullPath); } catch { // Best effort — skip files we can't parse or clean up } @@ -210,7 +193,7 @@ function cleanupLegacyState(): void { f.startsWith('browse-console') || f.startsWith('browse-network') || f.startsWith('browse-dialog') ); for (const file of logFiles) { - try { fs.unlinkSync(`/tmp/${file}`); } catch {} + safeUnlink(`/tmp/${file}`); } } catch { // /tmp read failed — skip legacy cleanup @@ -222,8 +205,8 @@ async function startServer(extraEnv?: Record): Promise void) | null { const fd = fs.openSync(lockPath, 'wx'); fs.writeSync(fd, `${process.pid}\n`); fs.closeSync(fd); - return () => { try { fs.unlinkSync(lockPath); } catch {} }; + return () => { safeUnlink(lockPath); }; } catch { // Lock already held — check if the holder is still alive try { @@ -469,7 +452,9 @@ function isNgrokAvailable(): boolean { try { const content = fs.readFileSync(conf, 'utf-8'); if (content.includes('authtoken:')) return true; - } catch {} + } catch (err: any) { + if (err?.code !== 'ENOENT') throw err; + } } return false; @@ -797,10 +782,10 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: // Kill ANY existing server (SIGTERM → wait 2s → SIGKILL) if (existingState && isProcessAlive(existingState.pid)) { - try { process.kill(existingState.pid, 'SIGTERM'); } catch {} + safeKill(existingState.pid, 'SIGTERM'); await new Promise(resolve => setTimeout(resolve, 2000)); if (isProcessAlive(existingState.pid)) { - try { process.kill(existingState.pid, 'SIGKILL'); } catch {} + safeKill(existingState.pid, 'SIGKILL'); await new Promise(resolve => setTimeout(resolve, 1000)); } } @@ -814,24 +799,24 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: const lockTarget = fs.readlinkSync(singletonLock); // e.g. "hostname-12345" const orphanPid = parseInt(lockTarget.split('-').pop() || '', 10); if (orphanPid && isProcessAlive(orphanPid)) { - try { process.kill(orphanPid, 'SIGTERM'); } catch {} + safeKill(orphanPid, 'SIGTERM'); await new Promise(resolve => setTimeout(resolve, 1000)); if (isProcessAlive(orphanPid)) { - try { process.kill(orphanPid, 'SIGKILL'); } catch {} + safeKill(orphanPid, 'SIGKILL'); await new Promise(resolve => setTimeout(resolve, 500)); } } - } catch { - // No lock symlink or not readable — nothing to kill + } catch (err: any) { + if (err?.code !== 'ENOENT' && err?.code !== 'EINVAL') throw err; } // Clean up Chromium profile locks (can persist after crashes) for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) { - try { fs.unlinkSync(path.join(profileDir, lockFile)); } catch {} + safeUnlink(path.join(profileDir, lockFile)); } // Delete stale state file - try { fs.unlinkSync(config.stateFile); } catch {} + safeUnlink(config.stateFile); console.log('Launching headed Chromium with extension + sidebar agent...'); try { @@ -877,7 +862,9 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: try { fs.mkdirSync(path.dirname(agentQueue), { recursive: true, mode: 0o700 }); fs.writeFileSync(agentQueue, '', { mode: 0o600 }); - } catch {} + } catch (err: any) { + if (err?.code !== 'EACCES') throw err; + } // Resolve browse binary path the same way — execPath-relative let browseBin = path.resolve(__dirname, '..', 'dist', 'browse'); @@ -891,7 +878,9 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: try { const { spawnSync } = require('child_process'); spawnSync('pkill', ['-f', 'sidebar-agent\\.ts'], { stdio: 'ignore', timeout: 3000 }); - } catch {} + } catch (err: any) { + if (err?.code !== 'ENOENT') throw err; + } const agentProc = Bun.spawn(['bun', 'run', agentScript], { cwd: config.projectDir, @@ -947,18 +936,18 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: } // Force kill + cleanup if (isProcessAlive(existingState.pid)) { - try { process.kill(existingState.pid, 'SIGTERM'); } catch {} + safeKill(existingState.pid, 'SIGTERM'); await new Promise(resolve => setTimeout(resolve, 2000)); if (isProcessAlive(existingState.pid)) { - try { process.kill(existingState.pid, 'SIGKILL'); } catch {} + safeKill(existingState.pid, 'SIGKILL'); } } // Clean profile locks and state file const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile'); for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) { - try { fs.unlinkSync(path.join(profileDir, lockFile)); } catch {} + safeUnlink(path.join(profileDir, lockFile)); } - try { fs.unlinkSync(config.stateFile); } catch {} + safeUnlink(config.stateFile); console.log('Disconnected (server was unresponsive — force cleaned).'); process.exit(0); }