Merge lp:~ev/apport/recoverable-errors into lp:~apport-hackers/apport/trunk
- recoverable-errors
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 2440 |
Proposed branch: | lp:~ev/apport/recoverable-errors |
Merge into: | lp:~apport-hackers/apport/trunk |
Diff against target: |
323 lines (+243/-6) 6 files modified
bin/apport-recoverable-problem (+46/-0) gtk/apport-gtk (+20/-3) kde/apport-kde (+17/-3) test/test_recoverable_problem.py (+83/-0) test/test_ui_gtk.py (+39/-0) test/test_ui_kde.py (+38/-0) |
To merge this branch: | bzr merge lp:~ev/apport/recoverable-errors |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pitt (community) | Approve | ||
Review via email: mp+111840@code.launchpad.net |
Commit message
Description of the change
This branch adds a DBus service for reporting 'recoverable errors.' These are
problems which the application can handle, but still wishes to notify the user
and http://
As an example, the application may wish to notify the user because handling the
error resulted in degraded functionality. The user interface may fail to load
items, or the action just performed may not return any data. The developer may
wish for these types of errors to appear on http://
they may correct the root cause of the report.
I've implemented the DBus service and tests for both it and the UI frontends.
Thanks!
On Tue, Jun 26, 2012 at 9:32 AM, Martin Pitt <email address hidden> wrote:
> I like the general idea, thanks for this Evan!
Thanks!
> I would like to discuss/reconsider whether this really should be a D-BUS service; it would be the only piece of Apport functionality that is. All similar cases (package failure, gcc internal compiler error, uncaught Java exception, uncaught Python exception, GPU hang, etc.) have a CLI API in /usr/share/apport/ instead. E. g. gcc calls /usr/share/
I do agree that is ideal. I believe my original motivation for not
going down that road was that it's hard to provide structure through a
stdin pipe, and command line arguments have a maximum length.
I suppose we end up with something fairly loose no matter what, given
that the DBus service took a small number of fixed arguments and
pushed the rest through a dictionary. Would then you be happy with
something akin to the JVM hook, where it takes key value pairs
separated by null bytes to form the report, getting the PID from
getppid?
Sanity checks could then be done on the other then of the pipe to
ensure that there is enough present to generate a signature. This
might be via the reporter pushing a backtrace
(http://
traceback, or DuplicateSignature through the pipe as a key value pair.
Martin Pitt (pitti) wrote : | # |
Evan Dandrea [2012-07-05 14:43 -0000]:
> I suppose we end up with something fairly loose no matter what, given
> that the DBus service took a small number of fixed arguments and
> pushed the rest through a dictionary. Would then you be happy with
> something akin to the JVM hook, where it takes key value pairs
> separated by null bytes to form the report, getting the PID from
> getppid?
Poor man's marshaller :-) Yes, I think that would be suitable as long
as we only need textual data. Binary data will most likely have null
bytes in them. But this does not seem to be a significant limitation
to me. If we need something more elaborate, we could also use an
existing marshaller such as pickle (but that would be difficult to
build from C clients) or GVariant.
Martin
--
Martin Pitt | http://
Ubuntu Developer (www.ubuntu.com) | Debian Developer (www.debian.org)
- 2405. By Evan
-
Instead of creating a DBus service to receive recoverable error reports, feed them into a binary with nul-separated key value pairs.
- 2406. By Evan
-
Make sure we're looking at the right report.
- 2407. By Evan
-
Refactor test for recoverable problems. Test for incomplete data.
- 2408. By Evan
-
One more.
- 2409. By Evan
-
Drop explicit python3 shebang.
- 2410. By Evan
-
Drop the DBus service.
- 2411. By Evan
-
s/RecoverableCr
ash/Recoverable Problem/ g - 2412. By Evan
-
Merge with trunk.
- 2413. By Evan
-
pep8 fixes.
- 2414. By Evan
-
Handle slight path differences in running tests.
- 2415. By Evan
-
Copyright header.
Okay, I've turned this into a separate binary with nul-separated key-value pairs sent over stdin. It's pretty simple, so perhaps I've missed something. :)
Martin Pitt (pitti) wrote : | # |
This is indeed a lot simpler and more robust.
I made the following changes:
- Move from bin to data/ and rename to recoverable_
- Add an error message for an odd number of fields in stdin
- Drop the check for /proc/sys/
- In test_recoverabl
- Add docstrings to the tests
Merged into trunk now. Thanks a lot!
Preview Diff
1 | === added file 'bin/apport-recoverable-problem' |
2 | --- bin/apport-recoverable-problem 1970-01-01 00:00:00 +0000 |
3 | +++ bin/apport-recoverable-problem 2012-07-12 16:28:35 +0000 |
4 | @@ -0,0 +1,46 @@ |
5 | +#!/usr/bin/python |
6 | + |
7 | +'''Report an error that can be recovered from. |
8 | + |
9 | +This application should be called with its stanard input pipe fed a |
10 | +nul-separated list of key-value pairs. |
11 | +''' |
12 | + |
13 | +# Copyright (C) 2012 Canonical Ltd. |
14 | +# Author: Evan Dandrea <ev@ubuntu.com> |
15 | +# |
16 | +# This program is free software; you can redistribute it and/or modify it |
17 | +# under the terms of the GNU General Public License as published by the |
18 | +# Free Software Foundation; either version 2 of the License, or (at your |
19 | +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for |
20 | +# the full text of the license. |
21 | + |
22 | +import apport.report |
23 | +import sys |
24 | +import os |
25 | + |
26 | + |
27 | +def main(): |
28 | + report = apport.report.Report('RecoverableProblem') |
29 | + items = sys.stdin.read().split('\0') |
30 | + if len(items) % 2 != 0: |
31 | + sys.exit(1) |
32 | + |
33 | + while items: |
34 | + key = items.pop(0) |
35 | + if not items: |
36 | + break |
37 | + value = items.pop(0) |
38 | + report[key] = value |
39 | + |
40 | + report.pid = os.getppid() |
41 | + if not report.pid: |
42 | + sys.exit(1) |
43 | + report.add_os_info() |
44 | + report.add_proc_info(report.pid) |
45 | + report.add_user_info() |
46 | + with open(apport.fileutils.make_report_path(report), 'wb') as fp: |
47 | + report.write(fp) |
48 | + |
49 | +if __name__ == '__main__': |
50 | + main() |
51 | |
52 | === modified file 'gtk/apport-gtk' |
53 | --- gtk/apport-gtk 2012-07-03 16:16:29 +0000 |
54 | +++ gtk/apport-gtk 2012-07-12 16:28:35 +0000 |
55 | @@ -264,8 +264,12 @@ |
56 | icon = self.desktop_info.get('icon') |
57 | n = self.desktop_info['name'] |
58 | n = GLib.markup_escape_text(n) |
59 | - self.w('title_label').set_label('<big><b>%s</b></big>' % |
60 | - _('The application %s has closed unexpectedly.') % n) |
61 | + if report_type == 'RecoverableProblem': |
62 | + t = _('The application %s has experienced ' |
63 | + 'an internal error.') % n |
64 | + else: |
65 | + t = _('The application %s has closed unexpectedly.') % n |
66 | + self.w('title_label').set_label('<big><b>%s</b></big>' % t) |
67 | self.w('subtitle_label').hide() |
68 | |
69 | if 'ProcCmdline' in self.report: |
70 | @@ -277,7 +281,11 @@ |
71 | self.w('continue_button').set_label(_('Continue')) |
72 | else: |
73 | icon = 'distributor-logo' |
74 | - title_text = self.get_system_application_title() |
75 | + if report_type == 'RecoverableProblem': |
76 | + title_text = _('The application %s has experienced ' |
77 | + 'an internal error.') % self.cur_package |
78 | + else: |
79 | + title_text = self.get_system_application_title() |
80 | self.w('title_label').set_label('<big><b>%s</b></big>' % |
81 | title_text) |
82 | self.w('subtitle_label').show() |
83 | @@ -292,6 +300,15 @@ |
84 | else: |
85 | self.w('ignore_future_problems').hide() |
86 | |
87 | + if report_type == 'RecoverableProblem': |
88 | + body = self.report.get('DialogBody', '') |
89 | + if body: |
90 | + del self.report['DialogBody'] |
91 | + self.w('subtitle_label').show() |
92 | + # Set a maximum size for the dialog body, so developers do |
93 | + # not try to shove entire log files into this dialog. |
94 | + self.w('subtitle_label').set_label(body[:1024]) |
95 | + |
96 | if icon: |
97 | from gi.repository import GdkPixbuf |
98 | builtin = Gtk.IconLookupFlags.USE_BUILTIN |
99 | |
100 | === modified file 'kde/apport-kde' |
101 | --- kde/apport-kde 2012-06-06 15:45:04 +0000 |
102 | +++ kde/apport-kde 2012-07-12 16:28:35 +0000 |
103 | @@ -193,9 +193,14 @@ |
104 | # Regular crash. |
105 | if desktop_info: |
106 | icon = desktop_info.get('icon') |
107 | - self.heading.setText(_('The application %s has closed ' |
108 | - 'unexpectedly.') % |
109 | - desktop_info['name']) |
110 | + if report_type == 'RecoverableProblem': |
111 | + self.heading.setText(_('The application %s has experienced ' |
112 | + 'an internal error.') % |
113 | + desktop_info['name']) |
114 | + else: |
115 | + self.heading.setText(_('The application %s has closed ' |
116 | + 'unexpectedly.') % |
117 | + desktop_info['name']) |
118 | self.text.hide() |
119 | if 'ProcCmdline' in report: |
120 | self.closed_button.show() |
121 | @@ -219,6 +224,15 @@ |
122 | else: |
123 | self.ignore_future_problems.hide() |
124 | |
125 | + if report_type == 'RecoverableProblem': |
126 | + body = report.get('DialogBody', '') |
127 | + if body: |
128 | + del report['DialogBody'] |
129 | + # Set a maximum size for the dialog body, so developers do |
130 | + # not try to shove entire log files into this dialog. |
131 | + self.text.setText(body[:1024]) |
132 | + self.text.show() |
133 | + |
134 | if icon: |
135 | base = QIcon.fromTheme(icon).pixmap(42, 42) |
136 | overlay = QIcon.fromTheme('dialog-error').pixmap(16, 16) |
137 | |
138 | === added file 'test/test_recoverable_problem.py' |
139 | --- test/test_recoverable_problem.py 1970-01-01 00:00:00 +0000 |
140 | +++ test/test_recoverable_problem.py 2012-07-12 16:28:35 +0000 |
141 | @@ -0,0 +1,83 @@ |
142 | +'''Test apport-recoverable-error''' |
143 | + |
144 | +# Copyright (C) 2012 Canonical Ltd. |
145 | +# Author: Evan Dandrea <ev@ubuntu.com> |
146 | +# |
147 | +# This program is free software; you can redistribute it and/or modify it |
148 | +# under the terms of the GNU General Public License as published by the |
149 | +# Free Software Foundation; either version 2 of the License, or (at your |
150 | +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for |
151 | +# the full text of the license. |
152 | + |
153 | +import unittest |
154 | +import sys |
155 | +import os |
156 | +import subprocess |
157 | +import tempfile |
158 | +import time |
159 | +import shutil |
160 | +import apport.report |
161 | + |
162 | + |
163 | +class T(unittest.TestCase): |
164 | + def setUp(self): |
165 | + self.report_dir = tempfile.mkdtemp() |
166 | + self.addCleanup(shutil.rmtree, self.report_dir) |
167 | + os.environ['APPORT_REPORT_DIR'] = self.report_dir |
168 | + |
169 | + def wait_for_report(self): |
170 | + cwd = os.getcwd().replace('/', '_') |
171 | + base = sys.argv[0] |
172 | + if base.startswith('./'): |
173 | + base = base[2:] |
174 | + base = base.replace('/', '_') |
175 | + path = '%s_%s.%d.crash' % (cwd, base, os.getuid()) |
176 | + path = os.path.join(self.report_dir, path) |
177 | + seconds = 0 |
178 | + while not os.path.exists(path): |
179 | + time.sleep(1) |
180 | + seconds += 1 |
181 | + self.assertTrue(seconds < 10, 'timeout while waiting for %s to be created.' % path) |
182 | + return path |
183 | + |
184 | + def call_recoverable_problem(self, data): |
185 | + (r, w) = os.pipe() |
186 | + cmd = ['apport-recoverable-problem'] |
187 | + with os.fdopen(r, 'r') as r: |
188 | + with os.fdopen(w, 'w') as w: |
189 | + proc = subprocess.Popen(cmd, stdin=r, close_fds=True) |
190 | + w.write(data) |
191 | + w.flush() |
192 | + proc.communicate() |
193 | + if proc.returncode != 0: |
194 | + raise subprocess.CalledProcessError(proc.returncode, cmd[0]) |
195 | + |
196 | + def test_recoverable_problem(self): |
197 | + self.call_recoverable_problem('hello\0there') |
198 | + path = self.wait_for_report() |
199 | + with open(path, 'rb') as report_path: |
200 | + report = apport.report.Report() |
201 | + report.load(report_path) |
202 | + self.assertEqual(report['hello'], 'there') |
203 | + self.assertTrue('Pid:\t%d' % os.getpid() in report['ProcStatus']) |
204 | + |
205 | + def test_incomplete_data(self): |
206 | + self.assertRaises(subprocess.CalledProcessError, |
207 | + self.call_recoverable_problem, 'hello') |
208 | + |
209 | + self.assertRaises(subprocess.CalledProcessError, |
210 | + self.call_recoverable_problem, |
211 | + 'hello\0there\0extraneous') |
212 | + |
213 | + self.assertRaises(subprocess.CalledProcessError, |
214 | + self.call_recoverable_problem, |
215 | + 'hello\0\0there') |
216 | + |
217 | + |
218 | +with open('/proc/sys/kernel/core_pattern') as f: |
219 | + core_pattern = f.read().strip() |
220 | + if core_pattern[0] != '|': |
221 | + sys.stderr.write('kernel crash dump helper is not active; please enable before running this test.\n') |
222 | + sys.exit(0) |
223 | + |
224 | +unittest.main() |
225 | |
226 | === modified file 'test/test_ui_gtk.py' |
227 | --- test/test_ui_gtk.py 2012-06-28 13:10:30 +0000 |
228 | +++ test/test_ui_gtk.py 2012-07-12 16:28:35 +0000 |
229 | @@ -403,6 +403,45 @@ |
230 | self.assertTrue(self.app.w('details_scrolledwindow').get_property('visible')) |
231 | self.assertTrue(self.app.w('dialog_crash_new').get_resizable()) |
232 | |
233 | + def test_recoverable_crash_layout(self): |
234 | + ''' |
235 | + +-----------------------------------------------------------------+ |
236 | + | [ logo ] The application Foo has experienced an internal error. | |
237 | + | Developer-specified error text. | |
238 | + | | |
239 | + | [x] Send an error report to help fix this problem. | |
240 | + | | |
241 | + | [ Show Details ] [ Continue ] | |
242 | + +-----------------------------------------------------------------+ |
243 | + ''' |
244 | + self.app.report['ProblemType'] = 'RecoverableProblem' |
245 | + self.app.report['Package'] = 'apport 1.2.3~0ubuntu1' |
246 | + self.app.report['DialogBody'] = 'Some developer-specified error text.' |
247 | + with tempfile.NamedTemporaryFile() as fp: |
248 | + fp.write(b'''[Desktop Entry] |
249 | +Version=1.0 |
250 | +Name=Apport |
251 | +Type=Application''') |
252 | + fp.flush() |
253 | + self.app.report['DesktopFile'] = fp.name |
254 | + GLib.idle_add(Gtk.main_quit) |
255 | + self.app.ui_present_report_details(True) |
256 | + self.assertEqual(self.app.w('dialog_crash_new').get_title(), |
257 | + self.distro) |
258 | + msg = 'The application Apport has experienced an internal error.' |
259 | + self.assertEqual(self.app.w('title_label').get_text(), msg) |
260 | + msg = 'Some developer-specified error text.' |
261 | + self.assertEqual(self.app.w('subtitle_label').get_text(), msg) |
262 | + self.assertTrue(self.app.w('subtitle_label').get_property('visible')) |
263 | + send_error_report = self.app.w('send_error_report') |
264 | + self.assertTrue(send_error_report.get_property('visible')) |
265 | + self.assertTrue(send_error_report.get_active()) |
266 | + self.assertTrue(self.app.w('show_details').get_property('visible')) |
267 | + self.assertTrue(self.app.w('continue_button').get_property('visible')) |
268 | + self.assertEqual(self.app.w('continue_button').get_label(), |
269 | + _('Continue')) |
270 | + self.assertFalse(self.app.w('closed_button').get_property('visible')) |
271 | + |
272 | def test_administrator_disabled_reporting(self): |
273 | GLib.idle_add(Gtk.main_quit) |
274 | self.app.ui_present_report_details(False) |
275 | |
276 | === modified file 'test/test_ui_kde.py' |
277 | --- test/test_ui_kde.py 2012-07-09 05:52:40 +0000 |
278 | +++ test/test_ui_kde.py 2012-07-12 16:28:35 +0000 |
279 | @@ -281,6 +281,44 @@ |
280 | self.assertTrue(self.app.dialog.cancel_button.isVisible()) |
281 | self.assertTrue(self.app.dialog.treeview.isVisible()) |
282 | |
283 | + def test_recoverable_crash_layout(self): |
284 | + ''' |
285 | + +-----------------------------------------------------------------+ |
286 | + | [ logo ] The application Foo has experienced an internal error. | |
287 | + | Developer-specified error text. | |
288 | + | | |
289 | + | [x] Send an error report to help fix this problem. | |
290 | + | | |
291 | + | [ Show Details ] [ Continue ] | |
292 | + +-----------------------------------------------------------------+ |
293 | + ''' |
294 | + self.app.report['ProblemType'] = 'RecoverableProblem' |
295 | + self.app.report['Package'] = 'apport 1.2.3~0ubuntu1' |
296 | + self.app.report['DialogBody'] = 'Some developer-specified error text.' |
297 | + |
298 | + with tempfile.NamedTemporaryFile() as fp: |
299 | + fp.write(b'''[Desktop Entry] |
300 | +Version=1.0 |
301 | +Name=Apport |
302 | +Type=Application''') |
303 | + fp.flush() |
304 | + self.app.report['DesktopFile'] = fp.name |
305 | + QTimer.singleShot(0, QCoreApplication.quit) |
306 | + self.app.ui_present_report_details(True) |
307 | + self.assertEqual(self.app.dialog.windowTitle(), |
308 | + self.distro.split()[0]) |
309 | + msg = 'The application Apport has experienced an internal error.' |
310 | + self.assertEqual(self.app.dialog.heading.text(), msg) |
311 | + msg = 'Some developer-specified error text.' |
312 | + self.assertEqual(self.app.dialog.text.text(), msg) |
313 | + self.assertTrue(self.app.dialog.text.isVisible()) |
314 | + self.assertTrue(self.app.dialog.send_error_report.isVisible()) |
315 | + self.assertTrue(self.app.dialog.send_error_report.isChecked()) |
316 | + self.assertTrue(self.app.dialog.details.isVisible()) |
317 | + self.assertTrue(self.app.dialog.continue_button.isVisible()) |
318 | + self.assertEqual(self.app.dialog.continue_button.text(), _('Continue')) |
319 | + self.assertFalse(self.app.dialog.closed_button.isVisible()) |
320 | + |
321 | @patch.object(MainUserInterface, 'open_url') |
322 | def test_1_crash_nodetails(self, *args): |
323 | '''Crash report without showing details''' |
I like the general idea, thanks for this Evan!
I would like to discuss/reconsider whether this really should be a D-BUS service; it would be the only piece of Apport functionality that is. All similar cases (package failure, gcc internal compiler error, uncaught Java exception, uncaught Python exception, GPU hang, etc.) have a CLI API in /usr/share/apport/ instead. E. g. gcc calls /usr/share/ apport/ gcc_ice_ hook and submits the data on stdin and as CLI arguments. This approach reduces the asumptions how much of the system is working; adding dbus, dbus-python, and a working session bus are no small additions here. Also, by doing this on the session bus you exclude a lot of programs from this functionality: system services, user programs running from cron, or remote users.
Some nitpicks about the code (referencing line numbers in the "Preview diff" below):
5: please use /usr/bin/apport in trunk for now, as we keep the code working for Python 2 still (backports, retracer environment only has Python 2, etc.)
26, 37, others: Can we rename it to "RecoverablePro blem"? It might not be an actual crash (in fact, the way you describe it it is particularly _not_ a crash)
43-46: "Traceback" is Python specific, but InterpreterPath applies to all non-ELF programs (also Shell, Perl, Ruby, Java, JS, etc.); I would not assume having a stack trace here; if the caller actually has one, it can still supply it through the arbitrary key/value pairs (additional_keys), and strongly recommend (in the documentation for that functionality) that the caller adds a "DuplicateSigna ture" field.
26, 48: additional_keys is misleading, as it's a dictionary (but this might go away with turning this into CLI)
test_apport_ service. py: Unless you convert this to CLI, this needs to launch a local D-BUS and run on this (Gio.TestDBus is very nice for this, but only works in Quantal; dbus-launch works everywhere). During package builds and in autopkgtest we won't have a running session (nor system) bus.
Thanks!