Rev 4579: Update the breakin support to support CTRL-BREAK on Windows. in http://bazaar.launchpad.net/~jameinel/bzr/1.18-sigbreak

John Arbash Meinel john at arbash-meinel.com
Fri Jul 31 00:54:43 BST 2009


At http://bazaar.launchpad.net/~jameinel/bzr/1.18-sigbreak

------------------------------------------------------------
revno: 4579
revision-id: john at arbash-meinel.com-20090730235426-o8h73swbh7seqaf7
parent: pqm at pqm.ubuntu.com-20090730142406-wg8gmxpcjz4c1z00
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: 1.18-sigbreak
timestamp: Thu 2009-07-30 18:54:26 -0500
message:
  Update the breakin support to support CTRL-BREAK on Windows.
  
  The signal handling code is very similar, but the testing code got a bit clumsy.
-------------- next part --------------
=== modified file 'bzr'
--- a/bzr	2009-07-13 02:20:36 +0000
+++ b/bzr	2009-07-30 23:54:26 +0000
@@ -122,7 +122,7 @@
 bzrlib.inspect_for_copy.import_copy_with_hacked_inspect()
 
 import bzrlib.breakin
-bzrlib.breakin.hook_sigquit()
+bzrlib.breakin.hook_debugger_to_signal()
 
 import bzrlib.decorators
 if ('--lsprof' in sys.argv

=== modified file 'bzrlib/breakin.py'
--- a/bzrlib/breakin.py	2009-03-23 14:59:43 +0000
+++ b/bzrlib/breakin.py	2009-07-30 23:54:26 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006, 2007 Canonical Ltd
+# Copyright (C) 2006, 2007, 2009 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -17,25 +17,77 @@
 import os
 import signal
 
+
+_breakin_signal_number = None
+_breakin_signal_name = None
+
+
 def _debug(signal_number, interrupted_frame):
     import pdb
     import sys
-    sys.stderr.write("** SIGQUIT received, entering debugger\n"
+    sys.stderr.write("** %s received, entering debugger\n"
             "** Type 'c' to continue or 'q' to stop the process\n"
-            "** Or SIGQUIT again to quit (and possibly dump core)\n"
-            )
+            "** Or %s again to quit (and possibly dump core)\n"
+            % (_breakin_signal_name, _breakin_signal_name))
+    # It seems that on Windows, when sys.stderr is to a PIPE, then we need to
+    # flush. Not sure why it is buffered, but that seems to be the case.
+    sys.stderr.flush()
     # restore default meaning so that you can kill the process by hitting it
     # twice
-    signal.signal(signal.SIGQUIT, signal.SIG_DFL)
+    signal.signal(_breakin_signal_number, signal.SIG_DFL)
     try:
         pdb.set_trace()
     finally:
-        signal.signal(signal.SIGQUIT, _debug)
+        signal.signal(_breakin_signal_number, _debug)
 
 
 def hook_sigquit():
-    # when sigquit (C-\) is received go into pdb
-    if (os.environ.get('BZR_SIGQUIT_PDB', '1') == '0'
-        or getattr(signal, 'SIGQUIT', None) is None):
-        return
-    signal.signal(signal.SIGQUIT, _debug)
+    # We import this late because breakin.py is loaded as part of the main
+    # 'bzr' script, so we want it to load as little as possible until things
+    # are up and running
+    from bzrlib import symbol_versioning, trace
+    trace.mutter_callsite(2, 'Deprecated function called')
+    symbol_versioning.warn(symbol_versioning.deprecation_string(
+        hook_sigquit, symbol_versioning.deprecated_in((1, 18, 0))),
+        DeprecationWarning, stacklevel=2)
+
+    return hook_debugger_to_signal()
+
+
+def determine_signal():
+    global _breakin_signal_number
+    global _breakin_signal_name
+    if _breakin_signal_number is not None:
+        return _breakin_signal_number
+    # Note: As near as I can tell, Windows is the only one to define SIGBREAK,
+    #       and other platforms defined SIGQUIT. There doesn't seem to be a
+    #       platform that defines both.
+    #       -- jam 2009-07-30
+    sigquit = getattr(signal, 'SIGQUIT', None)
+    sigbreak = getattr(signal, 'SIGBREAK', None)
+    if sigquit is not None:
+        _breakin_signal_number = sigquit
+        _breakin_signal_name = 'SIGQUIT'
+    elif sigbreak is not None:
+        _breakin_signal_number = sigbreak
+        _breakin_signal_name = 'SIGBREAK'
+
+    return _breakin_signal_number
+
+
+def hook_debugger_to_signal():
+    """Add a signal handler so we drop into the debugger.
+
+    On Linux and Mac, this is hooked into SIGQUIT (C-\\) on Windows, this is
+    hooked into SIGBREAK (C-Pause).
+    """
+
+    # when sigquit (C-\) or sigbreak (C-Pause) is received go into pdb
+    if os.environ.get('BZR_SIGQUIT_PDB', '1') == '0':
+        # User explicitly requested we don't support this
+        return
+    sig = determine_signal()
+    if sig is None:
+        return
+    # print 'hooking into %s' % (_breakin_signal_name,)
+    signal.signal(sig, _debug)

=== modified file 'bzrlib/tests/blackbox/test_breakin.py'
--- a/bzrlib/tests/blackbox/test_breakin.py	2009-03-23 14:59:43 +0000
+++ b/bzrlib/tests/blackbox/test_breakin.py	2009-07-30 23:54:26 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006, 2007 Canonical Ltd
+# Copyright (C) 2006, 2007, 2009 Canonical Ltd
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -16,6 +16,11 @@
 
 """Blackbox tests for debugger breakin"""
 
+try:
+    import ctypes
+    have_ctypes = True
+except ImportError:
+    have_ctypes = False
 import errno
 import os
 import signal
@@ -24,6 +29,7 @@
 import time
 
 from bzrlib import (
+    breakin,
     errors,
     tests,
     )
@@ -34,9 +40,65 @@
     # wait() waiting for the child to exit when it's not going to.
 
     def setUp(self):
-        if sys.platform == 'win32':
-            raise tests.TestSkipped('breakin signal not tested on win32')
         super(TestBreakin, self).setUp()
+        if breakin.determine_signal() is None:
+            raise tests.TestSkipped('this platform is missing SIGQUIT'
+                                    ' or SIGBREAK')
+        if sys.platform == 'win32':
+            # Windows doesn't have os.kill, and we catch the SIGBREAK signal.
+            # We trigger SIGBREAK via a Console api so we need ctypes to access
+            # the function
+            if not have_ctypes:
+                raise tests.UnavailableFeature('ctypes')
+            self._send_signal = self._send_signal_win32
+        else:
+            self._send_signal = self._send_signal_via_kill
+
+    def _send_signal_via_kill(self, pid, sig_type):
+        if sig == 'break':
+            sig_num = signal.SIGQUIT
+        elif sig == 'kill':
+            sig_num = signal.SIGKILL
+        else:
+            raise ValueError("unknown signal type: %s" % (sig_type,))
+        os.kill(pid, sig_num)
+
+    def _send_signal_win32(self, pid, sig_type):
+        """Send a 'signal' on Windows.
+
+        Windows doesn't really have signals in the same way. All it really
+        supports is:
+            1) Sending SIGINT to the *current* process group (so self, and all
+                children of self)
+            2) Sending SIGBREAK to a process that shares the current console,
+                which can be in its own process group.
+        So we have start_bzr_subprocess to create a new process group for the
+        spawned process, and then we map
+            SIGQUIT to GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT)
+            SIGKILL to TerminateProcess
+        """
+        if sig_type == 'break':
+            CTRL_BREAK_EVENT = 1
+            # CTRL_C_EVENT = 0
+            ret = ctypes.windll.kernel32.GenerateConsoleCtrlEvent(
+                    CTRL_BREAK_EVENT, pid)
+            if ret == 0: #error
+                err = ctypes.FormatError()
+                raise RuntimeError('failed to send CTRL_BREAK: %s'
+                                   % (err,))
+        elif sig_type == 'kill':
+            # Does the exit code matter? For now we are just setting it to
+            # something other than 0
+            exit_code = breakin.determine_signal()
+            ctypes.windll.kernel32.TerminateProcess(pid, exit_code)
+
+    def _popen(self, *args, **kwargs):
+        if sys.platform == 'win32':
+            CREATE_NEW_PROCESS_GROUP = 512
+            # This allows us to send a signal to the child, *without* also
+            # sending it to ourselves
+            kwargs['creationflags'] = CREATE_NEW_PROCESS_GROUP
+        return super(TestBreakin, self)._popen(*args, **kwargs)
 
     def _dont_SIGQUIT_on_darwin(self):
         if sys.platform == 'darwin':
@@ -54,13 +116,19 @@
         for i in range(100):
             time.sleep(0.1)
             if sig is not None:
-                os.kill(pid, sig)
+                self._send_signal(pid, sig)
             # Use WNOHANG to ensure we don't get blocked, doing so, we may
             # leave the process continue after *we* die...
+            # Win32 doesn't support WNOHANG, so we just pass 0
+            opts = getattr(os, 'WNOHANG', 0)
             try:
                 # note: waitpid is different on win32, but this test only runs
                 # on unix
-                pid_killed, returncode = os.waitpid(pid, os.WNOHANG)
+                # TODO: waitpid doesn't work well on windows, we might consider
+                #       using WaitForSingleObject(proc._handle, TIMEOUT)
+                #       instead. Most notably, the WNOHANG isn't allowed, so
+                #       this can hang indefinitely.
+                pid_killed, returncode = os.waitpid(pid, opts)
                 if (pid_killed, returncode) != (0, 0):
                     if sig is not None:
                         # high bit in low byte says if core was dumped; we
@@ -88,8 +156,11 @@
         # wait for it to get started, and print the 'listening' line
         proc.stderr.readline()
         # first sigquit pops into debugger
-        os.kill(proc.pid, signal.SIGQUIT)
+        self._send_signal(proc.pid, 'break')
         # Wait for the debugger to acknowledge the signal reception
+        # Note that it is possible for this to deadlock if the child doesn't
+        # acknowlege the signal and write to stderr. Perhaps we should try
+        # os.read(proc.stderr.fileno())?
         err = proc.stderr.readline()
         self.assertContainsRe(err, r'entering debugger')
         # Now that the debugger is entered, we can ask him to quit
@@ -99,7 +170,7 @@
         dead, sig = self._wait_for_process(proc.pid)
         if not dead:
             # The process didn't finish, let's kill it before reporting failure
-            dead, sig = self._wait_for_process(proc.pid, signal.SIGKILL)
+            dead, sig = self._wait_for_process(proc.pid, 'kill')
             if dead:
                 raise tests.KnownFailure(
                     "subprocess wasn't terminated, it had to be killed")
@@ -115,15 +186,17 @@
         # wait for it to get started, and print the 'listening' line
         proc.stderr.readline()
         # break into the debugger
-        os.kill(proc.pid, signal.SIGQUIT)
+        self._send_signal(proc.pid, 'break')
         # Wait for the debugger to acknowledge the signal reception (since we
         # want to send a second signal, we ensure it doesn't get lost by
         # validating the first get received and produce its effect).
         err = proc.stderr.readline()
         self.assertContainsRe(err, r'entering debugger')
-        dead, sig = self._wait_for_process(proc.pid, signal.SIGQUIT)
-        self.assertTrue((dead and sig == signal.SIGQUIT),
-                        msg="subprocess wasn't terminated by repeated SIGQUIT")
+        dead, sig = self._wait_for_process(proc.pid, 'break')
+        self.assertTrue(dead)
+        # Either the child was dead before we could read its status, or the
+        # child was dead from the signal we sent it.
+        self.assertTrue(sig in (None, breakin.determine_signal()))
 
     def test_breakin_disabled(self):
         self._dont_SIGQUIT_on_darwin()
@@ -132,5 +205,5 @@
         # wait for it to get started, and print the 'listening' line
         proc.stderr.readline()
         # first hit should just kill it
-        os.kill(proc.pid, signal.SIGQUIT)
+        self._send_signal(proc.pid, 'break')
         proc.wait()



More information about the bazaar-commits mailing list