From ce1915366d5012097b5b6814c2c672d04f16ab61 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 12 Feb 2026 07:51:22 +0700 Subject: [PATCH] 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 --- src/cursor/cursor-daemon.ts | 26 ++++++++++--------------- src/cursor/cursor-models.ts | 2 +- src/cursor/types.ts | 9 +++++++++ tests/unit/cursor/cursor-models.test.ts | 4 ++-- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/cursor/cursor-daemon.ts b/src/cursor/cursor-daemon.ts index eae9295c..87e886a0 100644 --- a/src/cursor/cursor-daemon.ts +++ b/src/cursor/cursor-daemon.ts @@ -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 } diff --git a/src/cursor/cursor-models.ts b/src/cursor/cursor-models.ts index 05a0a19d..ec68b801 100644 --- a/src/cursor/cursor-models.ts +++ b/src/cursor/cursor-models.ts @@ -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'; } /** diff --git a/src/cursor/types.ts b/src/cursor/types.ts index 5dbc4fe9..c16150cc 100644 --- a/src/cursor/types.ts +++ b/src/cursor/types.ts @@ -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 */ diff --git a/tests/unit/cursor/cursor-models.test.ts b/tests/unit/cursor/cursor-models.test.ts index d8872dfb..f998d2b6 100644 --- a/tests/unit/cursor/cursor-models.test.ts +++ b/tests/unit/cursor/cursor-models.test.ts @@ -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'); }); });