fix(cliproxy): register session on dashboard start and add port-based stop fallback

Dashboard Stop/Update buttons failed with contradictory errors:
- Stop: "No active CLIProxy session found"
- Update: "CLIProxy was already running"

Root cause: service-manager spawned proxy but never called registerSession(),
so stopProxy() couldn't find the lock file.

Changes:
- service-manager.ts: Add registerSession() after spawn
- session-tracker.ts: Make stopProxy() async with port-based fallback
- routes.ts: Update proxy-stop endpoint to async
- cliproxy-command.ts: Update handleStop() to await async stopProxy()
- port-utils.ts: Fix double-escaped regex for Windows port detection
- session-tracker.test.js: Update tests for async stopProxy()
This commit is contained in:
kaitranntt
2025-12-19 03:57:06 -05:00
parent f1dfd69a4a
commit a3b172cc43
6 changed files with 47 additions and 12 deletions
+8
View File
@@ -22,6 +22,7 @@ import {
getCliproxyWritablePath,
} from './config-generator';
import { isCliproxyRunning } from './stats-fetcher';
import { registerSession } from './session-tracker';
/** Background proxy process reference */
let proxyProcess: ChildProcess | null = null;
@@ -219,6 +220,13 @@ export async function ensureCliproxyService(
}
log(`CLIProxy service started on port ${port}`);
// 5. Register session so stopProxy() can find and kill this process
if (proxyProcess.pid) {
registerSession(port, proxyProcess.pid);
log(`Session registered for PID ${proxyProcess.pid}`);
}
return { started: true, alreadyRunning: false, port };
}
+30 -3
View File
@@ -18,6 +18,8 @@ import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { getCliproxyDir } from './config-generator';
import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
import { CLIPROXY_DEFAULT_PORT } from './config-generator';
/** Session lock file structure */
interface SessionLock {
@@ -227,18 +229,43 @@ export function cleanupOrphanedSessions(port: number): void {
/**
* Stop the CLIProxy process and clean up session lock.
* Falls back to port-based detection if no session lock exists.
* @returns Object with success status and details
*/
export function stopProxy(): {
export async function stopProxy(): Promise<{
stopped: boolean;
pid?: number;
sessionCount?: number;
error?: string;
} {
}> {
const lock = readSessionLock();
if (!lock) {
return { stopped: false, error: 'No active CLIProxy session found' };
// No session lock - try to find process by port (legacy/untracked proxy)
const portProcess = await getPortProcess(CLIPROXY_DEFAULT_PORT);
if (!portProcess) {
return { stopped: false, error: 'No active CLIProxy session found' };
}
if (!isCLIProxyProcess(portProcess)) {
return {
stopped: false,
error: `Port ${CLIPROXY_DEFAULT_PORT} is in use by ${portProcess.processName}, not CLIProxy`,
};
}
// Found CLIProxy running without session lock - kill it
try {
process.kill(portProcess.pid, 'SIGTERM');
return { stopped: true, pid: portProcess.pid, sessionCount: 0 };
} catch (err) {
const error = err as NodeJS.ErrnoException;
if (error.code === 'ESRCH') {
return { stopped: false, error: 'CLIProxy process already terminated' };
}
return { stopped: false, pid: portProcess.pid, error: `Failed to stop: ${error.message}` };
}
}
// Check if proxy is running
+1 -1
View File
@@ -1069,7 +1069,7 @@ async function handleStop(): Promise<void> {
console.log(header('Stop CLIProxy'));
console.log('');
const result = stopProxy();
const result = await stopProxy();
if (result.stopped) {
console.log(ok(`CLIProxy stopped (PID ${result.pid})`));
+2 -2
View File
@@ -88,7 +88,7 @@ async function getPortProcessWindows(port: number): Promise<PortProcess | null>
}
// Parse netstat output to get PID (last column)
const match = netstatOut.match(/\\s+(\d+)\\s*$/m);
const match = netstatOut.match(/\s+(\d+)\s*$/m);
if (!match) {
return null;
}
@@ -100,7 +100,7 @@ async function getPortProcessWindows(port: number): Promise<PortProcess | null>
timeout: 3000,
});
const taskMatch = tasklistOut.match(/^([^\\s]+)/);
const taskMatch = tasklistOut.match(/^([^\s]+)/);
const processName = taskMatch ? taskMatch[1] : `PID-${pid}`;
return { pid, processName };
+2 -2
View File
@@ -1394,9 +1394,9 @@ apiRoutes.post('/cliproxy/proxy-start', async (_req: Request, res: Response): Pr
* POST /api/cliproxy/proxy-stop - Stop the CLIProxy service
* Returns: { stopped, pid?, sessionCount?, error? }
*/
apiRoutes.post('/cliproxy/proxy-stop', (_req: Request, res: Response): void => {
apiRoutes.post('/cliproxy/proxy-stop', async (_req: Request, res: Response): Promise<void> => {
try {
const result = stopProxy();
const result = await stopProxy();
res.json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
+4 -4
View File
@@ -299,13 +299,13 @@ describe('Session Tracker', function () {
});
describe('stopProxy', function () {
it('should return error when no lock exists', function () {
const result = stopProxy();
it('should return error when no lock exists', async function () {
const result = await stopProxy();
assert.strictEqual(result.stopped, false);
assert.strictEqual(result.error, 'No active CLIProxy session found');
});
it('should cleanup stale lock when proxy is not running', function () {
it('should cleanup stale lock when proxy is not running', async function () {
// Create lock with dead PID
const lock = {
port: testPort,
@@ -315,7 +315,7 @@ describe('Session Tracker', function () {
};
fs.writeFileSync(sessionLockPath, JSON.stringify(lock));
const result = stopProxy();
const result = await stopProxy();
assert.strictEqual(result.stopped, false);
assert.ok(result.error.includes('not running'));
assert.strictEqual(fs.existsSync(sessionLockPath), false);