Skip to content

UniPty v0.2.2:扩展 Node 引擎选择,补上输出内存边界

jixoai v0.2.2 unipty

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 之类的参数改变。

两条路由的公共契约一致,差异都在引擎层,选型时对照:

维度zigptynode-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 的能力差异矩阵。除了上面两节,还有三条。

  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,另有一个 50 ms 静默窗兜底(每收到一个迟到 chunk 重新计时)。读错误与干净 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)

  • outputSpool 的畸形取值(false、数字或字符串等)以稳定的 invalid-argument 拒绝,不再静默取默认值(9bac5d7)

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

  • 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 社区。

链接