从Cocoa应用程序控制交互式命令行实用程序 - 与ptys有关

时间:2012-09-25 15:47:46

标签: macos cocoa grand-central-dispatch nstask pty

我想做什么

我的Cocoa应用程序需要运行一堆命令行程序。其中大多数都是非交互式的,所以我用一些命令行参数启动它们,它们做它们的事情,输出一些东西然后退出。其中一个程序是交互式的,所以它输出一些文本和stdout的提示,然后期望stdin上的输入,这一直持续到你发送一个退出命令。

什么有效

非交互式程序只是将大量数据转储到stdout然后终止,这些程序相对简单:

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

然后,

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

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

什么不起作用

这两种方法都完全打破了交互式工具。显然,我不能等到程序退出,因为它处于命令提示符,除非我告诉它,否则永远不会退出。另一方面,NSPipe缓冲数据,因此您以缓冲区大小的块接收它,除非CLI程序恰好刷新管道,在我的情况下不会。初始命令提示符比缓冲区大小小得多,所以我没有收到任何东西,它只是坐在那里。所以NSPipe也是不行的。

some research之后,我确定我需要使用伪终端(pty)代替NSPipe。不幸的是,我一无所获,只能让它发挥作用。

我尝试了什么

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

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

这给了我两个文件描述符;我将slaveFD交给NSFileHandle,它将传递给NSTask,仅用于stdout或stdout和stdin。然后我尝试从主方进行通常的异步读取。

如果我运行程序我在终端窗口中进行控制,它会通过输出2行文本开始,一行18字节长,包括换行符,一个22字节,并且没有命令提示符的换行符。在那40个字节之后,它等待输入。

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

如果我也将pty用于stdin,我通常会收到19个字节的输出(前面提到的行加上下一行的一个字符)然后受控程序立即死掉。如果我在尝试读取数据之前稍等一下(或者调度噪声导致一个小的暂停),我实际上会在程序再次立即死亡之前得到整个40个字节。

额外的死胡同

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

问题

经过一天徒劳无功的尝试到达这一点,我可以提供某种帮助。我尝试做的事情有一些根本问题吗?为什么将stdin连接到pty终止程序?我没有关闭pty的主端,所以它不应该接收EOF。抛开stdin,为什么我只得到一行的产出?我在pty的文件描述符上执行I / O的方式有问题吗?我是否正确使用主设备和从设备 - 控制过程中的主设备,NSTask中的从设备?

我没有尝试过

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

1 个答案:

答案 0 :(得分: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, NSPipe's and interactive UNIX command