Merge lp:~ev/apport/recoverable-errors into lp:~apport-hackers/apport/trunk

Proposed by Evan
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
Reviewer Review Type Date Requested Status
Martin Pitt (community) Approve
Review via email: mp+111840@code.launchpad.net

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://errors.ubuntu.com about.

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://errors.ubuntu.com so that
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!

To post a comment you must log in.
Revision history for this message
Martin Pitt (pitti) wrote :

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 "RecoverableProblem"? 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 "DuplicateSignature" 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!

review: Needs Fixing
Revision history for this message
Evan (ev) wrote :

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/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.

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://www.gnu.org/software/libc/manual/html_node/Backtraces.html),
traceback, or DuplicateSignature through the pipe as a key value pair.

Revision history for this message
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://www.piware.de
Ubuntu Developer (www.ubuntu.com) | Debian Developer (www.debian.org)

lp:~ev/apport/recoverable-errors updated
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/RecoverableCrash/RecoverableProblem/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.

Revision history for this message
Evan (ev) wrote :

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. :)

Revision history for this message
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_problem, to be consistent with the other scripts of that kind.
 - Add an error message for an odd number of fields in stdin
 - Drop the check for /proc/sys/kernel/core_pattern from test/test_recoverable_problem.py, this is unrelated
 - In test_recoverable_problem.py, simplify call_recoverable_problem() by dropping the os.pipe() stuff and just feeding "data" as argument of communicate()
 - Add docstrings to the tests

Merged into trunk now. Thanks a lot!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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'''

Subscribers

People subscribed via source and target branches