当输出重定向到管道时,Node CLI 程序 writeFileSync 收到“EAGAIN”错误

问题描述 投票:0回答:1

我有一个简单的 Node 程序,可以从命令行使用它来测试外部 API。该工具的工作原理大致如下:它根据命令行参数和标志进行 API 调用,对返回的内容 (JSON) 进行少量处理,然后将结果写入标准输出。输出通过以下小实用程序:

const output = (function() {
    let accum = "";

    return {
        add(content) {
            accum += content;
        },
        lpad(indent) {
            this.add(indent ? new Array(indent + 1).join(" ") : "");
        },
        nl() {
            this.add("\n");
        },
        write() {
            writeFileSync(1, accum);
        },
        reset() {
            accum = "";
        }
    };
})();

它将对

.add()
.lpad()
等的调用累积到一个字符串中,然后程序执行的最后一件事是调用
.write()
,它使用
writeFileSync()
将该字符串写入标准输出。后一个函数已导入:

import { writeFileSync } from "fs";

当标准输出未重定向(即终端窗口)时,一切正常。当我重定向到文件时它也可以正常工作。但是,如果我运行程序并将输出通过管道传输到

grep
或其他,我会得到:

node:fs:939
  handleErrorFromBinding(ctx);
  ^

Error: EAGAIN: resource temporarily unavailable, write
    at Object.writeSync (node:fs:939:3)
    at writeFileSync (node:fs:2301:26)
    at Object.write (file:///Users/m5/nodestuff/node_modules/nscall/src/output.mjs:20:13)
    at commit (file:///Users/m5/nodestuff/node_modules/nscall/src/output.mjs:109:12)
    at file:///Users/m5/nodestuff/node_modules/nscall/nscall.js:49:9
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  errno: -35,
  syscall: 'write',
  code: 'EAGAIN'
}

Node.js v20.1.0

在某些输出通过管道后会发生该错误。

积累的内容可能有点大,但不会有很多兆字节大。直观上,就像操作系统管道缓冲区停止时事情会中断一样,但令我困惑的是

writeFileSync()
会遇到问题。这种情况发生在 MacOS 上,这是一个我不太熟悉的平台,但多年来我在 Linux 和 UNIX 上编写了无数类似的工具,低级文件系统
write()
调用处理输出类型的性质- 透明的,除非在特殊情况下(我猜;管道几乎不是什么特殊情况)。

我真的不是 Node 专家,所以这可能属于关于为什么坚持使用异步 API 是一个更好的主意的示例类别。不过看起来还是很奇怪。

edit — 如果我更改输出

write
方法以使用回调
write()
API,我不会收到错误:

        async write() {
            const buf = Buffer.from(accum, "UTF-8");
            return new Promise(function(res, rej) {
                write(1, buf, function() {
                    res();
                });
            });
        },

我可以就这样保留它,但还是很奇怪。

javascript node.js macos pipe
1个回答
0
投票

我不知道这是否正确,但这是一个理论:

当使用 node 写入标准输出时,它将文件描述符置于

O_NONBLOCK
模式。请参阅此处

主要问题:为什么非阻塞模式下文件描述符为0?

抱歉,如果我不清楚:节点按顺序将其置于非阻塞模式

process.stdin.pipe(...)
process.stdin.on('data', ...)
有效。 更高级别:为了例如REPL 可以与事件集成 循环。

上面的 PR 特别讨论了

stdin
,但它也应该适用于
stdout
。非阻塞 I/O 引发了 EAGAIN 的担忧。接下来,这个问题:Can write() to a non-blocking fd return EAGAIN when select reports it as writable?包含这个答案(已投票但不接受):

您在这里使用管道。管道有一个有趣的原子写入属性—— 保证写入小于 PIPE_BUF(此处为 4096)字节 原子。因此,对管道的写入可能会因 EAGAIN 失败,即使它是 可以向管道写入较少数量的字节。

我不确定这是否是你在这里遇到的情况(我没有看过 太接近了),但这种行为记录在 man 7 管道中。

现在,对于一些猜想:我不知道有什么会强制

grep
在非阻塞模式下运行。如果其输入缓冲区设置为 64k 并且正在执行阻塞读取,则您可能会尝试写入字节 65530-65580,而 grep 已读取 64k 并且现在正在处理数据并且不调用
read()
,从而导致延迟,从而引发EAGAIN 因为节点的标准输出不允许阻塞。

在我的测试中

grep
似乎读取了96k块,所以事实并非如此:

    $ od -x /dev/urandom | strace grep "abcdef"
    [...]
    read(0, "1cf4 920f 31aa 5c95\n67602500 ad3"..., 98304) = 4096
    read(0, "3104 5390 7a92 ef03 ec5d 5910 be"..., 94208) = 4096
    read(0, " be6d 9fb2\n67607660 0604 d9a4 1b"..., 90112) = 4096
    [...]
    read(0, "7671360 b851 135f 79d9 4213 8275"..., 12288) = 4096
    read(0, "edd 6b65 d774 369d\n67674060 cbd0"..., 8192) = 4096
    read(0, "893 1ab4 57ad 1fa5 8ae8 6727 825"..., 4096) = 4096
    read(0, "648b 61c9\n67701240 8d70 7210 652"..., 98304) = 4096

我找不到在非阻塞模式下运行的程序,但是如果你能找到一个(或写一个),这就是我发现

socat
使用完全阻塞的
pselect()

    $ dd if=/dev/urandom bs=4k count=100 | strace socat - - | sleep 10

返回到

grep
的管道:stdio 进程缓冲区大小也可能存在转换。根据在C中,stdout缓冲区的大小是多少?

Linux 创建管道时使用默认管道大小 64K。在 /proc/sys/fs/pipe-max-size 存在最大管道大小。为了 默认 1048576 是典型值。

这符合您的经验。因为您已经建立了一个缓冲区,所以节点可能会如此快地将数据塞入管道中,以至于缓冲区在 grep 甚至有时间唤醒之前就被填满了。您应该能够通过跟踪节点应用程序并通过管道进入睡眠状态来确认 EAGAIN。如果这给出了相同的行为,我们至少可以得到部分解释。

© www.soinside.com 2019 - 2024. All rights reserved.