mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 12:16:38 +00:00
refactor(cliproxy): modularize top 4 giant files
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.
This commit is contained in:
1 parent
237f91ff69
commit
b149e252eb
29 files changed
+5001
-4392
No files matched your search
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in new issue
Block a user