refactor(backend): use systemd for daemon management

Replace manual process management with systemctl --user commands.
Uses ollama.service and llama-server.service units for proper lifecycle
management, VRAM cleanup, and integration with system services.
This commit is contained in:
William Valentin
2026-02-12 00:24:43 -08:00
parent 1c8da30905
commit 125af4e832
+37 -37
View File
@@ -52,7 +52,6 @@ export class MinimalTui {
private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 }; private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
private currentHint = ''; private currentHint = '';
private lastLine = ''; private lastLine = '';
private backendPids: Map<string, number> = new Map();
constructor(private config: MinimalTuiConfig) {} constructor(private config: MinimalTuiConfig) {}
@@ -323,61 +322,62 @@ export class MinimalTui {
} }
private async stopBackend(provider: string): Promise<void> { private async stopBackend(provider: string): Promise<void> {
const pid = this.backendPids.get(provider);
if (!pid) {
return; // No tracked PID, process not started by us
}
try { try {
process.kill(pid, 'SIGTERM'); const { exec } = await import('child_process');
// Wait up to 2 seconds for graceful shutdown let serviceName: string;
await new Promise<void>((resolve) => { switch (provider) {
const timeout = setTimeout(resolve, 2000); case 'ollama':
const checkInterval = setInterval(() => { serviceName = 'ollama.service';
try { break;
process.kill(pid, 0); // Check if process exists case 'llamacpp':
} catch { serviceName = 'llama-server.service';
clearInterval(checkInterval); break;
clearTimeout(timeout); default:
return;
}
await new Promise<void>((resolve, reject) => {
exec(`systemctl --user stop ${serviceName}`, (error) => {
if (error) {
reject(error);
} else {
resolve(); resolve();
} }
}, 100); });
}); });
this.backendPids.delete(provider);
} catch (error) { } catch (error) {
// Process already dead or permission error // Service might not exist or already stopped, ignore
this.backendPids.delete(provider); console.log(`${colors.gray}Note: ${provider} service not managed by systemd${colors.reset}\n`);
} }
} }
private async startBackend(provider: string, config: ModelConfig): Promise<void> { private async startBackend(provider: string, config: ModelConfig): Promise<void> {
try { try {
const { spawn } = await import('child_process'); const { exec } = await import('child_process');
const args: string[] = []; let serviceName: string;
let proc: ReturnType<typeof spawn> | undefined;
switch (provider) { switch (provider) {
case 'ollama': case 'ollama':
proc = spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' }); serviceName = 'ollama.service';
break; break;
case 'llamacpp': case 'llamacpp':
args.push('--model', config.model); serviceName = 'llama-server.service';
args.push('--port', new URL(config.endpoint ?? 'http://localhost:8080').port || '8080');
args.push('--host', '0.0.0.0');
proc = spawn('llama-server', args, { detached: true, stdio: 'ignore' });
break; break;
default:
return;
} }
await new Promise<void>((resolve, reject) => {
exec(`systemctl --user start ${serviceName}`, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
if (proc && proc.pid) { // Wait briefly for daemon to start
proc.unref();
this.backendPids.set(provider, proc.pid);
}
// Wait briefly for the daemon to start
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) { } catch (error) {
console.log(`${colors.gray}Warning: Failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}${colors.reset}\n`); console.log(`${colors.gray}Warning: Failed to start ${provider} via systemd: ${error instanceof Error ? error.message : String(error)}${colors.reset}\n`);
} }
} }