mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
Red-team review of #1340 found that backgroundProbe / shouldKeepSessionProxiesAlive / startKeepAliveWatcher and related types were never reachable: the sole caller (claude-launcher.ts) passed no hasBackgroundWorkerUsingBaseUrl detector, so backgroundProbe was always undefined and the keepalive branch never executed. Remove the unreachable scaffold: - Delete CLAUDE_BG_WORKER_ARGS, hasClaudeBackgroundWorkerUsingBaseUrl{,InProcessList}, shouldKeepSessionProxiesAlive, ShouldKeepSessionProxiesAliveOptions, and the SessionProxyKeepAliveOptions interface from session-bridge.ts - Remove the backgroundProbe/startKeepAliveWatcher block and keepalive exit branch from setupCleanupHandlers; the function now always calls stopSessionResources on Claude exit - Remove backgroundKeepAliveBaseUrl wiring from claude-launcher.ts - Remove the execFileSync import (was only used by the deleted ps-based detector) - Update test file: remove tests for deleted exports, keep placeholder A trusted bg-worker detector can be re-added in a future PR if the feature is actually needed; the keepalive logic in shouldKeepSessionProxiesAlive can be restored then.
239 lines
7.5 KiB
TypeScript
239 lines
7.5 KiB
TypeScript
/**
|
|
* Session Bridge - Integration with session tracking and proxy detection
|
|
*
|
|
* Handles:
|
|
* - Session registration and unregistration
|
|
* - Proxy detection and version checking
|
|
* - Orphaned proxy reclamation
|
|
* - Startup lock coordination
|
|
*/
|
|
|
|
import { ChildProcess } from 'child_process';
|
|
import { info, warn } from '../../utils/ui';
|
|
import { getInstalledCliproxyVersion } from '../binary-manager';
|
|
import { CLIProxyBackend } from '../types';
|
|
import {
|
|
cleanupOrphanedSessions,
|
|
registerSession,
|
|
unregisterSession,
|
|
stopProxy,
|
|
} from '../session-tracker';
|
|
import {
|
|
detectRunningProxy,
|
|
waitForProxyHealthy,
|
|
reclaimOrphanedProxy,
|
|
} from '../proxy/proxy-detector';
|
|
import { withStartupLock } from '../services/startup-lock';
|
|
import { killProcessOnPort } from '../../utils/platform-commands';
|
|
import { stopQuotaMonitor } from '../quota/quota-manager';
|
|
|
|
export interface ProxySessionResult {
|
|
sessionId?: string;
|
|
proxy?: ChildProcess;
|
|
shouldSpawn: boolean;
|
|
}
|
|
|
|
/**
|
|
* Check for existing proxy and handle version mismatch, or determine if new spawn needed
|
|
*/
|
|
export async function checkOrJoinProxy(
|
|
port: number,
|
|
timeout: number,
|
|
verbose: boolean
|
|
): Promise<ProxySessionResult> {
|
|
const log = (msg: string) => {
|
|
if (verbose) {
|
|
console.error(`[cliproxy] ${msg}`);
|
|
}
|
|
};
|
|
|
|
// Cleanup orphaned sessions before detection
|
|
cleanupOrphanedSessions(port);
|
|
|
|
let sessionId: string | undefined;
|
|
let shouldSpawn = false;
|
|
|
|
// Use startup lock to coordinate with other CCS processes
|
|
await withStartupLock(async () => {
|
|
// Detect running proxy using multiple methods (HTTP, session-lock, port-process)
|
|
let proxyStatus = await detectRunningProxy(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(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(port);
|
|
log(`Re-detection after version mismatch restart: ${JSON.stringify(proxyStatus)}`);
|
|
}
|
|
}
|
|
|
|
if (proxyStatus.running && proxyStatus.verified) {
|
|
// Healthy proxy found - join it
|
|
if (proxyStatus.pid) {
|
|
sessionId = reclaimOrphanedProxy(port, proxyStatus.pid, verbose) ?? undefined;
|
|
}
|
|
if (sessionId) {
|
|
console.log(info(`Joined existing CLIProxy on port ${port} (${proxyStatus.method})`));
|
|
} else {
|
|
// Failed to register session - proxy is running but we can't track it
|
|
console.log(info(`Using existing CLIProxy on port ${port} (session tracking unavailable)`));
|
|
log(`PID=${proxyStatus.pid ?? 'unknown'}, session registration skipped`);
|
|
}
|
|
return; // Exit lock early, skip spawning
|
|
}
|
|
|
|
if (proxyStatus.running && !proxyStatus.verified) {
|
|
// Proxy detected but not ready yet (another process is starting it)
|
|
log(`Proxy starting up (detected via ${proxyStatus.method}), waiting...`);
|
|
const becameHealthy = await waitForProxyHealthy(port, timeout);
|
|
if (becameHealthy) {
|
|
if (proxyStatus.pid) {
|
|
sessionId = reclaimOrphanedProxy(port, proxyStatus.pid, verbose) ?? undefined;
|
|
}
|
|
console.log(info(`Joined CLIProxy after startup wait`));
|
|
return; // Exit lock early
|
|
}
|
|
// Proxy didn't become healthy - kill and respawn
|
|
if (proxyStatus.pid) {
|
|
log(`Proxy PID ${proxyStatus.pid} not responding, killing...`);
|
|
killProcessOnPort(port, verbose);
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
}
|
|
|
|
if (proxyStatus.blocked && proxyStatus.blocker) {
|
|
// Port blocked by non-CLIProxy process
|
|
// Last resort: try HTTP health check (handles Windows PID-XXXXX case)
|
|
const isActuallyOurs = await waitForProxyHealthy(port, 1000);
|
|
if (isActuallyOurs) {
|
|
sessionId = reclaimOrphanedProxy(port, proxyStatus.blocker.pid, verbose) ?? undefined;
|
|
console.log(info(`Reclaimed CLIProxy with unrecognized process name`));
|
|
return;
|
|
}
|
|
|
|
// Truly blocked by another application
|
|
const { getPortCheckCommand } = await import('../../utils/platform-commands');
|
|
console.error('');
|
|
console.error(
|
|
warn(
|
|
`Port ${port} is blocked by ${proxyStatus.blocker.processName} (PID ${proxyStatus.blocker.pid})`
|
|
)
|
|
);
|
|
console.error('');
|
|
console.error('To fix this, close the blocking application or run:');
|
|
console.error(` ${getPortCheckCommand(port)}`);
|
|
console.error('');
|
|
throw new Error(`Port ${port} is in use by another application`);
|
|
}
|
|
|
|
// No proxy found - need to spawn
|
|
shouldSpawn = true;
|
|
});
|
|
|
|
return { sessionId, shouldSpawn };
|
|
}
|
|
|
|
/**
|
|
* Register a new proxy session after spawning
|
|
*/
|
|
export function registerProxySession(
|
|
port: number,
|
|
pid: number,
|
|
backend: CLIProxyBackend,
|
|
verbose: boolean
|
|
): string {
|
|
const installedVersion = getInstalledCliproxyVersion();
|
|
const sessionId = registerSession(port, pid, installedVersion, backend);
|
|
|
|
if (verbose) {
|
|
console.error(
|
|
`[cliproxy] Registered session ${sessionId} with new proxy (PID ${pid}, version ${installedVersion})`
|
|
);
|
|
}
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
/**
|
|
* Setup cleanup handlers for session unregistration
|
|
*/
|
|
export function setupCleanupHandlers(
|
|
claude: ChildProcess,
|
|
sessionId: string | undefined,
|
|
sessionPort: number,
|
|
codexReasoningProxy: unknown,
|
|
toolSanitizationProxy: unknown,
|
|
httpsTunnel: unknown,
|
|
verbose: boolean
|
|
): void {
|
|
const log = (msg: string) => {
|
|
if (verbose) {
|
|
console.error(`[cliproxy] ${msg}`);
|
|
}
|
|
};
|
|
|
|
let resourcesStopped = false;
|
|
|
|
const stopResource = (resource: unknown) => {
|
|
if (resource && typeof resource === 'object' && 'stop' in resource) {
|
|
(resource as { stop: () => void }).stop();
|
|
}
|
|
};
|
|
|
|
const stopSessionResources = () => {
|
|
if (resourcesStopped) return;
|
|
resourcesStopped = true;
|
|
stopQuotaMonitor();
|
|
stopResource(codexReasoningProxy);
|
|
stopResource(toolSanitizationProxy);
|
|
stopResource(httpsTunnel);
|
|
|
|
if (sessionId) {
|
|
unregisterSession(sessionId, sessionPort);
|
|
log(`Session ${sessionId} unregistered, proxy persists for other sessions or future use`);
|
|
}
|
|
};
|
|
|
|
const cleanup = () => {
|
|
log('Parent signal received, cleaning up');
|
|
stopSessionResources();
|
|
claude.kill('SIGTERM');
|
|
};
|
|
|
|
claude.on('exit', (code, signal) => {
|
|
log(`Claude exited: code=${code}, signal=${signal}`);
|
|
stopSessionResources();
|
|
|
|
if (signal) {
|
|
process.kill(process.pid, signal as NodeJS.Signals);
|
|
} else {
|
|
process.exit(code || 0);
|
|
}
|
|
});
|
|
|
|
claude.on('error', (error) => {
|
|
console.error(require('../../utils/ui').fail(`Claude CLI error: ${error}`));
|
|
stopSessionResources();
|
|
process.exit(1);
|
|
});
|
|
|
|
process.once('SIGTERM', cleanup);
|
|
process.once('SIGINT', cleanup);
|
|
}
|