執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

生成

使用 Bun.spawn 或 Bun.spawnSync 建立子行程

建立一個行程 (Bun.spawn())

提供一個字串陣列作為命令。Bun.spawn() 傳回一個 Bun.Subprocess 物件。

TypeScript
const proc = Bun.spawn(["bun", "--version"]);
console.log(await proc.exited); // 0

Bun.spawn 的第二個引數是一個用於設定子行程的引數物件。

TypeScript
const proc = Bun.spawn(["bun", "--version"], {
  cwd: "./path/to/subdir", // 指定工作目錄
  env: { ...process.env, FOO: "bar" }, // 指定環境變數
  onExit(proc, exitCode, signalCode, error) {
    // 退出處理器
  },
});

proc.pid; // 子行程的行程ID

輸入流

預設情況下,子行程的輸入流未定義;請使用 stdin 引數進行設定。

TypeScript
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" 時,父行程可以向子行程的輸入流增量寫入資料。

TypeScript
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 時,它會將資料直接通過管道傳輸到子行程的輸入流:

TypeScript
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。

TypeScript
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 回撥監聽行程退出或被殺死。

index.ts
const proc = Bun.spawn(["bun", "--version"], {
  onExit(proc, exitCode, signalCode, error) {
    // 退出處理器
  },
});

exited 屬性是一個 Promise,在行程退出時解析。

index.ts
const proc = Bun.spawn(["bun", "--version"]);

await proc.exited; // 行程退出時解析
proc.killed; // 布林值 — 行程是否被殺死?
proc.exitCode; // null | 數字
proc.signalCode; // null | "SIGABRT" | "SIGALRM" | ...

殺死行程:

index.ts
const proc = Bun.spawn(["bun", "--version"]);
proc.kill();
proc.killed; // true

proc.kill(15); // 指定訊號碼
proc.kill("SIGTERM"); // 指定訊號名

父 bun 行程不會終止,直到所有子行程都已退出。使用 proc.unref() 將子行程與父行程分離。

index.ts
const proc = Bun.spawn(["bun", "--version"]);
proc.unref();

資源使用

行程退出後,resourceUsage() 會報告其資源使用情況:

index.ts
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 下的一個目錄;使用普通檔案操作建立並設定它,然後傳入其路徑(或已開啟的目錄檔案描述符):

index.ts
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 中止子行程:

index.ts
const controller = new AbortController();
const { signal } = controller;

const proc = Bun.spawn({
  cmd: ["sleep", "100"],
  signal,
});

// 後續中止行程:
controller.abort();

使用 timeout 和 killSignal

將 timeout 設定為在經過指定的毫秒數後終止子行程:

index.ts
// 5秒後殺死行程
const proc = Bun.spawn({
  cmd: ["sleep", "10"],
  timeout: 5000, // 5秒,單位毫秒
});

await proc.exited; // 5秒後解析

預設情況下,Bun 會使用 SIGTERM 終止超時的行程。使用 killSignal 選項指定其他訊號:

index.ts
// 5秒後使用 SIGKILL 殺死行程
const proc = Bun.spawn({
  cmd: ["sleep", "10"],
  timeout: 5000,
  killSignal: "SIGKILL", // 可為訊號名稱或訊號編號
});

killSignal 選項也控制當 AbortSignal 中止時 Bun 傳送的訊號。

使用 maxBuffer

對於 Bun.spawnSync,maxBuffer 限制行程在 Bun 將其終止前可以輸出的位元組數:

index.ts
// “yes” 行程輸出超過 100 位元組後被終止
const result = Bun.spawnSync({
  cmd: ["yes"], // Windows 下使用 ["bun", "exec", "yes"]
  maxBuffer: 100,
});
// 行程退出

一旦超過限制,Bun 就會停止讀取。因此,傳回的輸出最多隻會因那次超過限制的讀取而超出 maxBuffer,不會超過行程在終止生效前設法寫入的任何額外內容。這與 Node.js 的行為一致。

行程間通訊(IPC)

Bun 支援兩個 bun 行程之間的直接行程間通訊通道。要接收由 Bun 子行程傳送的訊息,請指定一個 ipc 處理器。

parent.ts
const child = Bun.spawn(["bun", "child.ts"], {
  ipc(message) {
    /**
     * 從子行程收到的訊息
     **/
  },
});

父行程通過傳回的 Subprocess 例項上的 .send() 方法向子行程傳送訊息。ipc 處理器還會將傳送訊息的子行程作為第二個引數接收。

parent.ts
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。

child.ts
process.send("Hello from child as string");
process.send({ message: "Hello from child as object" });

process.on("message", message => {
  // 列印來自父行程的訊息
  console.log(message);
});
child.ts
// 傳送字串訊息
process.send("Hello from child as string");

// 傳送物件訊息
process.send({ message: "Hello from child as object" });

serialization 選項控制兩個行程間通訊的序列化格式:

  • advanced:(預設)Bun 使用 JSC serialize API 序列化訊息,該 API 支援克隆 structuredClone 支援的所有內容。此格式不支援轉移物件的所有權。
  • json:Bun 使用 JSON.stringify 和 JSON.parse 序列化訊息,相比 advanced,它支援的物件型別較少。

斷開與父行程的 IPC 通道可呼叫:

TypeScript
childProc.disconnect();

Bun 與 Node.js 之間的 IPC

要在 bun 和 Node.js 行程間使用 IPC,請在 Bun.spawn 中設定 serialization: "json"。這是因為 Node.js 和 Bun 使用不同的 JS 引擎及不同的物件序列化格式。

bun-node-ipc.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)的子行程。子行程會看到一個真實終端,從而支援彩色輸出、游標移動和互動式提示。

TypeScript
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—
exitPTY 流關閉時的回撥(EOF 或錯誤)。exitCode 是 PTY 生命週期狀態(0=EOF,1=錯誤),而不是子行程退出碼。使用 proc.exited 獲取行程退出狀態。—
drain準備好接收更多資料時的回撥 (terminal) => void—

終端方法

proc.terminal 傳回的 Terminal 物件具有以下方法:

TypeScript
// 向終端寫入資料
proc.terminal.write("echo hello\n");

// 調整終端大小
proc.terminal.resize(120, 40);

// 設定原生模式(停用行緩衝與回顯)
proc.terminal.setRawMode(true);

// 保持事件迴圈活動直到終端關閉
proc.terminal.ref();
proc.terminal.unref();

// 關閉終端
proc.terminal.close();

可複用終端

要通過同一個終端會話按順序執行多個命令,可以獨立建立終端,並在多個子行程之間複用:

TypeScript
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。

  1. 傳回的物件包含一個 success 屬性,用於指示行程是否以零退出碼退出。
  2. stdout 和 stderr 屬性是 Buffer 的例項,而不是 ReadableStream。
  3. 沒有 stdin 屬性。使用 Bun.spawn 可向子行程的輸入流增量寫入資料。
TypeScript
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%。

terminal
bun spawn.mjs
Text
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
terminal
node spawn.node.mjs
Text
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";