1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
#!/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()
|