mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-07 05:56:41 +02:00
refactor: add error-handling utility module with selective catches
safeUnlink (ignores ENOENT), safeKill (ignores ESRCH), isProcessAlive (extracted from cli.ts with Windows support), and json() Response helper. All catches check err.code and rethrow unexpected errors instead of swallowing silently. Unit tests cover happy path + error code paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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. Returns false for ESRCH, rethrows EPERM and others. */
|
||||
export 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.
|
||||
// Bun.spawnSync may throw if tasklist binary is missing (ENOENT)
|
||||
const result = Bun.spawnSync(
|
||||
['tasklist', '/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
|
||||
{ stdout: 'pipe', stderr: 'pipe', timeout: 3000 }
|
||||
);
|
||||
return result.stdout.toString().includes(`"${pid}"`);
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'ESRCH') return false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HTTP ──────────────────────────────────────────────────────
|
||||
|
||||
/** JSON Response constructor shorthand for Bun.serve routes. */
|
||||
export function json(data: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { safeUnlink, safeKill, isProcessAlive, json } from '../src/error-handling';
|
||||
|
||||
describe('safeUnlink', () => {
|
||||
test('removes an existing file', () => {
|
||||
const tmp = path.join(os.tmpdir(), `test-safeUnlink-${Date.now()}`);
|
||||
fs.writeFileSync(tmp, 'hello');
|
||||
safeUnlink(tmp);
|
||||
expect(fs.existsSync(tmp)).toBe(false);
|
||||
});
|
||||
|
||||
test('ignores ENOENT (file does not exist)', () => {
|
||||
expect(() => safeUnlink('/tmp/nonexistent-file-' + Date.now())).not.toThrow();
|
||||
});
|
||||
|
||||
test('rethrows non-ENOENT errors', () => {
|
||||
// Attempt to unlink a directory — throws EPERM/EISDIR
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-safeUnlink-'));
|
||||
expect(() => safeUnlink(dir)).toThrow();
|
||||
fs.rmdirSync(dir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeKill', () => {
|
||||
test('sends signal to a running process', () => {
|
||||
// signal 0 is a no-op existence check — safe to send to self
|
||||
expect(() => safeKill(process.pid, 0)).not.toThrow();
|
||||
});
|
||||
|
||||
test('ignores ESRCH (process does not exist)', () => {
|
||||
// PID 99999999 is extremely unlikely to exist
|
||||
expect(() => safeKill(99999999, 0)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProcessAlive', () => {
|
||||
test('returns true for current process', () => {
|
||||
expect(isProcessAlive(process.pid)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for non-existent process', () => {
|
||||
expect(isProcessAlive(99999999)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('json', () => {
|
||||
test('returns Response with JSON body and correct Content-Type', async () => {
|
||||
const resp = json({ ok: true });
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.headers.get('Content-Type')).toBe('application/json');
|
||||
const body = await resp.json();
|
||||
expect(body).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
test('uses custom status code', async () => {
|
||||
const resp = json({ error: 'not found' }, 404);
|
||||
expect(resp.status).toBe(404);
|
||||
const body = await resp.json();
|
||||
expect(body).toEqual({ error: 'not found' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user