我有一个perl脚本,该脚本将消息写入STDOUT和STDERR(通过print / croak语句),但我还将STDOUT和STDERR重定向到日志文件:
File::Tee::tee STDOUT, ">>", "$logFile" ;
File::Tee::tee STDERR, ">>", "$logFile" ;
现在,输出日志文件中的STDOUT和STDERR消息显示为乱序。端子上的实际输出也有问题。我尝试刷新缓冲区(如此处建议的https://perl.plover.com/FAQs/Buffering.html),但无济于事:
select(STDERR) ;
$| = 1 ;
select(STDOUT) ;
$| = 1 ;
有人知道我必须怎么做才能按顺序查看输出(我还尝试另外刷新与$ logfile对应的文件句柄,但仍然相同)?
编辑:
感谢所有答复。关于此的许多讨论都以评论结尾,因此我将根据大家的反馈列出我尝试过的几件事。
sub warning_handler {
my $msg = $_[0] ;
print STDERR $msg ;
print $log $msg if defined $log ;
}
$SIG{__WARN__} = \&warning_handler ;
这对于我控制下的所有代码都非常有用。现在一切都在控制台和日志文件上按顺序打印。但是我意识到我无法使用此解决方案,因为我还调用了其他人的perl程序包以实现某些功能,并且很明显,我无法拦截“现货”程序包中写入STDOUT / STDERR的打印/碎裂声等。所以现在,我没有一个好的解决方案。但是我怀疑如果我能找到某种方法在Perl中拦截STDOUT / STDERR,我也许能够得到我所需要的。
EDIT2: 我添加了自己的答案,这可能是我通过修改mob的使用IO :: Tee而不是File :: Tee的解决方案来解决问题的最接近的答案,但是即使这样,它也会丢失一些消息(尽管它可以解决排序问题)。
EDIT3: 终于找到了“解决方案”
use IO::Tee ;
use Capture::Tiny qw(capture);
...
...
select(STDERR) ;
$| = 1 ;
select(STDOUT) ;
$| = 1 ;
open (my $log, ">", $logfilename) ;
*REALSTDOUT = *STDOUT ;
*REALSTDERR = *STDERR ;
*STDOUT = IO::Tee->new(\*REALSTDOUT, $log);
*STDERR = IO::Tee->new(\*REALSTDERR, $log);
# Regular Perl code here which sends output to STDOUT/STDERR
...
...
# system calls / calls to .so needs to be catpured
&log_streams(sub { &some_func_which_calls_shared_object() ; }) ;
sub log_streams {
my ($cr, @args) = @_; # code reference, with its arguments
my ($out, $err, $exit) = capture { $cr->(@args) };
if ($out) {
print STDOUT $out;
}
if ($err) {
print STDERR $err;
}
}
使用IO :: Tee可确保所有perl生成的控制台输出也都进入日志文件,并且这种情况会立即发生,从而实时更新日志和控制台。由于IO :: Tee正在将STDOUT / STDERR文件句柄的含义更改为现在引用teed句柄,因此它只能从perl语句中拦截stdio,它会丢失sys调用,因为它们绕过了perl的STDOUT / STDERR句柄。因此,我们捕获了syscall输出,然后使用log_streams例程将其转发到现在别名为STDOUT / STDERR的流。这会在显示在日志/终端中的系统调用生成的输出中造成延迟,但是perl生成的输出没有延迟-即两全其美。请注意,通过保留子例程some_func_which_calls_shared_object的调用而生成的stderr和stdout的顺序不会保留,因为在log_streams例程中,我们首先打印到STDOUT,然后打印到STDERR-只要系统调用是原子的并且在执行时不做太多操作交错stdout / stderr消息的条件应该没问题。 感谢briandfoy,mob和zimd提出的解决方案,我结合他们的答案得出了这个解决方案!从来没有想到过需要解决这个看起来非常简单的问题的细节。
答案 0 :(得分:6)
具有两个单独的文件句柄,没有合同或保证您可以实时看到它们。各种设置和缓冲区都会影响该设置,这就是为什么您会看到自动刷新内容($|
)的原因。对于文件或终端,这是相同的想法。
意识到这是一个体系结构问题,而不是语法问题。您有两件事在争夺同一资源。那通常以眼泪结束。当我不知道问题出在哪里时,我会犹豫地提出解决方案,但请考虑将试图写入STDOUT
或STDERR
的内容写入某种消息代理,以收集所有消息并是唯一写入最终(共享)目标的内容。例如,想要向syslog添加条目的内容不会写入syslog。他们将消息发送到写入系统日志的事物。
更多Perly示例:在Log4perl中,您不写最终目标。您只需记录一条消息,然后记录器就可以弄清楚如何处理它。当我想要模块的这种行为时,我不直接使用输出工具:
debug( "Some debug message" );
sub debug {
my $message = shift;
output( "DEBUG: $message" );
}
sub output { # single thing that can output message
...
}
然后执行您在output
中要做的所有事情。
但是,您不能总是在试图输出内容的其他东西中控制它。 Perl让您通过在warn
中放置一个代码引用来重新定义$SIG{__WARN__}
和朋友所做的事情,以解决这个问题。您可以捕获警告消息并对其进行任何处理(例如将它们发送到标准输出)。除此之外,还有黑魔法可以将STDERR
重新打开到您可以控制的东西上。没那么糟,它被隔离在一个地方。
在某些情况下,另一个人不想要合并的输出,而侵入式解决方案使得无法将它们分开。与硬编码约束相比,我更喜欢灵活性。如果我只想要错误,那么我想要一种仅获取错误的方法。还有许多其他类型的变通办法,例如包装器既收集输出流(因此,一点也不侵入),又收集各种命令重定向。
答案 1 :(得分:4)
您将有两个写入$logfile
的文件句柄。除非File::Tee
小心翼翼地在每次写操作之前寻找到文件句柄的末尾(它似乎没有出现),否则您将获得竞争条件,其中一个文件句柄将覆盖另一个文件句柄。
一种解决方法是对reopen
函数使用File::Tee::tee
选项-它将在每次写入后关闭文件,并在下一次写入之前重新打开文件(在文件的正确结尾) 。但是,这可能会损害性能,具体取决于您写入这些文件句柄的频率。
使用IO::Tee
可能还可以带来更好的运气,这是一种比File::Tee
使用的方法(每个File::Tee::tee
的后台处理)更直接的实现(使用绑定的文件句柄),因此您可能会得到更少的惊喜。 IO::Tee
解决方案的外观如下:
use IO::Tee;
$| = 1;
open my $stdout, ">&=1"; # to write to original stdout
open my $stderr, ">&=2"; # to write to original stderr
open my $fh_log, ">>", $logfile;
*STDOUT = IO::Tee->new($stdout, $fh_log);
*STDERR = IO::Tee->new($stderr, $fh_log);
...
没有后台进程,多余的线程或任何其他导致竞争状况的事件。 STDOUT
和STDERR
都将从同一进程写入同一日志文件句柄。
答案 2 :(得分:3)
注意 第一部分通过扎带手柄完成;第二部分中的解决方案使用Capture::Tiny
使用tie-d句柄的方法的准概念证明。
通过将句柄打印到文件和STDOUT
流(的副本)上,将与句柄绑定的包
package DupePrints;
use warnings;
use strict;
use feature 'say';
my $log = 't_tee_log.out';
open my $fh_out, '>', $log or die $!; # for logging
# An independent copy of STDOUT (via dup2), for prints to terminal
open my $stdout, '>&', STDOUT or die $!;
sub TIEHANDLE { bless {} }
sub PRINT {
my $self = shift;
print $fh_out @_;
print $stdout @_;
}
1;
使用它的程序
use warnings;
use strict;
use feature 'say';
use DupePrints;
$| = 1;
tie *STDERR, 'DupePrints';
tie *STDOUT, 'DupePrints';
say "hi";
warn "\t==> ohno";
my $y;
my $x = $y + 7;
say "done";
这同时在终端和t_tee_log.out
上打印文本
hi ==> ohno at main_DupePrints.pl line 14. Use of uninitialized value $y in addition (+) at main_DupePrints.pl line 17. done
请参见perltie和Tie::Handle,以及this post及其相关示例,也许还有this post
将日志记录到STDOUT
和STDERR
流的文件中(以及复制的打印输出)还可以在主程序中也可能use
的其他模块中使用。 / p>
要获得未记录的“干净”打印,请像在模块中一样在主程序中复制STDOUT
句柄,然后进行打印。如果您需要以更具选择性和复杂性的方式使用它,请根据需要进行修改-就目前而言,这只是一个基本的演示。
在问题的编辑中进行了澄清,这是另一种方法:包装对Capture::Tiny的调用,该调用捕获 any 代码的所有输出,然后根据需要管理捕获的打印件
use warnings;
use strict;
use feature qw(say state);
use Capture::Tiny qw(capture);
sub log_streams {
my ($cr, @args) = @_; # code reference, with its arguments
# Initialize "state" variable, so it runs once and stays open over calls
state $fh_log = do {
open my $fh, '>', 'tee_log.txt' or die $!;
$fh;
};
my ($out, $err, $exit) = capture { $cr->(@args) };
if ($out) {
print $fh_log $out;
print $out;
}
if ($err) {
print $fh_log $err;
print $err;
}
}
log_streams( sub { say "hi" } );
log_streams( sub { warn "==> ohno" } );
log_streams( sub { my $y; my $x = $y + 7; } );
log_streams( sub { system('perl', '-wE', q(say "external perl one-liner")) } );
log_streams( sub { say "done" } );
这一切的缺点是一切都需要通过该子程序来运行。但话又说回来,那实际上是一件好事,即使有时不方便。
state feature用于“初始化”文件句柄,因为声明为state
的变量永远不会被初始化。因此该文件在第一次调用时仅打开一次,并保持打开状态。
这也是需要完成的演示。
答案 3 :(得分:2)
从@mob的答案中获得提示,以使用IO :: Tie而不是File :: Tee(因为后者使用fork导致STDERR vs STDOUT出现故障),我对mob的原始解决方案进行了一些修改,并且工作了(几乎-继续阅读):
use IO::Tee
...
...
open (my $log, ">", $logfilename) ;
*MYSTDOUT = *STDOUT ;
*MYSTDERR = *STDERR ;
*STDOUT = IO::Tee->new(\*MYSTDOUT, $log);
*STDERR = IO::Tee->new(\*MYSTDERR, $log);
这导致控制台和日志文件上的顺序正确(mob最初使用open来复制STDOUT / STDERR的原始解决方案不起作用-导致日志文件中的顺序正确,但控制台上的顺序混乱。出于某种奇怪的原因,使用typeglob别名而不是dup)。
但是,尽管听起来像这个解决方案一样好,但它错过了从我在日志文件中调用的程序包中打印一些消息的机会(尽管它们在控制台上打印)。我原来的文件File :: Tee确实导致来自软件包的这些相同消息显示在日志文件中,因此某处发生了一些伏都教。有问题的特定程序包是.so文件,因此我看不到它如何精确地打印其消息。
编辑: 我猜.so文件和打印到stdout / stderr的外部系统命令一样好。由于未经过perl IO,因此perl中STDOUT / STDERR类型组所指向的句柄将不会反映从perl调用的外部程序的输出。 我猜最好的解决方案是对来自perl代码中的消息使用此解决方案的组合,并使用@zdim指出的Capture :: Tiny :: capture捕获和重定向来自系统调用的消息。 swig界面。