Moreutils 的 Chronic 会在 STDOUT 中报告所有内容,但仅限于在 cron 中运行时

Moreutils 的 Chronic 会在 STDOUT 中报告所有内容,但仅限于在 cron 中运行时

我准备了以下脚本来测试chronic和cron:

echo "normal out"
echo "error out" >&2
exit 0

当我在普通 shell 中使用 运行它时chronic -ev,输出符合预期:

$ chronic -ev /path/to/test.sh 
STDOUT:
normal out

STDERR:
error out

RETVAL: 0

所以我将它添加到我的 crontab 中,如下所示

MAILTO="[email protected]"
* * * * * chronic -ev /path/to/test.sh

我收到如下电子邮件(使用 postfix 配置):

STDOUT:
normal out
error out

STDERR:

RETVAL: 0

因此发生了两件令人惊讶的事情:

  • 当由 cron 运行时,所有输出似乎都被重定向到 STDOUT
  • 无论如何, Chornic 的-e标志都会“触发”,因此它必须检测到 STDERR 上出现的内容。

系统信息:

$ lsb_release -d
Description:    Debian GNU/Linux 10 (buster)
$ chronic --version
/usr/bin/chronic version [unknown] calling Getopt::Std::getopts (version 1.12 [paranoid]),
running under Perl version 5.28.1.

这是否可能是chronic或crontab中的错误?或者我不了解cronjobs中输出处理的一些基本原理?

附言:我也尝试过:

  • * * * * * cd /path/to; chronic -ev ./test.sh
  • * * * * * /usr/bin/chronic -ev /path/to/test.sh 结果都一样

答案1

分析

无需涉及 cron,您可以通过比较 shell 中以下命令的输出来复制该问题:

chronic -ev /path/to/test.sh              # chronic printing to tty
chronic -ev /path/to/test.sh 2>&1 | cat   # chronic printing to non-tty

现在比较这两个的输出:

chronic -ev /path/to/test.sh  >/dev/null   # suppressed stdout
chronic -ev /path/to/test.sh 2>/dev/null   # suppressed stderr

你会发现只有error out被打印到 stderr。其他的包括 STDERR:打印到标准输出。

这个答案详细说明输出缓冲时标准输入输出库涉及。在我的 Debian 10 中,的输出chronic --version与问题中的完全相同。chronic是,它是shebang 中的/usr/bin/chronicPerl 脚本。然后是一个 ELF 可执行文件,并告诉我它需要。Libc 是/usr/bin/perl/usr/bin/perllddlibc.so.6“标准 C 库”。这些信息可能不足以确定 stdio 是否参与其中;但即使不参与,在本问题的上下文中,chronic其行为也好像参与其中一样。在最坏的情况下,链接的答案提供了有用的上下文。

我们假设 stdout 和 stderr 是同一个文件(注意:终端(tty)也是文件)从链接的答案中我们可以提取以下内容:

  • 默认情况下,stderr 是无缓冲的,并立即写入。
  • 默认情况下,stdout 是
    • 如果是非 tty,则完全缓冲,
    • 如果是 tty 则使用行缓冲。

程序可以重新编程每个文件以使其表现不同,并且可以明确刷新缓冲区。当程序关闭文件或正常退出时,缓冲区会自动刷新。

/usr/bin/chronic在我的 Debian 中包含以下相关代码:

sub showout {
        print "STDOUT:\n" if $opt_v;
        print STDOUT $out;
        STDOUT->flush();
        print "\nSTDERR:\n" if $opt_v;
        print STDERR $err;
        STDERR->flush();
        print "\nRETVAL: ".($ret >> 8)."\n" if $opt_v;
}

我不懂 Perl根本,但代码看起来很简单。提前知道打印到哪里会有所帮助。这是在某个时刻发生的事情:

  • STDOUT->flush();刷新标准输出缓冲区,
  • 然后\nSTDERR:\n打印到标准输出
  • 然后一些东西(例如我们的error out)被打印到stderr,
  • 最后STDERR->flush();刷新 stderr 的缓冲区。

如果所有内容都转到 tty,则会立即打印 line-buffered,因为末尾STDERR:还有一个换行符 ( )。然后立即打印 unbuffered,所有内容都按顺序打印。\nerror out

如果所有内容都转到非 tty,则完全缓冲STDERR:将放置在新刷新的缓冲区中(不会立即打印),但非缓冲error out将立即打印,因此它出现在之前STDERR:

即使 Perl 不依赖于 stdio 或者它改变了默认行为中的某些内容,因此上述描述并不完全准确,stderr 也会被明确刷新,因此它表现为无缓冲,而 stdout 的缓冲显然取决于它是否是 tty。

我认为你发现了一个错误。


您的选择

  • 等待chronicDebian 得到修补。

  • 修补您的版本/usr/bin/chronic并承担可能未修补的新版本在将来再次带来错误的风险。

  • 修补并使用的副本/usr/bin/chronic,但你可能会错过修补和否则有所改善将来的新版本。

  • 为未修补的 提供一个 tty chronic

  • 用 更改缓冲stdbuf

    我尝试使用,stdbuf -e0 -o0 chronic …但无济于事。也许 Perl 的行为类似于 Python(请参阅为什么stdbuf对Python没有效果?)。stdbuf不是一个选项。

无论您选择什么,您都可以报告或不报告该错误。


补丁

您可以chronic通过交换两行代码来修补或复制它。更改此代码片段:

STDOUT->flush();
print "\nSTDERR:\n" if $opt_v;
print STDERR $err;

更改为:

print "\nSTDERR:\n" if $opt_v;
STDOUT->flush();
print STDERR $err;

修补后的代码在将某些内容打印到 stderr 之前直接刷新 stdout 的缓冲区。现在STDERR:看来已经没有办法了。


提供一个 tty

使用unbuffer(来自expectDebian 中的包)为(可能未修补)提供一个 tty chronic

unbuffer chronic …

注意,即将运行某些命令(例如)的最外层工具(如 cron)chronic …可能会将命令的 stdout 和 stderr 重定向到不同的文件,那么它就可以区分这两个流。事实上,这就是它对chronic你的脚本或其他东西所做的。但是如果你告诉工具运行unbuffer chronic …,那么unbuffer它将重定向 stdout 和 stderrchronic相同伪终端。两个流将合并为一个,它将被打印unbuffer到其标准输出。最外层工具仍然可以重定向标准输出和标准错误unbuffer到不同的文件,但这不会取消合并两个原始流。对于 cron 来说,这似乎不是问题,因为它无论如何都会合并所有输出。

有了chronic unbuffer作品,stdbuf就不会。这是因为它们的工作方式完全不同

相关内容