Cagao has asked for the wisdom of the Perl Monks concerning the following question:
Hi,
I have a script running under Lighttpd/FastCGI which needs to make a call to a system command, this external command sometimes takes forever, so I've wrapped the call in an eval with an ALRM call.
However, when the alarm goes off, and I break out of the eval, the system command is still running.
what's the best way to kill this command?
Thanks,
Rob
Re: Killing a system/exec/`` call if it times out
by kennethk (Abbot) on Dec 01, 2011 at 18:47 UTC
|
What is the form of the command? If it involves redirection, for example, perl does not directly execute the command but rather invokes the shell to do it. Thus, when you break out of the call, you are actually killing a shell invocation, not the offending process. While I hate to invoke bash-like magic, the simplest solution I have found involves actual process management and essentially grepping the results of ps. The following is code I use to handle this problem in one of my scripts: I invite criticism if someone has a better way.
my $pid = open my $calc, '-|', "$command 2>&1"
or die "Pipe failed on open: $!\n";
my $vector = '';
vec($vector,fileno($calc),1) = 1; # Flip bit for pipe
unless (select($vector,undef,undef,$timeout)) { # Calculation
+is hanging
# collect list of spawned processes
my @pids = $pid;
my $i = -1;
push @pids, `ps --ppid $pids[$i] -o pid` =~ /\d+/g while +
++$i < @pids;
kill 9, $_ for @pids;
die "Calculation call failed to return with $timeout secon
+ds\n";
}
local $/; # Slurp
my $content = <$calc>;
close $calc;
| [reply] [d/l] |
|
Thanks, it's a simple call to a program which will hopefully create an image for me, just a program and 2 parameters, no redirection.
Although it is doing the "sh -c ..." approach as you mentioned. I don't care how it's called either, system(), exec(), or backticks.
I could collect the PIDs based on their parent, but would love a one-liner-ish. :)
| [reply] |
|
"I don't care how it's called either, system(), exec(), or backticks."
Actually, yes you do (or should): see the docs for exec, taking special notes of the first paragraph:
"The "exec" function executes a system command *and never
returns*; use "system" instead of "exec" if you want it to
return. It fails and returns false only if the command does not
exist *and* it is executed directly instead of via your system's
command shell (see below)."
And the difference between system and backticks? sgifford put it clearly and succinctly ( in Re: system() vs `backtick`, q.v. ) back in 2004, when he said "backticks send the executed program's STDOUT to a variable, and system sends it to your main program's STDOUT."
But, don't quit there; read similarly re system. And re backticks... well you get the idea.
| [reply] [d/l] |
|
Re: Killing a system/exec/`` call if it times out
by Eliya (Vicar) on Dec 01, 2011 at 20:15 UTC
|
However, when the alarm goes off, and I break out of the eval, the system command is still running.
In most cases, creating a new process group will help here. For example, something like this should work on POSIX-compliant systems (i.e. kill the entire tree of child processes):
if (my $pid = fork()) {
wait;
} elsif (defined $pid) {
setpgrp; # create new process group
eval {
local $SIG{ALRM} = sub { die "alarm\n" };
alarm 5;
system 'bash -c "cat; echo"'; # run something (several proce
+sses) that hangs
alarm 0;
};
if ($@ eq "alarm\n") {
kill 9, 0; # kill current process group
}
exit;
}
I said "in most cases", because in theory this approach will fail in case some subprocess should create another process group of its own, but practice shows this is only rarely done.
(Of course, you could wrap that up in a subroutine run_with_timeout(...), or something.) | [reply] [d/l] [select] |
Re: Killing a system/exec/`` call if it times out
by juster (Friar) on Dec 01, 2011 at 21:13 UTC
|
Here's a simple fork/exec with an alarm. It won't cleanup multiple processes so it must use the >1 arg version of exec. I think you might already know that exec with more than 1 argument avoids using sh.
#!/usr/bin/env perl
use warnings;
use strict;
my $ksecs = 5;
my $csecs = 10;
my $sig = 2;
my $pid = fork;
die "fork: $!" unless(defined $pid);
if($pid == 0){
exec 'sleep', $csecs; # >1 arg exec to avoid using sh
die "failed to spawn command: $!";
}
local $SIG{'ALRM'} = sub { kill $sig, $pid };
alarm $ksecs;
waitpid $pid, 0; # waitpid sets $?
alarm 0; # turn off alarm in case child finished before parent
if($? == $sig){
# we killed it (or someone else sent the same signal)
}elsif($?){
# child had another error or signal
}
| [reply] [d/l] |
|
Cheers, but yeah, I'm looking for a "kill all children of this PID, and their children too"
| [reply] |
|
You are looking for Proc::Killfam. Many, if not most times, when you fork to run a program, you end up getting the shell pid of actual program you ran. So if you killfam 9 that toplevel shell, you should get them all.
| [reply] |
|
|