UniPty v0.2.2:扩展 Node 引擎选择,补上输出内存边界
0.2.1 与 0.2.2 合并成篇。Node 多一条官方路由 @unipty/backend-zigpty,零运行时依赖、8 个平台的预编译全在主 tarball;Windows 可以运行,缓冲语义如实声明;停读的输出可溢写磁盘,内存有上界。无破坏性变更。
$ npm view @unipty/backend-zigpty version
0.2.1
UniPty v0.2.2 发布了(2026-09-07)。这一篇把 0.2.1 与 0.2.2 合并成一篇,按主题分节;两个 release 的清单在文末逐个列出。
主线一条:Node 侧多了一条官方路由 @unipty/backend-zigpty,底层是第三方 zigpty(npm zigpty@0.2.1,MIT,pi0 维护),用 Zig 实现的 PTY 引擎。加上既有的 @unipty/backend-node-pty,Node 有两条官方路由可选。
Node 有了第二条官方路由
node-pty 的 @lydell 发行版把平台二进制拆成 optionalDependencies 子包,安装器跳过 optional 依赖时装不上;受限 CI、离线镜像这类环境不可控的场合,这里就是故障点。
zigpty 不依赖安装器:零运行时依赖、零安装脚本,8 个平台元组(Linux/macOS/Windows × x64/arm64,Linux 另含 musl)的预编译全部打进主 tarball,解包约 420 KB(npm unpackedSize 口径),安装结果不随 --omit=optional 之类的参数改变。
两条路由的公共契约一致,差异都在引擎层,选型时对照:
| 维度 | zigpty | node-pty(@lydell 发行版) |
|---|---|---|
| 运行时依赖 | 0 | 平台子包(optionalDependencies) |
| 安装脚本 | 无 | 无 |
| 分发方式 | 8 个平台元组的预编译全在主 tarball,解包约 420 KB | 每平台一个子包,按平台各装一份 |
| 安装健壮性 | --omit=optional 等参数不影响 | optional 依赖被跳过时装不上 |
| 原生文本输出 | 有(encoding: "utf8") | 有(encoding: "utf8") |
| 字节写入 | 默认拒绝,需 writeDecode 选项 | 默认接受 |
| Windows | 可运行,输出为缓冲式(见下文) | 声明支持,证据门控 |
| 传输读错误上报 | 不可区分(见下文) | 上报 unsupported |
zigpty 的优势集中在分发。代价在后文「注意事项」与下面两节:字节写入要多开一个选项,Windows 上的输出是缓冲式,内存上界由选项提供。
上游有一处要当心的行为:预编译加载失败时,zigpty 会静默回退到基于管道的伪 PTY,isatty 与内核几何尺寸在伪 PTY 上都不成立。适配层在就绪阶段硬门禁这个回退,hasNative 不真就以 unsupported 失败,就绪的 Backend 永远意味着真实 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 个场景的公共契约套件对安装后的包产物全量通过:ubuntu 与 macos 的 CI 各一条证据进了发布目录(node 22.20.0,2026-09-07,见兼容性目录),本地 darwin-arm64 又对安装后的包跑过一遍 25/25。
Windows 上可以运行
zigpty 的 Windows 实现带 ConPTY 预编译,但其公开的 pause() / resume() 在该平台是空方法,消费端驱动的输出背压传导不到内核。这条路由没有因此失败关闭,而是由适配层持续排空引擎回调,照常运行。
「输出背压在该平台传导不到内核」是如实声明的路由级限制,与 Deno 路由对内部读线程的处理同类。UniPty 不去管束上游引擎:第三方开发者会有自己的 backend,用适配层弥合引擎差异是这套架构的本职工作(变更提案)。
安装与启动不需要平台分支:
const backend = await createZigptyBackend(); // Windows 与 unix 同一行
支持声明仍由证据门控。Windows 元组在拿到公开契约证据之前保持 declared-unverified,兼容性目录如实呈现;缓冲式运行不等于验证过的支持。
停读不再堆内存
停读的消费者是内存的主要风险。终端挂在浏览器标签页上,用户切走十分钟,这十分钟产出的输出没有去处,全堆在适配层的队列里;Windows 上背压传导不到内核,堆得更快。outputSpool 把这个队列换成磁盘化的 FIFO,内存头默认 1 MiB 封顶:
const backend = await createZigptyBackend({
outputSpool: { memoryBytes: 4 * 1024 * 1024 }, // 或直接 true 用默认值
});
公共流的字节与不开时完全一致:文本记录按完整记录往返,chunk 边界保持不变,回放严格跟随消费端的拉动节奏。子进程退出时,完成信号等积压排空才发出,快退输出的尾巴不会被 EOF 截断;显式 close() 仍同步完成并删除临时文件。Windows 上请开启它——那里没有内核背压,这是停读场景的内存边界。
机制与边界
越过内存头的记录溢写到适配层自有的临时文件(0600 权限、独占创建),默认落在系统临时目录,可用
directory指定溢写 IO 是同步的;积压期间的磁盘占用按设计不设上限,只受子进程自身输出量约束
进程被强杀时,临时文件交由系统临时目录清理
在引擎能够暂停的平台上(unix),越过内存界还会把压力传导回内核,阻塞子进程而不是继续堆积
溢写失败(ENOSPC、目录消失)以类型化错误终止输出源,而不是悄悄放开内存上限
注意事项
完整差异见 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,另有一个 50 ms 静默窗兜底(每收到一个迟到 chunk 重新计时)。读错误与干净 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)outputSpool的畸形取值(false、数字或字符串等)以稳定的invalid-argument拒绝,不再静默取默认值(9bac5d7)Bun 路由:干净 EOF 延迟一个 macrotask,同回合迟到的数据块不再丢失(a0852e8)
边界测试电池覆盖 memoryBytes 极值、空记录、损坏记录、积压取消等场景,包测试增至 70 项(f8ad01d)
README 重构并新增四条路由的能力差异矩阵,文档全仓同步新路由;文档站迁移到 jixoai 视觉体系并上线中文镜像(bb807db、021a82d、7915ac4、a635df2)
从 v0.2.0 到 v0.2.2 共 50 个提交:conventional 严格口径计 feat 10, fix 8, docs 18, test 2, chore 5, style 6, ci 1;带 ! 标记的破坏性提交 0 个。
完整清单见 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.1 · GitHub Release v0.2.2 · compare v0.2.0...v0.2.2
升级:无破坏,一行
npm inpm:@unipty/backend-zigpty · unipty
上一版:UniPty v0.2.0(2026-08-20 发布)
English version: /blog/2026-09-07-unipty-v0-2-2/
