一旦使用标准输入,在 SSH 分配的 TTY 中运行的进程就不会终止

一旦使用标准输入,在 SSH 分配的 TTY 中运行的进程就不会终止

我可以通过 SSH 执行文件传输*像这样:

ssh -T ${HOST} eval "cat > remote.txt" < local.txt

但是,如果我分配一个 TTY,它就会挂起,直到我按下 Ctrl+C:

ssh -tt ${HOST} eval "cat > remote.txt" < local.txt

问:为什么会这样?有解决方法吗?

我能想到的最好的方法是本地 EOF 没有传播到远程进程。

平台详细信息:OpenSSH_5.3p1、CentOS 6.7 x86_64


* 在我的实际用例中,我想使用此方法将文件直接传输给远程 sudo 用户;我无法使用 SCP,因为我无法以 sudo 用户身份进行 SSH。我的目标环境中的 sudoers 文件已requiretty设置,因此需要 TTY。

答案1

一些理论

我能想到的最好的方法是本地 EOF 没有传播到远程进程。

基本上可以这么说。在 *nix 中,EOF 不是一个字符,而是一个条件。当程序读取文件时,如果文件已读完,read()并且没有其他内容可读,则程序将获得 0 个字节,然后程序就知道已经到达文件末尾。

从终端读取数据有些不同。您ssh -tt在远程端分配一个终端,然后您的远程端cat从该终端读取数据。终端处于模式icanon。在此模式下,终端驱动程序缓冲数据(通常这允许进行基本编辑,例如使用Backspace),它会在 时发送数据Enter。在您的情况下,它将解释任何\r和任何\n字符来自local.txtEnter当没有更多内容可读时,也没有Enter,因此终端驱动程序不会发送任何内容,并且read()您的cat会继续等待。这是有道理的,因为通常是用户在终端上打字;当用户打字速度不够快时假设 EOF 是非常错误的。

有一种方法可以让处于icanon模式的终端驱动程序发送 0 个字节。如果它读取一个字符\004(ASCII EOT),那么它将发送它当前缓冲的所有内容(在等待 时缓冲Enter)。通常,您可以通过键入+来发送\004到终端。+而不是会让终端驱动程序发送已经输入的行,而不发送任何换行符(或类似字符)。+之后+ (或+之后)将发送恰好 0 个字节,并且CtrldCtrldEnterCtrldCtrldCtrldEnter然后像您这样的工具cat将看到 EOF 条件并且它将退出。

一个基本的(并不意味着“好的”)解决方法是\004在本地文件内容后发送至少两个字符:

{ cat local.txt; printf '\004\004'; } | ssh -tt …

你可以在这里阅读更多:

在 的情况下ssh -tt …,捕获\004字符并不是远程端终端驱动程序所做的全部工作。它所做的工作还有很多,既包括来自客户端的数据,也包括发送到客户端的数据(由远程命令打印)。让我们来研究一些可能性、障碍和怪癖。


一些练习

初步说明:

  • 所有示例均使用ssh -tt。假设您有自己的理由,-tt这是必须的。

  • 如果您想复制以下示例,请设置适合您remote='user@server'的值。这样做的目的是让您无需修改​​即可复制粘贴我的命令。userserver

  • 但是这些例子没有用eval。我不太明白你为什么需要eval这个问题。如果你发现你需要eval让这些例子工作,那么你就得修改一下。对不起。这些例子对我来说是有用的。

  • 这些示例表明ssh,它无需提示输入密码就可以工作,而且速度相当快。

  • $表示我的提示。#表示注释。带有命令的行始终采用提示后跟命令的形式。然后是我得到的输出(如果有)。最后一行末尾的附加提示表示命令已退出;最后一行末尾没有提示表示命令已停止。_表示命令退出或停止后本地插入符号(文本光标)的位置。

  • 我们想调查远程终端的行为。还有一个本地终端。它不会干扰输入(我们的命令不会将其用作输入),但可能会干扰输出。除了本地终端之外,我们可以本地写入常规文件或管道(例如)od -c。我认为这只会混淆示例。本地终端的存在不会影响我们得出的结论。

  • 我经常使用按键描述(例如Enter)来指代我们发送的字符(例如\n)。它并不总是完全准确的(例如,您Enter可能生成\r,而不是\n)。我认为按键更易读,将它们更多地解释为我们希望远程终端驱动程序执行的操作的指示。

