| #!/usr/bin/env python | 
 |  | 
 | import os | 
 | import re | 
 | import sys | 
 |  | 
 | def _write_message(kind, message): | 
 |     import inspect, os, sys | 
 |  | 
 |     # Get the file/line where this message was generated. | 
 |     f = inspect.currentframe() | 
 |     # Step out of _write_message, and then out of wrapper. | 
 |     f = f.f_back.f_back | 
 |     file,line,_,_,_ = inspect.getframeinfo(f) | 
 |     location = '%s:%d' % (os.path.basename(file), line) | 
 |  | 
 |     print >>sys.stderr, '%s: %s: %s' % (location, kind, message) | 
 |  | 
 | note = lambda message: _write_message('note', message) | 
 | warning = lambda message: _write_message('warning', message) | 
 | error = lambda message: (_write_message('error', message), sys.exit(1)) | 
 |  | 
 | def re_full_match(pattern, str): | 
 |     m = re.match(pattern, str) | 
 |     if m and m.end() != len(str): | 
 |         m = None | 
 |     return m | 
 |  | 
 | def parse_time(value): | 
 |     minutes,value = value.split(':',1) | 
 |     if '.' in value: | 
 |         seconds,fseconds = value.split('.',1) | 
 |     else: | 
 |         seconds = value | 
 |     return int(minutes) * 60 + int(seconds) + float('.'+fseconds) | 
 |  | 
 | def extractExecutable(command): | 
 |     """extractExecutable - Given a string representing a command line, attempt | 
 |     to extract the executable path, even if it includes spaces.""" | 
 |  | 
 |     # Split into potential arguments. | 
 |     args = command.split(' ') | 
 |  | 
 |     # Scanning from the beginning, try to see if the first N args, when joined, | 
 |     # exist. If so that's probably the executable. | 
 |     for i in range(1,len(args)): | 
 |         cmd = ' '.join(args[:i]) | 
 |         if os.path.exists(cmd): | 
 |             return cmd | 
 |  | 
 |     # Otherwise give up and return the first "argument". | 
 |     return args[0] | 
 |  | 
 | class Struct: | 
 |     def __init__(self, **kwargs): | 
 |         self.fields = kwargs.keys() | 
 |         self.__dict__.update(kwargs) | 
 |  | 
 |     def __repr__(self): | 
 |         return 'Struct(%s)' % ', '.join(['%s=%r' % (k,getattr(self,k)) | 
 |                                          for k in self.fields]) | 
 |  | 
 | kExpectedPSFields = [('PID', int, 'pid'), | 
 |                      ('USER', str, 'user'), | 
 |                      ('COMMAND', str, 'command'), | 
 |                      ('%CPU', float, 'cpu_percent'), | 
 |                      ('TIME', parse_time, 'cpu_time'), | 
 |                      ('VSZ', int, 'vmem_size'), | 
 |                      ('RSS', int, 'rss')] | 
 | def getProcessTable(): | 
 |     import subprocess | 
 |     p = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE, | 
 |                          stderr=subprocess.PIPE) | 
 |     out,err = p.communicate() | 
 |     res = p.wait() | 
 |     if p.wait(): | 
 |         error('unable to get process table') | 
 |     elif err.strip(): | 
 |         error('unable to get process table: %s' % err) | 
 |  | 
 |     lns = out.split('\n') | 
 |     it = iter(lns) | 
 |     header = it.next().split() | 
 |     numRows = len(header) | 
 |  | 
 |     # Make sure we have the expected fields. | 
 |     indexes = [] | 
 |     for field in kExpectedPSFields: | 
 |         try: | 
 |             indexes.append(header.index(field[0])) | 
 |         except: | 
 |             if opts.debug: | 
 |                 raise | 
 |             error('unable to get process table, no %r field.' % field[0]) | 
 |  | 
 |     table = [] | 
 |     for i,ln in enumerate(it): | 
 |         if not ln.strip(): | 
 |             continue | 
 |  | 
 |         fields = ln.split(None, numRows - 1) | 
 |         if len(fields) != numRows: | 
 |             warning('unable to process row: %r' % ln) | 
 |             continue | 
 |  | 
 |         record = {} | 
 |         for field,idx in zip(kExpectedPSFields, indexes): | 
 |             value = fields[idx] | 
 |             try: | 
 |                 record[field[2]] = field[1](value) | 
 |             except: | 
 |                 if opts.debug: | 
 |                     raise | 
 |                 warning('unable to process %r in row: %r' % (field[0], ln)) | 
 |                 break | 
 |         else: | 
 |             # Add our best guess at the executable. | 
 |             record['executable'] = extractExecutable(record['command']) | 
 |             table.append(Struct(**record)) | 
 |  | 
 |     return table | 
 |  | 
 | def getSignalValue(name): | 
 |     import signal | 
 |     if name.startswith('SIG'): | 
 |         value = getattr(signal, name) | 
 |         if value and isinstance(value, int): | 
 |             return value | 
 |     error('unknown signal: %r' % name) | 
 |  | 
 | import signal | 
 | kSignals = {} | 
 | for name in dir(signal): | 
 |     if name.startswith('SIG') and name == name.upper() and name.isalpha(): | 
 |         kSignals[name[3:]] = getattr(signal, name) | 
 |  | 
 | def main(): | 
 |     global opts | 
 |     from optparse import OptionParser, OptionGroup | 
 |     parser = OptionParser("usage: %prog [options] {pid}*") | 
 |  | 
 |     # FIXME: Add -NNN and -SIGNAME options. | 
 |  | 
 |     parser.add_option("-s", "", dest="signalName", | 
 |                       help="Name of the signal to use (default=%default)", | 
 |                       action="store", default='INT', | 
 |                       choices=kSignals.keys()) | 
 |     parser.add_option("-l", "", dest="listSignals", | 
 |                       help="List known signal names", | 
 |                       action="store_true", default=False) | 
 |  | 
 |     parser.add_option("-n", "--dry-run", dest="dryRun", | 
 |                       help="Only print the actions that would be taken", | 
 |                       action="store_true", default=False) | 
 |     parser.add_option("-v", "--verbose", dest="verbose", | 
 |                       help="Print more verbose output", | 
 |                       action="store_true", default=False) | 
 |     parser.add_option("", "--debug", dest="debug", | 
 |                       help="Enable debugging output", | 
 |                       action="store_true", default=False) | 
 |     parser.add_option("", "--force", dest="force", | 
 |                       help="Perform the specified commands, even if it seems like a bad idea", | 
 |                       action="store_true", default=False) | 
 |  | 
 |     inf = float('inf') | 
 |     group = OptionGroup(parser, "Process Filters") | 
 |     group.add_option("", "--name", dest="execName", metavar="REGEX", | 
 |                       help="Kill processes whose name matches the given regexp", | 
 |                       action="store", default=None) | 
 |     group.add_option("", "--exec", dest="execPath", metavar="REGEX", | 
 |                       help="Kill processes whose executable matches the given regexp", | 
 |                       action="store", default=None) | 
 |     group.add_option("", "--user", dest="userName", metavar="REGEX", | 
 |                       help="Kill processes whose user matches the given regexp", | 
 |                       action="store", default=None) | 
 |     group.add_option("", "--min-cpu", dest="minCPU", metavar="PCT", | 
 |                       help="Kill processes with CPU usage >= PCT", | 
 |                       action="store", type=float, default=None) | 
 |     group.add_option("", "--max-cpu", dest="maxCPU", metavar="PCT", | 
 |                       help="Kill processes with CPU usage <= PCT", | 
 |                       action="store", type=float, default=inf) | 
 |     group.add_option("", "--min-mem", dest="minMem", metavar="N", | 
 |                       help="Kill processes with virtual size >= N (MB)", | 
 |                       action="store", type=float, default=None) | 
 |     group.add_option("", "--max-mem", dest="maxMem", metavar="N", | 
 |                       help="Kill processes with virtual size <= N (MB)", | 
 |                       action="store", type=float, default=inf) | 
 |     group.add_option("", "--min-rss", dest="minRSS", metavar="N", | 
 |                       help="Kill processes with RSS >= N", | 
 |                       action="store", type=float, default=None) | 
 |     group.add_option("", "--max-rss", dest="maxRSS", metavar="N", | 
 |                       help="Kill processes with RSS <= N", | 
 |                       action="store", type=float, default=inf) | 
 |     group.add_option("", "--min-time", dest="minTime", metavar="N", | 
 |                       help="Kill processes with CPU time >= N (seconds)", | 
 |                       action="store", type=float, default=None) | 
 |     group.add_option("", "--max-time", dest="maxTime", metavar="N", | 
 |                       help="Kill processes with CPU time <= N (seconds)", | 
 |                       action="store", type=float, default=inf) | 
 |     parser.add_option_group(group) | 
 |  | 
 |     (opts, args) = parser.parse_args() | 
 |  | 
 |     if opts.listSignals: | 
 |         items = [(v,k) for k,v in kSignals.items()] | 
 |         items.sort() | 
 |         for i in range(0, len(items), 4): | 
 |             print '\t'.join(['%2d) SIG%s' % (k,v) | 
 |                              for k,v in items[i:i+4]]) | 
 |         sys.exit(0) | 
 |  | 
 |     # Figure out the signal to use. | 
 |     signal = kSignals[opts.signalName] | 
 |     signalValueName = str(signal) | 
 |     if opts.verbose: | 
 |         name = dict((v,k) for k,v in kSignals.items()).get(signal,None) | 
 |         if name: | 
 |             signalValueName = name | 
 |             note('using signal %d (SIG%s)' % (signal, name)) | 
 |         else: | 
 |             note('using signal %d' % signal) | 
 |  | 
 |     # Get the pid list to consider. | 
 |     pids = set() | 
 |     for arg in args: | 
 |         try: | 
 |             pids.add(int(arg)) | 
 |         except: | 
 |             parser.error('invalid positional argument: %r' % arg) | 
 |  | 
 |     filtered = ps = getProcessTable() | 
 |  | 
 |     # Apply filters. | 
 |     if pids: | 
 |         filtered = [p for p in filtered | 
 |                     if p.pid in pids] | 
 |     if opts.execName is not None: | 
 |         filtered = [p for p in filtered | 
 |                     if re_full_match(opts.execName, | 
 |                                      os.path.basename(p.executable))] | 
 |     if opts.execPath is not None: | 
 |         filtered = [p for p in filtered | 
 |                     if re_full_match(opts.execPath, p.executable)] | 
 |     if opts.userName is not None: | 
 |         filtered = [p for p in filtered | 
 |                     if re_full_match(opts.userName, p.user)] | 
 |     filtered = [p for p in filtered | 
 |                 if opts.minCPU <= p.cpu_percent <= opts.maxCPU] | 
 |     filtered = [p for p in filtered | 
 |                 if opts.minMem <= float(p.vmem_size) / (1<<20) <= opts.maxMem] | 
 |     filtered = [p for p in filtered | 
 |                 if opts.minRSS <= p.rss <= opts.maxRSS] | 
 |     filtered = [p for p in filtered | 
 |                 if opts.minTime <= p.cpu_time <= opts.maxTime] | 
 |  | 
 |     if len(filtered) == len(ps): | 
 |         if not opts.force and not opts.dryRun: | 
 |             error('refusing to kill all processes without --force') | 
 |  | 
 |     if not filtered: | 
 |         warning('no processes selected') | 
 |  | 
 |     for p in filtered: | 
 |         if opts.verbose: | 
 |             note('kill(%r, %s) # (user=%r, executable=%r, CPU=%2.2f%%, time=%r, vmem=%r, rss=%r)' % | 
 |                  (p.pid, signalValueName, p.user, p.executable, p.cpu_percent, p.cpu_time, p.vmem_size, p.rss)) | 
 |         if not opts.dryRun: | 
 |             try: | 
 |                 os.kill(p.pid, signal) | 
 |             except OSError: | 
 |                 if opts.debug: | 
 |                     raise | 
 |                 warning('unable to kill PID: %r' % p.pid) | 
 |  | 
 | if __name__ == '__main__': | 
 |     main() |