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
162 lines
4.6 KiB
TypeScript
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
|
|
}
|