当 stdout 和 stderr 都被重定向时,ssh 如何/为什么输出到 tty?

spr*_*aff 4 linux ssh bash redirection pipe

我刚刚注意到

ssh user@host >/tmp/out 2>/tmp/err
Run Code Online (Sandbox Code Playgroud)

可以写类似的东西

Warning the RAS host key...
...
Are you sure you want to continue connecting (yes/no)?
Run Code Online (Sandbox Code Playgroud)

stdout 和 stderr 都被重定向,但这仍然显示在 tty 中。

  1. ssh这是怎么做的?
  2. 为什么ssh要这样做?它不违反 *nix 习语吗?
  3. 当我ssh作为通过管道连接的 stdin/stdout/stderr 的子进程运行(或另一个具有类似行为的程序)时,我希望父进程看到子进程的输出。如果子进程像这样躲避stdout/stderr,父进程怎么捕获呢?

Kam*_*ski 5

如何?

ssh这是怎么做的?

它打开/dev/tty。来自的相关行strace ssh …是:

openat(AT_FDCWD, "/dev/tty", O_RDWR)    = 4
Run Code Online (Sandbox Code Playgroud)

4然后将文件描述符与write(2)和 一起使用read(2)

(测试平台:OpenSSH_7.9p1 Debian-10+deb10u2)。


为什么?

为什么ssh要这样做?它不违反 *nix 习语吗?

我不确定“*nix idioms”,不管它们是什么;但 POSIX明确允许这样做

/dev/tty
在每个进程中,与该进程的进程组关联的控制终端的同义词(如果有)。对于希望确保无论输出如何重定向都可以向终端写入消息或从终端读取数据的程序或外壳程序非常有用。[…]

(强调我的)。

需要与用户交互的工具倾向于使用,/dev/tty因为它有意义。通常当用户这样做时:

<local_file_0 ssh user@server tool >local_file_1 2>local_file_2
Run Code Online (Sandbox Code Playgroud)

他们希望它尽可能与此相似:

<local_file_0 tool >local_file_1 2>local_file_2
Run Code Online (Sandbox Code Playgroud)

唯一的区别应该是tool实际运行的位置。通常用户希望ssh是透明的。他们不希望它垫料local_file_1或者local_file_2,如果他们需要把他们不想怀疑yesnolocal_file_0万一ssh问。通常人们无法预测ssh在任何特定情况下是否会询问。

请注意,当您运行时ssh user@server tool,远程端会涉及一个 shell(比较我的这个答案)。shell 可以提供一些可以乱扔输出的启动脚本。这是一个不同的问题(也是相关启动脚本应该保持沉默的原因)。


解决方案

当我ssh作为通过管道连接的 stdin/stdout/stderr 的子进程运行(或另一个具有类似行为的程序)时,我希望父进程看到子进程的输出。如果子进程像这样躲避stdout/stderr,父进程怎么捕获呢?

如上所述,你的愿望很不寻常。这并不意味着它很奇怪或完全不常见。有些用例确实需要这样做。解决方案是提供一个您可以控制的伪终端。正确的工具是expect(1). 它不仅会提供一个 tty,而且还允许您实现一些逻辑。您将能够检测(并记录)Are you sure you want to continue connecting并回答yesno;如果ssh不问,或者什么都不问。

如果您想在正常交互时捕获整个输出,请考虑script(1).


更广阔的画面

到目前为止,我们对在客户端(即ssh运行的位置)分配 tty 感兴趣。通常,您可能希望运行一个需要/dev/tty在服务器端运行的工具。SSH 服务器是否能够分配伪终端,相关选项是-t-T(请参阅 参考资料man 1 ssh)。例如,如果你这样做:

ssh user@server 'sudo whatever'
Run Code Online (Sandbox Code Playgroud)

那么你很可能会看到sudo: no tty present …。在远程端提供一个 tty 它将起作用:

ssh -t user@server 'sudo whatever'
Run Code Online (Sandbox Code Playgroud)

但有一个怪癖。如果没有-t默认的stdin,远程命令的stdout 和stderr 连接到本地ssh进程的stdin、stdout 和stderr 。这意味着您可以在本地将远程 stdout 与远程 stderr 区分开来。用-t默认的标准输入,输出和错误(和/dev/tty远程命令点),以相同的伪终端; 现在 stdout、stderr 以及远程命令写入其/dev/ttyget 的任何内容组合成单个流,本地ssh打印到其(本地)stdout。您无法在本地区分它们。这个命令:

ssh -t user@server 'sudo whatever' >local_file_1
Run Code Online (Sandbox Code Playgroud)

将从文件中写入提示sudo!通过使用/dev/tty sudo它自己试图在重定向时变得透明,但ssh -t破坏了这一点。

在这种情况下,如果ssh提供一个选项来分配/dev/tty远程端的伪终端 ( ) 并将其连接到/dev/tty本地ssh同时仍将默认远程 stdin、stdout 和 stderr 连接到它们的本地对应项,这将是有用的。四个独立的通道(其中之一是双向的:)/dev/tty

AFAIK 没有这样的选择。目前,您可以拥有三个单向通道(不/dev/tty用于远程进程)或显示为远程进程的一个双向通道 ( /dev/tty) 和ssh本地用户的两个单向通道(本地的 stdin 和 stdout )。

您的原始命令:

ssh user@host >/tmp/out 2>/tmp/err
Run Code Online (Sandbox Code Playgroud)

没有指定远程命令,所以它在远程端运行一个交互式 shell,并为它提供了一个伪终端,就像你使用过的一样-t(除非没有本地终端)。这是“远程进程的一个双向通道”的情况。这意味着/tmp/err只能从ssh自身获取 stderr (例如,如果您使用了ssh -v)。

无法轻松交互使用输出未打印到(本地)终端的交互式shell。我希望这只是一个最小的例子(如果不是,那么也许你需要重新考虑这个)。

无论如何,您可以使用 来查看情况/dev/ttyssh而其他使用的工具/dev/tty可能会很复杂。