mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
fix(cliproxy): ensure version sync after binary update
- Add SIGKILL escalation after 3s SIGTERM timeout in session-tracker - Auto-stop running proxy before binary update in binary-manager - Add waitForPortFree utility to port-utils - Detect version mismatch on startup and auto-restart outdated proxy - Store and expose version in session lock for detection Fixes issue where old proxy version continues running after update, causing UI to show different version than actually running.
This commit is contained in:
1 parent
2093ae2cc8
commit
29f19308e6
5 files changed
+174
-12
No files matched your search
@@ -5,10 +5,12 @@
|
|||||||
* Pattern: Mirrors npm install behavior (fast check, download only when needed)
|
* Pattern: Mirrors npm install behavior (fast check, download only when needed)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { info } from '../utils/ui';
|
import { info, warn } from '../utils/ui';
|
||||||
import { getBinDir } from './config-generator';
|
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
|
||||||
import { BinaryInfo, BinaryManagerConfig } from './types';
|
import { BinaryInfo, BinaryManagerConfig } from './types';
|
||||||
import { CLIPROXY_FALLBACK_VERSION } from './platform-detector';
|
import { CLIPROXY_FALLBACK_VERSION } from './platform-detector';
|
||||||
|
import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service';
|
||||||
|
import { waitForPortFree } from '../utils/port-utils';
|
||||||
import {
|
import {
|
||||||
UpdateCheckResult,
|
UpdateCheckResult,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
@@ -108,12 +110,32 @@ export function getInstalledCliproxyVersion(): string {
|
|||||||
/** Install a specific version of CLIProxyAPI */
|
/** Install a specific version of CLIProxyAPI */
|
||||||
export async function installCliproxyVersion(version: string, verbose = false): Promise<void> {
|
export async function installCliproxyVersion(version: string, verbose = false): Promise<void> {
|
||||||
const manager = new BinaryManager({ version, verbose, forceVersion: true });
|
const manager = new BinaryManager({ version, verbose, forceVersion: true });
|
||||||
|
|
||||||
|
// Check if proxy is running and stop it first
|
||||||
|
if (isProxyRunning()) {
|
||||||
|
if (verbose) console.log(info('Stopping running CLIProxy before update...'));
|
||||||
|
const result = await stopProxy();
|
||||||
|
if (result.stopped) {
|
||||||
|
// Wait for port to be fully released
|
||||||
|
const portFree = await waitForPortFree(CLIPROXY_DEFAULT_PORT, 5000);
|
||||||
|
if (!portFree && verbose) {
|
||||||
|
console.log(warn('Port did not free up in time, proceeding anyway...'));
|
||||||
|
}
|
||||||
|
} else if (verbose && result.error) {
|
||||||
|
console.log(warn(`Could not stop proxy: ${result.error}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (manager.isBinaryInstalled()) {
|
if (manager.isBinaryInstalled()) {
|
||||||
if (verbose)
|
if (verbose)
|
||||||
console.log(info(`Removing existing CLIProxy Plus v${getInstalledCliproxyVersion()}`));
|
console.log(info(`Removing existing CLIProxy Plus v${getInstalledCliproxyVersion()}`));
|
||||||
manager.deleteBinary();
|
manager.deleteBinary();
|
||||||
}
|
}
|
||||||
await manager.ensureBinary();
|
await manager.ensureBinary();
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log(info('New version will be active on next CLIProxy command'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch the latest CLIProxyAPI version from GitHub API */
|
/** Fetch the latest CLIProxyAPI version from GitHub API */
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import * as net from 'net';
|
|||||||
import { ProgressIndicator } from '../utils/progress-indicator';
|
import { ProgressIndicator } from '../utils/progress-indicator';
|
||||||
import { ok, fail, info, warn } from '../utils/ui';
|
import { ok, fail, info, warn } from '../utils/ui';
|
||||||
import { escapeShellArg } from '../utils/shell-executor';
|
import { escapeShellArg } from '../utils/shell-executor';
|
||||||
import { ensureCLIProxyBinary } from './binary-manager';
|
import { ensureCLIProxyBinary, getInstalledCliproxyVersion } from './binary-manager';
|
||||||
import {
|
import {
|
||||||
generateConfig,
|
generateConfig,
|
||||||
getEffectiveEnvVars,
|
getEffectiveEnvVars,
|
||||||
@@ -48,7 +48,12 @@ import {
|
|||||||
installWebSearchHook,
|
installWebSearchHook,
|
||||||
displayWebSearchStatus,
|
displayWebSearchStatus,
|
||||||
} from '../utils/websearch-manager';
|
} from '../utils/websearch-manager';
|
||||||
import { registerSession, unregisterSession, cleanupOrphanedSessions } from './session-tracker';
|
import {
|
||||||
|
registerSession,
|
||||||
|
unregisterSession,
|
||||||
|
cleanupOrphanedSessions,
|
||||||
|
stopProxy,
|
||||||
|
} from './session-tracker';
|
||||||
import { detectRunningProxy, waitForProxyHealthy, reclaimOrphanedProxy } from './proxy-detector';
|
import { detectRunningProxy, waitForProxyHealthy, reclaimOrphanedProxy } from './proxy-detector';
|
||||||
import { withStartupLock } from './startup-lock';
|
import { withStartupLock } from './startup-lock';
|
||||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||||
@@ -429,9 +434,33 @@ export async function execClaudeWithCLIProxy(
|
|||||||
// Use startup lock to coordinate with other CCS processes
|
// Use startup lock to coordinate with other CCS processes
|
||||||
await withStartupLock(async () => {
|
await withStartupLock(async () => {
|
||||||
// Detect running proxy using multiple methods (HTTP, session-lock, port-process)
|
// Detect running proxy using multiple methods (HTTP, session-lock, port-process)
|
||||||
const proxyStatus = await detectRunningProxy(cfg.port);
|
let proxyStatus = await detectRunningProxy(cfg.port);
|
||||||
log(`Proxy detection: ${JSON.stringify(proxyStatus)}`);
|
log(`Proxy detection: ${JSON.stringify(proxyStatus)}`);
|
||||||
|
|
||||||
|
// Check for version mismatch - restart proxy if installed version differs from running
|
||||||
|
if (proxyStatus.running && proxyStatus.verified && proxyStatus.version) {
|
||||||
|
const installedVersion = getInstalledCliproxyVersion();
|
||||||
|
if (installedVersion !== proxyStatus.version) {
|
||||||
|
console.log(
|
||||||
|
warn(
|
||||||
|
`Version mismatch: running v${proxyStatus.version}, installed v${installedVersion}. Restarting proxy...`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
log(`Stopping outdated proxy (PID: ${proxyStatus.pid ?? 'unknown'})...`);
|
||||||
|
const stopResult = await stopProxy(cfg.port);
|
||||||
|
if (stopResult.stopped) {
|
||||||
|
log(`Stopped outdated proxy successfully`);
|
||||||
|
} else {
|
||||||
|
log(`Stop proxy result: ${stopResult.error ?? 'unknown error'}`);
|
||||||
|
}
|
||||||
|
// Wait for port to be released
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
// Re-detect proxy status (should now be not running)
|
||||||
|
proxyStatus = await detectRunningProxy(cfg.port);
|
||||||
|
log(`Re-detection after version mismatch restart: ${JSON.stringify(proxyStatus)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (proxyStatus.running && proxyStatus.verified) {
|
if (proxyStatus.running && proxyStatus.verified) {
|
||||||
// Healthy proxy found - join it
|
// Healthy proxy found - join it
|
||||||
if (proxyStatus.pid) {
|
if (proxyStatus.pid) {
|
||||||
@@ -542,9 +571,12 @@ export async function execClaudeWithCLIProxy(
|
|||||||
throw new Error(`CLIProxy startup failed: ${err.message}`);
|
throw new Error(`CLIProxy startup failed: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register this session with the new proxy
|
// Register this session with the new proxy, including the installed version
|
||||||
sessionId = registerSession(cfg.port, proxy.pid as number);
|
const installedVersion = getInstalledCliproxyVersion();
|
||||||
log(`Registered session ${sessionId} with new proxy (PID ${proxy.pid})`);
|
sessionId = registerSession(cfg.port, proxy.pid as number, installedVersion);
|
||||||
|
log(
|
||||||
|
`Registered session ${sessionId} with new proxy (PID ${proxy.pid}, version ${installedVersion})`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* Solves race conditions between cliproxy-executor.ts and service-manager.ts
|
* Solves race conditions between cliproxy-executor.ts and service-manager.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getExistingProxy, registerSession } from './session-tracker';
|
import { getExistingProxy, registerSession, getRunningProxyVersion } from './session-tracker';
|
||||||
import { isCliproxyRunning } from './stats-fetcher';
|
import { isCliproxyRunning } from './stats-fetcher';
|
||||||
import { getPortProcess, isCLIProxyProcess, PortProcess } from '../utils/port-utils';
|
import { getPortProcess, isCLIProxyProcess, PortProcess } from '../utils/port-utils';
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ export interface ProxyStatus {
|
|||||||
blocker?: PortProcess;
|
blocker?: PortProcess;
|
||||||
/** Number of active sessions (if session-lock found) */
|
/** Number of active sessions (if session-lock found) */
|
||||||
sessionCount?: number;
|
sessionCount?: number;
|
||||||
|
/** Version of the running proxy (from session lock) */
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Optional logger function for verbose output */
|
/** Optional logger function for verbose output */
|
||||||
@@ -86,13 +88,19 @@ export async function detectRunningProxy(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`HTTP check passed, proxy healthy (PID: ${pid ?? 'unknown'})`);
|
// Get version from session lock
|
||||||
|
const runningVersion = getRunningProxyVersion(port);
|
||||||
|
|
||||||
|
log(
|
||||||
|
`HTTP check passed, proxy healthy (PID: ${pid ?? 'unknown'}, version: ${runningVersion ?? 'unknown'})`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
running: true,
|
running: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
method: 'http',
|
method: 'http',
|
||||||
pid,
|
pid,
|
||||||
sessionCount: lock?.sessions?.length,
|
sessionCount: lock?.sessions?.length,
|
||||||
|
version: runningVersion ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
log('HTTP check failed, proxy not responding');
|
log('HTTP check failed, proxy not responding');
|
||||||
@@ -103,13 +111,17 @@ export async function detectRunningProxy(
|
|||||||
if (lock) {
|
if (lock) {
|
||||||
// Session lock exists - proxy might be starting up
|
// Session lock exists - proxy might be starting up
|
||||||
// The lock validates PID is running, so proxy exists but not ready
|
// The lock validates PID is running, so proxy exists but not ready
|
||||||
log(`Session lock found: PID ${lock.pid}, ${lock.sessions.length} sessions`);
|
const lockVersion = getRunningProxyVersion(port);
|
||||||
|
log(
|
||||||
|
`Session lock found: PID ${lock.pid}, ${lock.sessions.length} sessions, version: ${lockVersion ?? 'unknown'}`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
running: true,
|
running: true,
|
||||||
verified: false,
|
verified: false,
|
||||||
method: 'session-lock',
|
method: 'session-lock',
|
||||||
pid: lock.pid,
|
pid: lock.pid,
|
||||||
sessionCount: lock.sessions.length,
|
sessionCount: lock.sessions.length,
|
||||||
|
version: lockVersion ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
log('No session lock found');
|
log('No session lock found');
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface SessionLock {
|
|||||||
pid: number;
|
pid: number;
|
||||||
sessions: string[];
|
sessions: string[];
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
|
/** CLIProxy version running (added for version mismatch detection) */
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generate unique session ID */
|
/** Generate unique session ID */
|
||||||
@@ -125,6 +127,23 @@ function isProcessRunning(pid: number): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a process to exit within a timeout.
|
||||||
|
* @param pid Process ID to wait for
|
||||||
|
* @param timeoutMs Maximum time to wait in milliseconds
|
||||||
|
* @returns true if process exited, false if timeout
|
||||||
|
*/
|
||||||
|
async function waitForProcessExit(pid: number, timeoutMs: number): Promise<boolean> {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < timeoutMs) {
|
||||||
|
if (!isProcessRunning(pid)) {
|
||||||
|
return true; // Process exited
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
return false; // Timeout
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if there's an existing proxy running that we can reuse.
|
* Check if there's an existing proxy running that we can reuse.
|
||||||
* Returns the existing lock if proxy is healthy, null otherwise.
|
* Returns the existing lock if proxy is healthy, null otherwise.
|
||||||
@@ -153,9 +172,12 @@ export function getExistingProxy(port: number): SessionLock | null {
|
|||||||
/**
|
/**
|
||||||
* Register a new session with the proxy.
|
* Register a new session with the proxy.
|
||||||
* Call this when starting a new CCS session that will use an existing proxy.
|
* Call this when starting a new CCS session that will use an existing proxy.
|
||||||
|
* @param port Port the proxy is running on
|
||||||
|
* @param proxyPid PID of the proxy process
|
||||||
|
* @param version Optional CLIProxy version (stored when spawning new proxy)
|
||||||
* @returns Session ID for this session
|
* @returns Session ID for this session
|
||||||
*/
|
*/
|
||||||
export function registerSession(port: number, proxyPid: number): string {
|
export function registerSession(port: number, proxyPid: number, version?: string): string {
|
||||||
const sessionId = generateSessionId();
|
const sessionId = generateSessionId();
|
||||||
const existingLock = readSessionLockForPort(port);
|
const existingLock = readSessionLockForPort(port);
|
||||||
|
|
||||||
@@ -170,6 +192,7 @@ export function registerSession(port: number, proxyPid: number): string {
|
|||||||
pid: proxyPid,
|
pid: proxyPid,
|
||||||
sessions: [sessionId],
|
sessions: [sessionId],
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
writeSessionLockForPort(newLock);
|
writeSessionLockForPort(newLock);
|
||||||
}
|
}
|
||||||
@@ -315,6 +338,19 @@ export async function stopProxy(port: number = CLIPROXY_DEFAULT_PORT): Promise<{
|
|||||||
// Found CLIProxy running without session lock - kill it
|
// Found CLIProxy running without session lock - kill it
|
||||||
try {
|
try {
|
||||||
process.kill(portProcess.pid, 'SIGTERM');
|
process.kill(portProcess.pid, 'SIGTERM');
|
||||||
|
|
||||||
|
// Wait for graceful shutdown
|
||||||
|
const exited = await waitForProcessExit(portProcess.pid, 3000);
|
||||||
|
if (!exited) {
|
||||||
|
// Escalate to SIGKILL
|
||||||
|
try {
|
||||||
|
process.kill(portProcess.pid, 'SIGKILL');
|
||||||
|
await waitForProcessExit(portProcess.pid, 1000);
|
||||||
|
} catch {
|
||||||
|
// Process may have exited between check and kill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { stopped: true, pid: portProcess.pid, sessionCount: 0 };
|
return { stopped: true, pid: portProcess.pid, sessionCount: 0 };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as NodeJS.ErrnoException;
|
const error = err as NodeJS.ErrnoException;
|
||||||
@@ -338,6 +374,18 @@ export async function stopProxy(port: number = CLIPROXY_DEFAULT_PORT): Promise<{
|
|||||||
// Kill the proxy process
|
// Kill the proxy process
|
||||||
process.kill(pid, 'SIGTERM');
|
process.kill(pid, 'SIGTERM');
|
||||||
|
|
||||||
|
// Wait for graceful shutdown
|
||||||
|
const exited = await waitForProcessExit(pid, 3000);
|
||||||
|
if (!exited) {
|
||||||
|
// Escalate to SIGKILL
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGKILL');
|
||||||
|
await waitForProcessExit(pid, 1000);
|
||||||
|
} catch {
|
||||||
|
// Process may have exited between check and kill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up session lock
|
// Clean up session lock
|
||||||
deleteSessionLockForPort(port);
|
deleteSessionLockForPort(port);
|
||||||
|
|
||||||
@@ -362,6 +410,7 @@ export function getProxyStatus(port: number = CLIPROXY_DEFAULT_PORT): {
|
|||||||
pid?: number;
|
pid?: number;
|
||||||
sessionCount?: number;
|
sessionCount?: number;
|
||||||
startedAt?: string;
|
startedAt?: string;
|
||||||
|
version?: string;
|
||||||
} {
|
} {
|
||||||
const lock = readSessionLockForPort(port);
|
const lock = readSessionLockForPort(port);
|
||||||
|
|
||||||
@@ -381,5 +430,26 @@ export function getProxyStatus(port: number = CLIPROXY_DEFAULT_PORT): {
|
|||||||
pid: lock.pid,
|
pid: lock.pid,
|
||||||
sessionCount: lock.sessions.length,
|
sessionCount: lock.sessions.length,
|
||||||
startedAt: lock.startedAt,
|
startedAt: lock.startedAt,
|
||||||
|
version: lock.version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version of the running proxy from session lock.
|
||||||
|
* @param port Port to check (defaults to CLIPROXY_DEFAULT_PORT)
|
||||||
|
* @returns Version string if available, null otherwise
|
||||||
|
*/
|
||||||
|
export function getRunningProxyVersion(port: number = CLIPROXY_DEFAULT_PORT): string | null {
|
||||||
|
const lock = readSessionLockForPort(port);
|
||||||
|
if (!lock) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify proxy is still running
|
||||||
|
if (!isProcessRunning(lock.pid)) {
|
||||||
|
deleteSessionLockForPort(port);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lock.version ?? null;
|
||||||
|
}
|
||||||
@@ -181,6 +181,32 @@ export interface BindingTestResult {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a port to become free (no process listening)
|
||||||
|
* @param port Port number to wait for
|
||||||
|
* @param timeoutMs Maximum time to wait in milliseconds (default: 5000)
|
||||||
|
* @param pollIntervalMs Interval between checks in milliseconds (default: 200)
|
||||||
|
* @returns True if port became free, false if timeout
|
||||||
|
*/
|
||||||
|
export async function waitForPortFree(
|
||||||
|
port: number,
|
||||||
|
timeoutMs = 5000,
|
||||||
|
pollIntervalMs = 200
|
||||||
|
): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeoutMs) {
|
||||||
|
const process = await getPortProcess(port);
|
||||||
|
if (!process) {
|
||||||
|
return true; // Port is free
|
||||||
|
}
|
||||||
|
// Wait before next check
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Timeout reached
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test if we can bind to localhost on a specific port
|
* Test if we can bind to localhost on a specific port
|
||||||
* This verifies network stack is working and port is actually available
|
* This verifies network stack is working and port is actually available
|
||||||
|
|||||||
Reference in new issue
Block a user