Perl:如何传递IPC :: Open3重定向的STDOUT / STDERR fhs

5
我试图捕获我的 Perl 代码生成的所有输出,包括打印语句和外部命令的输出。
由于设计限制,我不能使用像 Capture::Tiny 这样的解决方案。我需要尽快将输出转发到缓冲变量,并且需要能够区分 STDOUTSTDERR。理想情况下,针对外部命令的解决方案应该与 system 函数基本相同,只是可以捕获 STDOUTSTDERR 而不是打印它们。
我的代码应该实现以下功能:
  1. 保存旧的 STDOUT/STDERR 文件句柄。
  2. STDOUTSTDERR 创建新的文件句柄。
  3. 将所有输出重定向到这里。
  4. 执行一些操作。
  5. 恢复旧的文件句柄。
  6. 处理已捕获的输出,例如打印它。
但是我无法捕获外部命令产生的输出。我无法使用 IPC::Run3 或 IPC::Open3 来实现它。
#!/usr/bin/perl -CSDAL
use warnings;
use strict;
use IPC::Open3;
#use IPC::Run3;

# Save old filehandles
open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!";
open(my $olderr, ">&STDERR") or die "Can't dup STDERR: $!";

my $buffer = "";

close(STDOUT);
close(STDERR);

open(STDOUT, '>', \$buffer) or die "Can't redirect STDOUT: $!";
*STDERR = *STDOUT; # In this example STDOUT and STDERR are printed to the same buffer.

print "1: Test\n";
#run3 ["date"], undef, \*STDOUT, \*STDERR; # This doesn't work as expected
my $pid = open3("<&STDIN", ">&STDOUT", ">&STDERR", "date");
waitpid($pid,0); # Nor does this.

print STDERR "2: Test\n";

open(STDOUT, ">&", $oldout) or die "Can't dup \$oldout: $!";
open(STDERR, ">&", $olderr) or die "Can't dup \$olderr: $!";

print "Restored!\n";
print $buffer;

预期结果:

Restored!
1: Test
Mo 25. Mär 13:44:53 CET 2019
2: Test

实际结果:

Restored!
1: Test
2: Test

也许 open3 无法处理字符串缓冲区句柄?为什么不在 open3 调用中使用常规文件句柄呢? - Håkon Hægland
使用 eval 我可以获取 run3 语句的错误信息:Error: run3(): Invalid argument redirecting STDOUT at /root/test_io.pl line 25.然而这并没有帮助我。如果不重新分配 STDOUTSTDERR,代码将按预期工作。我有点不知所措。 - K.A.B.
@HåkonHægland 我尽量避免这种情况,因为我不想有成百上千的临时文件。 - K.A.B.
你能为我澄清/确认一下吗 - 你是否想要简单地重定向stdout和stderr(分别),无论是来自程序还是外部命令的打印输出?只是这样,将流重定向?这可以通过几种简单的方式完成。(必须是缓冲区(变量)而不是文件吗?) - zdim
4个回答

4

我无法为您提供解决方案,但我可以提供一些关于你所见到的行为的解释。

首先,IPC::Open3 不应在您的文件句柄是变量时工作;有关更多解释,请参见 这个问题

现在,为什么 IPC::Run3 没有工作?首先,请注意如果不重定向 STDERR 并且运行

run3 ["date"], undef, \$buffer, { append_stdout => 1 };

替代

run3 ["date"], undef, \*STDOUT;

然后它会按预期工作。(你需要添加{ append_stdout => 1 }或者将之前的输出添加到$buffer,否则会被覆盖。)

为了理解发生了什么,在你的程序中,在

open(STDOUT, '>', \$buffer) or die "Can't redirect STDOUT: $!";

添加
print STDERR ref(\$buffer), "\n"
print STDERR ref(\*STDOUT), "\n"

这将打印出来

SCALAR
GLOB

