我准备了以下脚本来测试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/chronic
Perl 脚本。然后是一个 ELF 可执行文件,并告诉我它需要。Libc 是/usr/bin/perl
/usr/bin/perl
ldd
libc.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,所有内容都按顺序打印。\n
error out
如果所有内容都转到非 tty,则完全缓冲STDERR:
将放置在新刷新的缓冲区中(不会立即打印),但非缓冲error out
将立即打印,因此它出现在之前STDERR:
。
即使 Perl 不依赖于 stdio 或者它改变了默认行为中的某些内容,因此上述描述并不完全准确,stderr 也会被明确刷新,因此它表现为无缓冲,而 stdout 的缓冲显然取决于它是否是 tty。
我认为你发现了一个错误。
您的选择
等待
chronic
Debian 得到修补。修补您的版本
/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
(来自expect
Debian 中的包)为(可能未修补)提供一个 tty chronic
:
unbuffer chronic …
注意,即将运行某些命令(例如)的最外层工具(如 cron)chronic …
可能会将命令的 stdout 和 stderr 重定向到不同的文件,那么它就可以区分这两个流。事实上,这就是它对chronic
你的脚本或其他东西所做的。但是如果你告诉工具运行unbuffer chronic …
,那么unbuffer
它将重定向 stdout 和 stderrchronic
到相同伪终端。两个流将合并为一个,它将被打印unbuffer
到其标准输出。最外层工具仍然可以重定向标准输出和标准错误的unbuffer
到不同的文件,但这不会取消合并两个原始流。对于 cron 来说,这似乎不是问题,因为它无论如何都会合并所有输出。
有了chronic
unbuffer
作品,stdbuf
就不会。这是因为它们的工作方式完全不同。