refactor(daemon): share process ownership guard for safe shutdown

This commit is contained in:
Tam Nhu Tran
2026-03-04 16:42:59 +07:00
parent f4678d6397
commit 5ad2416a86
3 changed files with 18 additions and 82 deletions
+6 -2
View File
@@ -12,7 +12,7 @@ import * as http from 'http';
import { CopilotDaemonStatus } from './types';
import { CopilotConfig } from '../config/unified-config-types';
import { getCopilotDir, getCopilotApiBinPath } from './copilot-package-manager';
import { verifyCopilotDaemonOwnership } from './daemon-process-ownership';
import { verifyProcessOwnership } from '../cursor/daemon-process-ownership';
const DAEMON_HEALTH_MARKER = 'server running';
@@ -258,7 +258,11 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string }
}
try {
const ownership = verifyCopilotDaemonOwnership(pid);
const ownership = verifyProcessOwnership(pid, (commandLine) => {
const lower = commandLine.toLowerCase();
// copilot-api is launched as `... copilot-api start --port <n>`
return lower.includes('copilot-api') && lower.includes(' start');
});
if (ownership === 'not-running') {
removePidFile();
return { success: true };
-76
View File
@@ -1,76 +0,0 @@
import { spawnSync } from 'child_process';
import * as fs from 'fs';
export type DaemonOwnershipStatus = 'owned' | 'not-owned' | 'not-running' | 'unknown';
function getProcessCommandLine(pid: number): string | null {
if (process.platform === 'linux') {
try {
// /proc cmdline uses null separators between arguments.
return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim();
} catch {
return null;
}
}
if (process.platform === 'darwin') {
try {
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], {
encoding: 'utf8',
});
if (result.error || result.status !== 0) {
return null;
}
return result.stdout.trim();
} catch {
return null;
}
}
if (process.platform === 'win32') {
const command = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object -ExpandProperty CommandLine)`;
const shells = ['powershell.exe', 'powershell', 'pwsh.exe', 'pwsh'];
for (const shell of shells) {
try {
const result = spawnSync(shell, ['-NoProfile', '-Command', command], {
encoding: 'utf8',
});
if (result.error) {
continue;
}
if (result.status !== 0) {
return null;
}
return result.stdout.trim();
} catch {
// Try next shell candidate
}
}
return null;
}
return null;
}
export function verifyCopilotDaemonOwnership(pid: number): DaemonOwnershipStatus {
try {
process.kill(pid, 0);
} catch (err) {
const error = err as NodeJS.ErrnoException;
if (error.code === 'ESRCH') {
return 'not-running';
}
return 'unknown';
}
const commandLine = getProcessCommandLine(pid);
if (!commandLine) {
return 'unknown';
}
// copilot-api is launched as `... copilot-api start --port <n>`
const lower = commandLine.toLowerCase();
const looksLikeCopilotDaemon = lower.includes('copilot-api') && lower.includes(' start');
return looksLikeCopilotDaemon ? 'owned' : 'not-owned';
}
+12 -4
View File
@@ -52,7 +52,10 @@ function getProcessCommandLine(pid: number): string | null {
return null;
}
export function verifyDaemonOwnership(pid: number): DaemonOwnershipStatus {
export function verifyProcessOwnership(
pid: number,
ownershipMatcher: (commandLine: string) => boolean
): DaemonOwnershipStatus {
try {
process.kill(pid, 0);
} catch (err) {
@@ -68,8 +71,13 @@ export function verifyDaemonOwnership(pid: number): DaemonOwnershipStatus {
return 'unknown';
}
const looksLikeCursorDaemon =
commandLine.includes('--ccs-daemon') && commandLine.includes('cursor-daemon-entry');
return ownershipMatcher(commandLine) ? 'owned' : 'not-owned';
}
return looksLikeCursorDaemon ? 'owned' : 'not-owned';
export function verifyDaemonOwnership(pid: number): DaemonOwnershipStatus {
return verifyProcessOwnership(
pid,
(commandLine) =>
commandLine.includes('--ccs-daemon') && commandLine.includes('cursor-daemon-entry')
);
}