以下是一些示例:

  1. 让我们复制您的问题:

    $ printf 'Line1\nLine2' | ssh -tt "$remote" cat
    Line1
    Line2Line1
    _
    

    为什么会有Line1两次?因为远程终端会回显其输入。让我们对此采取一些措施。

  2. 让我们尝试抑制远程回声:

    $ printf 'Line1\nLine2' | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2Line1
    _
    

    它不起作用。虽然不明显,但原因是远程终端在stty -echo重新配置之前回显了它的输入。

  3. 让我们花点时间stty -echo

    $ { sleep 1; printf 'Line1\nLine2'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    _
    

    现在我们看到Line1cat,但Line2可能并没有。

    注意sleep 1已经是 hack 了。如果服务器繁忙或与服务器的通信速度很慢,sleep 1可能还不够。

  4. 让我们“打”Enter一下Line2

    $ { sleep 1; printf 'Line1\nLine2\n'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2
    _
    
  5. 让我们发送Ctrl+d来代替这个Enter

    $ { sleep 1; printf 'Line1\nLine2\004'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2_
    

    请注意,插入符号不再位于与之前相同的位置。

  6. 让我们发送两次Ctrl+ :d

    $ { sleep 1; printf 'Line1\nLine2\004\004'; } | ssh -tt "$remote" 'stty -echo; cat'
    Line1
    Line2Connection to … closed.
    $ _
    

    远程cat退出。以上是我们的基本解决方法。Connection to … closed.(带有一个尾随换行符)来自ssh其自身,并在其 stderr 本地打印,即本地终端。它与远程终端无关。从现在开始,我们将使用 来抑制它2>/dev/null

  7. 让我们发送更多内容:

    $ { sleep 1; printf 'Line1\nLine2\004\004 Line2 continues'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null
    Line1
    Line2$ _
    

    Ctrl+d确实让我们cat看到了 EOF,它没有读取其余的输入。

  8. 还有更多字符与远程终端驱动程序交互。\010(八进制 10,十进制 8,ASCII BS)就像Backspace。 让我们不要cat通过说我们喜欢狗来惹恼我们的遥控器:

    $ { sleep 1; printf 'I like dogs\010\010\010\010cats.\n\004'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null
    I like cats.
    $ _
    

    这里我们在 后面使用了一个Ctrl+ 。这也是最终的本地提示符位于单独一行而不是仅仅在点后面的原因。dEnterEnter

  9. 我们可以发送Ctrl+ c( \003, ASCII ETX):

    $ { sleep 1; printf 'I like...\ndogs\03'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null
    $ _
    

    尽管Enter在流中(通常会进行cat读取和打印I like...),输出为空。Ctrl+c被远程终端驱动程序捕获,驱动程序发送SIGINTcat在成功打印之前终止。 是的,\03在输入中确实引发了信号。 如果我们设法在本地通过之前在远程 shell 中设置陷阱,我们将更清楚地看到它sleep 1

  10. 陷印Ctrl+ c

    $ { sleep 1; printf '\03'; } | ssh -tt "$remote" 'stty -echo; trap "echo trap" INT; sleep 2' 2>/dev/null
    trap
    $ _
    

    如您所见,输入中的某些字符会导致远程端发生某些修改或操作。目前,我们测试了一些文本文件中很少出现的字符。您可能会认为,如果您的文件local.txt包含人们所说的文本(字母、数字、符号、换行符)并且是 ASCII,那么您就是安全的。好吧,不完全是;请参阅下一个示例。

  11. Windows 行尾,即\r\n

    $ # local test, for comparison
    $ printf 'Line1\r\nLine2\r\nLine3'
    Line1
    Line2
    Line3$ _
    
    $ # actual remote test
    $ { sleep 1; printf 'Line1\r\nLine2\r\nLine3'; } | ssh -tt "$remote" 'stty -echo; cat' 2>/dev/null
    Line1
    
    Line2
    
    _
    

    丢失Line3和停滞的命令并不奇怪;这些空行可能是。它们出现是因为远程端的终端驱动程序将每个都转换为\r\n而不是\r\n我们cat看到的\n\n,因此有额外的行。

    我们可以关闭这个功能。

  12. Windows 行尾,无需翻译。

    $ { sleep 1; printf 'Line1\r\nLine2\r\nLine3'; } | ssh -tt "$remote" 'stty -echo -icrnl; cat' 2>/dev/null
    Line1
    Line2
    _
    

    这次cat将每个\r字符都设为\r。在 中stty还有一个设置,使终端驱动程序忽略\r。有许多设置;一些影响输入,一些影响输出。请参阅 (local) 的输出stty -a,阅读man 1 stty。您会发现(除其他外)icanon不是终端的唯一模式,许多功能都可以关闭。

  13. raw模式。

    $ { sleep 1; printf 'Windows\r\nSIGINT\n\003Backspace\010\nEOF\004\004\nExtra\n'; } | ssh -tt "$remote" 'stty raw -echo; cat' 2>/dev/null
    Windows
    SIGINT
    Backspace
    EOF
    Extra
    _
    

    stty raw关闭许多功能。在上面的例子中,既没有\r被翻译,也没有\003导致SIGINT,也\010没有删除前一个字符。但也\004\004没有工作并且cat停滞了。在本地终端输出中,我们看不到不可打印的字符,但所有字符都按原样返回。您可以通过管道ssh来确认这一点od -c(尽管由于缓冲,您可能会看到部分结果)。

    知道这一点很好stty raw,但这个例子当然存在原来的问题:cat失速和ssh停顿。

    stty raw还会关闭影响从远程流向本地端的流的功能。本地端ssh会将此流打印到其标准输出。

  14. 拉取文件。示例文件来自/bin/bash远程端。请注意,您的/bin/bash文件可能不同(版本不同或其他原因),因此您可能会得到不同的校验和。

    $ # checksum on the remote, for comparison
    $ ssh -tt "$remote" 'md5sum </bin/bash' 2>/dev/null
    4600132e6a7ae0d451566943a9e79736  -
    $ _
    
    $ # pulling the file and calculating checksum locally
    $ ssh -tt "$remote" 'stty raw; cat /bin/bash' 2>/dev/null | md5sum
    4600132e6a7ae0d451566943a9e79736  -
    $ _
    
    $ # without stty raw, for comparison
    $ ssh -tt "$remote" 'cat /bin/bash' 2>/dev/null | md5sum
    358991d6d2182cbea143297d14d98f41  -
    $ _
    

    前两个命令显示stty raw本地出现的副本是(几乎可以确定) 与远程原始文件相同。显示的最后一个命令stty raw至关重要,如果没有它,本地副本将有所不同(即远程终端驱动程序会破坏文件)。

    在每种情况下都没有工具从远程终端读取,因此缺少 EOF 条件不是问题。

    如果客户端需要ssh -t,使用stty raw可能是将任意文件从 SSH 服务器复制到客户端的好方法。我实际上也用从 创建的 blob 证实了这一点/dev/urandom,大小为 1 GiB。一些观察结果:

    • 在这种情况下,在配置远程终端之前,没有相关数据可以获取到远程终端stty raw。所有相关数据都是在完成其工作cat后启动的stty raw。不需要本地sleep 1等。
    • 如果sttycat远程 shell(或您添加到远程命令的任何内容)打印到其 stderr,则其打印的内容将破坏副本。如果任何远程工具打印到 ,也会发生同样的情况/dev/tty。您可以在远程端重定向 stderr,但拒绝访问/dev/tty并不那么容易。

一些想法

如果需要ssh -t,似乎在某些情况下(远程终端上没有打印附加信息),您可以相当可靠地提取任意远程文件并期望本地ssh自动退出。您只需要stty raw在远程端。

以导致本地退出的方式放置文件ssh是有问题的。您无法使用 来执行此操作stty raw,因为您需要Ctrl+d机制才能工作。如果您设法(我不确定是否完全可能)配置远程终端驱动程序,除了+stty raw之外的各个方面,因此捕获+是它对输入流执行的唯一操作,那么您应该能够放置任何不包含字节的文件。Postpend最终应该会退出。CtrldCtrld\004\004\004ssh

更多,stty eof允许您为此目的选择一个字节。该工具理解单字节参数或插入符号表示法后者允许您简单地使用例如stty eof ^A(其中^是文字^并且A是文字A)并选择\001作为字节。

考虑到文本文件的 POSIX 定义,最好选择\0(空字节)并能够放入任何文本文件。不幸的是命令行参数中不能使用空字节。在我的测试中stty eof ^@没有起作用。

尽管如此,如果你发现另一个字节未出现在你的文件中,你可能会成功。然而,一般来说,任意文件可能至少包含每个可能的字节一次。Base64(或类似)在源端实时执行编码并在目标端进行解码应该是一种可靠的解决方案,可以与icanon远程终端驱动程序的模式配合使用。例如:

$ md5sum </bin/bash
11227b11f565de042c48654a241e9d1c  -
$ { sleep 1; base64 /bin/bash; printf '\004\004'; } | ssh -tt "$remote" 'stty -echo; base64 -d | md5sum' 2>/dev/null
11227b11f565de042c48654a241e9d1c  -
$ _

(校验和与4600…之前计算的不同,因为我的本地/bin/bash与远程不同/bin/bash。)

请注意,如果您想在服务器端保存文件,您实际上并不需要sleep 1(无论如何,这不是它应该解决的问题的完美解决方案)。事实上,您确实不需要stty -echo。允许远程终端驱动程序回显数据(部分或全部)只会浪费 CPU 周期和带宽,但不会破坏任何东西(您可以在本地使用>/dev/null)。在上面的例子中,我使用了sleep 1和,只是因为我想单独stty -echo查看远程的输出。md5sum


不同的方法

我实际上已经解决了一个更普遍的问题。请参阅我自己回答的问题:ssh具有单独的 stdin、stdout、stderr 和 tty

相关内容