Skip to content

UniPty v0.2.2:新增 zigpty Backend

jixoai v0.2.2 unipty

UniPty v0.2.2 新增官方 Backend 包 @unipty/backend-zigpty,基于 Zig 实现的零依赖 PTY 引擎 zigpty。Windows 可运行(适配层缓冲语义),输出可选磁盘化缓冲 outputSpool。本文对比它与 node-pty 路由的优缺点,以及使用前的注意事项。

$ npm view @unipty/backend-zigpty version
0.2.1

UniPty v0.2.2 发布了(GitHub Release 于 2026-09-07 20:53 UTC 切出)。这一版新增一个官方 Backend 包 @unipty/backend-zigpty,底层是 zigpty(npm zigpty@0.2.1,MIT,pi0 维护),一个用 Zig 实现的 PTY 引擎。Node 侧从此有两个引擎可选,另一个是既有的 @unipty/backend-node-pty

zigpty 与 node-pty 的对比

两条路由的公共契约一致,差异都在引擎层。选型时主要看下面这张表。

维度zigptynode-pty(@lydell 发行版)
运行时依赖0平台子包(optionalDependencies)
安装脚本
分发方式8 个平台元组的预编译全部打进主 tarball,约 420KB每个平台一个子包,按平台各装一份
安装健壮性--omit=optional 等安装姿势不影响optional 依赖被跳过时装不上
原生文本输出有(encoding: "utf8"有(encoding: "utf8"
字节写入默认拒绝,需 writeDecode 选项默认接受
Windows可运行,输出为缓冲式(见下文)声明支持,证据门控
传输读错误上报不可区分(见下文)上报 unsupported

zigpty 的优势集中在分发:没有运行时依赖、没有平台子包、预编译全在主包里,装什么就是什么,不依赖安装器对 optional 依赖的处理。对安装环境不可控的场景(受限 CI、离线镜像)更稳。

它的代价在后文「注意事项」与下面两节:字节写入要多开一个选项,Windows 上的输出是缓冲式,内存边界要靠选项兜住。

上游还有一处要当心的行为:预编译加载失败时,zigpty 库会静默回退到基于管道的伪 PTY,而 isatty 与内核几何尺寸在伪 PTY 上都不成立。适配器在就绪阶段硬门禁这个回退,hasNative 不真就直接给 unsupported,绝不拿假 PTY 冒充真的。

使用

npm install unipty @unipty/backend-zigpty
import { UniPty } from "unipty";
import { createZigptyBackend } from "@unipty/backend-zigpty";

const backend = await createZigptyBackend();
const unipty = new UniPty({ backend });

const pty = unipty.spawn(["/bin/sh", "-i"], {
  terminal: { cols: 120, rows: 40 },
});
for await (const text of pty.stream({ encoding: "utf8" })) {
  process.stdout.write(text);
}

spawnstreamwriteresizecloseterminateexited 的语义与另外三条路由完全一致。25 个场景的公共契约套件对安装后的包产物全量跑过,darwin-arm64 本地一份原生证据,ubuntu 与 macos 的 CI 各一份(2026-09-07,v0.2.2 证据),已进发布目录

Windows:适配层接管输出流控

zigpty 的 Windows 实现带 ConPTY 预编译,但其公开的 pause() / resume() 在该平台是空方法,消费端驱动的输出背压传导不到内核。UniPty 约束不了所有引擎,第三方开发者会有自己的 backend,用适配层弥合引擎差异本来就是这套架构的重点工作,所以这条路由没有在 Windows 上失败关闭,而是由适配器持续排空引擎回调,并把"输出背压在该平台传导不到内核"如实声明为路由级限制,与 Deno 路由对内部读线程的处理是同一类。安装与启动不需要任何平台分支:

const backend = await createZigptyBackend(); // Windows 与 unix 同一行

支持声明仍由证据门控。Windows 元组在拿到公开契约证据之前保持 declared-unverified,兼容性目录如实呈现,缓冲式运行不等于验证的支持。

outputSpool:磁盘化的输出缓冲

停读的消费者是内存的主要风险。终端挂到浏览器标签页上,用户切走十分钟,引擎在这十分钟里产出的输出没有去处,只能堆在适配器的队列里;Windows 上背压传导不到内核,堆得更快。

outputSpool 把这个队列换成磁盘化的 FIFO:

const backend = await createZigptyBackend({
  outputSpool: { memoryBytes: 4 * 1024 * 1024 }, // 或直接 true 用默认值
});

机制是三段。内存头默认 1 MiB 封顶;越界的记录溢写到适配器自有的临时文件;回放严格跟随消费端的拉动节奏,流的水位线之外不多塞一条记录。公开流的字节与不开时完全一致:文本记录按完整记录往返,chunk 边界保持不变。子进程退出时,完成信号会等积压排空才发出,快退输出的尾巴不会被 EOF 截断;显式 close() 仍然同步完成并删除临时文件。

三个使用前需要知道的边界。溢写是同步 IO;积压期间的磁盘占用按设计不设上限,只受子进程自身输出量约束;进程被强杀时临时文件交由系统临时目录清理。在引擎能够暂停的平台上(unix),越过内存界还会把压力传导回内核,阻塞子进程而不是继续堆积。

注意事项

完整差异见 README 的能力差异矩阵。除了上面两节,还有三条需要提前知道。

  1. 字节写入需要选项。 引擎的 write 只收字符串,Endpoint 默认拒绝字节输入并给 unsupported。字节输入流(xterm.js 前端这类)开 writeDecode 即可。解码有状态、跨 chunk 安全。

const backend = await createZigptyBackend({ writeDecode: true });
pty.write(new TextEncoder().encode("echo hi\r"));
  1. 传输读错误不可区分。 引擎不暴露传输层事件,适配器在退出窗口接管读流、用真实的 end / close 当 EOF,另有一个 50ms 的静默窗兜底。读错误与干净 EOF 在这条路由上无法区分,这是如实声明的限制。

  2. 退出观察的形状。 信号致死报 { exitCode: 0, signal: "SIGTERM" }(退出码保持引擎原值);可执行文件缺失报 { exitCode: 1, signal: null },是退出观察不是异常。

四条路由的信号致死报告对照
node-pty   { exitCode: null, signal: "SIGTERM" }
zigpty     { exitCode: 0,    signal: "SIGTERM" }
bun        { exitCode: null, signal: "SIGTERM" }
deno-ffi   { exitCode: 1,    signal: null }

其余变更

  • 修复被暂停读取的洪水子进程收不到终止确认:引擎在积压未排空时会推迟退出观察,terminate() 现在在 kill 后恢复读取(9bac5d7

  • 修复背压暂停恰发生在退出前时内核滞留输出丢失:退出窗口内接管读流并显式恢复(0a9a172

  • 修复 ubuntu 上快退子进程输出丢失:适配器改为在退出窗口接管读流(3d56f71

  • 修复 writeDecode 饱和拒绝推进解码器状态的问题(f083127

  • 发布目录注册表按路由身份键控并做双重身份校验(b878ce4f083127

  • 边界测试电池覆盖 memoryBytes 极值、空记录、损坏记录、积压取消等场景,包测试增至 70 项(f8ad01d

  • 文档全仓同步新路由(021a82d

完整清单见 compare v0.2.0...v0.2.2

升级

无破坏性变更,纯新增包。Backend 包在 npm 上的版本是 0.2.1。

npm i @unipty/backend-zigpty@0.2.1

已经在用 unipty@0.2.0 的话,Core 不用动。

致谢

路由基于 zigpty 构建并精确固定 0.2.1,感谢 pi0 与 UnJS 社区。

链接