Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
14 kB · 447 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448import { type ChildProcess, spawn } from 'child_process'import { createMessageConnection, type MessageConnection, StreamMessageReader, StreamMessageWriter, Trace,} from 'vscode-jsonrpc/node.js'import type { InitializeParams, InitializeResult, ServerCapabilities,} from 'vscode-languageserver-protocol'import { logForDebugging } from '../../utils/debug.js'import { errorMessage } from '../../utils/errors.js'import { logError } from '../../utils/log.js'import { subprocessEnv } from '../../utils/subprocessEnv.js'/** * LSP client interface. */export type LSPClient = { readonly capabilities: ServerCapabilities | undefined readonly isInitialized: boolean start: ( command: string, args: string[], options?: { env?: Record<string, string> cwd?: string }, ) => Promise<void> initialize: (params: InitializeParams) => Promise<InitializeResult> sendRequest: <TResult>(method: string, params: unknown) => Promise<TResult> sendNotification: (method: string, params: unknown) => Promise<void> onNotification: (method: string, handler: (params: unknown) => void) => void onRequest: <TParams, TResult>( method: string, handler: (params: TParams) => TResult | Promise<TResult>, ) => void stop: () => Promise<void>}
/** * Create an LSP client wrapper using vscode-jsonrpc. * Manages communication with an LSP server process via stdio. * * @param onCrash - Called when the server process exits unexpectedly (non-zero * exit code during operation, not during intentional stop). Allows the owner * to propagate crash state so the server can be restarted on next use. */export function createLSPClient( serverName: string, onCrash?: (error: Error) => void,): LSPClient { // State variables in closure let process: ChildProcess | undefined let connection: MessageConnection | undefined let capabilities: ServerCapabilities | undefined let isInitialized = false let startFailed = false let startError: Error | undefined let isStopping = false // Track intentional shutdown to avoid spurious error logging // Queue handlers registered before connection ready (lazy initialization support) const pendingHandlers: Array<{ method: string handler: (params: unknown) => void }> = [] const pendingRequestHandlers: Array<{ method: string handler: (params: unknown) => unknown | Promise<unknown> }> = []
function checkStartFailed(): void { if (startFailed) { throw startError || new Error(`LSP server ${serverName} failed to start`) } }
return { get capabilities(): ServerCapabilities | undefined { return capabilities },
get isInitialized(): boolean { return isInitialized },
async start( command: string, args: string[], options?: { env?: Record<string, string> cwd?: string }, ): Promise<void> { try { // 1. Spawn LSP server process process = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...subprocessEnv(), ...options?.env }, cwd: options?.cwd, // Prevent visible console window on Windows (no-op on other platforms) windowsHide: true, })
if (!process.stdout || !process.stdin) { throw new Error('LSP server process stdio not available') }
// 1.5. Wait for process to successfully spawn before using streams // This is CRITICAL: spawn() returns immediately, but the 'error' event // (e.g., ENOENT for command not found) fires asynchronously. // If we use the streams before confirming spawn succeeded, we get // unhandled promise rejections when writes fail on invalid streams. const spawnedProcess = process // Capture for closure await new Promise<void>((resolve, reject) => { const onSpawn = (): void => { cleanup() resolve() } const onError = (error: Error): void => { cleanup() reject(error) } const cleanup = (): void => { spawnedProcess.removeListener('spawn', onSpawn) spawnedProcess.removeListener('error', onError) } spawnedProcess.once('spawn', onSpawn) spawnedProcess.once('error', onError) })
// Capture stderr for server diagnostics and errors if (process.stderr) { process.stderr.on('data', (data: Buffer) => { const output = data.toString().trim() if (output) { logForDebugging(`[LSP SERVER ${serverName}] ${output}`) } }) }
// Handle process errors (after successful spawn, e.g., crash during operation) process.on('error', error => { if (!isStopping) { startFailed = true startError = error logError( new Error( `LSP server ${serverName} failed to start: ${error.message}`, ), ) } })
process.on('exit', (code, _signal) => { if (code !== 0 && code !== null && !isStopping) { isInitialized = false startFailed = false startError = undefined const crashError = new Error( `LSP server ${serverName} crashed with exit code ${code}`, ) logError(crashError) onCrash?.(crashError) } })
// Handle stdin stream errors to prevent unhandled promise rejections // when the LSP server process exits before we finish writing process.stdin.on('error', (error: Error) => { if (!isStopping) { logForDebugging( `LSP server ${serverName} stdin error: ${error.message}`, ) } // Error is logged but not thrown - the connection error handler will catch this })
// 2. Create JSON-RPC connection const reader = new StreamMessageReader(process.stdout) const writer = new StreamMessageWriter(process.stdin) connection = createMessageConnection(reader, writer)
// 2.5. Register error/close handlers BEFORE listen() to catch all errors // This prevents unhandled promise rejections when the server crashes or closes unexpectedly connection.onError(([error, _message, _code]) => { // Only log if not intentionally stopping (avoid spurious errors during shutdown) if (!isStopping) { startFailed = true startError = error logError( new Error( `LSP server ${serverName} connection error: ${error.message}`, ), ) } })
connection.onClose(() => { // Only treat as error if not intentionally stopping if (!isStopping) { isInitialized = false // Don't set startFailed here - the connection may close after graceful shutdown logForDebugging(`LSP server ${serverName} connection closed`) } })
// 3. Start listening for messages connection.listen()
// 3.5. Enable protocol tracing for debugging // Note: trace() sends a $/setTrace notification which can fail if the server // process has already exited. We catch and log the error rather than letting // it become an unhandled promise rejection. connection .trace(Trace.Verbose, { log: (message: string) => { logForDebugging(`[LSP PROTOCOL ${serverName}] ${message}`) }, }) .catch((error: Error) => { logForDebugging( `Failed to enable tracing for ${serverName}: ${error.message}`, ) })
// 4. Apply any queued notification handlers for (const { method, handler } of pendingHandlers) { connection.onNotification(method, handler) logForDebugging( `Applied queued notification handler for ${serverName}.${method}`, ) } pendingHandlers.length = 0 // Clear the queue
// 5. Apply any queued request handlers for (const { method, handler } of pendingRequestHandlers) { connection.onRequest(method, handler) logForDebugging( `Applied queued request handler for ${serverName}.${method}`, ) } pendingRequestHandlers.length = 0 // Clear the queue
logForDebugging(`LSP client started for ${serverName}`) } catch (error) { const err = error as Error logError( new Error(`LSP server ${serverName} failed to start: ${err.message}`), ) throw error } },
async initialize(params: InitializeParams): Promise<InitializeResult> { if (!connection) { throw new Error('LSP client not started') }
checkStartFailed()
try { const result: InitializeResult = await connection.sendRequest( 'initialize', params, )
capabilities = result.capabilities
// Send initialized notification await connection.sendNotification('initialized', {})
isInitialized = true logForDebugging(`LSP server ${serverName} initialized`)
return result } catch (error) { const err = error as Error logError( new Error( `LSP server ${serverName} initialize failed: ${err.message}`, ), ) throw error } },
async sendRequest<TResult>( method: string, params: unknown, ): Promise<TResult> { if (!connection) { throw new Error('LSP client not started') }
checkStartFailed()
if (!isInitialized) { throw new Error('LSP server not initialized') }
try { return await connection.sendRequest(method, params) } catch (error) { const err = error as Error logError( new Error( `LSP server ${serverName} request ${method} failed: ${err.message}`, ), ) throw error } },
async sendNotification(method: string, params: unknown): Promise<void> { if (!connection) { throw new Error('LSP client not started') }
checkStartFailed()
try { await connection.sendNotification(method, params) } catch (error) { const err = error as Error logError( new Error( `LSP server ${serverName} notification ${method} failed: ${err.message}`, ), ) // Don't re-throw for notifications - they're fire-and-forget logForDebugging(`Notification ${method} failed but continuing`) } },
onNotification(method: string, handler: (params: unknown) => void): void { if (!connection) { // Queue handler for application when connection is ready (lazy initialization) pendingHandlers.push({ method, handler }) logForDebugging( `Queued notification handler for ${serverName}.${method} (connection not ready)`, ) return }
checkStartFailed()
connection.onNotification(method, handler) },
onRequest<TParams, TResult>( method: string, handler: (params: TParams) => TResult | Promise<TResult>, ): void { if (!connection) { // Queue handler for application when connection is ready (lazy initialization) pendingRequestHandlers.push({ method, handler: handler as (params: unknown) => unknown | Promise<unknown>, }) logForDebugging( `Queued request handler for ${serverName}.${method} (connection not ready)`, ) return }
checkStartFailed()
connection.onRequest(method, handler) },
async stop(): Promise<void> { let shutdownError: Error | undefined
// Mark as stopping to prevent error handlers from logging spurious errors isStopping = true
try { if (connection) { // Try to send shutdown request and exit notification await connection.sendRequest('shutdown', {}) await connection.sendNotification('exit', {}) } } catch (error) { const err = error as Error logError( new Error(`LSP server ${serverName} stop failed: ${err.message}`), ) shutdownError = err // Continue to cleanup despite shutdown failure } finally { // Always cleanup resources, even if shutdown/exit failed if (connection) { try { connection.dispose() } catch (error) { // Log but don't throw - disposal errors are less critical logForDebugging( `Connection disposal failed for ${serverName}: ${errorMessage(error)}`, ) } connection = undefined }
if (process) { // Remove event listeners to prevent memory leaks process.removeAllListeners('error') process.removeAllListeners('exit') if (process.stdin) { process.stdin.removeAllListeners('error') } if (process.stderr) { process.stderr.removeAllListeners('data') }
try { process.kill() } catch (error) { // Process might already be dead, which is fine logForDebugging( `Process kill failed for ${serverName} (may already be dead): ${errorMessage(error)}`, ) } process = undefined }
isInitialized = false capabilities = undefined isStopping = false // Reset for potential restart // Don't reset startFailed - preserve error state for diagnostics // startFailed and startError remain as-is if (shutdownError) { startFailed = true startError = shutdownError }
logForDebugging(`LSP client stopped for ${serverName}`) }
// Re-throw shutdown error after cleanup is complete if (shutdownError) { throw shutdownError } }, }}