zkill 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #!/usr/bin/env python
  2. import os
  3. import re
  4. import sys
  5. def _write_message(kind, message):
  6. import inspect, os, sys
  7. # Get the file/line where this message was generated.
  8. f = inspect.currentframe()
  9. # Step out of _write_message, and then out of wrapper.
  10. f = f.f_back.f_back
  11. file,line,_,_,_ = inspect.getframeinfo(f)
  12. location = '%s:%d' % (os.path.basename(file), line)
  13. print >>sys.stderr, '%s: %s: %s' % (location, kind, message)
  14. note = lambda message: _write_message('note', message)
  15. warning = lambda message: _write_message('warning', message)
  16. error = lambda message: (_write_message('error', message), sys.exit(1))
  17. def re_full_match(pattern, str):
  18. m = re.match(pattern, str)
  19. if m and m.end() != len(str):
  20. m = None
  21. return m
  22. def parse_time(value):
  23. minutes,value = value.split(':',1)
  24. if '.' in value:
  25. seconds,fseconds = value.split('.',1)
  26. else:
  27. seconds = value
  28. return int(minutes) * 60 + int(seconds) + float('.'+fseconds)
  29. def extractExecutable(command):
  30. """extractExecutable - Given a string representing a command line, attempt
  31. to extract the executable path, even if it includes spaces."""
  32. # Split into potential arguments.
  33. args = command.split(' ')
  34. # Scanning from the beginning, try to see if the first N args, when joined,
  35. # exist. If so that's probably the executable.
  36. for i in range(1,len(args)):
  37. cmd = ' '.join(args[:i])
  38. if os.path.exists(cmd):
  39. return cmd
  40. # Otherwise give up and return the first "argument".
  41. return args[0]
  42. class Struct:
  43. def __init__(self, **kwargs):
  44. self.fields = kwargs.keys()
  45. self.__dict__.update(kwargs)
  46. def __repr__(self):
  47. return 'Struct(%s)' % ', '.join(['%s=%r' % (k,getattr(self,k))
  48. for k in self.fields])
  49. kExpectedPSFields = [('PID', int, 'pid'),
  50. ('USER', str, 'user'),
  51. ('COMMAND', str, 'command'),
  52. ('%CPU', float, 'cpu_percent'),
  53. ('TIME', parse_time, 'cpu_time'),
  54. ('VSZ', int, 'vmem_size'),
  55. ('RSS', int, 'rss')]
  56. def getProcessTable():
  57. import subprocess
  58. p = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE,
  59. stderr=subprocess.PIPE)
  60. out,err = p.communicate()
  61. res = p.wait()
  62. if p.wait():
  63. error('unable to get process table')
  64. elif err.strip():
  65. error('unable to get process table: %s' % err)
  66. lns = out.split('\n')
  67. it = iter(lns)
  68. header = it.next().split()
  69. numRows = len(header)
  70. # Make sure we have the expected fields.
  71. indexes = []
  72. for field in kExpectedPSFields:
  73. try:
  74. indexes.append(header.index(field[0]))
  75. except:
  76. if opts.debug:
  77. raise
  78. error('unable to get process table, no %r field.' % field[0])
  79. table = []
  80. for i,ln in enumerate(it):
  81. if not ln.strip():
  82. continue
  83. fields = ln.split(None, numRows - 1)
  84. if len(fields) != numRows:
  85. warning('unable to process row: %r' % ln)
  86. continue
  87. record = {}
  88. for field,idx in zip(kExpectedPSFields, indexes):
  89. value = fields[idx]
  90. try:
  91. record[field[2]] = field[1](value)
  92. except:
  93. if opts.debug:
  94. raise
  95. warning('unable to process %r in row: %r' % (field[0], ln))
  96. break
  97. else:
  98. # Add our best guess at the executable.
  99. record['executable'] = extractExecutable(record['command'])
  100. table.append(Struct(**record))
  101. return table
  102. def getSignalValue(name):
  103. import signal
  104. if name.startswith('SIG'):
  105. value = getattr(signal, name)
  106. if value and isinstance(value, int):
  107. return value
  108. error('unknown signal: %r' % name)
  109. import signal
  110. kSignals = {}
  111. for name in dir(signal):
  112. if name.startswith('SIG') and name == name.upper() and name.isalpha():
  113. kSignals[name[3:]] = getattr(signal, name)
  114. def main():
  115. global opts
  116. from optparse import OptionParser, OptionGroup
  117. parser = OptionParser("usage: %prog [options] {pid}*")
  118. # FIXME: Add -NNN and -SIGNAME options.
  119. parser.add_option("-s", "", dest="signalName",
  120. help="Name of the signal to use (default=%default)",
  121. action="store", default='INT',
  122. choices=kSignals.keys())
  123. parser.add_option("-l", "", dest="listSignals",
  124. help="List known signal names",
  125. action="store_true", default=False)
  126. parser.add_option("-n", "--dry-run", dest="dryRun",
  127. help="Only print the actions that would be taken",
  128. action="store_true", default=False)
  129. parser.add_option("-v", "--verbose", dest="verbose",
  130. help="Print more verbose output",
  131. action="store_true", default=False)
  132. parser.add_option("", "--debug", dest="debug",
  133. help="Enable debugging output",
  134. action="store_true", default=False)
  135. parser.add_option("", "--force", dest="force",
  136. help="Perform the specified commands, even if it seems like a bad idea",
  137. action="store_true", default=False)
  138. inf = float('inf')
  139. group = OptionGroup(parser, "Process Filters")
  140. group.add_option("", "--name", dest="execName", metavar="REGEX",
  141. help="Kill processes whose name matches the given regexp",
  142. action="store", default=None)
  143. group.add_option("", "--exec", dest="execPath", metavar="REGEX",
  144. help="Kill processes whose executable matches the given regexp",
  145. action="store", default=None)
  146. group.add_option("", "--user", dest="userName", metavar="REGEX",
  147. help="Kill processes whose user matches the given regexp",
  148. action="store", default=None)
  149. group.add_option("", "--min-cpu", dest="minCPU", metavar="PCT",
  150. help="Kill processes with CPU usage >= PCT",
  151. action="store", type=float, default=None)
  152. group.add_option("", "--max-cpu", dest="maxCPU", metavar="PCT",
  153. help="Kill processes with CPU usage <= PCT",
  154. action="store", type=float, default=inf)
  155. group.add_option("", "--min-mem", dest="minMem", metavar="N",
  156. help="Kill processes with virtual size >= N (MB)",
  157. action="store", type=float, default=None)
  158. group.add_option("", "--max-mem", dest="maxMem", metavar="N",
  159. help="Kill processes with virtual size <= N (MB)",
  160. action="store", type=float, default=inf)
  161. group.add_option("", "--min-rss", dest="minRSS", metavar="N",
  162. help="Kill processes with RSS >= N",
  163. action="store", type=float, default=None)
  164. group.add_option("", "--max-rss", dest="maxRSS", metavar="N",
  165. help="Kill processes with RSS <= N",
  166. action="store", type=float, default=inf)
  167. group.add_option("", "--min-time", dest="minTime", metavar="N",
  168. help="Kill processes with CPU time >= N (seconds)",
  169. action="store", type=float, default=None)
  170. group.add_option("", "--max-time", dest="maxTime", metavar="N",
  171. help="Kill processes with CPU time <= N (seconds)",
  172. action="store", type=float, default=inf)
  173. parser.add_option_group(group)
  174. (opts, args) = parser.parse_args()
  175. if opts.listSignals:
  176. items = [(v,k) for k,v in kSignals.items()]
  177. items.sort()
  178. for i in range(0, len(items), 4):
  179. print '\t'.join(['%2d) SIG%s' % (k,v)
  180. for k,v in items[i:i+4]])
  181. sys.exit(0)
  182. # Figure out the signal to use.
  183. signal = kSignals[opts.signalName]
  184. signalValueName = str(signal)
  185. if opts.verbose:
  186. name = dict((v,k) for k,v in kSignals.items()).get(signal,None)
  187. if name:
  188. signalValueName = name
  189. note('using signal %d (SIG%s)' % (signal, name))
  190. else:
  191. note('using signal %d' % signal)
  192. # Get the pid list to consider.
  193. pids = set()
  194. for arg in args:
  195. try:
  196. pids.add(int(arg))
  197. except:
  198. parser.error('invalid positional argument: %r' % arg)
  199. filtered = ps = getProcessTable()
  200. # Apply filters.
  201. if pids:
  202. filtered = [p for p in filtered
  203. if p.pid in pids]
  204. if opts.execName is not None:
  205. filtered = [p for p in filtered
  206. if re_full_match(opts.execName,
  207. os.path.basename(p.executable))]
  208. if opts.execPath is not None:
  209. filtered = [p for p in filtered
  210. if re_full_match(opts.execPath, p.executable)]
  211. if opts.userName is not None:
  212. filtered = [p for p in filtered
  213. if re_full_match(opts.userName, p.user)]
  214. filtered = [p for p in filtered
  215. if opts.minCPU <= p.cpu_percent <= opts.maxCPU]
  216. filtered = [p for p in filtered
  217. if opts.minMem <= float(p.vmem_size) / (1<<20) <= opts.maxMem]
  218. filtered = [p for p in filtered
  219. if opts.minRSS <= p.rss <= opts.maxRSS]
  220. filtered = [p for p in filtered
  221. if opts.minTime <= p.cpu_time <= opts.maxTime]
  222. if len(filtered) == len(ps):
  223. if not opts.force and not opts.dryRun:
  224. error('refusing to kill all processes without --force')
  225. if not filtered:
  226. warning('no processes selected')
  227. for p in filtered:
  228. if opts.verbose:
  229. note('kill(%r, %s) # (user=%r, executable=%r, CPU=%2.2f%%, time=%r, vmem=%r, rss=%r)' %
  230. (p.pid, signalValueName, p.user, p.executable, p.cpu_percent, p.cpu_time, p.vmem_size, p.rss))
  231. if not opts.dryRun:
  232. try:
  233. os.kill(p.pid, signal)
  234. except OSError:
  235. if opts.debug:
  236. raise
  237. warning('unable to kill PID: %r' % p.pid)
  238. if __name__ == '__main__':
  239. main()