STDOUT和STDERR出现故障

时间:2020-02-14 18:24:56

标签: perl io buffering

我有一个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对应的文件句柄,但仍然相同)?


编辑:

感谢所有答复。关于此的许多讨论都以评论结尾,因此我将根据大家的反馈列出我尝试过的几件事。

  1. 在使用File :: Tee之前,我已经刷新了STDOUT和STDERR。正如@jimtut所怀疑的,File :: Tee确实是罪魁祸首-删除它可以恢复控制台上的顺序。但是我确实想重定向STDOUT和STDERR。
  2. @mob建议改用IO :: Tee,但是我还没有完全理解如何按照自己的意愿在代码中进行这项工作。
  3. @briandfoy指出,没有一种可靠的方法来确保以正确的顺序实时看到2个单独的文件句柄,并且建议使用日志记录例程,这是唯一可以写入STDOUT / STDERR的位置。 @zimd进一步指出File :: Tee使用fork是问题的核心,因为2个进程无法保证输出的任何顺序。
  4. 由于要怪File :: Tee,所以我试图从代码中删除它。我更新了记录器功能,以打印到STDOUT / STDERR以及另外打印到$ log文件句柄。为了进一步捕获日志中的警告,我执行了以下操作:
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提出的解决方案,我结合他们的答案得出了这个解决方案!从来没有想到过需要解决这个看起来非常简单的问题的细节。

4 个答案:

答案 0 :(得分:6)

具有两个单独的文件句柄,没有合同或保证您可以实时看到它们。各种设置和缓冲区都会影响该设置,这就是为什么您会看到自动刷新内容($|)的原因。对于文件或终端,这是相同的想法。

意识到这是一个体系结构问题,而不是语法问题。您有两件事在争夺同一资源。那通常以眼泪结束。当我不知道问题出在哪里时,我会犹豫地提出解决方案,但请考虑将试图写入STDOUTSTDERR的内容写入某种消息代理,以收集所有消息并是唯一写入最终(共享)目标的内容。例如,想要向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);
...

没有后台进程,多余的线程或任何其他导致竞争状况的事件。 STDOUTSTDERR都将从同一进程写入同一日志文件句柄。

答案 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

请参见perltieTie::Handle,以及this post及其相关示例,也许还有this post

将日志记录到STDOUTSTDERR流的文件中(以及复制的打印输出)还可以在主程序中也可能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界面。

相关问题