【问题标题】:Redirecting STDOUT and STDERR for all child processes that will be started by the parent process为将由父进程启动的所有子进程重定向 STDOUT 和 STDERR
【发布时间】:2020-03-11 14:42:20
【问题描述】:

在 Perl(使用版本 5.30.0)中,是否可以通过编程方式(即在我的 Perl 代码中)更改一些设置,这些设置会导致 STDOUT 和 STDERR 被重定向到我的 Perl 进程将要的任何子进程的文本文件从那一刻开始?这里的困难在于我不控制实际启动这些子进程的代码(如果我这样做了,这将是非常微不足道的)。我希望存在某种我可以在我的 Perl 进程中设置的 IO 标志,这将导致所有新启动的子进程将其 STDOUT / STDERR 通道重定向到我选择的目的地(除非 system() / exec() / 用于启动它们的任何调用都显式提供了另一个 STDOUT / STDERR 目标)。

我需要的(高度简化的)用例如下。我编写了一个通用库函数,它执行调用者指定的任务,除其他外,将用户提供的任务生成的所有 STDOUT / STDERR 输出存储在文本文件中。该任务以 Perl 函数引用的形式提供。到目前为止,我只是设法重定向了我的 Perl 进程生成的 STDOUT / STDERR 输出:

    local *STDOUT;
    local *STDERR;

    open( STDOUT, '>', $buildLogFilePath ) or die sprintf(
        "ERROR: [%s] Failed to redirect STDOUT to \"%s\"!\n",
        $messageSubject,
        $buildLogFilePath
    );

    open ( STDERR, '>&', STDOUT ) or die sprintf(
        "ERROR: [%s] Failed to redirect STDERR to \"%s\"!\n",
        $messageSubject,
        $buildLogFilePath
    );

因此,只要用户提供的函数不启动任何子进程,我的功能就可以按预期工作,但是当它启动子进程时,该进程的输出只会转到默认的 STDOUT / STDERR 通道,有效地破坏了我的功能。

编辑:我最终使用了下面 ikegami 描述的 STDOUT / STDERR 重新指向技巧。为了更容易地重用这个技巧,我将代码包装在一个小实用程序类中,该类在构造函数中重定向 STDOUT / STDERR,然后在析构函数中将它们恢复为原始值:

package TemporaryOutputRedirector;

use strict;
use warnings FATAL => 'all';

use Carp::Assert;

sub new
{
    my ( $class, $newDest ) = @_;

    open( my $savedStdout, '>&', STDOUT ) or die sprintf(
        "ERROR: Failed to save original STDOUT in process %i!\n",
        $$
    );

    open( STDOUT, '>', $newDest ) or die sprintf(
        "ERROR: Failed to redirect STDOUT to \"%s\" in child process %i!\n",
        $newDest,
        $$
    );

    open( my $savedStderr, '>&', STDERR ) or die sprintf(
        "ERROR: Failed to save original STDERR in process %i!\n",
        $$
    );

    open ( STDERR, '>&', STDOUT ) or die sprintf(
        "ERROR: Failed to redirect STDERR in child process %i!\n",
        $$
    );

    my %memberData = (
        'newDest'     => $newDest,
        'savedStdout' => $savedStdout,
        'savedStderr' => $savedStderr
    );

    return bless \%memberData, ref $class || $class;
}

sub DESTROY
{
    my ( $self ) = @_;

    my $savedStdout = $$self{ 'savedStdout' };
    assert( defined( $savedStdout ) );

    open( STDOUT, '>&', $savedStdout ) or warn sprintf(
        "ERROR: Failed to restore original STDOUT in process %i!\n",
        $$
    );

    my $savedStderr = $$self{ 'savedStderr' };
    assert( defined( $savedStderr ) );

    open( STDERR, '>&', $savedStderr ) or warn sprintf(
        "ERROR: Failed to restore original STDERR in process %i!\n",
        $$
    );
}

1;

这样,只需要创建一个TemporaryOutputRedirector的实例,执行应该重定向其输出的代码,然后让TemporaryOutputRedirector超出范围并让它的析构函数恢复原来的STDOUT / STDERR通道.

【问题讨论】:

  • 删除local *STDOUT; local *STDERR;

