|
@@ -1,4 +1,5 @@
|
|
|
import { spawn as launch, type ChildProcess } from "child_process"
|
|
import { spawn as launch, type ChildProcess } from "child_process"
|
|
|
|
|
+import { buffer } from "node:stream/consumers"
|
|
|
|
|
|
|
|
export namespace Process {
|
|
export namespace Process {
|
|
|
export type Stdio = "inherit" | "pipe" | "ignore"
|
|
export type Stdio = "inherit" | "pipe" | "ignore"
|
|
@@ -14,58 +15,112 @@ export namespace Process {
|
|
|
timeout?: number
|
|
timeout?: number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ export interface RunOptions extends Omit<Options, "stdout" | "stderr"> {
|
|
|
|
|
+ nothrow?: boolean
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ export interface Result {
|
|
|
|
|
+ code: number
|
|
|
|
|
+ stdout: Buffer
|
|
|
|
|
+ stderr: Buffer
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ export class RunFailedError extends Error {
|
|
|
|
|
+ readonly cmd: string[]
|
|
|
|
|
+ readonly code: number
|
|
|
|
|
+ readonly stdout: Buffer
|
|
|
|
|
+ readonly stderr: Buffer
|
|
|
|
|
+
|
|
|
|
|
+ constructor(cmd: string[], code: number, stdout: Buffer, stderr: Buffer) {
|
|
|
|
|
+ const text = stderr.toString().trim()
|
|
|
|
|
+ super(
|
|
|
|
|
+ text
|
|
|
|
|
+ ? `Command failed with code ${code}: ${cmd.join(" ")}\n${text}`
|
|
|
|
|
+ : `Command failed with code ${code}: ${cmd.join(" ")}`,
|
|
|
|
|
+ )
|
|
|
|
|
+ this.name = "ProcessRunFailedError"
|
|
|
|
|
+ this.cmd = [...cmd]
|
|
|
|
|
+ this.code = code
|
|
|
|
|
+ this.stdout = stdout
|
|
|
|
|
+ this.stderr = stderr
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
export type Child = ChildProcess & { exited: Promise<number> }
|
|
export type Child = ChildProcess & { exited: Promise<number> }
|
|
|
|
|
|
|
|
- export function spawn(cmd: string[], options: Options = {}): Child {
|
|
|
|
|
|
|
+ export function spawn(cmd: string[], opts: Options = {}): Child {
|
|
|
if (cmd.length === 0) throw new Error("Command is required")
|
|
if (cmd.length === 0) throw new Error("Command is required")
|
|
|
- options.abort?.throwIfAborted()
|
|
|
|
|
|
|
+ opts.abort?.throwIfAborted()
|
|
|
|
|
|
|
|
const proc = launch(cmd[0], cmd.slice(1), {
|
|
const proc = launch(cmd[0], cmd.slice(1), {
|
|
|
- cwd: options.cwd,
|
|
|
|
|
- env: options.env === null ? {} : options.env ? { ...process.env, ...options.env } : undefined,
|
|
|
|
|
- stdio: [options.stdin ?? "ignore", options.stdout ?? "ignore", options.stderr ?? "ignore"],
|
|
|
|
|
|
|
+ cwd: opts.cwd,
|
|
|
|
|
+ env: opts.env === null ? {} : opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
|
|
|
+ stdio: [opts.stdin ?? "ignore", opts.stdout ?? "ignore", opts.stderr ?? "ignore"],
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- let aborted = false
|
|
|
|
|
|
|
+ let closed = false
|
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
|
|
|
|
|
|
const abort = () => {
|
|
const abort = () => {
|
|
|
- if (aborted) return
|
|
|
|
|
|
|
+ if (closed) return
|
|
|
if (proc.exitCode !== null || proc.signalCode !== null) return
|
|
if (proc.exitCode !== null || proc.signalCode !== null) return
|
|
|
- aborted = true
|
|
|
|
|
-
|
|
|
|
|
- proc.kill(options.kill ?? "SIGTERM")
|
|
|
|
|
|
|
+ closed = true
|
|
|
|
|
|
|
|
- const timeout = options.timeout ?? 5_000
|
|
|
|
|
- if (timeout <= 0) return
|
|
|
|
|
|
|
+ proc.kill(opts.kill ?? "SIGTERM")
|
|
|
|
|
|
|
|
- timer = setTimeout(() => {
|
|
|
|
|
- proc.kill("SIGKILL")
|
|
|
|
|
- }, timeout)
|
|
|
|
|
|
|
+ const ms = opts.timeout ?? 5_000
|
|
|
|
|
+ if (ms <= 0) return
|
|
|
|
|
+ timer = setTimeout(() => proc.kill("SIGKILL"), ms)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const exited = new Promise<number>((resolve, reject) => {
|
|
const exited = new Promise<number>((resolve, reject) => {
|
|
|
const done = () => {
|
|
const done = () => {
|
|
|
- options.abort?.removeEventListener("abort", abort)
|
|
|
|
|
|
|
+ opts.abort?.removeEventListener("abort", abort)
|
|
|
if (timer) clearTimeout(timer)
|
|
if (timer) clearTimeout(timer)
|
|
|
}
|
|
}
|
|
|
- proc.once("exit", (exitCode, signal) => {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ proc.once("exit", (code, signal) => {
|
|
|
done()
|
|
done()
|
|
|
- resolve(exitCode ?? (signal ? 1 : 0))
|
|
|
|
|
|
|
+ resolve(code ?? (signal ? 1 : 0))
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
proc.once("error", (error) => {
|
|
proc.once("error", (error) => {
|
|
|
done()
|
|
done()
|
|
|
reject(error)
|
|
reject(error)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- if (options.abort) {
|
|
|
|
|
- options.abort.addEventListener("abort", abort, { once: true })
|
|
|
|
|
- if (options.abort.aborted) abort()
|
|
|
|
|
|
|
+ if (opts.abort) {
|
|
|
|
|
+ opts.abort.addEventListener("abort", abort, { once: true })
|
|
|
|
|
+ if (opts.abort.aborted) abort()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const child = proc as Child
|
|
const child = proc as Child
|
|
|
child.exited = exited
|
|
child.exited = exited
|
|
|
return child
|
|
return child
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result> {
|
|
|
|
|
+ const proc = spawn(cmd, {
|
|
|
|
|
+ cwd: opts.cwd,
|
|
|
|
|
+ env: opts.env,
|
|
|
|
|
+ stdin: opts.stdin,
|
|
|
|
|
+ abort: opts.abort,
|
|
|
|
|
+ kill: opts.kill,
|
|
|
|
|
+ timeout: opts.timeout,
|
|
|
|
|
+ stdout: "pipe",
|
|
|
|
|
+ stderr: "pipe",
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (!proc.stdout || !proc.stderr) throw new Error("Process output not available")
|
|
|
|
|
+
|
|
|
|
|
+ const [code, stdout, stderr] = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
|
|
|
|
|
+ const out = {
|
|
|
|
|
+ code,
|
|
|
|
|
+ stdout,
|
|
|
|
|
+ stderr,
|
|
|
|
|
+ }
|
|
|
|
|
+ if (out.code === 0 || opts.nothrow) return out
|
|
|
|
|
+ throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|