生成
使用 Bun.spawn 或 Bun.spawnSync 建立子行程
建立一個行程 (Bun.spawn())
提供一個字串陣列作為命令。Bun.spawn() 傳回一個 Bun.Subprocess 物件。
const proc = Bun.spawn(["bun", "--version"]);
console.log(await proc.exited); // 0
Bun.spawn 的第二個引數是一個用於設定子行程的引數物件。
const proc = Bun.spawn(["bun", "--version"], {
cwd: "./path/to/subdir", // 指定工作目錄
env: { ...process.env, FOO: "bar" }, // 指定環境變數
onExit(proc, exitCode, signalCode, error) {
// 退出處理器
},
});
proc.pid; // 子行程的行程ID
輸入流
預設情況下,子行程的輸入流未定義;請使用 stdin 引數進行設定。
const proc = Bun.spawn(["cat"], {
stdin: await fetch("https://raw.githubusercontent.com/oven-sh/bun/main/examples/hashing.js"),
});
const text = await proc.stdout.text();
console.log(text); // "const input = "hello world".repeat(400); ..."
| 值 | 描述 |
|---|---|
null | 預設。 不向子行程提供輸入 |
"pipe" | 傳回一個 FileSink 用於快速增量寫入 |
"inherit" | 繼承父行程的 stdin |
Bun.file() | 從指定檔案讀取 |
TypedArray | DataView | 使用二進位制緩衝區作為輸入 |
Response | 使用響應體 body 作為輸入 |
Request | 使用請求體 body 作為輸入 |
ReadableStream | 使用可讀流作為輸入 |
Blob | 使用 Blob 作為輸入 |
number | 從給定檔案描述符的檔案讀取 |
使用 "pipe" 時,父行程可以向子行程的輸入流增量寫入資料。
const proc = Bun.spawn(["cat"], {
stdin: "pipe", // 傳回可寫的 FileSink
});
// 入隊字串資料
proc.stdin.write("hello");
// 入隊二進位制資料
const enc = new TextEncoder();
proc.stdin.write(enc.encode(" world!"));
// 傳送緩衝資料
proc.stdin.flush();
// 關閉輸入流
proc.stdin.end();
將 ReadableStream 傳遞給 stdin 時,它會將資料直接通過管道傳輸到子行程的輸入流:
const stream = new ReadableStream({
start(controller) {
controller.enqueue("Hello from ");
controller.enqueue("ReadableStream!");
controller.close();
},
});
const proc = Bun.spawn(["cat"], {
stdin: stream,
stdout: "pipe",
});
const output = await proc.stdout.text();
console.log(output); // "Hello from ReadableStream!"
輸出流
從 stdout 和 stderr 屬性讀取子行程的輸出。預設情況下,stdout 是 ReadableStream 的一個例項;stderr 繼承自父行程,因此 proc.stderr 為 undefined。傳入 stderr: "pipe",也可以為它獲取一個 ReadableStream。
const proc = Bun.spawn(["bun", "--version"]);
const text = await proc.stdout.text();
console.log(text); // => "1.3.3\n"
通過向 stdout/stderr 傳入以下值之一來設定輸出流:
| 值 | 描述 |
|---|---|
"pipe" | stdout 的預設值。 將輸出匯入傳回的 Subprocess 物件的 ReadableStream |
"inherit" | stderr 的預設值。 繼承自父行程 |
"ignore" | 丟棄輸出 |
Bun.file() | 寫入指定檔案 |
number | 寫入指定檔案描述符的檔案 |
退出處理
使用 onExit 回撥監聽行程退出或被殺死。
const proc = Bun.spawn(["bun", "--version"], {
onExit(proc, exitCode, signalCode, error) {
// 退出處理器
},
});
exited 屬性是一個 Promise,在行程退出時解析。
const proc = Bun.spawn(["bun", "--version"]);
await proc.exited; // 行程退出時解析
proc.killed; // 布林值 — 行程是否被殺死?
proc.exitCode; // null | 數字
proc.signalCode; // null | "SIGABRT" | "SIGALRM" | ...
殺死行程:
const proc = Bun.spawn(["bun", "--version"]);
proc.kill();
proc.killed; // true
proc.kill(15); // 指定訊號碼
proc.kill("SIGTERM"); // 指定訊號名
父 bun 行程不會終止,直到所有子行程都已退出。使用 proc.unref() 將子行程與父行程分離。
const proc = Bun.spawn(["bun", "--version"]);
proc.unref();
資源使用
行程退出後,resourceUsage() 會報告其資源使用情況:
const proc = Bun.spawn(["bun", "--version"]);
await proc.exited;
const usage = proc.resourceUsage();
console.log(`最大記憶體使用量: ${usage.maxRSS} 位元組`);
console.log(`CPU 時間(使用者態): ${usage.cpuTime.user} 微秒`);
console.log(`CPU 時間(核心態): ${usage.cpuTime.system} 微秒`);
使用 cgroups 限制資源(Linux)
在 Linux 上,傳入 cgroup 可在控制組內啟動子行程。子行程會在開始執行前加入 cgroup,因此在 cgroup 上設定的限制(記憶體、pids、CPU)從第一條指令起就會生效。這些限制也適用於子行程隨後啟動的每個行程。超過記憶體限制時,核心會在 cgroup 內部對行程執行 OOM 終止,而不是從父行程回收記憶體。
cgroup 是 /sys/fs/cgroup 下的一個目錄;使用普通檔案操作建立並設定它,然後傳入其路徑(或已開啟的目錄檔案描述符):
import { mkdirSync, writeFileSync } from "node:fs";
const dir = "/sys/fs/cgroup/build-jobs";
mkdirSync(dir, { recursive: true });
writeFileSync(`${dir}/memory.max`, String(2 * 1024 ** 3)); // cgroup v1: memory.limit_in_bytes
const proc = Bun.spawn({
cmd: ["make", "-j8"],
cgroup: dir,
});
可以將同一個目錄傳遞給任意數量的 spawn;限制會應用於它們的總用量。Bun 同時支援 cgroup v1 和 v2 層級結構。建立 cgroup 通常需要 root 許可權或一個已委派的子樹。在其他平臺上,Bun 會忽略此選項;在 Linux 上,如果子行程無法加入 cgroup,spawn 就會失敗。
使用 AbortSignal
你可以使用 AbortSignal 中止子行程:
const controller = new AbortController();
const { signal } = controller;
const proc = Bun.spawn({
cmd: ["sleep", "100"],
signal,
});
// 後續中止行程:
controller.abort();
使用 timeout 和 killSignal
將 timeout 設定為在經過指定的毫秒數後終止子行程:
// 5秒後殺死行程
const proc = Bun.spawn({
cmd: ["sleep", "10"],
timeout: 5000, // 5秒,單位毫秒
});
await proc.exited; // 5秒後解析
預設情況下,Bun 會使用 SIGTERM 終止超時的行程。使用 killSignal 選項指定其他訊號:
// 5秒後使用 SIGKILL 殺死行程
const proc = Bun.spawn({
cmd: ["sleep", "10"],
timeout: 5000,
killSignal: "SIGKILL", // 可為訊號名稱或訊號編號
});
killSignal 選項也控制當 AbortSignal 中止時 Bun 傳送的訊號。
使用 maxBuffer
對於 Bun.spawnSync,maxBuffer 限制行程在 Bun 將其終止前可以輸出的位元組數:
// “yes” 行程輸出超過 100 位元組後被終止
const result = Bun.spawnSync({
cmd: ["yes"], // Windows 下使用 ["bun", "exec", "yes"]
maxBuffer: 100,
});
// 行程退出
一旦超過限制,Bun 就會停止讀取。因此,傳回的輸出最多隻會因那次超過限制的讀取而超出 maxBuffer,不會超過行程在終止生效前設法寫入的任何額外內容。這與 Node.js 的行為一致。
行程間通訊(IPC)
Bun 支援兩個 bun 行程之間的直接行程間通訊通道。要接收由 Bun 子行程傳送的訊息,請指定一個 ipc 處理器。
const child = Bun.spawn(["bun", "child.ts"], {
ipc(message) {
/**
* 從子行程收到的訊息
**/
},
});
父行程通過傳回的 Subprocess 例項上的 .send() 方法向子行程傳送訊息。ipc 處理器還會將傳送訊息的子行程作為第二個引數接收。
const childProc = Bun.spawn(["bun", "child.ts"], {
ipc(message, childProc) {
/**
* 從子行程收到的訊息
**/
childProc.send("Respond to child");
},
});
childProc.send("I am your father"); // 父行程也可向子行程傳送訊息
子行程使用 process.send() 向父行程傳送訊息,並通過 process.on("message") 接收訊息。Node.js 在 child_process.fork() 中使用相同的 API。
process.send("Hello from child as string");
process.send({ message: "Hello from child as object" });
process.on("message", message => {
// 列印來自父行程的訊息
console.log(message);
});
// 傳送字串訊息
process.send("Hello from child as string");
// 傳送物件訊息
process.send({ message: "Hello from child as object" });
serialization 選項控制兩個行程間通訊的序列化格式:
advanced:(預設)Bun 使用 JSCserializeAPI 序列化訊息,該 API 支援克隆structuredClone支援的所有內容。此格式不支援轉移物件的所有權。json:Bun 使用JSON.stringify和JSON.parse序列化訊息,相比advanced,它支援的物件型別較少。
斷開與父行程的 IPC 通道可呼叫:
childProc.disconnect();
Bun 與 Node.js 之間的 IPC
要在 bun 和 Node.js 行程間使用 IPC,請在 Bun.spawn 中設定 serialization: "json"。這是因為 Node.js 和 Bun 使用不同的 JS 引擎及不同的物件序列化格式。
if (typeof Bun !== "undefined") {
const prefix = `[bun ${process.versions.bun} 🐇]`;
const node = Bun.spawn({
cmd: ["node", __filename],
ipc({ message }) {
console.log(message);
node.send({ message: `${prefix} 👋 hey node` });
node.kill();
},
stdio: ["inherit", "inherit", "inherit"],
serialization: "json",
});
node.send({ message: `${prefix} 👋 hey node` });
} else {
const prefix = `[node ${process.version}]`;
process.on("message", ({ message }) => {
console.log(message);
process.send({ message: `${prefix} 👋 hey bun` });
});
}
終端(PTY)支援
對於互動式終端應用,使用 terminal 選項啟動附加偽終端(PTY)的子行程。子行程會看到一個真實終端,從而支援彩色輸出、游標移動和互動式提示。
const proc = Bun.spawn(["bash"], {
terminal: {
cols: 80,
rows: 24,
data(terminal, data) {
// 終端接收資料時被呼叫
process.stdout.write(data);
},
},
});
// 向終端寫資料
proc.terminal.write("echo hello\n");
// 等待行程退出
await proc.exited;
// 關閉終端
proc.terminal.close();
傳入 terminal 選項時:
- 子行程中
process.stdout.isTTY為true stdin、stdout和stderr都繫結到終端proc.stdin、proc.stdout和proc.stderr傳回null—— 使用終端物件代替- 可通過
proc.terminal訪問終端
終端選項
| 選項 | 描述 | 預設值 |
|---|---|---|
cols | 列數 | 80 |
rows | 行數 | 24 |
name | 用於 PTY 設定的終端型別(使用 env 選項單獨設定 TERM 環境變數) | "xterm-256color" |
data | 接收到資料時的回撥 (terminal, data) => void | — |
exit | PTY 流關閉時的回撥(EOF 或錯誤)。exitCode 是 PTY 生命週期狀態(0=EOF,1=錯誤),而不是子行程退出碼。使用 proc.exited 獲取行程退出狀態。 | — |
drain | 準備好接收更多資料時的回撥 (terminal) => void | — |
終端方法
proc.terminal 傳回的 Terminal 物件具有以下方法:
// 向終端寫入資料
proc.terminal.write("echo hello\n");
// 調整終端大小
proc.terminal.resize(120, 40);
// 設定原生模式(停用行緩衝與回顯)
proc.terminal.setRawMode(true);
// 保持事件迴圈活動直到終端關閉
proc.terminal.ref();
proc.terminal.unref();
// 關閉終端
proc.terminal.close();
可複用終端
要通過同一個終端會話按順序執行多個命令,可以獨立建立終端,並在多個子行程之間複用:
await using terminal = new Bun.Terminal({
cols: 80,
rows: 24,
data(term, data) {
process.stdout.write(data);
},
});
// 建立第一個行程
const proc1 = Bun.spawn(["echo", "first"], { terminal });
await proc1.exited;
// 複用終端啟動另一個行程
const proc2 = Bun.spawn(["echo", "second"], { terminal });
await proc2.exited;
// 終端會被 `await using` 自動關閉
傳入已有的 Terminal 物件時:
- 可以在多個 spawn 之間複用終端
- 可以自行控制何時關閉終端
exit回撥會在呼叫terminal.close()時觸發,而不是在每個子行程退出時觸發- 使用
proc.exited檢測各個子行程的退出狀態
平臺差異
Bun.Terminal 在 Linux 和 macOS 上使用 openpty(),在 Windows 上使用 ConPTY(CreatePseudoConsole)。所有平臺上的核心行為都相同:子行程會看到一個 TTY,write() 會將資料寫入子行程的 stdin,子行程輸出會傳遞給 data 回撥,resize() 會更新子行程看到的終端檢視。部分細節有所不同:
- Windows 上沒有 termios。
inputFlags、outputFlags、localFlags和controlFlags始終讀取為0,設定這些值也不會產生效果。setRawMode()會記錄該標誌,但不會影響子行程;子行程會自行控制其控制台模式。 - Windows 上沒有子行程時不會回顯。 在 POSIX 上,即使沒有附加行程,核心行規程也會將
write()輸入回顯到data回撥。ConPTY 沒有行規程;它會緩衝輸入,等待下一個讀取方。如果需要回顯,請啟動一個會回顯輸入的行程。 - ConPTY 會重新編碼輸出。 ConPTY 會將子行程的輸出渲染到虛擬螢幕,並行出描述渲染結果的 VT 序列。因此,
data回撥接收的轉義序列在語義上等效,但位元組內容不完全相同。ConPTY 會保留顏色和文本,但可能會重新排序或合併游標定位和重置序列。在任何子行程輸出之前,ConPTY 還會發出一段簡短的 VT 初始化序列(\x1b[?9001h\x1b[?1004h…)。 - Windows 上的輸入
\r不會轉換為\n。 POSIX 的ICRNL會在輸入時將回車對映為換行;ConPTY 則會原樣傳遞\r。 - 在 ConPTY 下,子行程中的
process.on('SIGWINCH')不會觸發,除非子行程以原生模式讀取 stdin。呼叫resize()後,process.stdout.columns/rows會更新。缺少此訊號是 libuv 的限制,影響所有基於 libuv 的子行程(包括 Node.js)。 - 在 Windows 11 24H2(內部版本 26100)之前,
terminal.close()可能無法及時終止仍在執行的子行程。延遲來自ClosePseudoConsole:在這些版本中,它會阻塞,直到 conhost 將輸出重新整理到管道中。如果需要在子行程仍執行環境拆除終端,請先殺死附加的行程。 - Windows 10 或 Windows Server 2019 上的滑鼠跟蹤不起作用。 這些版本中內建的 ConPTY(conhost 內部版本 17763 至 19045)早於 microsoft/terminal#4856,會丟棄子行程寫入的滑鼠跟蹤啟用序列(
\x1b[?1000h、\x1b[?1002h、\x1b[?1006h)。這些序列永遠不會到達data回撥,因此外部終端不會報告滑鼠事件,子行程也不會收到滑鼠事件。Windows 11 和 Server 2022 會轉發這些序列,但只有在子行程寫入這些序列前啟用了 VT 輸入模式(例如使用process.stdin.setRawMode(true))時才會轉發。
阻塞式 API (Bun.spawnSync())
Bun.spawnSync 是 Bun.spawn 的阻塞式等價 API。它支援相同的輸入和引數,並傳回一個 SyncSubprocess 物件,該物件在幾個方面不同於 Subprocess。
- 傳回的物件包含一個
success屬性,用於指示行程是否以零退出碼退出。 stdout和stderr屬性是Buffer的例項,而不是ReadableStream。- 沒有
stdin屬性。使用Bun.spawn可向子行程的輸入流增量寫入資料。
const proc = Bun.spawnSync(["echo", "hello"]);
console.log(proc.stdout.toString());
// => "hello\n"
原則上,非同步的 Bun.spawn 適合 HTTP 伺服器和應用,Bun.spawnSync 更適合建置命令列工具。
效能基準
Bun 的 spawnSync 比 Node.js 的 child_process 模組快 60%。
bun spawn.mjs
cpu: Apple M1 Max
runtime: bun 1.x (arm64-darwin)
benchmark time (avg) (min … max) p75 p99 p995
--------------------------------------------------------- -----------------------------
spawnSync echo hi 888.14 µs/iter (821.83 µs … 1.2 ms) 905.92 µs 1 ms 1.03 ms
node spawn.node.mjs
cpu: Apple M1 Max
runtime: node v18.9.1 (arm64-darwin)
benchmark time (avg) (min … max) p75 p99 p995
--------------------------------------------------------- -----------------------------
spawnSync echo hi 1.47 ms/iter (1.14 ms … 2.64 ms) 1.57 ms 2.37 ms 2.52 ms
參考
以下是 Spawn API 和型別的參考。實際型別包含複雜的泛型,用於根據傳遞給 Bun.spawn 和 Bun.spawnSync 的選項,對 Subprocess 流進行強型別約束。完整詳情請參閱 bun.d.ts。
interface Bun {
spawn(command: string[], options?: SpawnOptions.OptionsObject): Subprocess;
spawnSync(command: string[], options?: SpawnOptions.OptionsObject): SyncSubprocess;
spawn(options: { cmd: string[] } & SpawnOptions.OptionsObject): Subprocess;
spawnSync(options: { cmd: string[] } & SpawnOptions.OptionsObject): SyncSubprocess;
}
namespace SpawnOptions {
interface OptionsObject {
cwd?: string;
env?: Record<string, string | undefined>;
stdio?: [Writable, Readable, Readable];
stdin?: Writable;
stdout?: Readable;
stderr?: Readable;
onExit?(
subprocess: Subprocess,
exitCode: number | null,
signalCode: NodeJS.Signals | number | null,
error?: ErrorLike,
): void | Promise<void>;
ipc?(message: any, subprocess: Subprocess): void;
serialization?: "json" | "advanced";
windowsHide?: boolean;
windowsVerbatimArguments?: boolean;
argv0?: string;
signal?: AbortSignal;
timeout?: number;
killSignal?: string | number;
maxBuffer?: number;
cgroup?: string | number; // Linux only; cgroup directory path or open directory fd
terminal?: TerminalOptions | Terminal; // Bun.spawn only (spawnSync throws); PTY (POSIX) / ConPTY (Windows) support
}
type Readable =
| "pipe"
| "inherit"
| "ignore"
| null // 等價於 "ignore"
| undefined // 使用預設值
| BunFile
| ArrayBufferView
| number;
type Writable =
| "pipe"
| "inherit"
| "ignore"
| null // 等價於 "ignore"
| undefined // 使用預設值
| BunFile
| ArrayBufferView
| number
| ReadableStream
| Blob
| Response
| Request;
}
interface Subprocess extends AsyncDisposable {
readonly stdin: FileSink | number | undefined | null;
readonly stdout: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
readonly stderr: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
readonly readable: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
readonly terminal: Terminal | undefined;
readonly pid: number;
readonly exited: Promise<number>;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | number | null;
readonly killed: boolean;
kill(exitCode?: number | NodeJS.Signals): void;
ref(): void;
unref(): void;
send(message: any): void;
disconnect(): void;
resourceUsage(): ResourceUsage | undefined;
}
interface SyncSubprocess {
stdout: Buffer | undefined;
stderr: Buffer | undefined;
exitCode: number;
success: boolean;
resourceUsage: ResourceUsage;
signalCode?: NodeJS.Signals | number;
exitedDueToTimeout?: true;
pid: number;
}
interface TerminalOptions {
cols?: number;
rows?: number;
name?: string;
data?: (terminal: Terminal, data: Uint8Array<ArrayBuffer>) => void;
/** PTY 流關閉時呼叫(EOF 或錯誤)。exitCode 是 PTY 生命週期狀態(0=EOF,1=錯誤),非子行程退出碼。 */
exit?: (terminal: Terminal, exitCode: number, signal: string | null) => void;
drain?: (terminal: Terminal) => void;
}
interface Terminal extends AsyncDisposable {
readonly closed: boolean;
inputFlags: number; // termios c_iflag
outputFlags: number; // termios c_oflag
localFlags: number; // termios c_lflag
controlFlags: number; // termios c_cflag
write(data: string | BufferSource): number;
resize(cols: number, rows: number): void;
setRawMode(enabled: boolean): void;
ref(): void;
unref(): void;
close(): void;
}
interface ResourceUsage {
contextSwitches: {
voluntary: number;
involuntary: number;
};
cpuTime: {
user: number;
system: number;
total: number;
};
maxRSS: number;
messages: {
sent: number;
received: number;
};
ops: {
in: number;
out: number;
};
shmSize: number;
signalCount: number;
swapCount: number;
}
type Signal =
| "SIGABRT"
| "SIGALRM"
| "SIGBUS"
| "SIGCHLD"
| "SIGCONT"
| "SIGFPE"
| "SIGHUP"
| "SIGILL"
| "SIGINT"
| "SIGIO"
| "SIGIOT"
| "SIGKILL"
| "SIGPIPE"
| "SIGPOLL"
| "SIGPROF"
| "SIGPWR"
| "SIGQUIT"
| "SIGSEGV"
| "SIGSTKFLT"
| "SIGSTOP"
| "SIGSYS"
| "SIGTERM"
| "SIGTRAP"
| "SIGTSTP"
| "SIGTTIN"
| "SIGTTOU"
| "SIGUNUSED"
| "SIGURG"
| "SIGUSR1"
| "SIGUSR2"
| "SIGVTALRM"
| "SIGWINCH"
| "SIGXCPU"
| "SIGXFSZ"
| "SIGBREAK"
| "SIGLOST"
| "SIGINFO";