2
# Copyright 2010 David D. Lowe
5
# Permission is hereby granted, free of charge, to any person obtaining a copy
6
# of this software and associated documentation files (the "Software"), to deal
7
# in the Software without restriction, including without limitation the rights
8
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
# copies of the Software, and to permit persons to whom the Software is
10
# furnished to do so, subject to the following conditions:
12
# The above copyright notice and this permission notice shall be included in
13
# all copies or substantial portions of the Software.
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
"""GTK except hook for your applications.
25
To use, simply import this module and call gtkcrashhandler.initialize().
26
Import this module before calling gtk.main().
28
If gtkcrashhandler cannot import gtk, pygtk, pango or gobject,
29
gtkcrashhandler will print a warning and use the default excepthook.
31
If you're using multiple threads, use gtkcrashhandler_thread decorator."""
37
from contextlib import contextmanager
44
pygtk.require("2.0") # not tested on earlier versions
48
_gtk_initialized = True
50
print >> sys.stderr, "gtkcrashhandler could not load GTK 2.0"
51
_gtk_initialized = False
53
from gettext import gettext as _
57
MESSAGE = _("We're terribly sorry. Could you help us fix the problem by " \
58
"reporting the crash?")
61
_old_sys_excepthook = None # None means that initialize() has not been called
66
def initialize(app_name=None, message=None, use_apport=False):
67
"""Initialize the except hook built on GTK.
70
app_name -- The current application's name to be read by humans,
72
message -- A message that will be displayed in the error dialog,
73
replacing the default message string. Untranslated.
74
If you don't want a message, pass "".
75
use_apport -- If set to True, gtkcrashhandler will override the settings
76
in /etc/default/apport and call apport if possible,
77
silently failing if not.
78
If set to False, the normal behaviour will be executed,
79
which may mean Apport kicking in anyway.
82
global APP_NAME, MESSAGE, USE_APPORT, _gtk_initialized, _old_sys_excepthook
84
APP_NAME = _(app_name)
85
if not message is None:
88
USE_APPORT = use_apport
89
if _gtk_initialized == True and _old_sys_excepthook is None:
90
# save sys.excepthook first, as it may not be sys.__excepthook__
91
# (for example, it might be Apport's python hook)
92
_old_sys_excepthook = sys.excepthook
93
# replace sys.excepthook with our own
94
sys.excepthook = _replacement_excepthook
97
def _replacement_excepthook(type, value, tracebk, thread=None):
98
"""This function will replace sys.excepthook."""
99
# create traceback string and print it
100
tb = "".join(traceback.format_exception(type, value, tracebk))
102
if not isinstance(thread, threading._MainThread):
103
tb = "Exception in thread %s:\n%s" % (thread.getName(), tb)
104
print >> sys.stderr, tb
106
# determine whether to add a "Report problem..." button
107
add_apport_button = False
110
# see if this file is from a properly installed distribution package
112
from apport.fileutils import likely_packaged
114
filename = os.path.realpath(os.path.join(os.getcwdu(),
117
filename = os.path.realpath("/proc/%i/exe" % os.getpid())
118
if not os.path.isfile(filename) or not os.access(filename, os.X_OK):
120
add_apport_button = likely_packaged(filename)
122
add_apport_button = False
124
res = show_error_window(tb, add_apport_button=add_apport_button)
126
if res == 3: # report button clicked
127
# enable apport, overriding preferences
129
# create new temporary configuration file, where enabled=1
131
from apport.packaging_impl import impl as apport_packaging
132
newconfiguration = "# temporary apport configuration file " \
133
"by gtkcrashhandler.py\n\n"
135
for line in open(apport_packaging.configuration):
136
if re.search('^\s*enabled\s*=\s*0\s*$', line) is None:
137
newconfiguration += line
139
newconfiguration += "enabled=1"
141
tempfile, tempfilename = tempfile.mkstemp()
142
os.write(tempfile, newconfiguration)
145
# set apport to use this configuration file, temporarily
146
apport_packaging.configuration = tempfilename
147
# override Apport's ignore settings for this app
148
from apport.report import Report
149
Report.check_ignored = lambda self: False
153
if res in (2, 3): # quit
154
sys.stderr = os.tmpfile()
155
global _old_sys_excepthook
156
_old_sys_excepthook(type, value, tracebk)
157
sys.stderr = sys.__stderr__
160
def show_error_window(error_string, add_apport_button=False):
161
"""Displays an error dialog, and returns the response ID.
163
error_string -- the error's output (usually a traceback)
164
add_apport_button -- whether to add a 'report with apport' button
166
Returns the response ID of the dialog, 1 for ignore, 2 for close and
170
title = _("An error has occurred")
175
# Do not allow more than one error window
176
if dialog is not None:
179
dialog = gtk.Dialog(title)
184
label.set_markup("<b>" + _("It looks like an error has occurred.") + "</b>")
185
label.set_alignment(0, 0.5)
186
dialog.get_content_area().pack_start(label, False)
190
text_label = gtk.Label()
191
text_label.set_markup(MESSAGE)
192
text_label.set_alignment(0, 0.5)
193
text_label.set_line_wrap(True)
194
def text_label_size_allocate(widget, rect):
195
"""Lets label resize correctly while wrapping text."""
196
widget.set_size_request(rect.width, -1)
197
text_label.connect("size-allocate", text_label_size_allocate)
198
if not MESSAGE == "":
199
dialog.get_content_area().pack_start(text_label, False)
201
# TextView with error_string
202
buffer = gtk.TextBuffer()
203
buffer.set_text(error_string)
204
textview = gtk.TextView()
205
textview.set_buffer(buffer)
206
textview.set_editable(False)
208
textview.modify_font(pango.FontDescription("monospace 8"))
210
print >> sys.stderr, "gtkcrashhandler: modify_font raised an exception"
212
# allow scrolling of textview
213
scrolled = gtk.ScrolledWindow()
214
scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
215
scrolled.add_with_viewport(textview)
217
# hide the textview in an Expander widget
218
expander = gtk.expander_new_with_mnemonic(_("_Details"))
219
expander.add(scrolled)
220
expander.connect('activate', on_expanded)
221
dialog.get_content_area().pack_start(expander, True)
224
if add_apport_button:
225
dialog.add_button(_("_Report this problem..."), 3)
226
# If we're have multiple threads, or if we're in a GTK callback,
227
# execution can continue normally in other threads, so add button
228
if gtk.main_level() > 0 or threading.activeCount() > 1:
229
dialog.add_button(_("_Ignore the error"), 1)
230
dialog.add_button(("_Close the program"), 2)
231
dialog.set_default_response(2)
233
# set dialog aesthetic preferences
234
dialog.set_border_width(12)
235
dialog.get_content_area().set_spacing(4)
236
dialog.set_resizable(False)
238
# show the dialog and act on it
246
def on_expanded(widget):
248
dialog.set_size_request(600, 600)
251
def gtkcrashhandler_thread(run):
252
"""gtkcrashhandler_thread is a decorator for the run() method of
255
If you forget to use this decorator, exceptions in threads will be
256
printed to standard error output, and GTK's main loop will continue to run.
259
class ExampleThread(threading.Thread):
260
@gtkcrashhandler_thread
262
1 / 0 # this error will be caught by gtkcrashhandler
266
arg / 0 # this error will be caught by gtkcrashhandler
267
threading.Thread(target=gtkcrashhandler_thread(function), args=(1,)).start()
269
def gtkcrashhandler_wrapped_run(*args, **kwargs):
272
except Exception, ee:
273
lock = threading.Lock()
275
tb = sys.exc_info()[2]
276
if gtk.main_level() > 0:
278
lambda ee=ee, tb=tb, thread=threading.currentThread():
279
_replacement_excepthook(ee.__class__, ee, tb, thread=thread))
281
time.sleep(0.1) # ugly hack, seems like threads that are
282
# started before running gtk.main() cause
284
# This delay allows gtk.main() to initialize
286
# My advice: run gtk.main() before starting
287
# any threads or don't run gtk.main() at all
288
_replacement_excepthook(ee.__class__, ee, tb,
289
thread=threading.currentThread())
291
# return wrapped run if gtkcrashhandler has been initialized
292
global _gtk_initialized, _old_sys_excepthook
293
if _gtk_initialized and _old_sys_excepthook:
294
return gtkcrashhandler_wrapped_run
298
if __name__ == "__main__":
299
# throw test exception
300
initialize(app_name="gtkcrashhandler", message="Don't worry, though. This "
301
"is just a test. To use the code properly, call "
302
"gtkcrashhandler.initialize() in your PyGTK app to automatically catch "
303
" any Python exceptions like this.")
304
class DoNotRunException(Exception):
306
return "gtkcrashhandler.py should imported, not run"
307
raise DoNotRunException()
310
## We handle initialization directly here, since this module will be used as a
312
#we listen for signals from the system in order to save our configuration
313
# if GTG is forcefully terminated (e.g.: on shutdown).
315
def signal_catcher(callback):
316
#if TERM or ABORT are caught, we execute the callback function
317
for s in [signal.SIGABRT, signal.SIGTERM]:
318
signal.signal(s, lambda a, b: callback())
321
initialize(app_name = "Getting Things GNOME!",
322
message = "GTG" + info.VERSION +
323
_(" has crashed. Please report the bug on <a "\
324
"href=\"http://bugs.edge.launchpad.net/gtg\">our Launchpad page</a>."\
325
" If you have Apport installed, it will be started for you."), \