Files
oh-my-openagent/src/tools/lsp/lsp-process.ts
YeonGyu-Kim 422db236fe fix(lsp): remove unreliable Windows binary availability check
The isBinaryAvailableOnWindows() function used spawnSync("where")

which fails even when the binary IS on PATH, causing false negatives.

Removed the redundant pre-check and let nodeSpawn handle binary

resolution naturally with proper OS-level error messages.

Fixes #1805
2026-02-14 14:58:01 +09:00

162 lines
4.6 KiB
TypeScript

import { spawn as bunSpawn } from "bun"
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process"
import { existsSync, statSync } from "fs"
import { log } from "../../shared/logger"
// Bun spawn segfaults on Windows (oven-sh/bun#25798) — unfixed as of v1.3.8+
function shouldUseNodeSpawn(): boolean {
return process.platform === "win32"
}
// Prevents segfaults when libuv gets a non-existent cwd (oven-sh/bun#25798)
export function validateCwd(cwd: string): { valid: boolean; error?: string } {
try {
if (!existsSync(cwd)) {
return { valid: false, error: `Working directory does not exist: ${cwd}` }
}
const stats = statSync(cwd)
if (!stats.isDirectory()) {
return { valid: false, error: `Path is not a directory: ${cwd}` }
}
return { valid: true }
} catch (err) {
return { valid: false, error: `Cannot access working directory: ${cwd} (${err instanceof Error ? err.message : String(err)})` }
}
}
interface StreamReader {
read(): Promise<{ done: boolean; value: Uint8Array | undefined }>
}
// Bridges Bun Subprocess and Node.js ChildProcess under a common API
export interface UnifiedProcess {
stdin: { write(chunk: Uint8Array | string): void }
stdout: { getReader(): StreamReader }
stderr: { getReader(): StreamReader }
exitCode: number | null
exited: Promise<number>
kill(signal?: string): void
}
function wrapNodeProcess(proc: ChildProcess): UnifiedProcess {
let resolveExited: (code: number) => void
let exitCode: number | null = null
const exitedPromise = new Promise<number>((resolve) => {
resolveExited = resolve
})
proc.on("exit", (code) => {
exitCode = code ?? 1
resolveExited(exitCode)
})
proc.on("error", () => {
if (exitCode === null) {
exitCode = 1
resolveExited(1)
}
})
const createStreamReader = (nodeStream: NodeJS.ReadableStream | null): StreamReader => {
const chunks: Uint8Array[] = []
let streamEnded = false
type ReadResult = { done: boolean; value: Uint8Array | undefined }
let waitingResolve: ((result: ReadResult) => void) | null = null
if (nodeStream) {
nodeStream.on("data", (chunk: Buffer) => {
const uint8 = new Uint8Array(chunk)
if (waitingResolve) {
const resolve = waitingResolve
waitingResolve = null
resolve({ done: false, value: uint8 })
} else {
chunks.push(uint8)
}
})
nodeStream.on("end", () => {
streamEnded = true
if (waitingResolve) {
const resolve = waitingResolve
waitingResolve = null
resolve({ done: true, value: undefined })
}
})
nodeStream.on("error", () => {
streamEnded = true
if (waitingResolve) {
const resolve = waitingResolve
waitingResolve = null
resolve({ done: true, value: undefined })
}
})
} else {
streamEnded = true
}
return {
read(): Promise<ReadResult> {
return new Promise((resolve) => {
if (chunks.length > 0) {
resolve({ done: false, value: chunks.shift()! })
} else if (streamEnded) {
resolve({ done: true, value: undefined })
} else {
waitingResolve = resolve
}
})
},
}
}
return {
stdin: {
write(chunk: Uint8Array | string) {
if (proc.stdin) {
proc.stdin.write(chunk)
}
},
},
stdout: {
getReader: () => createStreamReader(proc.stdout),
},
stderr: {
getReader: () => createStreamReader(proc.stderr),
},
get exitCode() {
return exitCode
},
exited: exitedPromise,
kill(signal?: string) {
try {
if (signal === "SIGKILL") {
proc.kill("SIGKILL")
} else {
proc.kill()
}
} catch {}
},
}
}
export function spawnProcess(
command: string[],
options: { cwd: string; env: Record<string, string | undefined> }
): UnifiedProcess {
const cwdValidation = validateCwd(options.cwd)
if (!cwdValidation.valid) {
throw new Error(`[LSP] ${cwdValidation.error}`)
}
if (shouldUseNodeSpawn()) {
const [cmd, ...args] = command
log("[LSP] Using Node.js child_process on Windows to avoid Bun spawn segfault")
const proc = nodeSpawn(cmd, args, {
cwd: options.cwd,
env: options.env as NodeJS.ProcessEnv,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
shell: true,
})
return wrapNodeProcess(proc)
}
const proc = bunSpawn(command, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
cwd: options.cwd,
env: options.env,
})
return proc as unknown as UnifiedProcess
}