Merge lp:~flimm/python-snippets/gtkcrashhandler into lp:~jonobacon/python-snippets/trunk

Proposed by David D Lowe
Status: Merged
Merged at revision: not available
Proposed branch: lp:~flimm/python-snippets/gtkcrashhandler
Merge into: lp:~jonobacon/python-snippets/trunk
Diff against target: 300 lines (+296/-0)
1 file modified
pygtk/gtkcrashhandler.py (+296/-0)
To merge this branch: bzr merge lp:~flimm/python-snippets/gtkcrashhandler
Reviewer Review Type Date Requested Status
Jono Bacon Needs Information
Review via email: mp+20388@code.launchpad.net
To post a comment you must log in.
Revision history for this message
David D Lowe (flimm) wrote :

gtkcrashhandler catches Python exceptions and displays a user-friendly error dialog.

To use, just add this code at the top of your main module:
import gtkcrashhandler
gtkcrashhandler.initialize()

If gtkcrashhandler.py is run, a test exception is thrown and caught.

Revision history for this message
Jono Bacon (jonobacon) wrote :

Thanks for the snippet, David! Do you think this should really be in the pygtk dir or would it make better sense to include it in an apport dir?

review: Needs Information
Revision history for this message
David D Lowe (flimm) wrote :

The Apport support is optional. I designed the module so that it would work perfectly in systems where Apport is not installed, like Debian.

The error dialog that is displayed is a GtkDialog, which is why I classified this under PyGTK. If PyGTK is not installed, an error message is printed to stderr, and the custom except hook is not activated.

So yes, I think it should be under the pygtk directory.

Revision history for this message
Jono Bacon (jonobacon) wrote :

On 03/02/10 03:51, David D Lowe wrote:
> The Apport support is optional. I designed the module so that it would work perfectly in systems where Apport is not installed, like Debian.
>
> The error dialog that is displayed is a GtkDialog, which is why I classified this under PyGTK. If PyGTK is not installed, an error message is printed to stderr, and the custom except hook is not activated.
>
> So yes, I think it should be under the pygtk directory.
>
Great. Sounds reasonable: I can also tag this under multiple categories
anyway. Thanks!

     Jono

--
Jono Bacon
Ubuntu Community Manager
www.ubuntu.com / www.jonobacon.org
www.identi.ca/jonobacon www.twitter.com/jonobacon

27. By Jono Bacon

