6

我正在尝试做的事情

我的 Cocoa 应用程序需要运行一堆命令行程序。其中大部分是非交互式的,所以我用一些命令行参数启动它们,它们做他们的事情,输出一些东西并退出。其中一个程序是交互式的,因此它会向标准输出输出一些文本和提示,然后期待标准输入上的输入,并且这种情况一直持续到您向其发送退出命令。

什么有效

仅将大量数据转储到标准输出然后终止的非交互式程序相对来说是微不足道的:

  • NSPipe为 stdout/stdin/stderr创建s
  • NSTask使用这些管道启动

那么,要么

  • NSFileHandle管道的另一端读取所有数据,直到流结束,并在任务结束时一次性处理它

或者

  • 从输出管道的另一端获取-fileDescriptors 。NSFileHandle
  • 将文件描述符设置为使用非阻塞模式
  • 使用每个文件描述符创建一个 GCD 调度源dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, ...
  • 恢复调度源并使用它处理它向您抛出的数据read()
  • 继续进行,直到任务结束并且管道文件描述符报告 EOF(read()报告读取 0 字节)

什么不起作用

对于交互式工具,这两种方法都完全失效。显然我不能等到程序退出,因为它位于命令提示符下,除非我告诉它,否则永远不会退出。另一方面,NSPipe缓冲数据,因此您以缓冲区大小的块接收它,除非 CLI 程序碰巧显式地刷新管道,而在我的情况下则没有。初始命令提示符比缓冲区大小小得多,所以我什么也没有收到,它就在那里。所以NSPipe也是不行的。

经过一番研究,我确定我需要使用伪终端(pty) 来代替 NSPipe。不幸的是,除了让它正常工作之外,我什么都没有。

我试过的

我创建了一个像这样的 pty,而不是 stdout 管道:

struct termios termp;
bzero(&termp, sizeof(termp));
int res = openpty(&masterFD, &slaveFD, NULL, &termp, NULL);

这给了我两个文件描述符;我把它slaveFD交给一个NSFileHandle,它被传递给NSTaskstdout 或 stdout 和 stdin 。然后我尝试从主端进行通常的异步读取。

如果我在终端窗口中运行我正在控制的程序,它首先输出 2 行文本,一行 18 字节长,包括换行符,一个 22 字节长,命令提示符没有换行符。在这 40 个字节之后,它等待输入。

如果我只是将 pty 用于标准输出,我会从受控程序收到 18 个字节的输出(正好是一行,以换行符结尾),仅此而已。在最初的 18 个字节之后,一切都在那里,没有更多的事件 - GCD 事件源的处理程序不会被调用。

如果我还将 pty 用于标准输入,我通常会收到 19 个字节的输出(上述行加上下一行的一个字符),然后受控程序立即终止。如果我在尝试读取数据之前稍等片刻(或调度噪音会导致短暂的停顿),我实际上会在程序再次立即终止之前获得整个 40 个字节。

额外的死胡同

NSFileHandle有一次我想知道我的异步读取代码是否有缺陷,所以我使用s 及其-readInBackgroundAndNotify方法重新做了所有事情。这与使用 GCD 时的行为相同。(我最初选择 GCD 而不是NSFileHandleAPI,因为在 中似乎没有任何异步写入支持NSFileHandle

问题

经过一天多的徒劳尝试,我已经到了这一点,我可以得到某种帮助。我正在尝试做的事情是否存在一些基本问题?为什么将标准输入连接到 pty 会终止程序?我没有关闭 pty 的主端,所以它不应该收到 EOF。撇开标准输入不谈,为什么我只能得到一行的输出?我在 pty 的文件描述符上执行 I/O 的方式有问题吗?我是否正确使用了主从端-控制过程中的主端,NSTask中的从端?

我没试过的

到目前为止,我只在管道和 pty 上执行了非阻塞(异步)I/O。我唯一能想到的是 pty 根本不支持这一点。(如果是这样,为什么会fcntl(fd, F_SETFL, O_NONBLOCK);成功?)我可以尝试在后台线程上阻塞 I/O 并将消息发送到主线程。我希望避免不得不处理多线程,但考虑到所有这些 API 看起来有多糟糕,它不会比尝试另一种异步 I/O 排列更耗时。不过,我很想知道我到底做错了什么。

4

1 回答 1

2

问题可能是里面的stdio库正在缓冲输出。当命令行程序刷新它时,输出只会出现在读取管道中,因为它通过 stdio 库或 fflush()s 写入“\n”,或者缓冲区已满,或者退出(这会导致stdio 库自动刷新仍然缓冲的任何输出),或者可能是其他一些条件。如果这些 printf 字符串以“\n”结尾,那么您可能会更快地输出。这是因为有三种输出缓冲样式——无缓冲、行缓冲(\n 导致刷新)和块缓冲(当输出缓冲区满时,它会自动刷新)。

如果输出文件描述符是 tty(或 pty),则 stdout 的缓冲默认为行缓冲;否则,块缓冲。stderr 默认是无缓冲的。setvbuf() 函数用于更改缓冲模式。这些都是我在这里描述的标准 BSD UNIX(可能还有通用 UNIX)。

NSTask 不会为您设置任何 ttys/ptys。在这种情况下无论如何它都无济于事,因为 printfs 没有打印出 \n。

现在,问题是 setvbuf() 需要在命令行程序中执行。除非(1)你有命令行程序的源代码并且可以修改它并使用修改后的程序,或者(2)命令行程序有一个特性允许你告诉它不要缓冲它的输出[即,调用 setvbuf() 本身],据我所知,没有办法改变这一点。父进程根本无法以这种方式影响子进程,要么在某些点强制刷新,要么更改 stdio 缓冲行为,除非命令行实用程序内置了这些功能(这很少见)。

资料来源:Re:NSTask、NSipe 和交互式 UNIX 命令

于 2012-09-25T16:30:30.630 回答