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
2026-02-12 07:51:22 +07:00
parent 94789676b9
commit ce1915366d
4 changed files with 22 additions and 19 deletions
+10 -16
View File
@@ -9,15 +9,9 @@ import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
import type { CursorDaemonStatus } from './types';
import type { CursorConfig, CursorDaemonStatus } from './types';
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.
*/
@@ -155,11 +149,11 @@ export async function startDaemon(
const safeResolve = (result: { success: boolean; pid?: number; error?: string }) => {
if (resolved) return;
resolved = true;
if (checkInterval) clearInterval(checkInterval);
if (checkTimeout) clearTimeout(checkTimeout);
resolve(result);
};
let checkInterval: NodeJS.Timeout | null = null;
let checkTimeout: NodeJS.Timeout | null = null;
try {
// Spawn a placeholder Node.js process
@@ -185,9 +179,8 @@ export async function startDaemon(
];
proc = spawn(process.execPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
stdio: 'ignore',
detached: true,
shell: process.platform === 'win32',
});
// 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)
let attempts = 0;
const maxAttempts = 30;
checkInterval = setInterval(async () => {
const pollHealth = async () => {
attempts++;
if (await isDaemonRunning(config.port)) {
@@ -218,8 +211,11 @@ export async function startDaemon(
success: false,
error: 'Daemon did not start within 30 seconds',
});
} else {
checkTimeout = setTimeout(pollHealth, 1000);
}
}, 1000);
};
checkTimeout = setTimeout(pollHealth, 1000);
proc.on('error', (err) => {
safeResolve({
@@ -263,7 +259,6 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string }
if (!pid) {
// No PID file — daemon is not running or was already stopped
removePidFile();
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
try {
process.kill(pid, 0); // Check if still alive
process.kill(pid, 'SIGKILL'); // Escalate to force kill
process.kill(pid, 'SIGKILL');
} catch {
// 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('gemini')) return 'google';
if (modelId.includes('cursor')) return 'cursor';
return 'openai';
return 'unknown';
}
/**
+9
View File
@@ -4,6 +4,15 @@
* 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
*/
+2 -2
View File
@@ -72,8 +72,8 @@ describe('detectProvider', () => {
expect(detectProvider('cursor-small')).toBe('cursor');
});
it('defaults to openai for unknown models', () => {
expect(detectProvider('unknown-model')).toBe('openai');
it('defaults to unknown for unrecognized models', () => {
expect(detectProvider('unknown-model')).toBe('unknown');
});
});