Recipe 23.10. Killing All Processes for a Given User
Problem
You want an easy way to kill all the running processes of a user whose processes get out of control.
Solution
You can send a Unix signal (including the deadly SIGTERM or the even deadlier SIGKILL) from Ruby with the Process.kill method. But how to get the list of processes for a given user? The simplest way is to call out to the unix ps command and parse the output. Running ps -u#{username} gives us the processes for a particular user.
#!/usr/bin/ruby -w # banish.rb def signal_all(username, signal) lookup_uid(username) killed = 0 %x{ps -u#{username}}.each_with_index do |proc, i| next if i == 0 # Skip the header provided by ps pid = proc.split[0].to_i begin Process.kill(signal, pid) rescue SystemCallError => e raise e unless e.errno == Errno::ESRCH end killed += 1 end return killed end
There are a couple things to look out for here.
ps dumps a big error message if we pass in the name of a nonexistent user. It would look better if we could handle that error ourselves. That's what the call to lookup_uid will do. ps prints out a header as its first line. We want to skip that line because it doesn't represent a process. Killing a process also kills all of its children. This can be a problem if the child process shows up later in the ps list: killing it again will raise a SystemCallError. We deal with that possibility by catching and ignoring that particular SystemCallError. We still count the process as "killed," though.
Here's the implementation of lookup_id:
def lookup_uid(username) require 'etc' begin user = Etc.getpwnam(username) rescue ArgumentError raise ArgumentError, "No such user: #{username}" end return user.uid end
Now all that remains is the command-line interface:
require 'optparse' signal = "SIGHUP" opts = OptionParser.new do |opts| opts.banner = "Usage: #{__FILE__} [-9] [USERNAME]" opts.on("-9", "--with-extreme-prejudice", "Send an uncatchable kill signal.") { signal = "SIGKILL" } end opts.parse!(ARGV)
if ARGV.size != 1 $stderr.puts opts.banner exit end
username = ARGV[0] if username == "root" $stderr.puts "Sorry, killing all of root's processes would bring down the system." exit end puts "Killed #{signal_all(username, signal)} process(es)."
As root, you can do some serious damage with this tool:
$ ./banish.rb peon 5 process(es) killed
Discussion
The main problem with banish.rb as written is that it depends on an external program. What's worse, it depends on parsing the human-readable output of an external program. For a quick script this is fine, but this would be more reliable as a self-contained program.
You can get a Ruby interface to the Unix process table by installing the sysproctable library. This makes it easy to treat the list of currently running processes as a Ruby data structure. Here's an alternate implementation of signal_all that uses sys-proctable instead of invoking a separate program. Note that, unlike the other implementation, this one actually uses the return value of lookup_uid:
def signal_all(username, signal) uid = lookup_uid(username) require 'sys/proctable' killed = 0 Sys::ProcTable.ps.each do |proc| if proc.uid == uid begin Process.kill(signal, proc.pid) rescue SystemCallError => e raise e unless e.errno == Errno::ESRCH end killed += 1 end end return killed end
See Also
sys-proctable is in the RAA at http://raa.ruby-lang.org/project/sys-proctable/; it's one of the sysutils packages: see http://rubyforge.org/projects/sysutils for the others To write an equivalent program for Windows, you'd either use WMIthrough Ruby's win32ole standard library, or install a native binary of GNU's ps and use win32-process
|
No comments:
Post a Comment