From cda037e7e5a9a77bb6bb9768e3d601008d102b70 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 12 Feb 2026 08:11:41 +0700 Subject: [PATCH] fix(cursor): clean up PID file on startDaemon failure and improve daemon robustness - Add removePidFile() in safeResolve on failure to prevent stale PIDs - Only send SIGKILL in stopDaemon if SIGTERM wait loop exhausted - Check isDaemonRunning before model fetch to avoid 5s timeout - Add port validation unit tests for startDaemon --- src/cursor/cursor-daemon.ts | 13 ++++++++----- src/cursor/cursor-models.ts | 5 +++++ tests/unit/cursor/cursor-daemon.test.ts | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/cursor/cursor-daemon.ts b/src/cursor/cursor-daemon.ts index 87e886a0..1ed5a484 100644 --- a/src/cursor/cursor-daemon.ts +++ b/src/cursor/cursor-daemon.ts @@ -150,6 +150,7 @@ export async function startDaemon( if (resolved) return; resolved = true; if (checkTimeout) clearTimeout(checkTimeout); + if (!result.success) removePidFile(); resolve(result); }; @@ -280,11 +281,13 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string } } } - // Escalate to SIGKILL if process still alive after SIGTERM attempts - try { - process.kill(pid, 'SIGKILL'); - } catch { - // Already dead — good + // Escalate to SIGKILL only if SIGTERM wait loop exhausted + if (attempts >= 10) { + try { + process.kill(pid, 'SIGKILL'); + } catch { + // Already dead — good + } } removePidFile(); diff --git a/src/cursor/cursor-models.ts b/src/cursor/cursor-models.ts index ec68b801..931e89c5 100644 --- a/src/cursor/cursor-models.ts +++ b/src/cursor/cursor-models.ts @@ -7,6 +7,7 @@ import * as http from 'http'; import type { CursorModel } from './types'; +import { isDaemonRunning } from './cursor-daemon'; /** Default daemon port */ export const DEFAULT_CURSOR_PORT = 4242; @@ -142,8 +143,12 @@ export async function fetchModelsFromDaemon(port: number): Promise { + if (!(await isDaemonRunning(port))) { + return DEFAULT_CURSOR_MODELS; + } return fetchModelsFromDaemon(port); } diff --git a/tests/unit/cursor/cursor-daemon.test.ts b/tests/unit/cursor/cursor-daemon.test.ts index b07012a7..acdecab8 100644 --- a/tests/unit/cursor/cursor-daemon.test.ts +++ b/tests/unit/cursor/cursor-daemon.test.ts @@ -13,6 +13,7 @@ import { isDaemonRunning, getDaemonStatus, stopDaemon, + startDaemon, } from '../../../src/cursor/cursor-daemon'; // Test isolation @@ -116,6 +117,26 @@ describe('removePidFile', () => { }); }); +describe('startDaemon', () => { + it('rejects invalid port (0)', async () => { + const result = await startDaemon({ port: 0, model: 'test' }); + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid port'); + }); + + it('rejects invalid port (65536)', async () => { + const result = await startDaemon({ port: 65536, model: 'test' }); + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid port'); + }); + + it('rejects non-integer port', async () => { + const result = await startDaemon({ port: 3.14, model: 'test' }); + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid port'); + }); +}); + describe('isDaemonRunning', () => { it('returns false when no daemon is running on port', async () => { // Use a port that should not have anything running