问题
考虑这样的命令:
<binary_input ssh user@server 'sudo tool' >binary_output 2>error.log
其中tool
是任意的,ssh
是一个包装器或一些ssh-like-contraption
允许上述工作的包装器。常规的话ssh
是行不通的。
我在这里用过sudo
,但它只是一个例子需要 tty 的命令。我想要一个通用的解决方案,而不是特定于sudo
.
研究:原因
常规情况下ssh
它不起作用,因为:
sudo
需要 tty 询问密码(或者干脆去工作),所以我需要ssh -t
;实际上在这种情况下我需要ssh -tt
.- 另一方面
ssh -tt
将使sudo
读取密码binary_input
。我想通过本地 tty 提供密码。即使sudo
配置为无需密码即可工作,或者如果我将密码注入到binary_input
,ssh -tt
也会从远程 tty 进行读取并写入sudo
输出tool
和向远程 tty 发送错误和提示。我不仅无法在本地区分输出和错误/提示。所有流都将由远程 tty 处理,这将破坏数据(您可以在以下示例中看到这一点)我的这个答案,在标题为“一些实践”的部分中)。
研究:与有效命令的比较
该本地命令是参考点。假设它成功处理了一些二进制数据:
<binary_input tool >binary_output
如果我需要
tool
在服务器上运行,我可以这样做。即使ssh
询问我的密码,这将起作用:<binary_input ssh user@server tool >binary_output
在这种情况下
ssh
对于二进制数据是透明的。同样本地也
sudo
可以是透明的。即使sudo
询问我的密码,以下命令也不会破坏数据:<binary_input sudo tool >binary_output
tool
但是在服务器上运行sudo
就麻烦了:<binary_input ssh user@server 'sudo tool' >binary_output
在此配置中
ssh
和sudo
一起一般情况下不能做到透明。找到一种使它们透明的方法是这个问题的要点。
研究:类似问题
我发现了几个类似的问题:
-
这个问题只关心标准输出。现有的答案(来自问题的作者)建议
sudo -S
哪个消耗标准输入。我真的不想改变我的binary_input
.我希望有一个不特定于 的解决方案sudo
。 -
这里集中传递Ctrl+ c,背景是 GNU
parallel
。仅使Ctrl+c在没有远程 tty 的情况下工作的解决方法对我来说是不够的。 SSH:除了 stdin、stdout、stderr 之外,还提供额外的“管道”fd
这是一个好的开始(特别是这个答案, 我认为)。不过这里我想强调一下tty的必要性。我想要一个能够自动化操作并允许我
sudo
像在本地一样使用远程(或其他)的解决方案。
我明确提出的问题
在以下命令中:
<binary_input ssh user@server 'requires-tty' >binary_output 2>error.log
requires-tty
是需要 tty 但将二进制数据从其 stdin 处理到其 stdout 的代码的占位符。看来我需要ssh -tt
,否则requires-tty
不行;同时我不能使用ssh -tt
,否则二进制数据将被破坏。我怎样才能方便地解决这个问题呢?
requires-tty
可以sudo …
,但我不想要特定于sudo
.
我想理想的(?)解决方案将是一个脚本/工具,它可以替换ssh
上面的调用并且可以正常工作。它应该(?)将远程标准输入、标准输出和标准错误分别连接到其本地对应部分,和远程 tty 到本地 tty。
如果可能的话,我更喜欢不需要任何服务器端配套程序的客户端解决方案。
答案1
脚本
我是这个问题的作者,这是我尝试构建一个解决问题的脚本。该脚本旨在在客户端工作,它替换ssh
有问题的命令。它是实验性的。我称之为sshe
。这是脚本:
#!/bin/sh -
# the name of the script
me="${0##*/}"
# error handling functions
scream() { printf '%s\n' >&2 "$1"; }
die() { scream "$2"; exit "$1"; }
# initialization of variables
redir0=''
redir1=''
redir2=''
tty="/dev/$(ps -p "$$" -o tty=)"
# edge cases
[ "$tty" = '/dev/?' ] && { scream "$me: no tty detected, falling back to regular ssh"
exec ssh "$@"; }
[ "$#" -lt 2 ] && die 1 "usage: $me [options] [user@]hostname command"
# see what needs to be redirected
exec 7>&1
if [ "$(<&0 tty 2>/dev/null)" != "$tty" ]; then redir0=y; fi
if [ "$(<&7 tty 2>/dev/null)" != "$tty" ]; then redir1=y; fi
if [ "$(<&2 tty 2>/dev/null)" != "$tty" ]; then redir2=y; fi
exec 7>&-
# edge case
[ "$redir0$redir1$redir2" ] || { scream "$me: no redirection detected, falling back to ssh -t"
exec ssh -t "$@"; }
# command line parsing, extract two last arguments: ... host command
z="$#"
n="$z"
for arg do
if [ "$n" -eq "$z" ]; then
set --
fi
case "$n" in
1) command="$arg"
;;
2) host="$arg"
;;
*)
set -- "$@" "$arg"
esac
n="$(($n - 1))"
done
# prepare to clean on exit
trap 'status="$?"; rm -r "$tmpd" 2>/dev/null; trap - EXIT; exit "$status"' EXIT HUP INT QUIT PIPE TERM
# temporary directory and socket
tmpd="$(mktemp -d)"
[ "$?" -eq 0 ] || exit 1
sock="$tmpd/sock"
# main pipe: ssh master connection -> background cat
(
[ "$redir0" ] || exec 0</dev/null
# ssh master connection, it will report the remote PID of the remote shell via its stdout
ssh -M -S "$sock" "$@" -T "$host" '</dev/null echo "$$"; exec sleep 2147483647'
) | {
# read the remote PID
IFS= read -r rpid || exit 1
# background process to pass data
exec 6<&0
cat <&6 2>/dev/null &
# move original descriptors out of the way
exec </dev/tty >/dev/tty 6>&-
# prepare remote redirections
if [ "$redir0" ]; then redir0="<&6"; fi
if [ "$redir1" ]; then redir1=">&7"; fi
if [ "$redir2" ]; then redir2="2>&8"; fi
# ssh to run the command, with remote tty
ssh -S "$sock" -t "$host" "
trap 'status=\"\$?\"; kill $rpid 2>/dev/null; trap - EXIT; exit \"\$status\"' EXIT HUP INT QUIT PIPE TERM
exec 6</proc/$rpid/fd/0 7>/proc/$rpid/fd/1 8>/proc/$rpid/fd/2 9>/dev/tty $redir0 $redir1 $redir2 || exit 3;
$command"
}
一般免责声明
该脚本在许多情况下都运行良好。我不意味着它随机失败。它不会随机失败。我不意味着它无法处理某些特定数据。它处理任意数据。
它“无法正常工作”的情况仅由其与本地 tty 的交互引起。通过不包含任何 tty 的通道流动的数据(包括任意二进制数据)始终没问题。请阅读本答案的其余部分,尤其是“障碍和警告”部分,以了解问题并了解应避免的内容。
像这样的命令:
<binary_input sshe user@server 'sudo tool' >binary_output 2>error.log
避免了这个问题。只要满足技术要求(请参阅下面的“要求”),它就应该可以正常工作。
该脚本是实验性的,我尝试设置
trap
s,以保留退出状态并以理智(?)的方式在退出时进行清理。我不确定我是否成功了。该脚本从来就不是万无一失的。将其视为概念证明。
用法
sshe
像这样使用:
sshe … [user@]hostname command
其中…
表示如果可执行文件是ssh
.这里不需要放置-t
nor -tt
(nor -T
)。该脚本假设您希望在远程端使用 tty (否则只需使用ssh
)。剧本期望至少本地 stdin、stdout、stderr 之一要从本地 tty 重定向。ssh -t
如果一切都连接到本地 tty,则脚本将回退。
重要的事物:
command
是您要在服务器上运行的 shell 代码。它必须是单个参数,是 的最后一个参数sshe
。不能省略。hostname
或者user@hostname
必须是倒数第二个参数。不能省略。
在内部,脚本需要知道command
在前面添加一些代码。它需要知道,[user@]hostname
因为它使用了两次。该脚本仅分别选择最后一个和倒数第二个参数,因此存在上述限制。
并非每个有效的调用都可以通过仅替换为ssh
来转换为sshe
调用。但我相信任何运行代码的有效调用(而不是生成交互式 shell)都可以重新排列为有效命令。例子:ssh
sshe
ssh
sshe
ssh user@server -p 1234 echo foo
应重新排列为:
sshe -p 1234 user@server 'echo foo'
sshe
(除非你真的不需要这案件;这只是正确语法的一个例子)。如果您使用了脚本sshe user@server -p 1234 echo foo
,那么该脚本将echo
作为服务器和foo
命令,因为它不会像ssh
以前那样解析其参数。
下面有一些例子。
要求、可移植性问题
当地要求(sshe
运行地点):
/dev/$(ps -p "$$" -o tty=)
假定为控制终端的“真实姓名”。比较这个问题。mktemp -d
。ssh
支持-M
和-S
;该脚本创建主连接和从连接。
远程要求(在服务器上):
- SSH 服务器能够处理主从连接。
/proc
伪文件系统。- 能够使用
/proc/nnnn/fd/N
属于同一用户的另一个进程。 - 符合 POSIX 标准的外壳。
- 静默启动脚本(比较
echo
SCP 在进入时不工作.bashrc
,与sshe
情况类似)。
在测试期间,我成功从 Kubuntu (18.04.5 LTS) 连接到各种 Debian 或 Debian 衍生服务器。我的ssh
和sshd
来自 OpenSSH。
手术
sshe
(除非它决定回退到ssh
或ssh -t
)运行ssh
两次:
ssh -M … -T …
是一个主连接,不在远程端分配 tty。它在那里运行的 shell 代码通过 stdout 和exec
s 向长期运行的sleep
(大约 68 年)报告其 PID。该进程的标准文件描述符将由另一个进程使用。从主站报告的 PID
ssh
由 获取read
。此后,主站的标准输出ssh
将转到后台,cat
其唯一目的是将其中继到 的(本地)标准输出sshe
。稍后
ssh … -t …
是一个从属连接,它在远程端分配 tty。已经从主连接知道远程 PID 后,它会设置重定向,因此提供给sshe
as的代码可以在远程端command
使用单独的 stdin、stdout、stderr(通过主ssh
连接)和 tty(通过从连接)。ssh
从机ssh
不使用原始的 stdin 或 stdoutsshe
,而是使用本地/dev/tty
。
这个想法类似于这个答案(已经链接到问题中)确实如此。链接答案中的代码运行ssh
(隐式ssh -T
)两次以提供额外的描述符。我的脚本运行ssh -T
并ssh -t
提供标准描述符和 tty。并且它使用了主从功能ssh
,因此它会进行一次身份验证(例如要求密码)。
如果本地 stdin、stdout、stderr 都不是本地 tty,则数据流向如下:
本地 stdin 转到 master
ssh
,没有其他本地进程从脚本的 stdin 读取。通过从(远程)读取,/proc/nnnn/fd/0
远程进程可以访问本地标准输入。从属ssh
连接预先重定向到command
,因此远程端的 shell 用作/proc/nnnn/fd/0
其标准输入。类似地,远程端的 shell 用作
/proc/nnnn/fd/1
其标准输出。无论那里发生什么,都会从当地的主人那里出来ssh
。这是在 masterssh
从它运行的(远程)shell 代码中检索到正确的 PID 之后。 PID 被消耗,随后的任何数据都会通过后台read
发送到原始标准输出。sshe
cat
类似地,远程端的 shell 使用
/proc/nnnn/fd/2
它的 stderr。该流将直接从本地 master 发送ssh
到 的 stderrsshe
。脚本生成的一些本地进程使用脚本的 stderr 作为它们的 stderr,因此如果您sshe … 2>error.log
这样做,那么日志也将包含它们的错误消息。特别期待Shared connection to server closed.
。这类似于ssh -T … 2>error.log
日志从远程命令和自身收集消息的情况ssh
。我认为可以制作一个变体,sshe
通过与另一个 stdout 关联的通道从远程命令传递 stderrssh
;在这种情况下,人们将能够区分远程 stderr 和本地工具生成的诊断消息。但脚本并没有这样做。本地 tty 可用于 master
ssh
(如果需要询问密码),然后可用于 Slavessh
。 (坦率地说,脚本使用的更多本地工具可以访问 local/dev/tty
,但他们只是不使用它。)从属设备ssh -t
用作/dev/tty
其标准输入和标准输出。通过这种方式,它可以连接本地和远程,/dev/tty
尽管有其他重定向(例如ssh -t
在没有重定向的终端中运行)。远程进程从它们读取的内容/dev/tty
将得到本地从属ssh
从本地读取的内容/dev/tty
。远程进程写入它们/dev/tty
将使本地从站ssh
写入本地/dev/tty
。
如果本地 stdin、stdout 或 stderr 是本地 tty,则其在远程端(command
由从机远程运行ssh
)的相应对应部分将不会被重定向到,/proc/nnnn/fd/N
并且将保持与远程 tty 的连接。不管怎样,它都会到达本地终端。重点是它不应该绕过远程 tty。其原因很快就会清楚。
很少有本地和远程重定向不一定需要sshe
工作。这是因为我的其他实验性的东西。我决定保留额外的重定向,以防万一它们对鞋底来说sshe
比我记忆中更重要。
障碍和注意事项
整个概念并不像看起来那么容易。 tty 可以处理您输入的内容(例如翻译^M
成^J
)以及将要打印的内容(例如,如果我cat
在终端上使用 *nix 行结尾的文件,则每个换行符将像carriage_return + newline 一样工作)。调用stty -a
看到很多设置。
这就是为什么在处理任意二进制数据时不需要 tty 的原因。而且你在互动时确实需要它。
进程可以配置 tty 以满足它们的需要。看生的与煮熟的。
当您ssh
以某种方式在服务器上分配 tty 时,那里的进程会将其视为它们的 tty。如果他们需要配置他们的 tty,他们将配置他们在服务器上看到的 tty。他们没有办法直接配置本地tty ssh
。所有(?)“烹饪”都是由远程 tty 完成的,并ssh
配置本地 tty,因此它不会干扰。
sshe
这就是在重定向最终应连接到本地 tty 的远程描述符时不应绕过远程 tty 的原因。如果远程 tty 被绕过,那么将没有实体来“烹饪”流。通过将 stdin、stdout 或 stderr 连接sshe
到本地 tty,您表明您希望它“煮熟”。这sshe
类似于ssh … command
在交互式 shell 中运行,即所有标准流都由本地 tty“烹调”的情况(注意这ssh
就像ssh -T
并且它不会将本地终端置于“原始”模式)。
所以sshe
“煮”你显然想“煮”的东西。当您执行以下操作时会出现问题:
sshe … command | whatever
数据将从远程流向command
本地,whatever
而不会被“煮熟”(正如人们所期望的那样ssh … command | whatever
),但 的输出whatever
不会在本地“煮熟”。sshe
可以重新配置本地 tty,使其“烹饪”,但如果碰巧command
打印到其 tty(即打印到可能或可能不“烹饪”的远程 tty,具体取决于其设置),则本地 tty 不应“烹饪”。
sshe
不尝试解决这个问题。它基本上是为了支持最终输出到终端以外的其他地方(例如到常规文件或块设备)的情况。在这个问题上,下面的代码更好:
sshe … command | whatever >some_file
尽管 stderrwhatever
不会被“煮熟”。预计诊断消息看诡异的。请注意,您可以将它们重定向到一个文件(或另一个将“烹饪”它们的本地 tty)。
输入端的情况更糟。如果另一个本地进程尝试从本地 tty 读取数据,它不仅会读取原始数据,还会竞争sshe
输入。这是两个进程从同一终端读取数据的常见问题。
总结一下:构建一个本地命令(管道),因此只有(一个)sshe
想要从本地tty读取;不要让除打印之外的工具sshe
打印到本地 tty,除非您可以忍受“原始”输出。
我开发了sshe
能够传递或处理二进制数据的能力。就我而言,几乎不需要从本地 tty 读取数据或将数据写入本地 tty。我可以忍受来自本地工具的诊断消息不够“成熟”。作为回报,我可以像本地一样sshe
使用远程。sudo
例子
读取或写入需要访问的远程块设备
sudo
。阅读:
sshe user@server 'sudo cat /dev/sdx1' >local_file # or sshe user@server 'sudo pv /dev/sdx1' >local_file
写作:
<local_file sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
在我的测试中,本地
pv
显然不介意本地 tty 是“原始”的;或者更确切地说,它强加的配置和sshe
强加的配置并不矛盾,并且哪个工具首先配置本地 tty 并不重要。所以这似乎有效:pv local_file | sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
请注意,如果设置受到
pv
干扰sshe
,那么您可能无法向遥控器提供密码sudo
。如果设置受到sshe
干扰,pv
那么pv
打印到终端的内容可能看起来被破坏了。即使在这些假设的情况下,内容local_file
也会一字不差地传到远程/dev/sdx1
。
本地
sudo
和远程sudo
一起。如果 local
sudo
会询问你的密码,sudo … | sshe … 'sudo …'
或者sshe … 'sudo …' | sudo …
不是一个好主意,因为 localsudo
和sshe
都会同时从本地 tty 读取。完全本地sudo … | sudo …
工作是因为sudo
实现了锁定机制,因此两个本地sudo
不会同时与同一终端交互。这不适用于本地和远程的混合sudo
。希望你当地的
sudo
允许超时。如果是这样,请sudo -v
提前调用以提供本地密码(如果需要),而不会受到干扰;然后用管道:从远程设备复制到本地设备:
sudo -v # input local password if needed sshe user@server 'sudo cat /dev/sdx1' | sudo tee /dev/sdy1 >/dev/null
从本地设备复制到远程设备:
sudo -v # input local password if needed sudo cat /dev/sdy1 | sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
正是问题所要求的。
和
sudo
:<binary_input sshe user@server 'sudo tool' >binary_output 2>error.log
或者更一般地说:
<binary_input sshe user@server 'requires-tty' >binary_output 2>error.log
最后说明
我曾经认为通过隧道将 stdin 传输到 stdin,将 stdout 传输到 stdout,将 stderr 传输到 stderr和 /dev/tty
to/dev/tty
是微不足道的。我曾经想知道为什么ssh
不提供一个选项(类似于-t
)来执行此操作。现在我知道事情没那么简单;我怀疑或许我还缺少一些东西。