Crash Handler
#!/usr/bin/env python # # [SNIPPET_NAME: Crash Handler] # [SNIPPET_CATEGORIES: PyGTK, Apport] # [SNIPPET_DESCRIPTION: a custom except hook for your applications that catches Python exceptions and displays a GTK dialog] # [SNIPPET_AUTHOR: David D. Lowe <daviddlowe.flimm@gmail.com>] # [SNIPPET_LICENSE: MIT] # # Copyright 2010 David D. Lowe # All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # """GTK except hook for your applications. To use, simply import this module and call gtkcrashhandler.initialize(). Import this module before calling gtk.main(). If gtkcrashhandler cannot import gtk, pygtk, pango or gobject, gtkcrashhandler will print a warning and use the default excepthook. If you're using multiple threads, use gtkcrashhandler_thread decorator.""" import sys import os import time try: import pygtk pygtk.require("2.0") # not tested on earlier versions import gtk import pango import gobject _gtk_initialized = True except Exception: print >> sys.stderr, "gtkcrashhandler could not load GTK 2.0" _gtk_initialized = False import traceback from gettext import gettext as _ import threading APP_NAME = None MESSAGE = _("We're terribly sorry. Could you help us fix the problem by " \ "reporting the crash?") USE_APPORT = False _old_sys_excepthook = None # None means that initialize() has not been called # yet. def initialize(app_name=None, message=None, use_apport=False): """Initialize the except hook built on GTK. Keyword arguments: app_name -- The current application's name to be read by humans, untranslated. message -- A message that will be displayed in the error dialog, replacing the default message string. Untranslated. If you don't want a message, pass "". use_apport -- If set to True, gtkcrashhandler will override the settings in /etc/default/apport and call apport if possible, silently failing if not. If set to False, the normal behaviour will be executed, which may mean Apport kicking in anyway. """ global APP_NAME, MESSAGE, USE_APPORT, _gtk_initialized, _old_sys_excepthook if app_name: APP_NAME = _(app_name) if not message is None: MESSAGE = _(message) if use_apport: USE_APPORT = use_apport if _gtk_initialized == True and _old_sys_excepthook is None: # save sys.excepthook first, as it may not be sys.__excepthook__ # (for example, it might be Apport's python hook) _old_sys_excepthook = sys.excepthook # replace sys.excepthook with our own sys.excepthook = _replacement_excepthook def _replacement_excepthook(type, value, tracebk, thread=None): """This function will replace sys.excepthook.""" # create traceback string and print it tb = "".join(traceback.format_exception(type, value, tracebk)) if thread: if not isinstance(thread, threading._MainThread): tb = "Exception in thread %s:\n%s" % (thread.getName(), tb) print >> sys.stderr, tb # determine whether to add a "Report problem..." button add_apport_button = False global USE_APPORT if USE_APPORT: # see if this file is from a properly installed distribution package try: from apport.fileutils import likely_packaged try: filename = os.path.realpath(os.path.join(os.getcwdu(), sys.argv[0])) except: filename = os.path.realpath("/proc/%i/exe" % os.getpid()) if not os.path.isfile(filename) or not os.access(filename, os.X_OK): raise Exception() add_apport_button = likely_packaged(filename) except: add_apport_button = False res = show_error_window(tb, add_apport_button=add_apport_button) if res == 3: # report button clicked # enable apport, overriding preferences try: # create new temporary configuration file, where enabled=1 import re from apport.packaging_impl import impl as apport_packaging newconfiguration = "# temporary apport configuration file " \ "by gtkcrashhandler.py\n\n" try: for line in open(apport_packaging.configuration): if re.search('^\s*enabled\s*=\s*0\s*$', line) is None: newconfiguration += line finally: newconfiguration += "enabled=1" import tempfile tempfile, tempfilename = tempfile.mkstemp() os.write(tempfile, newconfiguration) os.close(tempfile) # set apport to use this configuration file, temporarily apport_packaging.configuration = tempfilename # override Apport's ignore settings for this app from apport.report import Report Report.check_ignored = lambda self: False except: pass if res in (2, 3): # quit sys.stderr = os.tmpfile() global _old_sys_excepthook _old_sys_excepthook(type, value, tracebk) sys.stderr = sys.__stderr__ os._exit(1) def show_error_window(error_string, add_apport_button=False): """Displays an error dialog, and returns the response ID. error_string -- the error's output (usually a traceback) add_apport_button -- whether to add a 'report with apport' button Returns the response ID of the dialog, 1 for ignore, 2 for close and 3 for apport. """ # initialize dialog title = _("An error has occurred") global APP_NAME if APP_NAME: title = APP_NAME dialog = gtk.Dialog(title) # title Label label = gtk.Label() label.set_markup("<b>" + _("It looks like an error has occurred.") + "</b>") label.set_alignment(0, 0.5) dialog.get_content_area().pack_start(label, False) # message Label global MESSAGE text_label = gtk.Label(MESSAGE) text_label.set_alignment(0, 0.5) text_label.set_line_wrap(True) def text_label_size_allocate(widget, rect): """Lets label resize correctly while wrapping text.""" widget.set_size_request(rect.width, -1) text_label.connect("size-allocate", text_label_size_allocate) if not MESSAGE == "": dialog.get_content_area().pack_start(text_label, False) # TextView with error_string buffer = gtk.TextBuffer() buffer.set_text(error_string) textview = gtk.TextView() textview.set_buffer(buffer) textview.set_editable(False) try: textview.modify_font(pango.FontDescription("monospace 8")) except Exception: print >> sys.stderr, "gtkcrashhandler: modify_font raised an exception" # allow scrolling of textview scrolled = gtk.ScrolledWindow() scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scrolled.add_with_viewport(textview) # hide the textview in an Expander widget expander = gtk.expander_new_with_mnemonic(_("_Details")) expander.add(scrolled) dialog.get_content_area().pack_start(expander, False) # add buttons if add_apport_button: dialog.add_button(_("_Report this problem..."), 3) # If we're have multiple threads, or if we're in a GTK callback, # execution can continue normally in other threads, so add button if gtk.main_level() > 0 or threading.activeCount() > 1: dialog.add_button(_("_Ignore the error"), 1) dialog.add_button(("_Close the program"), 2) dialog.set_default_response(2) # set dialog aesthetic preferences dialog.set_border_width(12) dialog.get_content_area().set_spacing(4) dialog.set_resizable(False) # show the dialog and act on it dialog.show_all() res = dialog.run() dialog.destroy() if res < 0: res = 2 return res def gtkcrashhandler_thread(run): """gtkcrashhandler_thread is a decorator for the run() method of threading.Thread. If you forget to use this decorator, exceptions in threads will be printed to standard error output, and GTK's main loop will continue to run. #Example 1: class ExampleThread(threading.Thread): @gtkcrashhandler_thread def run(self): 1 / 0 # this error will be caught by gtkcrashhandler #Example 2: def function(arg): arg / 0 # this error will be caught by gtkcrashhandler threading.Thread(target=gtkcrashhandler_thread(function), args=(1,)).start() """ def gtkcrashhandler_wrapped_run(*args, **kwargs): try: run(*args, **kwargs) except Exception, ee: lock = threading.Lock() lock.acquire() tb = sys.exc_info()[2] if gtk.main_level() > 0: gobject.idle_add( lambda ee=ee, tb=tb, thread=threading.currentThread(): _replacement_excepthook(ee.__class__,ee,tb,thread=thread)) else: time.sleep(0.1) # ugly hack, seems like threads that are # started before running gtk.main() cause # this one to crash. # This delay allows gtk.main() to initialize # properly. # My advice: run gtk.main() before starting # any threads or don't run gtk.main() at all _replacement_excepthook(ee.__class__, ee, tb, thread=threading.currentThread()) lock.release() # return wrapped run if gtkcrashhandler has been initialized global _gtk_initialized, _old_sys_excepthook if _gtk_initialized and _old_sys_excepthook: return gtkcrashhandler_wrapped_run else: return run if __name__ == "__main__": # throw test exception initialize(app_name="gtkcrashhandler", message="Don't worry, though. This " "is just a test. To use the code properly, call " "gtkcrashhandler.initialize() in your PyGTK app to automatically catch " " any Python exceptions like this.") class DoNotRunException(Exception): def __str__(self): return "gtkcrashhandler.py should imported, not run" raise DoNotRunException()