UniPty v0.2.2:新增 zigpty Backend
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 的对比
两条路由的公共契约一致,差异都在引擎层。选型时主要看下面这张表。
| 维度 | zigpty | node-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);
}
spawn、stream、write、resize、close、terminate、exited 的语义与另外三条路由完全一致。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 的能力差异矩阵。除了上面两节,还有三条需要提前知道。
字节写入需要选项。 引擎的
write只收字符串,Endpoint 默认拒绝字节输入并给unsupported。字节输入流(xterm.js 前端这类)开writeDecode即可。解码有状态、跨 chunk 安全。
const backend = await createZigptyBackend({ writeDecode: true });
pty.write(new TextEncoder().encode("echo hi\r"));
传输读错误不可区分。 引擎不暴露传输层事件,适配器在退出窗口接管读流、用真实的
end/close当 EOF,另有一个 50ms 的静默窗兜底。读错误与干净 EOF 在这条路由上无法区分,这是如实声明的限制。退出观察的形状。 信号致死报
{ 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)
边界测试电池覆盖 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 社区。
链接
Changelog:GitHub Release v0.2.2 · compare v0.2.0...v0.2.2
升级:无破坏,一行
npm inpm:@unipty/backend-zigpty · unipty
English version: /blog/2026-09-07-unipty-v0-2-2/
