-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathexecutor.ts
More file actions
81 lines (70 loc) · 2.25 KB
/
Copy pathexecutor.ts
File metadata and controls
81 lines (70 loc) · 2.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Spawns a child process and streams its stdout/stderr line-by-line through
// a callback. Returns a promise that resolves with the exit code.
import type { Subprocess } from "bun"
export type StreamCallback = (stream: "stdout" | "stderr", text: string) => void
export type RunOptions = {
cmd: string[]
cwd?: string
env?: Record<string, string>
/** kill the process if it runs longer than this many ms */
timeoutMs?: number
/**
* Called once with the spawned process handle so the caller can register
* it for cancellation. Used by the cancel handler in index.ts.
*/
onSpawn?: (proc: Subprocess) => void
}
export type RunResult = {
exitCode: number
timedOut: boolean
}
export async function runStreaming(
opts: RunOptions,
onChunk: StreamCallback
): Promise<RunResult> {
const proc = Bun.spawn(opts.cmd, {
cwd: opts.cwd,
env: opts.env ? { ...process.env, ...opts.env } : process.env,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
})
opts.onSpawn?.(proc)
let timedOut = false
let timeoutId: ReturnType<typeof setTimeout> | undefined
if (opts.timeoutMs) {
timeoutId = setTimeout(() => {
timedOut = true
proc.kill()
}, opts.timeoutMs)
}
// pipe both stdout and stderr concurrently
await Promise.all([
pipeStream(proc.stdout, "stdout", onChunk),
pipeStream(proc.stderr, "stderr", onChunk),
])
const exitCode = await proc.exited
if (timeoutId) clearTimeout(timeoutId)
return { exitCode, timedOut }
}
async function pipeStream(
source: ReadableStream<Uint8Array>,
label: "stdout" | "stderr",
onChunk: StreamCallback
): Promise<void> {
const decoder = new TextDecoder()
const reader = source.getReader()
// We forward the raw decoded text (including its existing newlines) so that
// docker's progress lines render naturally on the frontend. The frontend is
// responsible for splitting on \n if it wants discrete rows.
while (true) {
const { value, done } = await reader.read()
if (done) break
if (!value || value.length === 0) continue
const text = decoder.decode(value, { stream: true })
if (text) onChunk(label, text)
}
// flush any trailing bytes the decoder is still holding
const tail = decoder.decode()
if (tail) onChunk(label, tail)
}