mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 02:15:36 +00:00
Merge pull request #210 from kaitranntt/fix/cliproxy-version-sync-restart
fix(cliproxy): ensure version sync after binary update
This commit is contained in:
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)
|
||||
*/
|
||||
|
||||
import { info } from '../utils/ui';
|
||||
import { getBinDir } from './config-generator';
|
||||
import { info, warn } from '../utils/ui';
|
||||
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
|
||||
import { BinaryInfo, BinaryManagerConfig } from './types';
|
||||
import { CLIPROXY_FALLBACK_VERSION } from './platform-detector';
|
||||
import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service';
|
||||
import { waitForPortFree } from '../utils/port-utils';
|
||||
import {
|
||||
UpdateCheckResult,
|
||||
checkForUpdates,
|
||||
@@ -108,12 +110,32 @@ export function getInstalledCliproxyVersion(): string {
|
||||
/** Install a specific version of CLIProxyAPI */
|
||||
export async function installCliproxyVersion(version: string, verbose = false): Promise<void> {
|
||||
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 (verbose)
|
||||
console.log(info(`Removing existing CLIProxy Plus v${getInstalledCliproxyVersion()}`));
|
||||
manager.deleteBinary();
|
||||
}
|
||||
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 */
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as net from 'net';
|
||||
import { ProgressIndicator } from '../utils/progress-indicator';
|
||||
import { ok, fail, info, warn } from '../utils/ui';
|
||||
import { escapeShellArg } from '../utils/shell-executor';
|
||||
import { ensureCLIProxyBinary } from './binary-manager';
|
||||
import { ensureCLIProxyBinary, getInstalledCliproxyVersion } from './binary-manager';
|
||||
import {
|
||||
generateConfig,
|
||||
getEffectiveEnvVars,
|
||||
@@ -48,7 +48,12 @@ import {
|
||||
installWebSearchHook,
|
||||
displayWebSearchStatus,
|
||||
} 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 { withStartupLock } from './startup-lock';
|
||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||
@@ -429,9 +434,33 @@ export async function execClaudeWithCLIProxy(
|
||||
// Use startup lock to coordinate with other CCS processes
|
||||
await withStartupLock(async () => {
|
||||
// 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)}`);
|
||||
|
||||
// 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) {
|
||||
// Healthy proxy found - join it
|
||||
if (proxyStatus.pid) {
|
||||
@@ -542,9 +571,12 @@ export async function execClaudeWithCLIProxy(
|
||||
throw new Error(`CLIProxy startup failed: ${err.message}`);
|
||||
}
|
||||
|
||||
// Register this session with the new proxy
|
||||
sessionId = registerSession(cfg.port, proxy.pid as number);
|
||||
log(`Registered session ${sessionId} with new proxy (PID ${proxy.pid})`);
|
||||
// Register this session with the new proxy, including the installed version
|
||||
const installedVersion = getInstalledCliproxyVersion();
|
||||
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
|
||||
*/
|
||||
|
||||
import { getExistingProxy, registerSession } from './session-tracker';
|
||||
import { getExistingProxy, registerSession, getRunningProxyVersion } from './session-tracker';
|
||||
import { isCliproxyRunning } from './stats-fetcher';
|
||||
import { getPortProcess, isCLIProxyProcess, PortProcess } from '../utils/port-utils';
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface ProxyStatus {
|
||||
blocker?: PortProcess;
|
||||
/** Number of active sessions (if session-lock found) */
|
||||
sessionCount?: number;
|
||||
/** Version of the running proxy (from session lock) */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
running: true,
|
||||
verified: true,
|
||||
method: 'http',
|
||||
pid,
|
||||
sessionCount: lock?.sessions?.length,
|
||||
version: runningVersion ?? undefined,
|
||||
};
|
||||
}
|
||||
log('HTTP check failed, proxy not responding');
|
||||
@@ -103,13 +111,17 @@ export async function detectRunningProxy(
|
||||
if (lock) {
|
||||
// Session lock exists - proxy might be starting up
|
||||
// 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 {
|
||||
running: true,
|
||||
verified: false,
|
||||
method: 'session-lock',
|
||||
pid: lock.pid,
|
||||
sessionCount: lock.sessions.length,
|
||||
version: lockVersion ?? undefined,
|
||||
};
|
||||
}
|
||||
log('No session lock found');
|
||||
|
||||
@@ -27,6 +27,8 @@ interface SessionLock {
|
||||
pid: number;
|
||||
sessions: string[];
|
||||
startedAt: string;
|
||||
/** CLIProxy version running (added for version mismatch detection) */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/** 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.
|
||||
* 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.
|
||||
* 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
|
||||
*/
|
||||
export function registerSession(port: number, proxyPid: number): string {
|
||||
export function registerSession(port: number, proxyPid: number, version?: string): string {
|
||||
const sessionId = generateSessionId();
|
||||
const existingLock = readSessionLockForPort(port);
|
||||
|
||||
@@ -170,6 +192,7 @@ export function registerSession(port: number, proxyPid: number): string {
|
||||
pid: proxyPid,
|
||||
sessions: [sessionId],
|
||||
startedAt: new Date().toISOString(),
|
||||
version,
|
||||
};
|
||||
writeSessionLockForPort(newLock);
|
||||
}
|
||||
@@ -315,6 +338,19 @@ export async function stopProxy(port: number = CLIPROXY_DEFAULT_PORT): Promise<{
|
||||
// Found CLIProxy running without session lock - kill it
|
||||
try {
|
||||
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 };
|
||||
} catch (err) {
|
||||
const error = err as NodeJS.ErrnoException;
|
||||
@@ -338,6 +374,18 @@ export async function stopProxy(port: number = CLIPROXY_DEFAULT_PORT): Promise<{
|
||||
// Kill the proxy process
|
||||
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
|
||||
deleteSessionLockForPort(port);
|
||||
|
||||
@@ -362,6 +410,7 @@ export function getProxyStatus(port: number = CLIPROXY_DEFAULT_PORT): {
|
||||
pid?: number;
|
||||
sessionCount?: number;
|
||||
startedAt?: string;
|
||||
version?: string;
|
||||
} {
|
||||
const lock = readSessionLockForPort(port);
|
||||
|
||||
@@ -381,5 +430,26 @@ export function getProxyStatus(port: number = CLIPROXY_DEFAULT_PORT): {
|
||||
pid: lock.pid,
|
||||
sessionCount: lock.sessions.length,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* This verifies network stack is working and port is actually available
|
||||
|
||||
Reference in new issue
Block a user