Bug 947974 - Add signal parameter to mozprocess.kill(), r=wlach
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Fri, 24 Jan 2014 16:26:57 -0500
changeset 181247 c48b1f578d7d675f18c926bced0cebcfc814d56c
parent 181246 5a65a4ef5cd5dfe54203cd9d93eb38e3a3e2897d
child 181248 c1c5c07399e13c12f59326facee72e50d9bf3ce9
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswlach
bugs947974
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 947974 - Add signal parameter to mozprocess.kill(), r=wlach
testing/mozbase/mozprocess/mozprocess/processhandler.py
testing/mozbase/mozprocess/tests/test_mozprocess_kill.py
testing/mozbase/mozprocess/tests/test_mozprocess_kill_broad_wait.py
testing/mozbase/mozrunner/mozrunner/runner.py
--- a/testing/mozbase/mozprocess/mozprocess/processhandler.py
+++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py
@@ -105,46 +105,44 @@ class ProcessHandlerMixin(object):
                         self._internal_poll(_deadstate=_maxint)
                     else:
                         self.poll(_deadstate=sys.maxint)
                 if self._handle or self._job or self._io_port:
                     self._cleanup()
             else:
                 subprocess.Popen.__del__(self)
 
-        def kill(self):
+        def kill(self, sig=None):
             self.returncode = 0
             if isWin:
                 if not self._ignore_children and self._handle and self._job:
                     winprocess.TerminateJobObject(self._job, winprocess.ERROR_CONTROL_C_EXIT)
                     self.returncode = winprocess.GetExitCodeProcess(self._handle)
                 elif self._handle:
                     err = None
                     try:
                         winprocess.TerminateProcess(self._handle, winprocess.ERROR_CONTROL_C_EXIT)
                     except:
                         err = "Could not terminate process"
                     self.returncode = winprocess.GetExitCodeProcess(self._handle)
                     self._cleanup()
                     if err is not None:
                         raise OSError(err)
-                else:
-                    pass
             else:
+                sig = sig or signal.SIGKILL
                 if not self._ignore_children:
                     try:
-                        os.killpg(self.pid, signal.SIGKILL)
+                        os.killpg(self.pid, sig)
                     except BaseException, e:
                         if getattr(e, "errno", None) != 3:
                             # Error 3 is "no such process", which is ok
                             print >> sys.stdout, "Could not kill process, could not find pid: %s, assuming it's already dead" % self.pid
                 else:
-                    os.kill(self.pid, signal.SIGKILL)
-                if self.returncode is None:
-                    self.returncode = subprocess.Popen._internal_poll(self)
+                    os.kill(self.pid, sig)
+                self.returncode = -sig
 
             self._cleanup()
             return self.returncode
 
         def wait(self):
             """ Popen.wait
                 Called to wait for a running process to shut down and return
                 its exit code
@@ -412,17 +410,17 @@ falling back to not using job objects fo
 
                 # Python 2.5 uses isAlive versus is_alive use the proper one
                 threadalive = False
                 if hasattr(self, "_procmgrthread"):
                     if hasattr(self._procmgrthread, 'is_alive'):
                         threadalive = self._procmgrthread.is_alive()
                     else:
                         threadalive = self._procmgrthread.isAlive()
-                if self._job and threadalive: 
+                if self._job and threadalive:
                     # Then we are managing with IO Completion Ports
                     # wait on a signal so we know when we have seen the last
                     # process come through.
                     # We use queues to synchronize between the thread and this
                     # function because events just didn't have robust enough error
                     # handling on pre-2.7 versions
                     err = None
                     try:
@@ -638,30 +636,33 @@ falling back to not using job objects fo
         # build process arguments
         args.update(self.keywordargs)
 
         # launch the process
         self.proc = self.Process([self.cmd] + self.args, **args)
 
         self.processOutput(timeout=timeout, outputTimeout=outputTimeout)
 
-    def kill(self):
+    def kill(self, sig=None):
         """
         Kills the managed process.
 
         If you created the process with 'ignore_children=False' (the
         default) then it will also also kill all child processes spawned by
         it. If you specified 'ignore_children=True' when creating the
         process, only the root process will be killed.
 
         Note that this does not manage any state, save any output etc,
         it immediately kills the process.
