fix(docker): wrap session registration in try-catch and narrow ETXTBSY guard

Session registration in spawn handler can throw on lock contention or
disk errors — wrap in try-catch to prevent silent proxy-untracked state.

Narrow EBUSY catch to ETXTBSY only since EBUSY on non-Linux platforms
can mean mount point or directory in use, not running binary. Fix
misleading "skip" comment to say "abort".
This commit is contained in:
Tam Nhu Tran
2026-03-28 19:00:54 -04:00
parent 7d410b26d0
commit e8b7ac730f
2 changed files with 16 additions and 7 deletions
+8 -5
View File
@@ -36,8 +36,8 @@ export async function downloadAndInstall(
fs.mkdirSync(config.binPath, { recursive: true }); fs.mkdirSync(config.binPath, { recursive: true });
// Delete existing binary before install to prevent mismatched binaries. // Delete existing binary before install to prevent mismatched binaries.
// Skip if binary is currently running (ETXTBSY) — happens in Docker when // Abort if binary is currently running (ETXTBSY) — cannot replace in-use binary.
// the dashboard tries to update while bootstrap's instance is active. // Happens in Docker when dashboard tries to update while bootstrap's instance is active.
const existingBinary = path.join(config.binPath, getExecutableName(backend)); const existingBinary = path.join(config.binPath, getExecutableName(backend));
if (fs.existsSync(existingBinary)) { if (fs.existsSync(existingBinary)) {
try { try {
@@ -46,9 +46,12 @@ export async function downloadAndInstall(
} catch (error: unknown) { } catch (error: unknown) {
const code = const code =
error instanceof Error && 'code' in error ? (error as { code: string }).code : ''; error instanceof Error && 'code' in error ? (error as { code: string }).code : '';
if (code === 'ETXTBSY' || code === 'EBUSY') { // ETXTBSY: Linux-specific error when unlinking a running executable.
// EBUSY on Windows may mean something different (mount point, etc.),
// so only treat ETXTBSY as "binary in use" to avoid misleading messages.
if (code === 'ETXTBSY') {
if (verbose) if (verbose)
console.error(`[cliproxy] Binary is running, skipping update: ${existingBinary}`); console.error(`[cliproxy] Binary is running, cannot replace: ${existingBinary}`);
throw new Error( throw new Error(
'CLIProxy binary is currently running and cannot be replaced. Restart the container to apply the update.' 'CLIProxy binary is currently running and cannot be replaced. Restart the container to apply the update.'
); );
@@ -120,7 +123,7 @@ export function deleteBinary(binPath: string, verbose = false, backend?: CLIProx
} catch (error: unknown) { } catch (error: unknown) {
const code = const code =
error instanceof Error && 'code' in error ? (error as { code: string }).code : ''; error instanceof Error && 'code' in error ? (error as { code: string }).code : '';
if (code === 'ETXTBSY' || code === 'EBUSY') { if (code === 'ETXTBSY') {
throw new Error('CLIProxy binary is currently running and cannot be deleted.'); throw new Error('CLIProxy binary is currently running and cannot be deleted.');
} }
throw error; throw error;
+8 -2
View File
@@ -38,8 +38,14 @@ async function runCliproxy(): Promise<number> {
let sessionId: string | undefined; let sessionId: string | undefined;
child.on('spawn', () => { child.on('spawn', () => {
if (!child.pid) return; if (!child.pid) return;
const version = getInstalledCliproxyVersion(); try {
sessionId = registerSession(CLIPROXY_DEFAULT_PORT, child.pid, version, 'plus'); const version = getInstalledCliproxyVersion();
sessionId = registerSession(CLIPROXY_DEFAULT_PORT, child.pid, version, 'plus');
} catch (err) {
console.error(
`[cliproxy] Failed to register session lock: ${err instanceof Error ? err.message : String(err)}`
);
}
}); });
child.on('error', reject); child.on('error', reject);