我正在 ssh 到远程服务器以将多个命令传递给交互式命令
#!/bin/bash
ssh -T -o StrictHostKeyChecking=no root@host << "EOF"
java -jar jmx.jar
--commands
quit
EOF
echo "finished"
jmx.jar 打开了一个新的交互式终端,如 mysql、sftp 等。我的命令的输出和我预期的一样,退出后有响应。但之后它就挂起了,永远无法到达“完成”
答案1
假设
我的猜测是:您的代码中的某些内容产生了一个异步进程,使其标准输出保持连接到 SSH 服务器;并且服务器设计为在这种情况下不会终止连接。
分析
SSH 服务器(例如sshd
)在未在远程端分配 tty 时(而您使用了-T
,因此情况就是这样),提供至少三个单独的数据“管道”。您的本地ssh
从其 stdin 读取的任何内容都将出现在服务器生成的 shell 的 stdin 上(远程 shell;始终有一个 shell);该 shell(或任何内容)打印到服务器提供的标准输出或标准错误的任何内容都将由您的本地分别打印ssh
到其标准输出或标准错误。
通常,shell 生成的进程会从其继承 stdin、stdout 和 stderr。远程 shell 中也是如此。这样,您可以这样做:
<local_file_0 ssh user@server tool >local_file_1 2>local_file_2
它的行为如下:
<local_file_0 tool >local_file_1 2>local_file_2
除了tool
将在远程端运行(从生成的远程 shell 执行sshd
)。即使ssh
需要输入密码也可以使用(但当这些还不够时,还有更复杂的用例)。
请注意,如果您使用此处文档(就像您所做的那样<< "EOF"
),文档将ssh
通过其标准输入“进入”。有时人们希望远程 shell 读取所有内容,而当其他任何东西从其(继承的)标准输入读取并消耗 shell 代码时,他们会感到不愉快的惊讶。您故意依赖这种行为。字符串java -jar jmx.jar
由 shell 执行,但随后java
开始读取标准输入,其余代码不是 shell 代码,而是特定于的东西jmx.jar
。我提到这一点是为了向您展示您的java
继承标准描述符与上述完全相同。
异步命令(后台命令)以相同方式继承标准描述符。父进程可能会在生成子进程之前重定向其中一些描述符(例如,在某些情况下,shell 会将 的标准输入重定向asynchronous_command &
到/dev/null
),也可能不会。
现在重点是:如果任何远程进程(无论是否在后台,无关紧要)仍使用 SSH 服务器提供的标准输出,则服务器将保持连接打开(至少sshd
OpenSSH 会这样做),并且您的本地ssh
不会退出。我猜服务器假设如果有任何东西保持标准输出打开,那么它可能会在将来使用它,而您(在本地)可能不想丢失输出。但标准输出不会发生这种情况。
一般来说,保持连接打开的异步进程可能是您在后台明确运行的进程,或者是您运行的任何进程的某个(((…)曾孙)子进程。在您的例子中,只有java
,因此麻烦的进程一定是它的某个后代。可能有多个麻烦的进程。--commands
我不知道在您的特定情况下它(它们)可能是什么。
想法
我不知道 的任何配置选项sshd
会使其行为不同。我不知道 的任何配置选项ssh
会使服务器行为不同,除了-t
或类似。分配一个 ttyssh -t
可能不是一个好主意(特别是在使用此处文档时)。如果没有ssh -t
,如果您想ssh
在远程 shell 退出时退出,您需要确保留下的进程不使用连接到 SSH 服务器进程的标准输出;这意味着您需要将它们的标准输出重定向出去。可能性:
- 运行程序时,当知道不再使用其标准输出时,关闭它的标准输出。
- 将有问题的进程的标准输出重定向到
/dev/null
。显然,您将丢失该进程的所有标准输出。 - 将有问题的进程的标准输出重定向到远程端的常规文件。(请注意,您可能会找到解决方案
nohup
。在我们的问题中,重要的是nohup
将标准输出重定向(nohup.out
默认情况下为),但您可以在没有的情况下做到这一点nohup
。我怀疑的所有其他功能都nohup
与该问题无关。) - 将有问题的进程的标准输出重定向到其标准错误(
>&2
shell 中的运算符)。在本地,您将看到输出,直到ssh
您期望它退出时退出。当进程尝试写入不再存在的管道时(这可能永远不会发生),它们将收到 SIGPIPE;进程如何对 SIGPIPE 做出反应取决于进程本身。另请注意,重定向的数据将ssh
通过其标准错误在本地出现,您将无法将其与真正属于标准错误的任何内容区分开来。 - 将有问题的进程的标准输出重定向到“中继”,在要退出时销毁中继
ssh
。重点是只保留一个进程(中继)连接到原始标准输出。
一般而言,由于有问题的进程可能是您明确运行的某个进程的远程后代,因此您可能不容易(或根本不可能)仅重定向其 stdout。您可能需要重定向祖先的 stdout,这将影响祖先和所有后代;这可能太多了,尤其是在重定向到 时/dev/null
。重定向到中继似乎是一个好主意。
例子
date
如果我的假设是正确的,那么这将复制该问题(您将看到之后打印的输出DONE
):
ssh user@server '
for i in 1 2 3 4 5 6; do
date; sleep 1
done &
sleep 3
echo DONE
'
将异步子 shell 从原始 stdout 重定向出去将使得在打印ssh
后立即退出:DONE
ssh user@server '
for i in 1 2 3 4 5 6; do
date; sleep 1
done >/dev/null &
sleep 3
echo DONE
'
重定向到 stderr 允许您查看输出直到DONE
:
ssh user@server '
for i in 1 2 3 4 5 6; do
date; sleep 1
done >&2 &
sleep 3
echo DONE
'
这是中继的基本实现。理想情况下,代码应该以 开头mktemp -d
,但让我们的概念证明保持相对简单:
ssh user@server '
tmpf=/tmp/fifo
mkfifo "$tmpf"
<"$tmpf" cat &
relay="$!"
trap "kill \"$relay\"" EXIT
exec >"$tmpf"
# from now on the cat is the only process that uses the original stdout
# unlinking the fifo early is allowed, inheritance works via descriptors anyway
rm "$tmpf"
for i in 1 2 3 4 5 6; do
date; sleep 1
done >&2 &
sleep 3
echo DONE
'
cat
注意,即使管道缓冲区中有未读数据,陷阱也可能会终止做想看。在我的测试中,有时我看到DONE
,有时看不到,这就是我所说的。延迟kill
(trap "sleep 1; kill \"$relay\"" EXIT
)可能会缓解问题(并增加某些输出仍然被传递的可能性后 DONE
!)。
你的具体情况
也许您将能够识别有问题的进程并通过修改传递--commands
给 的 来更改其行为java
。如果您不关心区分远程 stdout 和 stderr,java -jar jmx.jar >&2
可能是您唯一需要的修改。或者从中继开始:代替java …
上面for … echo DONE
最后一个示例中的片段。注意/tmp/fifo
在这个简单的示例中是硬编码的。为了稳健性、安全性和/或如果您想运行代码的多个实例,您应该每次在私有临时目录中创建一个 fifo(mktemp -d
)。