Skip to content

Commit 405a5ef

Browse files
sahrizviclaude
andcommitted
fix(drivers): kill the npm process tree on timeout, and quote the last install hint
Kilo Code's review, which finally ran after its provider rate limit cleared. **A timed-out install kept running.** `runNpm` spawns with `shell: true`, so the child is the shell and npm is its descendant; `child.kill()` therefore reaped the shell and left npm writing to the shared driver directory after the promise had already resolved with a timeout error. The in-flight map cannot help here — it serializes promises, not processes — so a subsequent install could overlap an orphan. POSIX runs now get their own process group (`detached`) and are killed group-wide; Windows, which has no process groups, uses `taskkill /T /F`. **One install hint was still unquoted.** `warehouse_install_driver`'s failure branch interpolated the directory raw while the two equivalents in `DriverNotInstalledError` and `driverReadinessNote` were already wrapped in `shellQuote`. That is the branch a user reads precisely when the automatic install has failed them, and on a path like `C:\Users\John Doe\...` the copied command splits and npm receives the wrong prefix. A test now asserts every printed `--prefix` is quoted. Also merges origin/main (12 commits). The only conflict was in the driver externals list, where main had reformatted it one-per-line while this branch added mongodb, @clickhouse/client, trino-client and @azure/identity; the resolution keeps main's formatting and this branch's four additions. Tests: 186 drivers unit, 4,696 opencode. Typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
1 parent 8573f73 commit 405a5ef

3 files changed

Lines changed: 60 additions & 4 deletions

File tree

‎packages/drivers/src/resolve.ts‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,11 +399,43 @@ export interface InstallResult {
399399
readonly error?: string
400400
}
401401

402+
/** Terminate a spawned shell and everything it started. */
403+
function killTree(child: ReturnType<typeof spawn>): void {
404+
const pid = child.pid
405+
if (pid === undefined) return
406+
if (process.platform === "win32") {
407+
try {
408+
// No process groups on Windows; taskkill walks the tree instead.
409+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" })
410+
} catch {
411+
child.kill()
412+
}
413+
return
414+
}
415+
try {
416+
// Negative pid targets the whole process group created by `detached`.
417+
process.kill(-pid, "SIGTERM")
418+
} catch {
419+
try {
420+
child.kill("SIGTERM")
421+
} catch {
422+
// Already gone.
423+
}
424+
}
425+
}
426+
402427
function runNpm(args: string[], cwd: string, timeoutMs: number): Promise<{ code: number; output: string }> {
403428
return new Promise((resolve) => {
404429
// npm ships as a shell script on POSIX and a .cmd on Windows; `shell: true`
405-
// lets the platform resolve whichever is present on PATH.
406-
const child = spawn("npm", args, { cwd, shell: true, stdio: ["ignore", "pipe", "pipe"] })
430+
// lets the platform resolve whichever is present on PATH. `detached` puts
431+
// the shell in its own process group on POSIX so a timeout can take the
432+
// whole group down — see killTree below.
433+
const child = spawn("npm", args, {
434+
cwd,
435+
shell: true,
436+
stdio: ["ignore", "pipe", "pipe"],
437+
detached: process.platform !== "win32",
438+
})
407439
let output = ""
408440
let settled = false
409441
const finish = (code: number) => {
@@ -413,7 +445,12 @@ function runNpm(args: string[], cwd: string, timeoutMs: number): Promise<{ code:
413445
resolve({ code, output: output.trim() })
414446
}
415447
const timer = setTimeout(() => {
416-
child.kill()
448+
// Killing the child alone leaves npm running: with `shell: true` the child
449+
// is the shell, and npm is its descendant. An orphaned install keeps
450+
// writing to the shared driver directory long after this promise settles,
451+
// which the in-flight map cannot prevent — it serializes promises, not
452+
// processes.
453+
killTree(child)
417454
output += `\nTimed out after ${Math.round(timeoutMs / 1000)}s.`
418455
finish(124)
419456
}, timeoutMs)

‎packages/drivers/test/resolve-unit.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,3 +578,21 @@ describe("shellQuote on Windows", () => {
578578
expect(shellQuote("C:\\Users\\x\\drivers", "win32")).toBe("C:\\Users\\x\\drivers")
579579
})
580580
})
581+
582+
describe("manual-install hints are copy-pasteable", () => {
583+
test("every printed --prefix is quoted", () => {
584+
// The failure branch of warehouse_install_driver was the one site that
585+
// interpolated the directory raw, and it is the branch a user reads
586+
// precisely when the automatic install has just failed them.
587+
process.env["ALTIMATE_DRIVER_DIR"] = "/Users/John Doe/Library/drivers"
588+
589+
const err = new DriverNotInstalledError("snowflake", DRIVER_PACKAGES.snowflake, [])
590+
const prefixes = [...err.message.matchAll(/--prefix (\S+)/g)].map((m) => m[1]!)
591+
592+
expect(prefixes.length).toBeGreaterThan(0)
593+
for (const prefix of prefixes) {
594+
// An unquoted path with a space splits, and npm receives the wrong prefix.
595+
expect(prefix.startsWith("'") || prefix.startsWith('"')).toBe(true)
596+
}
597+
})
598+
})

‎packages/opencode/src/altimate/tools/warehouse-install-driver.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import z from "zod"
22
import { Tool } from "../../tool/tool"
33
import {
44
DRIVER_PACKAGES,
5+
shellQuote,
56
loadOptionalDriver,
67
driverInstallDir,
78
driverLabel,
@@ -96,7 +97,7 @@ export const WarehouseInstallDriverTool = Tool.define("warehouse_install_driver"
9697
output:
9798
`Could not install the ${label} driver (${packages}).\n` +
9899
`${result.error}\n\n` +
99-
`Install it manually with:\n npm install --prefix ${result.dir} ${packages}`,
100+
`Install it manually with:\n npm install --prefix ${shellQuote(result.dir)} ${packages}`,
100101
}
101102
}
102103

0 commit comments

Comments
 (0)