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 committed 2025-12-19 03:57:06 -05:00
1 parent f1dfd69a4a
commit a3b172cc43
6 files changed
+47 -12

No files matched your search

+8
View File
@@ -22,6 +22,7 @@ import {
getCliproxyWritablePath, getCliproxyWritablePath,
} from './config-generator'; } from './config-generator';
import { isCliproxyRunning } from './stats-fetcher'; import { isCliproxyRunning } from './stats-fetcher';
import { registerSession } from './session-tracker';
/** Background proxy process reference */ /** Background proxy process reference */
let proxyProcess: ChildProcess | null = null; let proxyProcess: ChildProcess | null = null;
@@ -219,6 +220,13 @@ export async function ensureCliproxyService(
} }
log(`CLIProxy service started on port ${port}`); 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 }; 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 path from 'path';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { getCliproxyDir } from './config-generator'; import { getCliproxyDir } from './config-generator';
import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
import { CLIPROXY_DEFAULT_PORT } from './config-generator';
/** Session lock file structure */ /** Session lock file structure */
interface SessionLock { interface SessionLock {
@@ -227,18 +229,43 @@ export function cleanupOrphanedSessions(port: number): void {
/** /**
* Stop the CLIProxy process and clean up session lock. * 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 * @returns Object with success status and details
*/ */
export function stopProxy(): { export async function stopProxy(): Promise<{
stopped: boolean; stopped: boolean;
pid?: number; pid?: number;
sessionCount?: number; sessionCount?: number;
error?: string; error?: string;
} { }> {
const lock = readSessionLock(); const lock = readSessionLock();
if (!lock) { 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 // 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(header('Stop CLIProxy'));
console.log(''); console.log('');
const result = stopProxy(); const result = await stopProxy();
if (result.stopped) { if (result.stopped) {
console.log(ok(`CLIProxy stopped (PID ${result.pid})`)); 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) // 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) { if (!match) {
return null; return null;
} }
@@ -100,7 +100,7 @@ async function getPortProcessWindows(port: number): Promise<PortProcess | null>
timeout: 3000, timeout: 3000,
}); });
const taskMatch = tasklistOut.match(/^([^\\s]+)/); const taskMatch = tasklistOut.match(/^([^\s]+)/);
const processName = taskMatch ? taskMatch[1] : `PID-${pid}`; const processName = taskMatch ? taskMatch[1] : `PID-${pid}`;
return { pid, processName }; 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 * POST /api/cliproxy/proxy-stop - Stop the CLIProxy service
* Returns: { stopped, pid?, sessionCount?, error? } * 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 { try {
const result = stopProxy(); const result = await stopProxy();
res.json(result); res.json(result);
} catch (error) { } catch (error) {
res.status(500).json({ error: (error as Error).message }); res.status(500).json({ error: (error as Error).message });
+4 -4
View File
@@ -299,13 +299,13 @@ describe('Session Tracker', function () {
}); });
describe('stopProxy', function () { describe('stopProxy', function () {
it('should return error when no lock exists', function () { it('should return error when no lock exists', async function () {
const result = stopProxy(); const result = await stopProxy();
assert.strictEqual(result.stopped, false); assert.strictEqual(result.stopped, false);
assert.strictEqual(result.error, 'No active CLIProxy session found'); 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 // Create lock with dead PID
const lock = { const lock = {
port: testPort, port: testPort,
@@ -315,7 +315,7 @@ describe('Session Tracker', function () {
}; };
fs.writeFileSync(sessionLockPath, JSON.stringify(lock)); fs.writeFileSync(sessionLockPath, JSON.stringify(lock));
const result = stopProxy(); const result = await stopProxy();
assert.strictEqual(result.stopped, false); assert.strictEqual(result.stopped, false);
assert.ok(result.error.includes('not running')); assert.ok(result.error.includes('not running'));
assert.strictEqual(fs.existsSync(sessionLockPath), false); assert.strictEqual(fs.existsSync(sessionLockPath), false);