Crash handler from David, thanks!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'pygtk/gtkcrashhandler.py'
2--- pygtk/gtkcrashhandler.py 1970-01-01 00:00:00 +0000
3+++ pygtk/gtkcrashhandler.py 2010-03-01 18:44:14 +0000
4@@ -0,0 +1,296 @@
5+#!/usr/bin/env python
6+#
7+# [SNIPPET_NAME: GTK Crash Handler]
8+# [SNIPPET_CATEGORIES: PyGTK]
9+# [SNIPPET_DESCRIPTION: a custom except hook for your applications that catches Python exceptions and displays a GTK dialog]
10+# [SNIPPET_AUTHOR: David D. Lowe <daviddlowe.flimm@gmail.com>]
11+# [SNIPPET_LICENSE: MIT]
12+#
13+# Copyright 2010 David D. Lowe
14+# All rights reserved.
15+#
16+# Permission is hereby granted, free of charge, to any person obtaining a copy
17+# of this software and associated documentation files (the "Software"), to deal
18+# in the Software without restriction, including without limitation the rights
19+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20+# copies of the Software, and to permit persons to whom the Software is
21+# furnished to do so, subject to the following conditions:
22+#
23+# The above copyright notice and this permission notice shall be included in
24+# all copies or substantial portions of the Software.
25+#
26+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32+# THE SOFTWARE.
33+#
34+
35+"""GTK except hook for your applications.
36+To use, simply import this module and call gtkcrashhandler.initialize().
37+Import this module before calling gtk.main().
38+
39+If gtkcrashhandler cannot import gtk, pygtk, pango or gobject,
40+gtkcrashhandler will print a warning and use the default excepthook.
41+
42+If you're using multiple threads, use gtkcrashhandler_thread decorator."""
43+
44+import sys
45+import os
46+import time
47+try:
48+ import pygtk
49+ pygtk.require("2.0") # not tested on earlier versions
50+ import gtk
51+ import pango
52+ import gobject
53+ _gtk_initialized = True
54+except Exception:
55+ print >> sys.stderr, "gtkcrashhandler could not load GTK 2.0"
56+ _gtk_initialized = False
57+import traceback
58+from gettext import gettext as _
59+import threading
60+
61+APP_NAME = None
62+MESSAGE = _("We're terribly sorry. Could you help us fix the problem by " \
63+ "reporting the crash?")
64+USE_APPORT = False
65+
66+_old_sys_excepthook = None # None means that initialize() has not been called
67+ # yet.
68+
69+def initialize(app_name=None, message=None, use_apport=False):
70+ """Initialize the except hook built on GTK.
71+
72+ Keyword arguments:
73+ app_name -- The current application's name to be read by humans,
74+ untranslated.
75+ message -- A message that will be displayed in the error dialog,
76+ replacing the default message string. Untranslated.
77+ If you don't want a message, pass "".
78+ use_apport -- If set to True, gtkcrashhandler will override the settings
79+ in /etc/default/apport and call apport if possible,
80+ silently failing if not.
81+ If set to False, the normal behaviour will be executed,
82+ which may mean Apport kicking in anyway.
83+
84+ """
85+ global APP_NAME, MESSAGE, USE_APPORT, _gtk_initialized, _old_sys_excepthook
86+ if app_name:
87+ APP_NAME = _(app_name)
88+ if not message is None:
89+ MESSAGE = _(message)
90+ if use_apport:
91+ USE_APPORT = use_apport
92+ if _gtk_initialized == True and _old_sys_excepthook is None:
93+ # save sys.excepthook first, as it may not be sys.__excepthook__
94+ # (for example, it might be Apport's python hook)
95+ _old_sys_excepthook = sys.excepthook
96+ # replace sys.excepthook with our own
97+ sys.excepthook = _replacement_excepthook
98+
99+
100+def _replacement_excepthook(type, value, tracebk, thread=None):
101+ """This function will replace sys.excepthook."""
102+ # create traceback string and print it
103+ tb = "".join(traceback.format_exception(type, value, tracebk))
104+ if thread:
105+ if not isinstance(thread, threading._MainThread):
106+ tb = "Exception in thread %s:\n%s" % (thread.getName(), tb)
107+ print >> sys.stderr, tb
108+
109+ # determine whether to add a "Report problem..." button
110+ add_apport_button = False
111+ global USE_APPORT
112+ if USE_APPORT:
113+ # see if this file is from a properly installed distribution package
114+ try:
115+ from apport.fileutils import likely_packaged
116+ try:
117+ filename = os.path.realpath(os.path.join(os.getcwdu(),
118+ sys.argv[0]))
119+ except:
120+ filename = os.path.realpath("/proc/%i/exe" % os.getpid())
121+ if not os.path.isfile(filename) or not os.access(filename, os.X_OK):
122+ raise Exception()
123+ add_apport_button = likely_packaged(filename)
124+ except:
125+ add_apport_button = False
126+
127+ res = show_error_window(tb, add_apport_button=add_apport_button)
128+
129+ if res == 3: # report button clicked
130+ # enable apport, overriding preferences
131+ try:
132+ # create new temporary configuration file, where enabled=1
133+ import re
134+ from apport.packaging_impl import impl as apport_packaging
135+ newconfiguration = "# temporary apport configuration file " \
136+ "by gtkcrashhandler.py\n\n"
137+ try:
138+ for line in open(apport_packaging.configuration):
139+ if re.search('^\s*enabled\s*=\s*0\s*$', line) is None:
140+ newconfiguration += line
141+ finally:
142+ newconfiguration += "enabled=1"
143+ import tempfile
144+ tempfile, tempfilename = tempfile.mkstemp()
145+ os.write(tempfile, newconfiguration)
146+ os.close(tempfile)
147+
148+ # set apport to use this configuration file, temporarily
149+ apport_packaging.configuration = tempfilename
150+ # override Apport's ignore settings for this app
151+ from apport.report import Report
152+ Report.check_ignored = lambda self: False
153+ except:
154+ pass
155+
156+ if res in (2, 3): # quit
157+ sys.stderr = os.tmpfile()
158+ global _old_sys_excepthook
159+ _old_sys_excepthook(type, value, tracebk)
160+ sys.stderr = sys.__stderr__
161+ os._exit(1)
162+
163+def show_error_window(error_string, add_apport_button=False):
164+ """Displays an error dialog, and returns the response ID.
165+
166+ error_string -- the error's output (usually a traceback)
167+ add_apport_button -- whether to add a 'report with apport' button
168+
169+ Returns the response ID of the dialog, 1 for ignore, 2 for close and
170+ 3 for apport.
171+ """
172+ # initialize dialog
173+ title = _("An error has occurred")
174+ global APP_NAME
175+ if APP_NAME:
176+ title = APP_NAME
177+ dialog = gtk.Dialog(title)
178+
179+ # title Label
180+ label = gtk.Label()
181+ label.set_markup("<b>" + _("It looks like an error has occurred.") + "</b>")
182+ label.set_alignment(0, 0.5)
183+ dialog.get_content_area().pack_start(label, False)
184+
185+ # message Label
186+ global MESSAGE
187+ text_label = gtk.Label(MESSAGE)
188+ text_label.set_alignment(0, 0.5)
189+
190+ text_label.set_line_wrap(True)
191+ def text_label_size_allocate(widget, rect):
192+ """Lets label resize correctly while wrapping text."""
193+ widget.set_size_request(rect.width, -1)
194+ text_label.connect("size-allocate", text_label_size_allocate)
195+ if not MESSAGE == "":
196+ dialog.get_content_area().pack_start(text_label, False)
197+
198+ # TextView with error_string
199+ buffer = gtk.TextBuffer()
200+ buffer.set_text(error_string)
201+ textview = gtk.TextView()
202+ textview.set_buffer(buffer)
203+ textview.set_editable(False)
204+ try:
205+ textview.modify_font(pango.FontDescription("monospace 8"))
206+ except Exception:
207+ print >> sys.stderr, "gtkcrashhandler: modify_font raised an exception"
208+
209+ # allow scrolling of textview
210+ scrolled = gtk.ScrolledWindow()
211+ scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
212+ scrolled.add_with_viewport(textview)
213+
214+ # hide the textview in an Expander widget
215+ expander = gtk.expander_new_with_mnemonic(_("_Details"))
216+ expander.add(scrolled)
217+ dialog.get_content_area().pack_start(expander, False)
218+
219+ # add buttons
220+ if add_apport_button:
221+ dialog.add_button(_("_Report this problem..."), 3)
222+ # If we're have multiple threads, or if we're in a GTK callback,
223+ # execution can continue normally in other threads, so add button
224+ if gtk.main_level() > 0 or threading.activeCount() > 1:
225+ dialog.add_button(_("_Ignore the error"), 1)
226+ dialog.add_button(("_Close the program"), 2)
227+ dialog.set_default_response(2)
228+
229+ # set dialog aesthetic preferences
230+ dialog.set_border_width(12)
231+ dialog.get_content_area().set_spacing(4)
232+ dialog.set_resizable(False)
233+
234+ # show the dialog and act on it
235+ dialog.show_all()
236+ res = dialog.run()
237+ dialog.destroy()
238+ if res < 0:
239+ res = 2
240+ return res
241+
242+
243+def gtkcrashhandler_thread(run):
244+ """gtkcrashhandler_thread is a decorator for the run() method of
245+ threading.Thread.
246+
247+ If you forget to use this decorator, exceptions in threads will be
248+ printed to standard error output, and GTK's main loop will continue to run.
249+
250+ #Example 1:
251+ class ExampleThread(threading.Thread):
252+ @gtkcrashhandler_thread
253+ def run(self):
254+ 1 / 0 # this error will be caught by gtkcrashhandler
255+
256+ #Example 2:
257+ def function(arg):
258+ arg / 0 # this error will be caught by gtkcrashhandler
259+ threading.Thread(target=gtkcrashhandler_thread(function), args=(1,)).start()
260+ """
261+ def gtkcrashhandler_wrapped_run(*args, **kwargs):
262+ try:
263+ run(*args, **kwargs)
264+ except Exception, ee:
265+ lock = threading.Lock()
266+ lock.acquire()
267+ tb = sys.exc_info()[2]
268+ if gtk.main_level() > 0:
269+ gobject.idle_add(
270+ lambda ee=ee, tb=tb, thread=threading.currentThread():
271+ _replacement_excepthook(ee.__class__,ee,tb,thread=thread))
272+ else:
273+ time.sleep(0.1) # ugly hack, seems like threads that are
274+ # started before running gtk.main() cause
275+ # this one to crash.
276+ # This delay allows gtk.main() to initialize
277+ # properly.
278+ # My advice: run gtk.main() before starting
279+ # any threads or don't run gtk.main() at all
280+ _replacement_excepthook(ee.__class__, ee, tb,
281+ thread=threading.currentThread())
282+ lock.release()
283+ # return wrapped run if gtkcrashhandler has been initialized
284+ global _gtk_initialized, _old_sys_excepthook
285+ if _gtk_initialized and _old_sys_excepthook:
286+ return gtkcrashhandler_wrapped_run
287+ else:
288+ return run
289+
290+if __name__ == "__main__":
291+ # throw test exception
292+ initialize(app_name="gtkcrashhandler", message="Don't worry, though. This "
293+ "is just a test. To use the code properly, call "
294+ "gtkcrashhandler.initialize() in your PyGTK app to automatically catch "
295+ " any Python exceptions like this.")
296+ class DoNotRunException(Exception):
297+ def __str__(self):
298+ return "gtkcrashhandler.py should imported, not run"
299+ raise DoNotRunException()
300+

Subscribers

People subscribed via source and target branches