Category: |
Utility Scripts |
Author/Contact Info |
northwind |
Description: |
Perl Process Killer (PPK)
Usage: ppk {process name, required} {iterations, optional}
Basically, think of this script as the system command kill on steroids. I originally wrote this script to allow me to reclaim CPU cycles from errant cron jobs that I did not have the authority to modify (system permissions and real world authority are two very different things). My solution was to run this program any time I was logged in. Thus, when I was logged in, I could get useful work done; and when I wasn't, the multiple heavyweight cron processes could have their way with the machine...
The code below has been updated to reflect the comments/input of merlyn and graff. One of my previous posts implied that this code would work under Solaris, Irix, and Linux. Because I have not had a chance to test the updated code under Solaris or Irix, I am backing off and stating that the code will run under Linux and should run on many other platforms. Unix::Process would probably improve the robustness of the program (as long as your system has ps on it). My problem with the Unix::Process module is its lack of documentation; thus my choice to switch modes with $^O (wow, making this script more cross-platform friendly nearly doubled its size).
As a special note, I would not recommend running this program as root because a mistyped command line parameter (i.e. ".*") will bring your system down! Also, this script is for use exclusively under Unix, or Unix like, operating systems (if the system command ps could be rewritten in pure Perl, then the OS restriction could be lifted).
Update: Added Linux as an OS in its own right instead of depending on the default.
|
#!/usr/bin/perl
#
# Perl Process Killer (PPK)
# ppk {process name, reqired} {iterations, optional}
#
use strict;
use warnings;
my $loop = -1;
my @immortal;
my $flags;
my $process;
my $id;
my $only_one = 0;
my @level = qw/1 2 3 15 9 -9/;
sub check_match
{
die "ACK, GASP: $id failed to match on $_[0]" if( (not $only_one) &
+& ((not defined $_[1]) || (not defined $_[2])) );
die "ACK, GASP: $id failed to match on $_[0]" if($only_one && (not
+defined $_[1]));
}
die "ACK, GASP: Need program name to search for!\n"
if( (not defined $ARGV[0]) || ($ARGV[0] =~ m/^\s*\d+\s*$/) );
# Other operating systems can be supported, I just do not have access
+to them
# to configure $^O, $flags, $process, and $id properly.
# $^O should be matched against the platform you wish to add support f
+or
# $flags must be set so "ps $flags" returns (at least) the User ID, Pr
+ocess ID, and Command Name
# $process is a regexp that matches against the Command Name
# $id is a regexp that matches the User ID and Process ID; putting the
+m into $1 and $2, respectivly
# Also, don't forget to anchor your $process and $id matches!
if($^O =~ m/linux/i)
{
$flags = "-ea";
eval { $process = qr/\s+(?:\d+[:])+?\d+\s+.*?$ARGV[0].*?\s*$/; };
if($@)
{
$@ =~ s/\s+at\s+.*?$0.*$//i;
die "ACK, GASP: \"$ARGV[0]\" is an invalid command line argument:
+\n" .
" $@";
}
$id = qr/^\s*(\d+)\s+/;
$only_one = 1;
}
elsif($^O =~ m/irix/i)
{
$flags = "-eaf";
eval { $process = qr/\s+(?:\d+[:])+?\d+\s+.*?$ARGV[0].*?\s*$/; };
if($@)
{
$@ =~ s/\s+at\s+.*?$0.*$//i;
die "ACK, GASP: \"$ARGV[0]\" is an invalid command line argument:
+\n" .
" $@";
}
$id = qr/^\s*(\w+)\s+(\d+)\s+/;
}
else # Provide crippled functionality...
{
$flags = "";
eval { $process = qr/\d+\s+.*?$ARGV[0].*?\s*$/; };
if($@)
{
$@ =~ s/\s+at\s+.*?$0.*$//i;
die "ACK, GASP: \"$ARGV[0]\" is an invalid command line argument:
+\n" .
" $@";
}
$id = qr/^\s*(\d+)\s+/;
$only_one = 1;
}
if( (defined $ARGV[1]) && !($ARGV[1] =~ m/\D/o) && ($ARGV[1] > 0) ) {
+$loop = int($ARGV[1]); }
my $login = (getpwuid($>))[0] || getlogin() || (getpwuid($<))[0];
while($loop != 0)
{
$loop-- if($loop > 0);
foreach (map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map { m/$id/;
check_match($_, $1, ((not $only_one) ? $2 : ""));
[$_, ((not $only_one) ? $2 : $1)] }
grep { m/$process/ } `ps $flags`)
{
m/$id/;
check_match($_, $1, ((not $only_one) ? $2 : ""));
next if( ((not $only_one) && ($1 ne $login) && ($login ne "root"))
||
(((not $only_one) ? $2 : $1) == $$)
||
(scalar grep { ((not $only_one) ? $2 : $1) == $_ } @immor
+tal) );
my $successful = 0;
foreach (@level)
{
if((kill $_, ((not $only_one) ? $2 : $1)) >= 1)
{
$successful = 1;
last;
}
}
if(not $successful)
{
warn "WARNING: I cannot kill PID " . ((not $only_one) ? $2 : $1
+) . "!\n";
push @immortal, ((not $only_one) ? $2 : $1);
}
}
sleep(1) if($loop != 0);
}
|
Re: ppk
by merlyn (Sage) on Apr 18, 2005 at 02:16 UTC
|
Two red flags, and some yellow flags, as follows:
First red flag:
$b =~m/^\s*\w+\s+(\d+)\s+/o;
my $b_pid = $1;
This code will get an incorrect $1 if the regex doesn't match. Never never Never look at $1 unless you've also tested the match.
Oh, and that applies a few lines later too. At least you're consistently wrong.
Second red flag:
my @args = ("kill", "-9", $2);
Never never never use kill -9 on a process, unless that process has resisted prior attempts to die via kill 1, 2, and 15.
First yellow flag, back to the earlier code snippet:
m/^\s*\w+\s+(\d+)\s+/o
What's the /o doing there? {insert cricket chirp sound effects} Right, nothing. Doesn't hurt it, but it absolutely doesn't help it either. In fact, you should remove it universally in this program (I just noticed it earlier too).
Second yellow flag... that sort screams for a Schwartzian Transform.
Third yellow flag: Perl has a kill function. Why not use it instead of system'ing out?
Conclusion: other than the bit about the Schwartzian Transform, the knowledge you are missing is contained within Learning Perl, which suggests that you read that book very soon.
| [reply] [d/l] [select] |
|
Never never never use kill -9 on a process, unless that process has resisted prior attempts to die via kill 1, 2, and 15.
Is that general advice for all processes, or is it only for programs that you don't know, that haven't failed to die properly earlier, etc?
I.e. if I have a program whose processes consistently fail to die properly, should I still kill it with 1, 2, and 15 before 9?
/J
| [reply] |
|
| [reply] |
|
|
Re: ppk
by graff (Chancellor) on Apr 18, 2005 at 06:24 UTC
|
I have couple things to add to what merlyn said, regarding this comment in your description:
this script is for use exclusively under Unix, or Unix like, operating systems (if the system command ps -eaf could be rewritten in pure Perl, then the OS restriction could be lifted)
Unfortunately, one of the issues that differ significantly across versions of unix and unix-like operating systems is the behavior of "ps". In particular, the options you use in this script will work on any "SysV" style of unix (e.g. Solaris), and might work on linux (if you're using linux, you know better than me), but it won't work on any BSD style (e.g. (free|open|net)bsd, or the bsd-based macosx), because the latter uses a completely different set of option flags.
It turns out there are a couple ways around this:
- Have some logic that looks at $^O and sets option flags for "ps" accordingly (update: unless you do this very carefully, you may still have problems because the output formats will be very different for BSD vs. SysV), or
- use Unix::Process, which allows you to get just the information you want from "/bin/ps", by way of the abbreviations assigned to the different possible output fields ("pid", "ppid", "vsz", etc). It still runs /bin/ps to do its work, and the different versions of "ps" are not 100% consistent in the field names (e.g. Solaris "ps" uses "comm" and "args" where BSD uses just "command"), but several important names are common to both versions -- whereas the command-line options are not. So you still might need to do different stuff depending on the value of $^O, but some things are more likely to be the same for all unixen.
| [reply] |
|
This post is actually in reply to both previous comments (merlyn's and graff's).
There is nothing quite like posting your code to an open forum to make you write better code... :) Thank you for your comments.
The o in m//o was something I pulled from Programming Perl, 3rd edition. It is supposed to give a hint to the regexp compiler that the regexp only needs to be compiled once. I suppose the same thing could be achieved with qr//. Also, good catch on blindly using $1.
I can see how, in a general purpose setting, -9ing a process would be a Very Bad Thing. This is the first thing on the bug list. As for systeming out for kill instead of using the built-in, does the built-in allow you access to the error codes? Also, because the code was already system dependent (ps -eaf), I figured why not add some more (sort of like using $& and friends in a regexp). The world in which I work/play/code consists of Solaris, Irix, and Fedora Core 2. So within my admittedly small world, the code works.
I will be updating the above code over the next day or two (as time permits) to fix your observations of what is wrong. BTW, does anyone know where there is a list of all the strings $^O could contain?
| [reply] |
|
the regexp only needs to be compiled once
You gotta read the rest of the text right around there. I'd quote it, but I don't have my camel with me (I'm on a trip). But the perlop manpage says:
PATTERN may contain variables, which will be interpolat
+ed (and
the pattern recompiled) every time the pattern search i
+s evalu-
ated, except for when the delimiter is a single quote.
+ (Note
that $(, $), and $| are not interpolated because they l
+ook like
end-of-string tests.) If you want such a pattern to be
+ com-
piled only once, add a "/o" after the trailing delimite
+r. This
avoids expensive run-time recompilations, and is useful
+ when
the value you are interpolating won't change over the l
+ife of
the script. However, mentioning "/o" constitutes a pro
+mise
that you won't change the variables in the pattern. If
+ you
change them, Perl won't even notice. See also
"qr/STRING/imosx".
Thus, /o is useful only when there are variables in the pattern. And you had no patterns with variables!
| [reply] [d/l] |
|
Re: ppk
by northwind (Hermit) on Apr 19, 2005 at 08:23 UTC
|
I'm mildly suprised that no-one caught the problem in the following line:
$process = qr/\s+(?:\d+[:])+?\d+\s+.*?$ARGV[0].*?\s*$/;
Because I'm using user input in a regexp, I should have done one of the following:
# use \Q and \E
$process = qr/\s+(?:\d+[:])+?\d+\s+.*?\Q$ARGV[0]\E.*?\s*$/;
# or enclose in an eval
eval { $process = qr/\s+(?:\d+[:])+?\d+\s+.*?$ARGV[0].*?\s*$/; };
if($@) { ... }
I chose to go with the eval { ... } version mainly because I actually want regexp functionality on the command line. | [reply] [d/l] [select] |
|
|