From 1c8da30905f90a3d383d5e0940651fa8e2c3c8de Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 12 Feb 2026 00:19:26 -0800 Subject: [PATCH] fix(backend): only kill processes started by TUI Track PIDs of backends started by /backend command and only kill those specific PIDs. Previous implementation used pkill which would kill all Ollama/llama-server processes including those started by the user or systemd services. Now we only terminate processes we started. --- src/frontends/tui/minimal.ts | 46 +++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index 4d34d44..4634f77 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -52,6 +52,7 @@ export class MinimalTui { private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 }; private currentHint = ''; private lastLine = ''; + private backendPids: Map = new Map(); constructor(private config: MinimalTuiConfig) {} @@ -322,24 +323,30 @@ export class MinimalTui { } private async stopBackend(provider: string): Promise { + const pid = this.backendPids.get(provider); + if (!pid) { + return; // No tracked PID, process not started by us + } + try { - const { spawn } = await import('child_process'); - let processName: string; - switch (provider) { - case 'ollama': - processName = 'ollama'; - break; - case 'llamacpp': - processName = 'llama-server'; - break; - default: - return; - } + process.kill(pid, 'SIGTERM'); + // Wait up to 2 seconds for graceful shutdown await new Promise((resolve) => { - spawn('pkill', [processName]).on('close', resolve); + const timeout = setTimeout(resolve, 2000); + const checkInterval = setInterval(() => { + try { + process.kill(pid, 0); // Check if process exists + } catch { + clearInterval(checkInterval); + clearTimeout(timeout); + resolve(); + } + }, 100); }); + this.backendPids.delete(provider); } catch (error) { - // Ignore errors stopping backends + // Process already dead or permission error + this.backendPids.delete(provider); } } @@ -348,18 +355,25 @@ export class MinimalTui { const { spawn } = await import('child_process'); const args: string[] = []; + let proc: ReturnType | undefined; + switch (provider) { case 'ollama': - spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' }).unref(); + proc = spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' }); break; case 'llamacpp': args.push('--model', config.model); args.push('--port', new URL(config.endpoint ?? 'http://localhost:8080').port || '8080'); args.push('--host', '0.0.0.0'); - spawn('llama-server', args, { detached: true, stdio: 'ignore' }).unref(); + proc = spawn('llama-server', args, { detached: true, stdio: 'ignore' }); break; } + if (proc && proc.pid) { + proc.unref(); + this.backendPids.set(provider, proc.pid); + } + // Wait briefly for the daemon to start await new Promise(resolve => setTimeout(resolve, 500)); } catch (error) {