Ωmega
Ωmega

Reputation: 43703

Reading STDOUT and STDERR of external command with no wait

I would like to execute external command rtmpdump and read it's STDOUT and STDERR separately, but not to wait till such command ends, but read its partial outputs in bulks, when available...

What is a safe way to do it in Perl?


This is a code I have that works "per-line" basis:

#!/usr/bin/perl

use warnings;
use strict;
use Symbol;
use IPC::Open3;
use IO::Select;

sub execute {
  my($cmd) = @_;
  print "[COMMAND]: $cmd\n";
  my $pid = open3(my $in, my $out, my $err = gensym(), $cmd);
  print "[PID]: $pid\n";
  my $sel = new IO::Select;
  $sel->add($out, $err);
  while(my @fhs = $sel->can_read) {
    foreach my $fh (@fhs) {
      my $line = <$fh>;
      unless(defined $line) {
        $sel->remove($fh);
        next;
      }
      if($fh == $out) {
        print "[OUTPUT]: $line";
      } elsif($fh == $err) {
        print "[ERROR] : $line";
      } else {
        die "[ERROR]: This should never execute!";
      }
    }
  }
  waitpid($pid, 0);
}

But the above code works in text mode only, I believe. To use rtmpdump as a command, I need to collect partial outputs in binary mode, so do not read STDOUT line-by-line as it is in the above code.

Binary output of STDOUT should be stored in variable, not printed.

Upvotes: 1

Views: 591

Answers (2)

ikegami
ikegami

Reputation: 386621

Using blocking functions (e.g. readline aka <>, read, etc) inside a select loop defies the use of select.

$sel->add($out, $err);

my %bufs;
while ($sel->count) {
   for my $fh ($sel->can_read) {
      my $rv = sysread($fh, $bufs{$fh}, 128*1024, length($bufs{$fh}));

      if (!defined($rv)) {
         # Error
         die $! ;
      }

      if (!$rv) {
         # Eof
         $sel->remove($fh);
         next;
      }        

      if ($fh == $err) {
         while ($bufs{$err} =~ s/^(.*\n)//) {
            print "[ERROR] $1";
         }
      }
   }
}

print "[ERROR] $bufs{$err}\n" if length($bufs{$err});

waitpid($pid, 0);

... do something with $bufs{$out} ...

But it would be much simpler to use IPC::Run.

use IPC::Run qw( run );

my ($out_buf, $err_buf);
run [ 'sh', '-c', $cmd ],
   '>', \$out_buf, 
   '2>', sub {
      $err_buf .= $_[0];
      while ($err_buf =~ s/^(.*\n)//) {
         print "[ERROR] $1";
      }
   };

print "[ERROR] $err_buf\n" if length($err_buf);

... do something with $out_buf ...

Upvotes: 1

Drew Shafer
Drew Shafer

Reputation: 4802

If you're on a POSIX system, try using Expect.pm. This is exactly the sort of problem it is designed to solve, and it also simplifies the task of sending keystrokes to the spawned process.

Upvotes: 1

Related Questions