killableprocess.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. # killableprocess - subprocesses which can be reliably killed
  2. #
  3. # Parts of this module are copied from the subprocess.py file contained
  4. # in the Python distribution.
  5. #
  6. # Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se>
  7. #
  8. # Additions and modifications written by Benjamin Smedberg
  9. # <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
  10. # <http://www.mozilla.org/>
  11. #
  12. # More Modifications
  13. # Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com>
  14. # Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com>
  15. #
  16. # By obtaining, using, and/or copying this software and/or its
  17. # associated documentation, you agree that you have read, understood,
  18. # and will comply with the following terms and conditions:
  19. #
  20. # Permission to use, copy, modify, and distribute this software and
  21. # its associated documentation for any purpose and without fee is
  22. # hereby granted, provided that the above copyright notice appears in
  23. # all copies, and that both that copyright notice and this permission
  24. # notice appear in supporting documentation, and that the name of the
  25. # author not be used in advertising or publicity pertaining to
  26. # distribution of the software without specific, written prior
  27. # permission.
  28. #
  29. # THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
  30. # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
  31. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
  32. # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
  33. # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  34. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  35. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  36. """killableprocess - Subprocesses which can be reliably killed
  37. This module is a subclass of the builtin "subprocess" module. It allows
  38. processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method.
  39. It also adds a timeout argument to Wait() for a limited period of time before
  40. forcefully killing the process.
  41. Note: On Windows, this module requires Windows 2000 or higher (no support for
  42. Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with
  43. Python 2.5+ or available from http://python.net/crew/theller/ctypes/
  44. """
  45. import subprocess
  46. import sys
  47. import os
  48. import time
  49. import datetime
  50. import types
  51. import exceptions
  52. try:
  53. from subprocess import CalledProcessError
  54. except ImportError:
  55. # Python 2.4 doesn't implement CalledProcessError
  56. class CalledProcessError(Exception):
  57. """This exception is raised when a process run by check_call() returns
  58. a non-zero exit status. The exit status will be stored in the
  59. returncode attribute."""
  60. def __init__(self, returncode, cmd):
  61. self.returncode = returncode
  62. self.cmd = cmd
  63. def __str__(self):
  64. return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
  65. mswindows = (sys.platform == "win32")
  66. if mswindows:
  67. import winprocess
  68. else:
  69. import signal
  70. # This is normally defined in win32con, but we don't want
  71. # to incur the huge tree of dependencies (pywin32 and friends)
  72. # just to get one constant. So here's our hack
  73. STILL_ACTIVE = 259
  74. def call(*args, **kwargs):
  75. waitargs = {}
  76. if "timeout" in kwargs:
  77. waitargs["timeout"] = kwargs.pop("timeout")
  78. return Popen(*args, **kwargs).wait(**waitargs)
  79. def check_call(*args, **kwargs):
  80. """Call a program with an optional timeout. If the program has a non-zero
  81. exit status, raises a CalledProcessError."""
  82. retcode = call(*args, **kwargs)
  83. if retcode:
  84. cmd = kwargs.get("args")
  85. if cmd is None:
  86. cmd = args[0]
  87. raise CalledProcessError(retcode, cmd)
  88. if not mswindows:
  89. def DoNothing(*args):
  90. pass
  91. class Popen(subprocess.Popen):
  92. kill_called = False
  93. if mswindows:
  94. def _execute_child(self, *args_tuple):
  95. # workaround for bug 958609
  96. if sys.hexversion < 0x02070600: # prior to 2.7.6
  97. (args, executable, preexec_fn, close_fds,
  98. cwd, env, universal_newlines, startupinfo,
  99. creationflags, shell,
  100. p2cread, p2cwrite,
  101. c2pread, c2pwrite,
  102. errread, errwrite) = args_tuple
  103. to_close = set()
  104. else: # 2.7.6 and later
  105. (args, executable, preexec_fn, close_fds,
  106. cwd, env, universal_newlines, startupinfo,
  107. creationflags, shell, to_close,
  108. p2cread, p2cwrite,
  109. c2pread, c2pwrite,
  110. errread, errwrite) = args_tuple
  111. if not isinstance(args, types.StringTypes):
  112. args = subprocess.list2cmdline(args)
  113. # Always or in the create new process group
  114. creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP
  115. if startupinfo is None:
  116. startupinfo = winprocess.STARTUPINFO()
  117. if None not in (p2cread, c2pwrite, errwrite):
  118. startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES
  119. startupinfo.hStdInput = int(p2cread)
  120. startupinfo.hStdOutput = int(c2pwrite)
  121. startupinfo.hStdError = int(errwrite)
  122. if shell:
  123. startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW
  124. startupinfo.wShowWindow = winprocess.SW_HIDE
  125. comspec = os.environ.get("COMSPEC", "cmd.exe")
  126. args = comspec + " /c " + args
  127. # determine if we can create create a job
  128. canCreateJob = winprocess.CanCreateJobObject()
  129. # set process creation flags
  130. creationflags |= winprocess.CREATE_SUSPENDED
  131. creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT
  132. if canCreateJob:
  133. # Uncomment this line below to discover very useful things about your environment
  134. #print "++++ killableprocess: releng twistd patch not applied, we can create job objects"
  135. creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB
  136. # create the process
  137. hp, ht, pid, tid = winprocess.CreateProcess(
  138. executable, args,
  139. None, None, # No special security
  140. 1, # Must inherit handles!
  141. creationflags,
  142. winprocess.EnvironmentBlock(env),
  143. cwd, startupinfo)
  144. self._child_created = True
  145. self._handle = hp
  146. self._thread = ht
  147. self.pid = pid
  148. self.tid = tid
  149. if canCreateJob:
  150. # We create a new job for this process, so that we can kill
  151. # the process and any sub-processes
  152. self._job = winprocess.CreateJobObject()
  153. winprocess.AssignProcessToJobObject(self._job, int(hp))
  154. else:
  155. self._job = None
  156. winprocess.ResumeThread(int(ht))
  157. ht.Close()
  158. if p2cread is not None:
  159. p2cread.Close()
  160. if c2pwrite is not None:
  161. c2pwrite.Close()
  162. if errwrite is not None:
  163. errwrite.Close()
  164. time.sleep(.1)
  165. def kill(self, group=True):
  166. """Kill the process. If group=True, all sub-processes will also be killed."""
  167. self.kill_called = True
  168. if mswindows:
  169. if group and self._job:
  170. winprocess.TerminateJobObject(self._job, 127)
  171. else:
  172. winprocess.TerminateProcess(self._handle, 127)
  173. self.returncode = 127
  174. else:
  175. if group:
  176. try:
  177. os.killpg(self.pid, signal.SIGKILL)
  178. except: pass
  179. else:
  180. os.kill(self.pid, signal.SIGKILL)
  181. self.returncode = -9
  182. def wait(self, timeout=None, group=True):
  183. """Wait for the process to terminate. Returns returncode attribute.
  184. If timeout seconds are reached and the process has not terminated,
  185. it will be forcefully killed. If timeout is -1, wait will not
  186. time out."""
  187. if timeout is not None:
  188. # timeout is now in milliseconds
  189. timeout = timeout * 1000
  190. starttime = datetime.datetime.now()
  191. if mswindows:
  192. if timeout is None:
  193. timeout = -1
  194. rc = winprocess.WaitForSingleObject(self._handle, timeout)
  195. if (rc == winprocess.WAIT_OBJECT_0 or
  196. rc == winprocess.WAIT_ABANDONED or
  197. rc == winprocess.WAIT_FAILED):
  198. # Object has either signaled, or the API call has failed. In
  199. # both cases we want to give the OS the benefit of the doubt
  200. # and supply a little time before we start shooting processes
  201. # with an M-16.
  202. # Returns 1 if running, 0 if not, -1 if timed out
  203. def check():
  204. now = datetime.datetime.now()
  205. diff = now - starttime
  206. if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000):
  207. if self._job:
  208. if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0):
  209. # Job Object is still containing active processes
  210. return 1
  211. else:
  212. # No job, we use GetExitCodeProcess, which will tell us if the process is still active
  213. self.returncode = winprocess.GetExitCodeProcess(self._handle)
  214. if (self.returncode == STILL_ACTIVE):
  215. # Process still active, continue waiting
  216. return 1
  217. # Process not active, return 0
  218. return 0
  219. else:
  220. # Timed out, return -1
  221. return -1
  222. notdone = check()
  223. while notdone == 1:
  224. time.sleep(.5)
  225. notdone = check()
  226. if notdone == -1:
  227. # Then check timed out, we have a hung process, attempt
  228. # last ditch kill with explosives
  229. self.kill(group)
  230. else:
  231. # In this case waitforsingleobject timed out. We have to
  232. # take the process behind the woodshed and shoot it.
  233. self.kill(group)
  234. else:
  235. if sys.platform in ('linux2', 'sunos5', 'solaris') \
  236. or sys.platform.startswith('freebsd'):
  237. def group_wait(timeout):
  238. try:
  239. os.waitpid(self.pid, 0)
  240. except OSError, e:
  241. pass # If wait has already been called on this pid, bad things happen
  242. return self.returncode
  243. elif sys.platform == 'darwin':
  244. def group_wait(timeout):
  245. try:
  246. count = 0
  247. if timeout is None and self.kill_called:
  248. timeout = 10 # Have to set some kind of timeout or else this could go on forever
  249. if timeout is None:
  250. while 1:
  251. os.killpg(self.pid, signal.SIG_DFL)
  252. while ((count * 2) <= timeout):
  253. os.killpg(self.pid, signal.SIG_DFL)
  254. # count is increased by 500ms for every 0.5s of sleep
  255. time.sleep(.5); count += 500
  256. except exceptions.OSError:
  257. return self.returncode
  258. if timeout is None:
  259. if group is True:
  260. return group_wait(timeout)
  261. else:
  262. subprocess.Popen.wait(self)
  263. return self.returncode
  264. returncode = False
  265. now = datetime.datetime.now()
  266. diff = now - starttime
  267. while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ):
  268. if group is True:
  269. return group_wait(timeout)
  270. else:
  271. if subprocess.poll() is not None:
  272. returncode = self.returncode
  273. time.sleep(.5)
  274. now = datetime.datetime.now()
  275. diff = now - starttime
  276. return self.returncode
  277. return self.returncode
  278. # We get random maxint errors from subprocesses __del__
  279. __del__ = lambda self: None
  280. def setpgid_preexec_fn():
  281. os.setpgid(0, 0)
  282. def runCommand(cmd, **kwargs):
  283. if sys.platform != "win32":
  284. return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs)
  285. else:
  286. return Popen(cmd, **kwargs)