Merge lp:~flimm/python-snippets/gtkcrashhandler into lp:~jonobacon/python-snippets/trunk
- gtkcrashhandler
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jono Bacon | Needs Information | ||
Review via email: mp+20388@code.launchpad.net |
Commit message
Description of the change
David D Lowe (flimm) wrote : | # |
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?
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.
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.
- 27. By Jono Bacon
-
Crash handler from David, thanks!
Preview Diff
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 | + |
gtkcrashhandler catches Python exceptions and displays a user-friendly error dialog.
To use, just add this code at the top of your main module: .initialize( )
import gtkcrashhandler
gtkcrashhandler
If gtkcrashhandler.py is run, a test exception is thrown and caught.