标签: windows perl


【解决方案1】:

当您打开一个文件句柄时,它会使用下一个可用的文件描述符。 STDIN、STDOUT 和 STDERR 通常分别与 fd 0、1 和 2 相关联。如果进程中没有其他打开的句柄,则open创建的下一个句柄将使用文件描述符3。

如果您将 fd 3 与 STDOUT 相关联,许多事情将继续工作。这是因为 Perl 代码通常处理 Perl 文件句柄而不是文件描述符。例如,print LIST 实际上是print { select() } LIST,默认情况下与print STDOUT LIST 相同。因此,您的更改主要在 Perl 中起作用。

然而,当你执行一个程序时,它得到的只是文件描述符。它得到 fd 0、1 和 2。它甚至可能得到 fd 3,但它并不关心这些。它将输出到 fd 1。


一个简单的解决方案是删除local *STDOUT; local *STDERR;

*STDOUT 是一个 glob,一个包含 *STDOUT{IO} 的结构,即相关句柄。

通过使用local *STDOUT,您正在将 glob 替换为一个空的。原始文件不会被销毁——当local 超出范围时它会被恢复——因此与现在匿名的 glob 关联的 Perl 文件句柄不会关闭,因此与该句柄关联的 fd 不会被关闭,因此后续的open 不能重用该 fd。

如果您避免使用local *STDOUT,则意味着您将一个打开的句柄传递给openopen 在这种情况下表现得特别:它将“重新打开”已经与 Perl 句柄关联的 fd,而不是创建新的 fd。

$ perl -e'
   open( local *STDOUT, ">", "a" ) or die;
   open( local *STDERR, ">&", STDOUT ) or die;
   print(fileno(STDOUT), "\n");
   system("echo foo");
'
foo

$ cat a
3

$ perl -e'
   open( STDOUT, ">", "a" ) or die;
   open( STDERR, ">&", STDOUT ) or die;
   print(fileno(STDOUT), "\n");
   system("echo foo");
'

$ cat a
1
foo

如果您希望重定向是临时的。您必须使用文件描述符。

$ perl -e'
   open( local *SAVED_STDOUT, ">&", STDOUT) or die;
   open( STDOUT, ">", "a" ) or die;
   print(fileno(STDOUT), "\n");
   system("echo foo");

   open( STDOUT, ">&", SAVED_STDOUT) or die;
   print(fileno(STDOUT), "\n");
   system("echo bar");
'
1
bar

$ cat a
1
foo

【讨论】:

  • 好吧,我会……!有用!它也比我担心的要简单得多! :)我确实希望重定向是临时的,但我通过分叉一个子进程然后在该子进程中启动输出重定向来解决这个问题。这样,我确保我原来的 Perl 进程不受影响。 :)
  • 好的,现在我发现了一些奇怪的东西。尽管我采用了 fork-a-child-strategy,但重定向似乎是永久性的,而不是暂时的。出于某种原因,虽然我只在分叉的子进程中进行了重定向,但它也影响了父进程的输出,现在也将其发送到我的文本文件。我的印象是,如果我在 Perl 进程中重定向 STDOUT / STDERR,这不会影响启动这个子 Perl 进程的父 Perl 进程的 STDOUT / STDERR 通道,但我现在观察到的似乎与此相矛盾假设。
  • 好的,我想我需要重新阅读并完全理解您的整个帖子,而不是盲目地遵循您作为评论发布的单行字。
  • 删除 {local *STDOUT; local *STDERR;} plus 在你的第二个 sn-p 中额外的 restore-original-stdout 舞蹈也解决了最后一个问题。 :-)
  • It doesn't affect the parent. 你在 Windows 上吗? Windows 上没有 fork 这样的东西。在 Perl 中,使用线程的模拟效果很差。由于它创建一个线程而不是一个新进程,因此使用文件描述符会影响“父级”。 (文件描述符属于进程,而不是线程。)你不应该在 Windows 上使用fork
猜你喜欢
  • 2012-07-14
  • 2021-10-01
  • 2015-08-18
  • 1970-01-01
  • 1970-01-01
  • 2010-10-10
  • 1970-01-01
  • 2023-04-06
  • 2014-09-15
相关资源
最近更新 更多