diff --git a/src/agent/run.ts b/src/agent/run.ts index 5dd9f3c1..5fd53ecc 100644 --- a/src/agent/run.ts +++ b/src/agent/run.ts @@ -6,7 +6,7 @@ import { HOST_WORKSPACE_NAME } from '../shared/client-types'; import { DEFAULT_AGENT_PORT } from '../shared/constants'; import { WorkspaceManager } from '../workspace/manager'; import { containerRunning, getContainerName } from '../docker'; -import { startEagerImagePull } from '../docker/eager-pull'; +import { startEagerImagePull, stopEagerImagePull } from '../docker/eager-pull'; import { TerminalWebSocketServer } from '../terminal/websocket'; import { ChatWebSocketServer } from '../chat/websocket'; import { OpencodeWebSocketServer } from '../chat/opencode-websocket'; @@ -297,17 +297,39 @@ export async function startAgent(options: StartAgentOptions = {}): Promise startEagerImagePull(); }); + let isShuttingDown = false; + const shutdown = async () => { + if (isShuttingDown) { + console.log('[agent] Force exit'); + process.exit(0); + } + isShuttingDown = true; + console.log('[agent] Shutting down...'); + + const forceExitTimeout = setTimeout(() => { + console.log('[agent] Force exit after timeout'); + process.exit(0); + }, 3000); + forceExitTimeout.unref(); + + stopEagerImagePull(); fileWatcher.stop(); + if (tailscaleServeActive) { console.log('[agent] Stopping Tailscale Serve...'); await stopTailscaleServe(); } + chatServer.close(); opencodeServer.close(); terminalServer.close(); + + server.closeAllConnections(); + server.close(() => { + clearTimeout(forceExitTimeout); console.log('[agent] Server closed'); process.exit(0); }); diff --git a/src/docker/eager-pull.ts b/src/docker/eager-pull.ts index f3839e0c..91bc3032 100644 --- a/src/docker/eager-pull.ts +++ b/src/docker/eager-pull.ts @@ -7,6 +7,7 @@ const MAX_RETRIES = 10; let pullInProgress = false; let pullComplete = false; +let abortController: AbortController | null = null; async function isDockerAvailable(): Promise { try { @@ -44,8 +45,15 @@ export async function startEagerImagePull(): Promise { } pullInProgress = true; + abortController = new AbortController(); + const signal = abortController.signal; const attemptPull = async (attempt: number): Promise => { + if (signal.aborted) { + pullInProgress = false; + return; + } + if (attempt > MAX_RETRIES) { console.log('[agent] Max retries reached for image pull - giving up background pull'); pullInProgress = false; @@ -58,7 +66,8 @@ export async function startEagerImagePull(): Promise { if (attempt === 1) { console.log('[agent] Docker not available - will retry in background'); } - setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + timer.unref(); return; } @@ -67,14 +76,23 @@ export async function startEagerImagePull(): Promise { if (success) { pullComplete = true; pullInProgress = false; - } else { - setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + } else if (!signal.aborted) { + const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + timer.unref(); } }; attemptPull(1); } +export function stopEagerImagePull(): void { + if (abortController) { + abortController.abort(); + abortController = null; + } + pullInProgress = false; +} + export function isImagePullComplete(): boolean { return pullComplete; }