From acf9fd690a17878733150cfe0a4609aa09aed014 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Fri, 22 May 2026 12:04:28 -0400 Subject: [PATCH] fix: prevent Docker dashboard from orphaning CLIProxy --- .../config-command-supervisord.test.ts | 48 ++++++++++++++++++ src/commands/config-command.ts | 49 ++++++++++++------- 2 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 src/commands/__tests__/config-command-supervisord.test.ts diff --git a/src/commands/__tests__/config-command-supervisord.test.ts b/src/commands/__tests__/config-command-supervisord.test.ts new file mode 100644 index 00000000..3a199524 --- /dev/null +++ b/src/commands/__tests__/config-command-supervisord.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'bun:test'; + +import { handleConfigCommand } from '../config-command'; + +describe('handleConfigCommand Docker supervisord lifecycle', () => { + it('does not spawn CLIProxy directly when supervisord owns the runtime', async () => { + let ensureCalled = false; + const logs: string[] = []; + const originalLog = console.log; + + console.log = (message?: unknown) => { + logs.push(String(message ?? '')); + }; + + try { + await handleConfigCommand([], { + getPort: async () => 3000, + openBrowser: async () => ({}), + startServer: async () => + ({ + server: { address: () => ({ address: '0.0.0.0' }) }, + wss: {}, + cleanup: () => {}, + }) as never, + setupGracefulShutdown: () => {}, + ensureCliproxyService: async () => { + ensureCalled = true; + return { started: false, alreadyRunning: false, port: 8317 }; + }, + isRunningUnderSupervisord: () => true, + getDashboardAuthConfig: () => ({ enabled: true }), + initUI: async () => {}, + header: (text: string) => text, + ok: (text: string) => text, + info: (text: string) => text, + warn: (text: string) => text, + fail: (text: string) => text, + resolveNamedCommand: () => undefined, + configSubcommandRoutes: [], + }); + } finally { + console.log = originalLog; + } + + expect(ensureCalled).toBe(false); + expect(logs).toContain('CLIProxy is managed by supervisord on port 8317'); + }); +}); diff --git a/src/commands/config-command.ts b/src/commands/config-command.ts index 91aa4ed8..b360badf 100644 --- a/src/commands/config-command.ts +++ b/src/commands/config-command.ts @@ -12,6 +12,7 @@ import { startServer } from '../web-server'; import { setupGracefulShutdown } from '../web-server/shutdown'; import { ensureCliproxyService } from '../cliproxy/service-manager'; import { resolveLifecyclePort } from '../cliproxy/config/port-manager'; +import { isRunningUnderSupervisord } from '../docker/supervisord-lifecycle'; import { initUI, header, ok, info, warn, fail } from '../utils/ui'; import { resolveNamedCommand, type NamedCommandRoute } from './named-command-router'; import { @@ -63,6 +64,7 @@ interface ConfigCommandDependencies { startServer: typeof startServer; setupGracefulShutdown: typeof setupGracefulShutdown; ensureCliproxyService: typeof ensureCliproxyService; + isRunningUnderSupervisord?: typeof isRunningUnderSupervisord; getDashboardAuthConfig: typeof getDashboardAuthConfig; initUI: typeof initUI; header: typeof header; @@ -80,6 +82,7 @@ const defaultConfigCommandDependencies: ConfigCommandDependencies = { startServer, setupGracefulShutdown, ensureCliproxyService, + isRunningUnderSupervisord, getDashboardAuthConfig, initUI, header, @@ -135,29 +138,37 @@ export async function handleConfigCommand( console.log(deps.header('CCS Config Dashboard')); console.log(''); - // Ensure CLIProxy service is running for dashboard features - console.log(deps.info('Starting CLIProxy service...')); - const cliproxyResult = await deps.ensureCliproxyService(resolveLifecyclePort(), verbose); - logger.info('cliproxy.ensure_result', 'Config command checked CLIProxy availability', { - started: cliproxyResult.started, - alreadyRunning: cliproxyResult.alreadyRunning, - configRegenerated: cliproxyResult.configRegenerated, - port: cliproxyResult.port || null, - error: cliproxyResult.error || null, - }); + const lifecyclePort = resolveLifecyclePort(); + if (deps.isRunningUnderSupervisord?.() ?? false) { + logger.info('cliproxy.supervisord_managed', 'Skipping direct CLIProxy startup in Docker', { + port: lifecyclePort, + }); + console.log(deps.info(`CLIProxy is managed by supervisord on port ${lifecyclePort}`)); + } else { + // Ensure CLIProxy service is running for dashboard features + console.log(deps.info('Starting CLIProxy service...')); + const cliproxyResult = await deps.ensureCliproxyService(lifecyclePort, verbose); + logger.info('cliproxy.ensure_result', 'Config command checked CLIProxy availability', { + started: cliproxyResult.started, + alreadyRunning: cliproxyResult.alreadyRunning, + configRegenerated: cliproxyResult.configRegenerated, + port: cliproxyResult.port || null, + error: cliproxyResult.error || null, + }); - if (cliproxyResult.started) { - if (cliproxyResult.alreadyRunning) { - console.log(deps.ok(`CLIProxy already running on port ${cliproxyResult.port}`)); - if (cliproxyResult.configRegenerated) { - console.log(deps.warn('Config updated - restart CLIProxy to apply changes')); + if (cliproxyResult.started) { + if (cliproxyResult.alreadyRunning) { + console.log(deps.ok(`CLIProxy already running on port ${cliproxyResult.port}`)); + if (cliproxyResult.configRegenerated) { + console.log(deps.warn('Config updated - restart CLIProxy to apply changes')); + } + } else { + console.log(deps.ok(`CLIProxy started on port ${cliproxyResult.port}`)); } } else { - console.log(deps.ok(`CLIProxy started on port ${cliproxyResult.port}`)); + console.log(deps.warn(`CLIProxy not available: ${cliproxyResult.error}`)); + console.log(deps.info('Dashboard will work but Control Panel/Stats may be limited')); } - } else { - console.log(deps.warn(`CLIProxy not available: ${cliproxyResult.error}`)); - console.log(deps.info('Dashboard will work but Control Panel/Stats may be limited')); } console.log('');