fix: use safeUnlinkQuiet in shutdown and cleanup paths

Shutdown, emergency cleanup, and disconnect paths should never throw
on file deletion failures. Switched from safeUnlink (throws on EPERM)
to safeUnlinkQuiet (swallows all errors) in these best-effort paths.
Normal operation paths (startup, lock release) keep safeUnlink.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-04-09 05:17:54 -10:00
parent 6a857d41ba
commit ad38e006f6
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 11 additions and 11 deletions

View File

@ -11,7 +11,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { safeUnlink, safeKill, isProcessAlive } from './error-handling'; import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling';
import { resolveConfig, ensureStateDir, readVersionHash } from './config'; import { resolveConfig, ensureStateDir, readVersionHash } from './config';
const config = resolveConfig(); const config = resolveConfig();
@ -812,11 +812,11 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
// Clean up Chromium profile locks (can persist after crashes) // Clean up Chromium profile locks (can persist after crashes)
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) { for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlink(path.join(profileDir, lockFile)); safeUnlinkQuiet(path.join(profileDir, lockFile));
} }
// Delete stale state file // Delete stale state file
safeUnlink(config.stateFile); safeUnlinkQuiet(config.stateFile);
console.log('Launching headed Chromium with extension + sidebar agent...'); console.log('Launching headed Chromium with extension + sidebar agent...');
try { try {
@ -945,9 +945,9 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
// Clean profile locks and state file // Clean profile locks and state file
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile'); const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) { for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlink(path.join(profileDir, lockFile)); safeUnlinkQuiet(path.join(profileDir, lockFile));
} }
safeUnlink(config.stateFile); safeUnlinkQuiet(config.stateFile);
console.log('Disconnected (server was unresponsive — force cleaned).'); console.log('Disconnected (server was unresponsive — force cleaned).');
process.exit(0); process.exit(0);
} }

View File

@ -38,7 +38,7 @@ import { emitActivity, subscribe, getActivityAfter, getActivityHistory, getSubsc
import { inspectElement, modifyStyle, resetModifications, getModificationHistory, detachSession, type InspectorResult } from './cdp-inspector'; import { inspectElement, modifyStyle, resetModifications, getModificationHistory, detachSession, type InspectorResult } from './cdp-inspector';
// Bun.spawn used instead of child_process.spawn (compiled bun binaries // Bun.spawn used instead of child_process.spawn (compiled bun binaries
// fail posix_spawn on all executables including /bin/bash) // fail posix_spawn on all executables including /bin/bash)
import { safeUnlink, safeKill } from './error-handling'; import { safeUnlink, safeUnlinkQuiet, safeKill } from './error-handling';
import * as fs from 'fs'; import * as fs from 'fs';
import * as net from 'net'; import * as net from 'net';
import * as path from 'path'; import * as path from 'path';
@ -1188,11 +1188,11 @@ async function shutdown() {
// Clean up Chromium profile locks (prevent SingletonLock on next launch) // Clean up Chromium profile locks (prevent SingletonLock on next launch)
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile'); const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) { for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlink(path.join(profileDir, lockFile)); safeUnlinkQuiet(path.join(profileDir, lockFile));
} }
// Clean up state file // Clean up state file
safeUnlink(config.stateFile); safeUnlinkQuiet(config.stateFile);
process.exit(0); process.exit(0);
} }
@ -1204,7 +1204,7 @@ process.on('SIGINT', shutdown);
// Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check. // Defense-in-depth — primary cleanup is the CLI's stale-state detection via health check.
if (process.platform === 'win32') { if (process.platform === 'win32') {
process.on('exit', () => { process.on('exit', () => {
safeUnlink(config.stateFile); safeUnlinkQuiet(config.stateFile);
}); });
} }
@ -1223,9 +1223,9 @@ function emergencyCleanup() {
// Clean Chromium profile locks // Clean Chromium profile locks
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile'); const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) { for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlink(path.join(profileDir, lockFile)); safeUnlinkQuiet(path.join(profileDir, lockFile));
} }
safeUnlink(config.stateFile); safeUnlinkQuiet(config.stateFile);
} }
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
console.error('[browse] FATAL uncaught exception:', err.message); console.error('[browse] FATAL uncaught exception:', err.message);