fix(cursor): use stdio ignore, sequential polling, move CursorConfig to types

- Change spawn stdio from piped to 'ignore' preventing buffer deadlock
- Replace setInterval with sequential setTimeout polling
- Fix TOCTOU in SIGKILL escalation (send directly without probing)
- Move CursorConfig interface to types.ts
- Change detectProvider default from 'openai' to 'unknown'
- Remove redundant removePidFile() when PID is null
This commit is contained in:
Tam Nhu Tran committed 2026-02-12 07:51:22 +07:00
1 parent 94789676b9
commit ce1915366d
4 files changed
+22 -19

No files matched your search

+10 -16
View File
@@ -9,15 +9,9 @@ import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as http from 'http'; import * as http from 'http';
import type { CursorDaemonStatus } from './types'; import type { CursorConfig, CursorDaemonStatus } from './types';
import { getCcsDir } from '../utils/config-manager'; import { getCcsDir } from '../utils/config-manager';
// Temporary interface until #521 adds cursor to unified config
interface CursorConfig {
port: number;
model: string;
}
/** /**
* Get Cursor directory path. * Get Cursor directory path.
*/ */
@@ -155,11 +149,11 @@ export async function startDaemon(
const safeResolve = (result: { success: boolean; pid?: number; error?: string }) => { const safeResolve = (result: { success: boolean; pid?: number; error?: string }) => {
if (resolved) return; if (resolved) return;
resolved = true; resolved = true;
if (checkInterval) clearInterval(checkInterval); if (checkTimeout) clearTimeout(checkTimeout);
resolve(result); resolve(result);
}; };
let checkInterval: NodeJS.Timeout | null = null; let checkTimeout: NodeJS.Timeout | null = null;
try { try {
// Spawn a placeholder Node.js process // Spawn a placeholder Node.js process
@@ -185,9 +179,8 @@ export async function startDaemon(
]; ];
proc = spawn(process.execPath, args, { proc = spawn(process.execPath, args, {
stdio: ['ignore', 'pipe', 'pipe'], stdio: 'ignore',
detached: true, detached: true,
shell: process.platform === 'win32',
}); });
// Unref so parent can exit // Unref so parent can exit
@@ -200,7 +193,7 @@ export async function startDaemon(
// Wait for daemon to be ready (poll for up to 30 seconds) // Wait for daemon to be ready (poll for up to 30 seconds)
let attempts = 0; let attempts = 0;
const maxAttempts = 30; const maxAttempts = 30;
checkInterval = setInterval(async () => { const pollHealth = async () => {
attempts++; attempts++;
if (await isDaemonRunning(config.port)) { if (await isDaemonRunning(config.port)) {
@@ -218,8 +211,11 @@ export async function startDaemon(
success: false, success: false,
error: 'Daemon did not start within 30 seconds', error: 'Daemon did not start within 30 seconds',
}); });
} else {
checkTimeout = setTimeout(pollHealth, 1000);
} }
}, 1000); };
checkTimeout = setTimeout(pollHealth, 1000);
proc.on('error', (err) => { proc.on('error', (err) => {
safeResolve({ safeResolve({
@@ -263,7 +259,6 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string }
if (!pid) { if (!pid) {
// No PID file — daemon is not running or was already stopped // No PID file — daemon is not running or was already stopped
removePidFile();
return { success: true }; return { success: true };
} }
@@ -287,8 +282,7 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string }
// Escalate to SIGKILL if process still alive after SIGTERM attempts // Escalate to SIGKILL if process still alive after SIGTERM attempts
try { try {
process.kill(pid, 0); // Check if still alive process.kill(pid, 'SIGKILL');
process.kill(pid, 'SIGKILL'); // Escalate to force kill
} catch { } catch {
// Already dead — good // Already dead — good
} }
+1 -1
View File
@@ -163,7 +163,7 @@ export function detectProvider(modelId: string): string {
if (modelId.includes('gpt') || /^o\d/.test(modelId)) return 'openai'; if (modelId.includes('gpt') || /^o\d/.test(modelId)) return 'openai';
if (modelId.includes('gemini')) return 'google'; if (modelId.includes('gemini')) return 'google';
if (modelId.includes('cursor')) return 'cursor'; if (modelId.includes('cursor')) return 'cursor';
return 'openai'; return 'unknown';
} }
/** /**
+9
View File
@@ -4,6 +4,15 @@
* TypeScript interfaces for the Cursor module. * TypeScript interfaces for the Cursor module.
*/ */
/**
* Cursor daemon configuration.
* Temporary interface until #521 adds cursor to unified config.
*/
export interface CursorConfig {
port: number;
model: string;
}
/** /**
* Cursor authentication credentials * Cursor authentication credentials
*/ */
+2 -2
View File
@@ -72,8 +72,8 @@ describe('detectProvider', () => {
expect(detectProvider('cursor-small')).toBe('cursor'); expect(detectProvider('cursor-small')).toBe('cursor');
}); });
it('defaults to openai for unknown models', () => { it('defaults to unknown for unrecognized models', () => {
expect(detectProvider('unknown-model')).toBe('openai'); expect(detectProvider('unknown-model')).toBe('unknown');
}); });
}); });