IPC::Open3 を使って、子プロセスの標準出力と標準エラー出力をポーリングする。(Windows では動かなかった…)


Windowsでも動く続編を書いたので、そちらを参照ください。
IPC::Open3 を使って、子プロセスの標準出力と標準エラー出力をポーリングする。(Windows でも動くよ!) - ◆F99a.q8oVEの日記



とか言っていましたが、一時ファイルに吐かせる方法で書いてみました。

qx2_win が Windows 用、qx2 が Windows 以外(FD にたいして select が使えるプラットフォーム)用。 ただし、Windows 上で動かしてみていないので、実際に動くかどうかは不明。

#!/usr/bin/env perl
use strict;
use warnings;

sub qx2_win {
    my @cmd = @_;

    use IPC::Open3;
    use Symbol qw/gensym/;
    use POSIX ":sys_wait_h";
    use File::Temp qw/tempfile/;

    my $child_out = tempfile();
    my $child_err = tempfile();
    my $pid = open3(undef, '>&'. fileno($child_out), '>&' . fileno($child_err), @cmd);

    while (1) {
        if (waitpid($pid, WNOHANG) > 0) {
            last;
        }
    }

    my $code = WIFEXITED($?) ? WEXITSTATUS($?) : WTERMSIG($?);

    seek $child_out, 0, 0;
    seek $child_err, 0, 0;
    my $out = do { local $/; <$child_out>; };
    my $err = do { local $/; <$child_err>; };

    return ($out, $err, $code);
}

sub qx2 {
    my @cmd = @_;

    use IPC::Open3;
    use Symbol qw/gensym/;
    use IO::Select;
    use POSIX ":sys_wait_h";

    my ($child_out, $child_err) = (gensym, gensym);
    my $pid = open3(undef, $child_out, $child_err, @cmd);

    my $s = new IO::Select($child_out, $child_err);
    my $out = my $err = '';

    while (1) {
        while (my @ready = $s->can_read) {
            for my $fh (@ready) {
                if (sysread($fh, my $buf, 4096) > 0) {
                    if ($fh == $child_out) {
                        $out .= $buf;
                    } elsif ($fh == $child_err) {
                        $err .= $buf;
                    }
                } else {
                    $s->remove($fh);
                    close $fh;
                }
            }
        }

        if (waitpid($pid, WNOHANG) > 0) {
            last;
        }
    }

    my $code = WIFEXITED($?) ? WEXITSTATUS($?) : WTERMSIG($?);
    return ($out, $err, $code);
}

use Test::More;

my @tests = (
    { command => [ 'perl', '-e', q{print 'a'} ], out => 'a', err => '', code => 0 },
    { command => [ 'perl', '-e', q{exit 0} ], out => '', err => '', code => 0 },
    { command => [ 'perl', '-e', q{exit 1} ], out => '', err => '', code => 1 },
    { command => [ 'perl', '-e', q{exit 255} ], out => '', err => '', code => 255 },
    { command => [ 'perl', '-e', q{print 'a' x (1024 ** 2)} ], out => 'a' x (1024 ** 2), err => '', code => 0 },
    { command => [ 'perl', '-e', q{print STDERR 'a' x (1024 ** 2)} ], out => '', err => 'a' x (1024 ** 2), code => 0 },
    { command => [ 'perl', '-e', q{print 'a' x (1024 ** 2); print STDERR 'b' x (1024 ** 2)} ], out => 'a' x (1024 ** 2), err => 'b' x (1024 ** 2), code => 0 },
);


for my $test (@tests) {
    my ($out, $err, $code) = qx2(@{$test->{command}});
    is $out, $test->{out};
    is $err, $test->{err};
    is $code, $test->{code};
}

for my $test (@tests) {
    my ($out, $err, $code) = qx2_win(@{$test->{command}});
    is $out, $test->{out};
    is $err, $test->{err};
    is $code, $test->{code};
}

done_testing;