mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-05 12:22:35 +00:00
fix(cursor): add resolve guard, port validation, and daemon tests
- Add double-resolve guard in startDaemon with safeResolve wrapper - Add port validation (1-65535) before Node.js script interpolation - Fix misleading comment in stopDaemon (no PID file handling) - Add getDaemonStatus test for no daemon running case - Add stopDaemon tests for graceful non-existent PID handling
This commit is contained in:
+24
-13
@@ -140,10 +140,25 @@ export async function startDaemon(
|
|||||||
return { success: true, pid: getPidFromFile() ?? undefined };
|
return { success: true, pid: getPidFromFile() ?? undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate port before interpolation (prevents injection)
|
||||||
|
if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
|
||||||
|
return { success: false, error: `Invalid port: ${config.port}` };
|
||||||
|
}
|
||||||
|
|
||||||
// For now, create a simple structure that will be filled in later
|
// For now, create a simple structure that will be filled in later
|
||||||
// The actual server implementation will be added in a separate task
|
// The actual server implementation will be added in a separate task
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let proc: ChildProcess;
|
let proc: ChildProcess;
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const safeResolve = (result: { success: boolean; pid?: number; error?: string }) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
if (checkInterval) clearInterval(checkInterval);
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
let checkInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Spawn a placeholder Node.js process
|
// Spawn a placeholder Node.js process
|
||||||
@@ -184,15 +199,13 @@ 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;
|
||||||
const checkInterval = setInterval(async () => {
|
checkInterval = setInterval(async () => {
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
if (await isDaemonRunning(config.port)) {
|
if (await isDaemonRunning(config.port)) {
|
||||||
clearInterval(checkInterval);
|
safeResolve({ success: true, pid: proc.pid });
|
||||||
resolve({ success: true, pid: proc.pid });
|
|
||||||
} else if (attempts >= maxAttempts) {
|
} else if (attempts >= maxAttempts) {
|
||||||
clearInterval(checkInterval);
|
safeResolve({
|
||||||
resolve({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Daemon did not start within 30 seconds',
|
error: 'Daemon did not start within 30 seconds',
|
||||||
});
|
});
|
||||||
@@ -200,34 +213,32 @@ export async function startDaemon(
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
proc.on('error', (err) => {
|
||||||
clearInterval(checkInterval);
|
safeResolve({
|
||||||
resolve({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to start daemon: ${err.message}`,
|
error: `Failed to start daemon: ${err.message}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
proc.on('exit', (code, signal) => {
|
proc.on('exit', (code, signal) => {
|
||||||
clearInterval(checkInterval);
|
|
||||||
if (code === null) {
|
if (code === null) {
|
||||||
resolve({
|
safeResolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Daemon process was killed by signal ${signal}`,
|
error: `Daemon process was killed by signal ${signal}`,
|
||||||
});
|
});
|
||||||
} else if (code === 0) {
|
} else if (code === 0) {
|
||||||
resolve({
|
safeResolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Daemon process exited unexpectedly with code 0',
|
error: 'Daemon process exited unexpectedly with code 0',
|
||||||
});
|
});
|
||||||
} else if (code !== null) {
|
} else if (code !== null) {
|
||||||
resolve({
|
safeResolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Daemon process exited with code ${code}`,
|
error: `Daemon process exited with code ${code}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resolve({
|
safeResolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to spawn daemon: ${(err as Error).message}`,
|
error: `Failed to spawn daemon: ${(err as Error).message}`,
|
||||||
});
|
});
|
||||||
@@ -242,7 +253,7 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string }
|
|||||||
const pid = getPidFromFile();
|
const pid = getPidFromFile();
|
||||||
|
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
// No PID file, try to find by port
|
// No PID file — daemon is not running or was already stopped
|
||||||
removePidFile();
|
removePidFile();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
writePidToFile,
|
writePidToFile,
|
||||||
removePidFile,
|
removePidFile,
|
||||||
isDaemonRunning,
|
isDaemonRunning,
|
||||||
|
getDaemonStatus,
|
||||||
|
stopDaemon,
|
||||||
} from '../../../src/cursor/cursor-daemon';
|
} from '../../../src/cursor/cursor-daemon';
|
||||||
|
|
||||||
// Test isolation
|
// Test isolation
|
||||||
@@ -121,3 +123,40 @@ describe('isDaemonRunning', () => {
|
|||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDaemonStatus', () => {
|
||||||
|
it('returns status with running=false when no daemon running', async () => {
|
||||||
|
const status = await getDaemonStatus(19999);
|
||||||
|
expect(status.running).toBe(false);
|
||||||
|
expect(status.port).toBe(19999);
|
||||||
|
expect(status.pid).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns status with pid when PID file exists but daemon not running', async () => {
|
||||||
|
writePidToFile(99999);
|
||||||
|
const status = await getDaemonStatus(19999);
|
||||||
|
expect(status.running).toBe(false);
|
||||||
|
expect(status.port).toBe(19999);
|
||||||
|
expect(status.pid).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopDaemon', () => {
|
||||||
|
it('returns success when no PID file exists', async () => {
|
||||||
|
const result = await stopDaemon();
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns success when PID refers to non-existent process', async () => {
|
||||||
|
// Write a PID that doesn't exist
|
||||||
|
writePidToFile(999999);
|
||||||
|
const result = await stopDaemon();
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
|
||||||
|
// PID file should be removed
|
||||||
|
const pidFile = path.join(ccsDir(), 'cursor', 'daemon.pid');
|
||||||
|
expect(fs.existsSync(pidFile)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user