http://qs1969.pair.com?node_id=266703

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

I know this has been asked a couple of times before, but please bear with me. I'd like to be able to run shell commands (system() or qx()) that time-out after a given time, something like:
$exit = &command( "/bin/snore", 60 );
I pretty new to the world of pipe/fork/exec stuff, so please excuse any ignorance!

I fould this in perlipc:
eval { local $SIG{ALRM} = sub { die "alarm clock restart" }; alarm 10; flock(FH, 2); # blocking write lock alarm 0; }; if ($@ and $@ !~ /alarm clock restart/) { die }
Along with the quote:

           If the operation being timed out is system() or qx(), this technique is liable to generate zombies. If this matters to you, you'll need to do your own fork() and exec(), and kill the errant child process.

So after a bit of research i came up with this:
#! /usr/bin/perl use strict; my $exit = &command( "sleep 4; exit 99;", 1 ); if(! defined( $exit ) ) { print "error: command timed-out\n" } else { print "yay, it worked, exit: $exit\n" } sub command { my( $command, $timeout ) = @_; my $pid; eval { local $SIG{ALRM} = sub { die }; if( $pid = fork ) { alarm $timeout } else { exec( $command ) || die( "Couldn't exec $comma +nd" ) } waitpid( $pid, 0 ); alarm 0; }; if( $@ ) { return undef } else { return $? / 256 } }
  • fork... check
  • exec... check
  • kill... ? oops, where do i do this?

    Am i still breeding zombies? And how do i go about capturing output? Thanks.

    - ><iper


    my JAPH: print"Just another Perl hacker\n"; # ^ look, no space! pretty tricky hey?
  • Replies are listed 'Best First'.
    Re: The 'ol shell timeout question
    by Zaxo (Archbishop) on Jun 18, 2003 at 02:44 UTC

      You're probably still breeding zombies, which happen if the child exits before an unprepared parent does. You have placed waitpid in the wrong position. It must be called in the parent, not the child, so line 27 should be moved to within the block on line 24.

      if( $pid = fork ) { alarm $timeout; waitpid( $pid, 0 ); } else { exec( $command ) || die( "Couldn't exec $command" ) }
      Once you call exec successfully, nothing else you write matters. exec does not return.

      I think that you could benefit by first studying fork and exec in detail. Every perl command that fires an external command uses fork in some way, except exec which is metamorphosis, not cloning.

      In studying fork you will need to also learn wait and waitpid. What you should aim for is an understanding of unix processes. Advanced Programming in the Unix Environment, by W. Richard Stevens, is the bible on that.

      As for kill, that is the simplest form of ipc. You should also learn about signals to use it, and the Stevens book is just as good for that. You're already using signals with your alarms, but knowledge of signals would have shown you that the parent can enforce timeouts on a child by setting its own alarm and firing off SIGINT at it. Avoid rolling your own fancy signal handlers at first. The default handlers and 'IGNORE' are enough while learning.

      (Added) You're right that your code and my correction are equivalent, I missed the left brace in the else clause, If I was interested in style, I'd have written

      defined( my $cpid = fork) or die $!; $cpid or exec $command; alarm $timeout; waitpid( $pid, 0 ); alarm 0;
      I urge you to reflect on why you asked the question. The code was working, but you didn't know why. You reject advice on saving time and worry every time you need this kind of code.

      'man 7 signal' will inform you of what the default SIGINT handler does.

      After Compline,
      Zaxo

      A reply falls below the community's threshold of quality. You may see it by logging in.
    Re: The 'ol shell timeout question
    by hacker (Priest) on Jun 18, 2003 at 02:24 UTC
      This tutorial on IPC may be useful.

      Also, perlfaq8 has one entry titled:

      How do I timeout a slow event?

      Use the alarm() function, probably in conjunction with a signal handler, as documented in "Signals" in perlipc and the section on ``Signals'' in the Camel. You may instead use the more flexible Sys::AlarmCall module available from CPAN.

      Be careful when implementing signals. Take a look at the %SIG section of the perlvar POD for some clues and warnings. Remember that the alarm signal, like other signals, can't be trapped with eval directly, it will exit unconditionally. You have to catch the signal in %SIG and die() to make it catchable. Something like:

      $SIG{ALRM} = sub { die 'timeout' };
      Now you can catch it with eval.

      Another possibility is something like the following (untested) code snippet:

      use strict; use IO::Select; open(CMD, "shell_cmd|") or die $!; unless(IO::Select->can_read(1) ) { die "timed out!"; } print while(sysread(CMD, $_, 4096)); close(CMD);

      For capturing the output, you probably want (my old favorite), IPC::Open3, which gives you STDIN, STDOUT, and STDERR. Be wary of the race conditions though, read the POD and examples very carefully. Good luck!