Rev 4580: (jam) Support SIGBREAK on Windows dropping you into the debugger. in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Fri Jul 31 17:22:14 BST 2009
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 4580 [merge]
revision-id: pqm at pqm.ubuntu.com-20090731162211-zvddnooijve9nbmu
parent: pqm at pqm.ubuntu.com-20090731013652-3xfv3f47i6rxun4v
parent: john at arbash-meinel.com-20090731145655-y034rdf2csnr8kxe
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2009-07-31 17:22:11 +0100
message:
(jam) Support SIGBREAK on Windows dropping you into the debugger.
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzr bzr.py-20050313053754-5485f144c7006fa6
bzrlib/breakin.py breakin.py-20070417043829-so46nevf978u713k-1
bzrlib/tests/blackbox/test_breakin.py test_breakin.py-20070424043903-qyy6zm4pj3h4sbp3-1
doc/en/developer-guide/HACKING.txt HACKING-20050805200004-2a5dc975d870f78c
=== modified file 'NEWS'
--- a/NEWS 2009-07-31 01:36:52 +0000
+++ b/NEWS 2009-07-31 16:22:11 +0000
@@ -116,6 +116,11 @@
than VFS methods. For example, pushes of small branches with tags take
11 rather than 18 smart server requests. (Andrew Bennetts, #398608)
+* Sending Ctrl-Break on Windows will now drop you into the debugger, in
+ the same way that sending Ctrl-\\ does on other platforms.
+ (John Arbash Meinel)
+
+
Documentation
*************
=== 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-31 14:56:55 +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_type == 'break':
+ sig_num = signal.SIGQUIT
+ elif sig_type == '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 create a new process group for the
+ spawned process (via a flag to Popen), 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,17 @@
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 +154,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 +168,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 +184,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 +203,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()
=== modified file 'doc/en/developer-guide/HACKING.txt'
--- a/doc/en/developer-guide/HACKING.txt 2009-06-15 07:22:34 +0000
+++ b/doc/en/developer-guide/HACKING.txt 2009-07-31 14:52:10 +0000
@@ -1167,10 +1167,12 @@
then bzr will go into pdb post-mortem mode when an unhandled exception
occurs.
-If you send a SIGQUIT signal to bzr, which can be done by pressing
-Ctrl-\\ on Unix, bzr will go into the debugger immediately. You can
-continue execution by typing ``c``. This can be disabled if necessary
-by setting the environment variable ``BZR_SIGQUIT_PDB=0``.
+If you send a SIGQUIT or SIGBREAK signal to bzr then it will drop into the
+debugger immediately. SIGQUIT can be generated by pressing Ctrl-\\ on
+Unix. SIGBREAK is generated with Ctrl-Pause on Windows (some laptops have
+this as Fn-Pause). You can continue execution by typing ``c``. This can
+be disabled if necessary by setting the environment variable
+``BZR_SIGQUIT_PDB=0``.
Debug Flags
More information about the bazaar-commits
mailing list