运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

生成

使用 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";