5

I'm writing a perl script that needs to work in windows and linux that will run a process, timeout if it takes too long, return the exit code assuming it didn't timeout, and return stdout assuming the exitcode was zero and it didn't timeout. I don't need STDIN or STDERR. I've tried to use IPC::run but couldn't get it to work.

The closest I got is with IPC::Open3 and waitpid($pid, WNOHANG). But I've hit a snag. I'm seeing different results on windows and linux. In the code below I give open3 a command that will fail (ping doesn't have any argument -z). On linux the code immediately returns a negative exit code. On windows the command times out. Running ping google.com -z on the windows command line immediately returns telling me there is no such argument. Why does ``waitpid` return a zero?

use strict;
use warnings;
use POSIX ":sys_wait_h";
use IPC::Open3;

my $inFH;
my $outFH;
my @cmd = ("ping", "google.com", "-z");
my $pid = open3( $inFH, $outFH, 0, @cmd);
my $counter=0;
my $waitret;
my $exitcode;
do{
    $counter++;
    $waitret = waitpid($pid,WNOHANG);   
    $exitcode = $?;
}while( !$waitret and ($counter < 4_000_000));

if ($counter >= 4_000_000) {
    print "Command timed out\n";
    kill(9, $pid);
} else {
    print "Exit Code: $exitcode\n";
}

Other solutions are welcome. The only reason I use waitpid with nohang is to kill the process if it takes too long. I dont have any other processing I need to do in the meantime. I'm using windows 10 strawberry perl 5.28 portable on windows. My debian machine has perl 5.24.

GMB
  • 216,147
  • 25
  • 84
  • 135
serpixo
  • 310
  • 1
  • 7
  • 1
    See [IPC::Cmd](https://metacpan.org/pod/IPC::Cmd) it has a timeout parameter, so you can avoid the busy waiting loop. See also [Timeout for launching a bash command in perl](https://stackoverflow.com/q/50040421/2173773) – Håkon Hægland Dec 13 '18 at 21:49
  • Using IPC::Cmd and run my script seems to patiently wait for the process to end and THEN notifiy me if it violated the timeout or not. Running with 'ping google.com /t' causes run to hang waiting for ping to stop even though i have timeout set to 4. In linux it seems to work as intended. – serpixo Dec 13 '18 at 22:41
  • Unfortunately Windows does not have real forking so it is emulated via ithreads. Windows also does not support POSIX signaling. This is probably why you're seeing different behavior. – Grinnz Dec 13 '18 at 23:07

1 Answers1

3

The IPC::Run has support for various timeouts and timers, which should also work on Win32.

Basic:

use warnings;
use strict;
use feature 'say';

use IPC::Run qw(run timeout);

my $out;

eval {
    run [ qw(sleep 20) ], \undef, \$out, timeout(2) or die "Can't run: $?"
};  
if ($@) { 
    die $@ if $@ !~ /^IPC::Run: timeout/;
    say "Eval: $@";
}

The timeout throws an exception, thus the eval. There are other timers, with behavior which is more subtle and manageable. Please see documentation.

The exception is re-thrown if the one caught isn't caused by IPC::Run's timer -- judged by what the module's version on my system prints in the message, a string starting with IPC::Run: timeout. Please check what that is on your system.

While some things don't work on Windows the timers should, I think. I can't test right now.


It's reported that, while the above works, with a more meaningful command in place the SIGBREAK (21) is emitted at timeout, and once it is handled the process stays around.

In this case terminate it manually in the handler

use warnings;
use strict;
use feature 'say';
use IPC::Run qw(run harness timeout);

my $out;
my @cmd = qw(sleep 20);

my $h = harness \@cmd, \undef, \$out, timeout(2);

HANDLE_RUN: {
    local $SIG{BREAK} = sub {
        say "Got $_[0]. Terminate IPC::Run's process";
        $h->kill_kill;
    };  

    eval { run $h };  
    if ($@) { 
        die $@ if $@ !~ /^IPC::Run: timeout/;
        say "Eval: $@";
    }
};

In order to be able to use the module's kill_kill here I first create the harness, which is then available when the handler is installed, and on which kill_kill can be called when it fires.

Another way would be to find the process ID (with Win32::Process::Info or Win32::Process::List), and terminate it with kill, or Win32::Process, or TASKKILL. See this post (second half).

A block is added only to local-ize the signal handler. In real code all this is likely scoped in some way so an extra block may not be needed.

Note that Windows doesn't have POSIX signals and that Perl emulates only a few very basic UNIX signals, along with the Windows-specific SIGBREAK.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • That works.... strangely...If I just use your code and replace sleep 20 with ping google.com /t then I get the following message on prompt: "Terminating on signal SIGBREAK(21)" and my script completely dies. If it put in a catch for $SIG{BREAK} then i see my SIG{BREAK} message at about the timeout period i put in.... but the script still hangs on the ping for at least another 20 seconds.... Maybe it's trying to somehow gracefully kill the command? Can i make it more forceful? – serpixo Dec 13 '18 at 23:58
  • @serpixo heh ... I had searched before posting (can't test on Win right now), and there were a few references to `SIGBREAK` -- but only with regards to the module _installation_. There are other ways to time stuff, and full example(s) on the linked section of the docs. Can you look there? I just can't test on Windows right now :( – zdim Dec 14 '18 at 00:07
  • @serpixo If you can catch it then you can almost surely rig it. I'd try this: [kill_kill](https://metacpan.org/pod/IPC::Run#kill_kill) that command in the "signal" handler (no true signals on Win32 but only emulation of basic ones). Of course, it'd be better to not have to do all that; try other other way(s) to time from docs. – zdim Dec 14 '18 at 00:24
  • Thanks zdim, I'll get back to you if I find something that works better. For now your solution is the best for my needs. Hopefully I'll find a more... timely solution. – serpixo Dec 14 '18 at 00:29
  • @serpixo OK. I'll look more and try to test on Windows later. If it stays at terminating the process manually out of the handler please let me know and I can add code for that. – zdim Dec 14 '18 at 00:31
  • @serpixo Added a way to stop the process from the handler. Still can't test on Windows (my VM is busted) ... :(. Tested on Linux with `$SIG{INT}` instead. – zdim Dec 14 '18 at 07:51