+
+        :param sig: Signal used to kill the process, defaults to SIGKILL
+                    (has no effect on Windows)
         """
         try:
-            return self.proc.kill()
+            return self.proc.kill(sig=sig)
         except AttributeError:
             # Try to print a relevant error message.
             if not self.proc:
                 print >> sys.stderr, "Unable to kill Process because call to ProcessHandler constructor failed."
             else:
                 raise
 
     def readWithTimeout(self, f, timeout):
--- a/testing/mozbase/mozprocess/tests/test_mozprocess_kill.py
+++ b/testing/mozbase/mozprocess/tests/test_mozprocess_kill.py
@@ -18,57 +18,61 @@ class ProcTestKill(proctest.ProcTest):
                                           cwd=here)
         p.run()
         p.kill()
 
         detected, output = proctest.check_for_process(self.proclaunch)
         self.determine_status(detected,
                               output,
                               p.proc.returncode,
-                              p.didTimeout)
+                              p.didTimeout,
+                              expectedfail=('returncode',))
 
     def test_process_kill_deep(self):
         """Process is started, we kill it, we use a deep process tree"""
 
         p = processhandler.ProcessHandler([self.python, self.proclaunch, "process_normal_deep_python.ini"],
                                           cwd=here)
         p.run()
         p.kill()
 
         detected, output = proctest.check_for_process(self.proclaunch)
         self.determine_status(detected,
                               output,
                               p.proc.returncode,
-                              p.didTimeout)
+                              p.didTimeout,
+                              expectedfail=('returncode',))
 
     def test_process_kill_deep_wait(self):
         """Process is started, we use a deep process tree, we let it spawn
            for a bit, we kill it"""
 
         p = processhandler.ProcessHandler([self.python, self.proclaunch, "process_normal_deep_python.ini"],
                                           cwd=here)
         p.run()
         # Let the tree spawn a bit, before attempting to kill
         time.sleep(3)
         p.kill()
 
         detected, output = proctest.check_for_process(self.proclaunch)
         self.determine_status(detected,
                               output,
                               p.proc.returncode,
-                              p.didTimeout)
+                              p.didTimeout,
+                              expectedfail=('returncode',))
 
     def test_process_kill_broad(self):
         """Process is started, we kill it, we use a broad process tree"""
 
         p = processhandler.ProcessHandler([self.python, self.proclaunch, "process_normal_broad_python.ini"],
                                           cwd=here)
         p.run()
         p.kill()
 
         detected, output = proctest.check_for_process(self.proclaunch)
         self.determine_status(detected,
                               output,
                               p.proc.returncode,
-                              p.didTimeout)
+                              p.didTimeout,
+                              expectedfail=('returncode',))
 
 if __name__ == '__main__':
     unittest.main()
--- a/testing/mozbase/mozprocess/tests/test_mozprocess_kill_broad_wait.py
+++ b/testing/mozbase/mozprocess/tests/test_mozprocess_kill_broad_wait.py
@@ -24,12 +24,13 @@ class ProcTestKill(proctest.ProcTest):
         # Let the tree spawn a bit, before attempting to kill
         time.sleep(3)
         p.kill()
 
         detected, output = proctest.check_for_process(self.proclaunch)
         self.determine_status(detected,
                               output,
                               p.proc.returncode,
-                              p.didTimeout)
+                              p.didTimeout,
+                              expectedfail=('returncode',))
 
 if __name__ == '__main__':
     unittest.main()
--- a/testing/mozbase/mozrunner/mozrunner/runner.py
+++ b/testing/mozbase/mozrunner/mozrunner/runner.py
@@ -65,49 +65,60 @@ class Runner(object):
             # this run uses the managed processhandler
             self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
             self.process_handler.run(timeout, outputTimeout)
 
     def wait(self, timeout=None):
         """
         Wait for the process to exit.
         Returns the process return code if the process exited,
-        returns None otherwise.
+        returns -<signal> if the process was killed (Unix only)
+        returns None if the process is still running.
 
-        If timeout is not None, will return after timeout seconds.
-        Use is_running() to determine whether or not a timeout occured.
-        Timeout is ignored if interactive was set to True.
+        :param timeout: if not None, will return after timeout seconds.
+                        Use is_running() to determine whether or not a
+                        timeout occured. Timeout is ignored if
+                        interactive was set to True.
         """
         if self.process_handler is not None:
             if isinstance(self.process_handler, subprocess.Popen):
                 self.returncode = self.process_handler.wait()
             else:
                 self.process_handler.wait(timeout)
+
+                if not self.process_handler:
+                    # the process was killed by another thread
+                    return self.returncode
+
+                # the process terminated, retrieve the return code
                 self.returncode = self.process_handler.proc.poll()
                 if self.returncode is not None:
                     self.process_handler = None
         elif self.returncode is None:
             raise RunnerNotStartedError("Wait called before runner started")
 
         return self.returncode
 
     def is_running(self):
         """
         Returns True if the process is still running, False otherwise
         """
         return self.process_handler is not None
 
 
-    def stop(self):
+    def stop(self, sig=None):
         """
         Kill the process
+
+        :param sig: Signal used to kill the process, defaults to SIGKILL
+                    (has no effect on Windows).
         """
         if self.process_handler is None:
             return
-        self.process_handler.kill()
+        self.returncode = self.process_handler.kill(sig=sig)
         self.process_handler = None
 
     def reset(self):
         """
         Reset the runner to its default state
         """
         if getattr(self, 'profile', False):
             self.profile.reset()