这正是IPC::Run3::run3所做的,以了解如何处理您提供的“stdout”(请参见源代码:_fh_for_child_output,由run3调用):
  • 如果它是标量,则使用临时文件(相应的代码行是$fh = $fh_cache{$what} ||= tempfile,其中tempfile是来自File::Temp的函数。

  • 另一方面,当stdout是GLOB(或绑定到IO::Handle)时,直接使用该文件句柄(这就是此代码行)。

这就解释了为什么在使用\$buffer调用run3时可行,而使用\*STDOUT则不行。


当同时重定向STDERR并调用

run3 ["date"], undef, \$buffer, \$buffer, { append_stdout => 1, append_stderr => 1 };

当我修改了IPC::Run3的源代码并添加了一些内容后,一些怪异的事情开始出现。我不太明白发生了什么,但我会在这里分享我发现的东西,希望有人能理解它。
open my $FP, '>', 'logs.txt' or die "Can't open: $!";

在子函数run3的开头。运行时,我只看到。
Restored!
1: Test

在我的终端上, logs.txt 文件中包含了日期(类似于 Mon Mar 25 17:49:44 CET 2019),但在标准输出(STDOUT)上没有显示。

稍微研究一下,fileno $FP 返回了 1 (通常是 STDOUT,但您已关闭它,因此我不会惊讶其描述符能重新使用),fileno STDOUT 返回了 2(这可能取决于您的Perl版本和其他打开的文件句柄)。看起来正在发生的事情是,system 假定 STDOUT 是文件描述符 1,因此将输出打印到了 $FP 而不是 STDOUT(虽然我只是在猜测)。

如果您理解了正在发生的事情,请随意发表评论/编辑。


对于比我更了解的任何人:IPC::Run3 的这种行为是否应该被视为一个 bug? - Dada
@K.A.B. 确实,我的错,我很清楚为什么会发生这种情况;我正在尝试修复它。 - Dada
这不是一个错误,你必须将文件句柄传递给IPC::Open3。 - Grinnz
@Dada 关闭 STDOUT/STDERR 经常会导致出现这样的问题,因此得出这个结论是合理的。 - Grinnz
1
你链接的问题页面_还展示了一种可以工作的方法_(在其他答案中),使用IPC::Run3(在这里显然是可接受的)。 - zdim
显示剩余3条评论

2
我最终得到了以下代码:

Original Answer翻译成"最初的回答"


#!/usr/bin/perl -CSDAL
use warnings;
use strict;
use IPC::Run3;
use IO::Scalar;
use Encode;
use utf8;

# Save old filehandles
open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!";
open(my $olderr, ">&STDERR") or die "Can't dup STDERR: $!";

open(my $FH, "+>>:utf8", undef) or die $!;
$FH->autoflush;

close(STDOUT);
close(STDERR);

open(STDOUT, '>&', $FH) or die "Can't redirect STDOUT: $!";
open(STDERR, '>&', $FH) or die "Can't redirect STDOUT: $!";

print "1: Test\n";

run3 ["/bin/date"], undef, $FH, $FH, { append_stdout => 1, append_stderr => 1 };

print STDERR "2: Test\n";

open(STDOUT, ">&", $oldout) or die "Can't dup \$oldout: $!";
open(STDERR, ">&", $olderr) or die "Can't dup \$olderr: $!";

print "Restored!\n";
seek($FH, 0, 0);
while(<$FH>)
{
  # No idea why this is even required
  print Encode::decode_utf8($_);
}
close($FH);

这远非我最初想要的,但至少似乎能够工作。

我对此有以下问题:

  1. 我需要创建一个匿名文件句柄来减少硬盘上的混乱。
  2. 由于某种原因,我需要手动修复编码。

非常感谢那些在这里花费时间帮助我的人。


Translated:

binmode_stdin => ":utf8", binmode_stdout => ":utf8", binmode_stderr => ":utf8" 添加到 run3 选项中,幸运的是解决了我的编码问题。 - K.A.B.

1

你需要使用父进程的 STDOUT 和 STDERR 吗?IPC::Open3 可以轻松地将子进程的 STDOUT 和 STDERR 重定向到父进程中不相关的句柄,您可以从中读取。

use strict;
use warnings;
use IPC::Open3;

my $pid = open3 undef, my $outerr, undef, 'date';
my $output = do { local $/; readline $outerr };
waitpid $pid, 0;
my $exit = $? >> 8;

这将同时读取STDOUT和STDERR,如果你想分别读取它们,需要将my $stderr = Symbol::gensym作为第三个参数传递(如IPC::Open3文档中所示),并使用非阻塞循环避免在读取两个句柄时出现死锁。IO::Async::Process或类似工具可以完全自动化此过程,但是如果你只需要将输出存储在标量变量中,IPC::Run3提供了一个更简单的解决方案。IPC::Run3和Capture::Tiny也都可以轻松地进行fatpacked以便在脚本中部署。

请注意,在5.14之前,使用STDIN的词汇句柄存在问题。在此之前,运行 perl -MIPC::Open3 -we'open(my $fh, "<", "/dev/null") or die $!; open3("<&".fileno($fh), ">&STDOUT", ">&STDERR", "cat")' 会抛出 open3: close(3) failed: Bad file descriptor at -e line 1 的错误。 - ikegami
@ikegami 那个语法是复制句柄,对于在内存中打开的句柄也无法工作,因为它们没有文件描述符 - 我的示例不是指那个,而是直接传递词法句柄以供使用。 - Grinnz
你的帖子表明,在处理IPC::Open2/3时,词法变量和全局变量一样好用,但在5.14之前并非如此。这只是一个评论,如果对某人有影响,就这样吧。我并不是说你的帖子应该被编辑。 - ikegami

-1

这还不是一个答案,但似乎需要在调用open3时确保STDOUT是一个普通的 tty 文件句柄,例如:

use feature qw(say);
use strict;
use warnings;

use IPC::Open3;
use Symbol 'gensym';
{
    local *STDOUT;  # <-- if you comment out this line open3 works as expected
    my ($chld_in, $chld_out);
    my $chld_err = gensym;
    my $pid;
    eval {
        $pid = open3($chld_in, $chld_out, $chld_err, "date");
    };
    if ( $@ ) {
        say "IPC::Open::open3 failed: '$@'";
    }
    print "-> $_" for <$chld_out>;
    waitpid $pid, 0;
   # say "Cannot print to invalid handle..";
}
say "ok";

输出:

ma. 25. mars 16:00:01 +0100 2019
ok

请注意,该行开头的箭头“->”已经丢失,因此在这种情况下无法从“$chld_out”中读取任何内容。但是,如果我注释掉这一行:
local *STDOUT;

输出结果为:

-> ma. 25. mars 16:01:10 +0100 2019
ok

1
这段代码存在竞态条件。如果子进程向其标准错误流发送足够的数据,那么子进程和父进程都将永久阻塞。 - ikegami
@ikegami 很好的发现。我想你的意思是应该实现一个select循环来检查STDOUT STDERR是否准备好读取?然后从准备好的句柄中读取。 - Håkon Hægland
1
最好使用类似IO::Async::Process的东西来实现这个功能,但如果它们能满足您的需求,IPC::Run3或IPC::Run会更简单。 - Grinnz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接