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
This commit is contained in:
Tam Nhu Tran
2026-02-12 08:11:41 +07:00
parent 887efa4069
commit cda037e7e5
3 changed files with 34 additions and 5 deletions
+8 -5
View File
@@ -150,6 +150,7 @@ export async function startDaemon(
if (resolved) return; if (resolved) return;
resolved = true; resolved = true;
if (checkTimeout) clearTimeout(checkTimeout); if (checkTimeout) clearTimeout(checkTimeout);
if (!result.success) removePidFile();
resolve(result); 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 // Escalate to SIGKILL only if SIGTERM wait loop exhausted
try { if (attempts >= 10) {
process.kill(pid, 'SIGKILL'); try {
} catch { process.kill(pid, 'SIGKILL');
// Already dead — good } catch {
// Already dead — good
}
} }
removePidFile(); removePidFile();
+5
View File
@@ -7,6 +7,7 @@
import * as http from 'http'; import * as http from 'http';
import type { CursorModel } from './types'; import type { CursorModel } from './types';
import { isDaemonRunning } from './cursor-daemon';
/** Default daemon port */ /** Default daemon port */
export const DEFAULT_CURSOR_PORT = 4242; export const DEFAULT_CURSOR_PORT = 4242;
@@ -142,8 +143,12 @@ export async function fetchModelsFromDaemon(port: number): Promise<CursorModel[]
/** /**
* Get available models (from daemon or defaults). * Get available models (from daemon or defaults).
* Checks daemon health first to avoid 5s timeout when daemon is not running.
*/ */
export async function getAvailableModels(port: number): Promise<CursorModel[]> { export async function getAvailableModels(port: number): Promise<CursorModel[]> {
if (!(await isDaemonRunning(port))) {
return DEFAULT_CURSOR_MODELS;
}
return fetchModelsFromDaemon(port); return fetchModelsFromDaemon(port);
} }
+21
View File
@@ -13,6 +13,7 @@ import {
isDaemonRunning, isDaemonRunning,
getDaemonStatus, getDaemonStatus,
stopDaemon, stopDaemon,
startDaemon,
} from '../../../src/cursor/cursor-daemon'; } from '../../../src/cursor/cursor-daemon';
// Test isolation // 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', () => { describe('isDaemonRunning', () => {
it('returns false when no daemon is running on port', async () => { it('returns false when no daemon is running on port', async () => {
// Use a port that should not have anything running // Use a port that should not have anything running