如何确保 SSH 客户端保持标准输出文件描述符干净以供重用?

如何确保 SSH 客户端保持标准输出文件描述符干净以供重用?

此代码因错误而终止:

 (
  ssh localhost seq 100000
  seq 100000
 ) | wc
 #-> seq: write error: Resource temporarily unavailable

这是重现写入错误的最小代码。重点不是要更改子进程/管道架构以使其正常工作,而是要理解为什么当 fd 1 分配给稍后重用以写入大型输出的管道时会发生此错误。

为什么 SSH 客户端会让 stdout 文件描述符变脏?这是设计缺陷吗?是否有一个选项可以使其表现得像另一个进程一样?

编辑:根据评论中的线索,我怀疑它可能与 OpenSSH 版本 7.9 到 8.4 有关

答案1

鉴于强有力的线索表明这是由于特定于版本的回归造成的,以下是获取不依赖于 OpenSSH 版本的代码的三种方法。

经验证,只有 fd 1 受到影响,而 fd 0 和 2 保持干净,fd 1 和 2 之间的排列是一种可能的解决方法。在下面的代码中,fd 2 变脏,但除非写入足够多的数据,否则不会出现缺陷,而 fd 1 是干净的:

(
 ssh localhost '(seq 100000) {safe}>&2 2>&1 >&$safe' \
                             {safe}>&2 2>&1 >&$safe
 seq 100000
) | wc

或者使用额外的管道,将 ssh 包装在子进程中以隔离受影响的文件描述符:

(
 ssh localhost seq 100000 | cat
 seq 100000
) | wc

如果您必须编写一个可以共享 stdout 文件描述符且不依赖于 OpenSSH 受影响版本的 SSH 调用,这可能是一个可以接受的缺点。

第三种方法是将受影响的 fd 分配给文件而不是直接分配给管道。这是一个执行此操作的函数:

repipe(){
  tmp=$(mktemp)
  trap "rm -f $tmp" RETURN
  "$@" > $tmp &
  tail --pid $! -n+0 -f $tmp
}
(
  repipe ssh localhost seq 100000
  seq 100000
) | wc

编辑: https://bugzilla.mindrot.org/show_bug.cgi?id=3280 已在 8.5 版本上报告(但至少在 7.9 中存在)并在 8.9 版本上关闭。如果您无法升级二进制文件,您可能需要将此带有基本名称的脚本放在用户 PATH 的ssh前面:/usr/bin

#!/bin/bash
native=/usr/bin/ssh
if [[ ${vnum:=$($native -V 2>&1 | (IFS="[_. ]" read osef maj min osef && printf "%02d.%s" $maj $min))} > 07.9 && $vnum < 08.9 ]]
then
  exec $native "$@" | cat
else
  [[ $vnum > 08.9 ]] && echo "INFO: $0 may be removed on this host">&2
  exec $native "$@"
fi

相关内容