| #!/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('%s: %s: %s' % (location, kind, message), file=sys.stderr) | |
| 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 = list(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=list(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 list(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 list(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() |