mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
refactor: replace defensive try/catches in server.ts with utilities
Replace ~12 try/catch sites with safeUnlink/safeKill calls in shutdown, emergencyCleanup, killAgent, and log cleanup. Convert empty catches to selective catches with error code checks. Remove needless welcome page try/catches (fs.existsSync doesn't need wrapping). Reduces slop-scan empty-catch locations from 11 to 8 and error-swallowing from 24 to 18. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+38
-44
@@ -38,6 +38,7 @@ import { emitActivity, subscribe, getActivityAfter, getActivityHistory, getSubsc
|
||||
import { inspectElement, modifyStyle, resetModifications, getModificationHistory, detachSession, type InspectorResult } from './cdp-inspector';
|
||||
// Bun.spawn used instead of child_process.spawn (compiled bun binaries
|
||||
// fail posix_spawn on all executables including /bin/bash)
|
||||
import { safeUnlink, safeKill } from './error-handling';
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
@@ -233,7 +234,9 @@ function findBrowseBin(): string {
|
||||
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
|
||||
];
|
||||
for (const c of candidates) {
|
||||
try { if (fs.existsSync(c)) return c; } catch {}
|
||||
try { if (fs.existsSync(c)) return c; } catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
return 'browse'; // fallback to PATH
|
||||
}
|
||||
@@ -265,13 +268,17 @@ function findClaudeBin(): string | null {
|
||||
const p = proc.stdout.toString().trim();
|
||||
if (p) candidates.unshift(p);
|
||||
}
|
||||
} catch {}
|
||||
} catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
for (const c of candidates) {
|
||||
try {
|
||||
if (!fs.existsSync(c)) continue;
|
||||
// Resolve symlinks — posix_spawn can fail on symlinks in compiled bun binaries
|
||||
return fs.realpathSync(c);
|
||||
} catch {}
|
||||
} catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -465,8 +472,8 @@ function listSessions(): Array<SidebarSession & { chatLines: number }> {
|
||||
try {
|
||||
const session = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, d, 'session.json'), 'utf-8'));
|
||||
let chatLines = 0;
|
||||
try { chatLines = fs.readFileSync(path.join(SESSIONS_DIR, d, 'chat.jsonl'), 'utf-8').split('\n').filter(Boolean).length; } catch {
|
||||
// Expected: no chat file yet
|
||||
try { chatLines = fs.readFileSync(path.join(SESSIONS_DIR, d, 'chat.jsonl'), 'utf-8').split('\n').filter(Boolean).length; } catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
return { ...session, chatLines };
|
||||
} catch { return null; }
|
||||
@@ -602,7 +609,9 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null, forTabId
|
||||
try {
|
||||
fs.mkdirSync(gstackDir, { recursive: true, mode: 0o700 });
|
||||
fs.appendFileSync(agentQueue, entry + '\n');
|
||||
try { fs.chmodSync(agentQueue, 0o600); } catch {}
|
||||
try { fs.chmodSync(agentQueue, 0o600); } catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
} catch (err: any) {
|
||||
addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_error', error: `Failed to queue: ${err.message}` });
|
||||
agentStatus = 'idle';
|
||||
@@ -617,12 +626,11 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null, forTabId
|
||||
|
||||
function killAgent(targetTabId?: number | null): void {
|
||||
if (agentProcess) {
|
||||
try { agentProcess.kill('SIGTERM'); } catch (err: any) {
|
||||
console.warn('[browse] Failed to SIGTERM agent:', err.message);
|
||||
const pid = agentProcess.pid;
|
||||
if (pid) {
|
||||
safeKill(pid, 'SIGTERM');
|
||||
setTimeout(() => { safeKill(pid, 'SIGKILL'); }, 3000);
|
||||
}
|
||||
setTimeout(() => { try { agentProcess?.kill('SIGKILL'); } catch (err: any) {
|
||||
console.warn('[browse] Failed to SIGKILL agent:', err.message);
|
||||
} }, 3000);
|
||||
}
|
||||
// Signal the sidebar-agent worker to cancel via a per-tab cancel file.
|
||||
// Using per-tab files prevents race conditions where one agent's cancel
|
||||
@@ -631,7 +639,12 @@ function killAgent(targetTabId?: number | null): void {
|
||||
const cancelDir = path.join(process.env.HOME || '/tmp', '.gstack');
|
||||
const tabId = targetTabId ?? agentTabId ?? 0;
|
||||
const cancelFile = path.join(cancelDir, `sidebar-agent-cancel-${tabId}`);
|
||||
try { fs.writeFileSync(cancelFile, Date.now().toString()); } catch {}
|
||||
try {
|
||||
fs.mkdirSync(cancelDir, { recursive: true });
|
||||
fs.writeFileSync(cancelFile, Date.now().toString());
|
||||
} catch (err: any) {
|
||||
if (err?.code !== 'EACCES' && err?.code !== 'ENOENT') throw err;
|
||||
}
|
||||
agentProcess = null;
|
||||
agentStartTime = null;
|
||||
currentMessage = null;
|
||||
@@ -1175,15 +1188,11 @@ async function shutdown() {
|
||||
// Clean up Chromium profile locks (prevent SingletonLock on next launch)
|
||||
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 (err: any) {
|
||||
console.debug('[browse] Lock cleanup:', lockFile, err.message);
|
||||
}
|
||||
safeUnlink(path.join(profileDir, lockFile));
|
||||
}
|
||||
|
||||
// Clean up state file
|
||||
try { fs.unlinkSync(config.stateFile); } catch (err: any) {
|
||||
console.debug('[browse] State file cleanup:', err.message);
|
||||
}
|
||||
safeUnlink(config.stateFile);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -1195,9 +1204,7 @@ process.on('SIGINT', shutdown);
|
||||
// Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
|
||||
if (process.platform === 'win32') {
|
||||
process.on('exit', () => {
|
||||
try { fs.unlinkSync(config.stateFile); } catch {
|
||||
// Best-effort on exit
|
||||
}
|
||||
safeUnlink(config.stateFile);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1216,13 +1223,9 @@ function emergencyCleanup() {
|
||||
// Clean Chromium profile locks
|
||||
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 (err: any) {
|
||||
console.debug('[browse] Emergency lock cleanup:', lockFile, err.message);
|
||||
}
|
||||
}
|
||||
try { fs.unlinkSync(config.stateFile); } catch (err: any) {
|
||||
console.debug('[browse] Emergency state cleanup:', err.message);
|
||||
safeUnlink(path.join(profileDir, lockFile));
|
||||
}
|
||||
safeUnlink(config.stateFile);
|
||||
}
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[browse] FATAL uncaught exception:', err.message);
|
||||
@@ -1238,15 +1241,9 @@ process.on('unhandledRejection', (err: any) => {
|
||||
// ─── Start ─────────────────────────────────────────────────────
|
||||
async function start() {
|
||||
// Clear old log files
|
||||
try { fs.unlinkSync(CONSOLE_LOG_PATH); } catch (err: any) {
|
||||
if (err.code !== 'ENOENT') console.debug('[browse] Log cleanup console:', err.message);
|
||||
}
|
||||
try { fs.unlinkSync(NETWORK_LOG_PATH); } catch (err: any) {
|
||||
if (err.code !== 'ENOENT') console.debug('[browse] Log cleanup network:', err.message);
|
||||
}
|
||||
try { fs.unlinkSync(DIALOG_LOG_PATH); } catch (err: any) {
|
||||
if (err.code !== 'ENOENT') console.debug('[browse] Log cleanup dialog:', err.message);
|
||||
}
|
||||
safeUnlink(CONSOLE_LOG_PATH);
|
||||
safeUnlink(NETWORK_LOG_PATH);
|
||||
safeUnlink(DIALOG_LOG_PATH);
|
||||
|
||||
const port = await findPort();
|
||||
|
||||
@@ -1282,15 +1279,11 @@ async function start() {
|
||||
const slug = process.env.GSTACK_SLUG || 'unknown';
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
||||
const projectWelcome = `${homeDir}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
|
||||
try { if (require('fs').existsSync(projectWelcome)) return projectWelcome; } catch (err: any) {
|
||||
console.warn('[browse] Error checking project welcome page:', err.message);
|
||||
}
|
||||
if (fs.existsSync(projectWelcome)) return projectWelcome;
|
||||
// Fallback: built-in welcome page from gstack install
|
||||
const skillRoot = process.env.GSTACK_SKILL_ROOT || `${homeDir}/.claude/skills/gstack`;
|
||||
const builtinWelcome = `${skillRoot}/browse/src/welcome.html`;
|
||||
try { if (require('fs').existsSync(builtinWelcome)) return builtinWelcome; } catch (err: any) {
|
||||
console.warn('[browse] Error checking builtin welcome page:', err.message);
|
||||
}
|
||||
if (fs.existsSync(builtinWelcome)) return builtinWelcome;
|
||||
return null;
|
||||
})();
|
||||
if (welcomePath) {
|
||||
@@ -1814,8 +1807,9 @@ async function start() {
|
||||
chatBuffer = [];
|
||||
chatNextId = 0;
|
||||
if (sidebarSession) {
|
||||
try { fs.writeFileSync(path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl'), '', { mode: 0o600 }); } catch (err: any) {
|
||||
console.error('[browse] Failed to clear chat file:', err.message);
|
||||
const chatFile = path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl');
|
||||
try { fs.writeFileSync(chatFile, '', { mode: 0o600 }); } catch (err: any) {
|
||||
if (err?.code !== 'ENOENT') console.error('[browse] Failed to clear chat file:', err.message);
|
||||
}
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
Reference in New Issue
Block a user