mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
Split 4 files exceeding 900 lines into focused modules: - account-manager.ts (961 → 56 lines): 6 modules in accounts/ - registry, query, bulk-ops, token-file-ops, types, index - config-generator.ts (981 → 16 lines): 6 modules in config/ - generator, port-manager, env-builder, thinking-config, path-resolver, index - cliproxy-executor.ts (1,180 → 21 lines): 5 modules in executor/ - index, lifecycle-manager, env-resolver, retry-handler, session-bridge - cliproxy-command.ts (1,297 → 10 lines): 8 modules in commands/cliproxy/ - index, auth, quota, variant, install, help, proxy-lifecycle subcommands Total: 4,419 → 103 lines in original files (97.7% reduction) Backward compatibility maintained via re-export shims. Edge case fixes: CLI validation, thinking fallback, provider warning. BREAKING CHANGES: None - all exports preserved via barrel re-exports.
285 lines
8.9 KiB
TypeScript
285 lines
8.9 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-detector';
|
|
import { withStartupLock } from '../startup-lock';
|
|
import { killProcessOnPort } from '../../utils/platform-commands';
|
|
|
|
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}`);
|
|
}
|
|
};
|
|
|
|
const cleanup = () => {
|
|
log('Parent signal received, cleaning up');
|
|
|
|
if (
|
|
codexReasoningProxy &&
|
|
typeof codexReasoningProxy === 'object' &&
|
|
'stop' in codexReasoningProxy
|
|
) {
|
|
(codexReasoningProxy as { stop: () => void }).stop();
|
|
}
|
|
|
|
if (
|
|
toolSanitizationProxy &&
|
|
typeof toolSanitizationProxy === 'object' &&
|
|
'stop' in toolSanitizationProxy
|
|
) {
|
|
(toolSanitizationProxy as { stop: () => void }).stop();
|
|
}
|
|
|
|
if (httpsTunnel && typeof httpsTunnel === 'object' && 'stop' in httpsTunnel) {
|
|
(httpsTunnel as { stop: () => void }).stop();
|
|
}
|
|
|
|
// Unregister session, proxy keeps running (local mode only)
|
|
if (sessionId) {
|
|
unregisterSession(sessionId, sessionPort);
|
|
}
|
|
claude.kill('SIGTERM');
|
|
};
|
|
|
|
claude.on('exit', (code, signal) => {
|
|
log(`Claude exited: code=${code}, signal=${signal}`);
|
|
|
|
if (
|
|
codexReasoningProxy &&
|
|
typeof codexReasoningProxy === 'object' &&
|
|
'stop' in codexReasoningProxy
|
|
) {
|
|
(codexReasoningProxy as { stop: () => void }).stop();
|
|
}
|
|
|
|
if (
|
|
toolSanitizationProxy &&
|
|
typeof toolSanitizationProxy === 'object' &&
|
|
'stop' in toolSanitizationProxy
|
|
) {
|
|
(toolSanitizationProxy as { stop: () => void }).stop();
|
|
}
|
|
|
|
if (httpsTunnel && typeof httpsTunnel === 'object' && 'stop' in httpsTunnel) {
|
|
(httpsTunnel as { stop: () => void }).stop();
|
|
}
|
|
|
|
// Unregister this session (proxy keeps running for persistence) - only for local mode
|
|
if (sessionId) {
|
|
unregisterSession(sessionId, sessionPort);
|
|
log(`Session ${sessionId} unregistered, proxy persists for other sessions or future use`);
|
|
}
|
|
|
|
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}`));
|
|
|
|
if (
|
|
codexReasoningProxy &&
|
|
typeof codexReasoningProxy === 'object' &&
|
|
'stop' in codexReasoningProxy
|
|
) {
|
|
(codexReasoningProxy as { stop: () => void }).stop();
|
|
}
|
|
|
|
if (
|
|
toolSanitizationProxy &&
|
|
typeof toolSanitizationProxy === 'object' &&
|
|
'stop' in toolSanitizationProxy
|
|
) {
|
|
(toolSanitizationProxy as { stop: () => void }).stop();
|
|
}
|
|
|
|
if (httpsTunnel && typeof httpsTunnel === 'object' && 'stop' in httpsTunnel) {
|
|
(httpsTunnel as { stop: () => void }).stop();
|
|
}
|
|
|
|
// Unregister session, proxy keeps running (local mode only)
|
|
if (sessionId) {
|
|
unregisterSession(sessionId, sessionPort);
|
|
}
|
|
process.exit(1);
|
|
});
|
|
|
|
process.once('SIGTERM', cleanup);
|
|
process.once('SIGINT', cleanup);
|
|
}
|