Beefy Boxes and Bandwidth Generously Provided by pair Networks
Don't ask to ask, just ask
 
PerlMonks  

external command, progress bar and exit value

by svenXY (Deacon)
on Apr 01, 2009 at 12:36 UTC ( [id://754685]=perlquestion: print w/replies, xml ) Need Help??

svenXY has asked for the wisdom of the Perl Monks concerning the following question:

Hi,

I know that questions about progress bars have been answered here before, but they did not seem to address my special case here (forgive me if I'm wrong). Anyway, I tried to roll my own solution that works partly, but I ran into some trouble.

Here's what I try to achieve:

  • run an external command (e.c.) and dispaly some kind of "notification" to the user (that - yes - has nothing to do with how "far" the e.c. actually is)
  • capture output AND exit value from the e.c.

    the current design is as follows

  • the sub forks.
  • the child runs the command via open-pipe
  • the parent outputs some kind of progress bar
  • child communicates output and exit value via pipes to the parent
  • parent outputs the retrieved stuff after the child has exited

    My code works fine for some commands, but - for whatever reason - not for all of them.

    ($result, $exitcode) = run_w_progress(1, 'for i in `seq -w 4`; do slee +p 3; /bin/echo $i; done');
    returns
    Result: 1 2 3 4 Exitcode:0 finishing ...
    whereas
    ($result, $exitcode) = run_w_progress(1, '/bin/echo -e "foo\nbar"');
    only returns (note the missing 'bar')
    Result: foo Exitcode:0 finishing ...

    Here's my code (fully working example):
    #!/usr/bin/perl use strict; use warnings; use POSIX ":sys_wait_h"; use IO::Select; use IO::Handle; my ($result, $exitcode) = run_w_progress(1, 'for i in `seq -w 4`; do s +leep 3; /bin/echo $i; done'); print "Result:\n",$result,"\nExitcode:",$exitcode,"\n"; + ($result, $exitcode) = run_w_progress(1, '/bin/echo -e "foo\nbar"'); print "Result:\n",$result,"\nExitcode:",$exitcode,"\n"; sub run_w_progress { my ($sleep, $cmd) = @_; my($reader, $writer); pipe $reader, $writer; my($reader2, $writer2); pipe $reader2, $writer2; $writer->autoflush(1); $writer2->autoflush(1); my @watch = qw(| / - + \ | / - \ |); my $pos = 0; $|++; if ( fork()) { my $kid; my $output = ''; my $exitval; my $c=0; my $handles = IO::Select->new(); $handles->add($reader, $reader2); do { sleep $sleep; my @fhs; while (@fhs = $handles->can_read(0)) { for my $fh (@fhs){ if (fileno($fh) == fileno($rea +der)) { my $line = scalar <$re +ader>; $output .= $li +ne; } else { $exitval = sca +lar <$reader2>; chomp $exitval +; } } } print "\r",'|'x$pos,$watch[$c++]; if ($c == 10) { $c=0; $pos++; } $kid = waitpid(-1, WNOHANG); } until $kid > 0; print "|\n"; return ($output, $exitval); } else { open(my $pipe, '-|', $cmd); while (<$pipe>) { # pass output line to parent print $writer $_; } close $pipe; close $writer; # pass exit value to parent print $writer2 $?>>8, "\n"; close $writer2; exit 0; } } print "finishing ...\n";

    Any hints greatly appreciated!
    svenXY

    Todo: Make this also work with STDERR and such...

  • Replies are listed 'Best First'.
    Re: external command, progress bar and exit value
    by almut (Canon) on Apr 01, 2009 at 15:17 UTC

      The problem is that whenever can_read() says there's something to read, perl will (under the hood) read everything into an internal buffer, from which you then get one line at a time via scalar <$reader>. This only matters if there's more than one line being written faster than you're reading on the other end — i.e. in your last case, where you're writing "foo\nbar\n" without a delay in between.

      You could have discovered this using strace:

      $ strace -etrace=select,read ./754685.pl >/dev/null ... select(16, [6 9], NULL, NULL, {0, 0}) = 1 (in [6], left {0, 0}) read(6, "1\n", 4096) = 2 select(16, [6 9], NULL, NULL, {0, 0}) = 0 (Timeout) select(16, [6 9], NULL, NULL, {0, 0}) = 1 (in [6], left {0, 0}) read(6, "2\n", 4096) = 2 select(16, [6 9], NULL, NULL, {0, 0}) = 0 (Timeout) select(16, [6 9], NULL, NULL, {0, 0}) = 1 (in [6], left {0, 0}) read(6, "3\n", 4096) = 2 select(16, [6 9], NULL, NULL, {0, 0}) = 0 (Timeout) --- SIGCHLD (Child exited) @ 0 (0) --- select(16, [6 9], NULL, NULL, {0, 0}) = 2 (in [6 9], left {0, 0}) read(6, "4\n", 4096) = 2 read(9, "0\n", 4096) = 2 select(16, [6 9], NULL, NULL, {0, 0}) = 0 (Timeout) --- SIGCHLD (Child exited) @ 0 (0) --- select(16, [6 9], NULL, NULL, {0, 0}) = 2 (in [6 9], left {0, 0}) read(6, "foo\nbar\n", 4096) = 8 <- +-- !! read(9, "0\n", 4096) = 2 select(16, [6 9], NULL, NULL, {0, 0}) = 0 (Timeout)

      This works fine as long as there's more to read (in which case your loop would take care of reading everything from the internal buffer line by line), but if there's nothing more to read (after the last block (e.g. "foo\nbar\n") has been read under the hood, further select()s will just time out) you'll only read the first line of the block...

      One workaround would be to use non-blocking read()s:

      pipe $reader, $writer; $reader->blocking(0); ... if (fileno($fh) == fileno($reader)) { read $reader, my $line, 1e5; $output .= $line; } ...

        many thanks, almut and a warm ++! That completely solved my problem!

        I had realized that scalar <$fh> was the problem and also had tried to slurp it instead (to get "all" lines at once), but then it blocked. That's when I started composing this node.

        Just out of curiosity - how would you have implemented this? Similarly? Or is what I did very unusual?


        Regards,
        svenXY

    Log In?
    Username:
    Password:

    What's my password?
    Create A New User
    Domain Nodelet?
    Node Status?
    node history
    Node Type: perlquestion [id://754685]
    Approved by Corion
    help
    Chatterbox?
    and the web crawler heard nothing...

    How do I use this?Last hourOther CB clients
    Other Users?
    Others sharing their wisdom with the Monastery: (6)
    As of 2024-04-25 07:18 GMT
    Sections?
    Information?
    Find Nodes?
    Leftovers?
      Voting Booth?

      No recent polls found