diff --git a/src/cliproxy/service-manager.ts b/src/cliproxy/service-manager.ts index f6977a07..10992fda 100644 --- a/src/cliproxy/service-manager.ts +++ b/src/cliproxy/service-manager.ts @@ -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 }; } diff --git a/src/cliproxy/session-tracker.ts b/src/cliproxy/session-tracker.ts index 1b35657c..33b3fe19 100644 --- a/src/cliproxy/session-tracker.ts +++ b/src/cliproxy/session-tracker.ts @@ -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 diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index cd492753..5c7cf58e 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -1069,7 +1069,7 @@ async function handleStop(): Promise { 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})`)); diff --git a/src/utils/port-utils.ts b/src/utils/port-utils.ts index 24cb4333..e3351ac2 100644 --- a/src/utils/port-utils.ts +++ b/src/utils/port-utils.ts @@ -88,7 +88,7 @@ async function getPortProcessWindows(port: number): Promise } // 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 timeout: 3000, }); - const taskMatch = tasklistOut.match(/^([^\\s]+)/); + const taskMatch = tasklistOut.match(/^([^\s]+)/); const processName = taskMatch ? taskMatch[1] : `PID-${pid}`; return { pid, processName }; diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 6f68fc27..5bc1bcea 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -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 => { try { - const result = stopProxy(); + const result = await stopProxy(); res.json(result); } catch (error) { res.status(500).json({ error: (error as Error).message }); diff --git a/tests/unit/cliproxy/session-tracker.test.js b/tests/unit/cliproxy/session-tracker.test.js index 14f8aa0e..463a26cd 100644 --- a/tests/unit/cliproxy/session-tracker.test.js +++ b/tests/unit/cliproxy/session-tracker.test.js @@ -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);