Merge lp:~ev/apport/reports-from-hangs into lp:apport

Proposed by Evan on 2012-06-20
Status: Merged
Merged at revision: 2423
Proposed branch: lp:~ev/apport/reports-from-hangs
Merge into: lp:apport
Diff against target: 480 lines (+228/-23)
7 files modified
apport/fileutils.py (+13/-0)
apport/ui.py (+71/-2)
data/apport (+9/-0)
gtk/apport-gtk (+46/-5)
test/test_fileutils.py (+17/-0)
test/test_ui.py (+34/-16)
test/test_ui_gtk.py (+38/-0)
To merge this branch: bzr merge lp:~ev/apport/reports-from-hangs
Reviewer Review Type Date Requested Status
Martin Pitt 2012-06-20 Approve on 2012-07-04
Review via email: mp+111179@code.launchpad.net

Description of the Change

This branch is not ready yet. I need to write tests (I know, I know, tests first) and build support in both the console and KDE interfaces. However, as I press on I want to make sure that you find the general approach to be sensible.

The branch adds the ability to specify that a provided pid is that of a hanging application. In the near future, compiz will call apport with these arguments when it detects that such a condition is occurring.

After review with the security team, it was determined that the safest approach to obtaining a stack trace from these hangs was through the core pipe handler. Therefore, the workflow is as follows:
- Compiz calls apport with --hanging $PID
- Apport collects some basic information to present a dialog:
https://wiki.ubuntu.com/ErrorTracker#When_there_is_an_error
- A key is added to the report denoting whether the application needs to be restarted, based on the user's response.
- Apport sends the application SIGSEGV.
- The apport core pipe handler picks this up and identifies that we already have a "Hang" report for this binary. It appends the core file to the existing report.
- update-notifier notices that the file has changed and calls apport-gtk.
- apport-gtk presents no UI, but restarts the application and tells whoopsie to send the report, if these options were selected.

So, what do you think?

Background:
https://bugs.launchpad.net/ubuntu/+source/whoopsie-daisy/+bug/1006398
https://bugs.launchpad.net/whoopsie/+bug/1015080
https://bugs.launchpad.net/whoopsie/+bug/1014716

To post a comment you must log in.
Martin Pitt (pitti) wrote :

If compiz detects that it's hanging, it could just abort() and let the usual magic do its work. So I suppose this is primarily interesting for applications which need to ask the user "am I really broken"? Do we actually have an existing case where an application can detect that it is hanging? The only cases I've seen is that the kernel spits out some "process 12345 has not responded in 30 seconds" messages, but in this case it would probably be better to not rely on the application itself to call apport. I'd like to settle this question before we start adopting this rather complicated state machine.

Under the assumption that it indeed makes sense to have applications be able to self-detect that they are hung, but still able to call apport, this seems okay to me. One nitpick is that it would IMHO be better to kill them with SIGABRT.

Thanks for this!

Evan (ev) wrote :

The compiz binary detects that the application that it's painting for is hanging because it measures the time between glPaint calls (lp:compiz plugins/fade/src/fade.cpp:194). So calling abort() in compiz would kill compiz and generate an apport report for the compiz binary, not the hanging application.

Perhaps I'm missing something obvious, or misunderstanding how the window manager works? I agree that a complicated state machine is not ideal, and I am definitely open to alternatives.

Now, one could argue that compiz should send SIBABRT to the process, rather than making apport do it and thus simplifying the state that apport has to track. I would like to avoid this if possible. It means we cannot present the dialog while the application is still visible and it would require us to at least rework the text. "Force closed" would likely become "Leave closed", for example:

https://wiki.ubuntu.com/ErrorTracker#app-hang

Thanks Martin!

Martin Pitt (pitti) wrote :

Thanks for the explanations about how this is going to work, it makes much more sense now. I'm still pondering how we could avoid writing the report twice, with two different flags; this is so utterly prone to errors, if I had a dollar for each OSError/IOError and what not race condition I already fixed for those.. There might be half-written files, two parallel instances getting into each other's way, etc.

As we cannot augment a signal with additional data, nor intrude into the killed process' memory to set __abort_msg (that would require PTRACE permissions), the next best thing that comes to my mind is to not write a full report, but just an empty flag file into /var/crash that also contains the pid in the file name, which indicates to the next running apport instance that this was a manual SIGABRT due to a hang. That would be slightly more robust.

Otherwise I do not have a better idea as well.

Thanks!

lp:~ev/apport/reports-from-hangs updated on 2012-06-28
2399. By Evan on 2012-06-28

Rework hang reporting. We now drop a PID.hanging file in /var/crash, which the core pipe handler uses to assign a Hang problem type to the report it generates. This then hands back off to apport-gtk as before, but only marks the report for uploading. Restarting the application is now handled as the process is being terminated.

2400. By Evan on 2012-06-28

Merge with trunk.

2401. By Evan on 2012-06-28

It's hanging. It might not respond to SIGTERM. Send it SIGKILL instead.

Evan (ev) wrote :

Thanks again for the thorough review. I believe I've addressed the remaining issues by implementing your suggestion. The workflow is now as follows:

- Compiz detects a program is hanging and calls apport --hanging $PID.
- The apport UI is presented with some basic information about the hanging application.
- The user presses either the "Force closed" or "Relaunch" button.
- If the "report" box is checked, the ABRT signal is sent to the process and a /var/crash/$PID.hanging file will be written. Otherwise, just the KILL signal is sent to the process.
- If the user has pressed "Relaunch" then it is at this point that the process is restarted. This will occur after apport has waited for the previous pid to terminate.
- If the "report" box was checked and thus the ABRT signal was sent to the process, /usr/share/apport/apport will pick up the crash report. It will check for a /var/crash/$PID.hanging file that matches the pid it was just notified about. If it finds one, then it will create a "Hang" report rather than a "Crash" one.
- When apport-gtk is notified of new .crash files to process, it will check their problem type. For any that are "Hang" reports, it will simply mark the .crash file to be uploaded by whoopsie and then mark it as seen.

I'll write some tests and add support to the KDE and console frontends now. I'm not tied to the naming of anything. So if you think there's a better way of labelling any of these things, do let me know.

lp:~ev/apport/reports-from-hangs updated on 2012-06-28
2402. By Evan on 2012-06-28

Add tests for mark_report_upload and mark_hanging_process.

2403. By Evan on 2012-06-28

Add a test for wait_for_pid()

2404. By Evan on 2012-06-28

Added a test for the GTK layout of Hang problem reports.

Evan (ev) wrote :

Correction. It obviously does not make sense to add console support. Likewise, I am not sure what would be entailed in adding support to KDE's compositor, so I'd rather we revisit KDE frontend support when we have a plan there.

lp:~ev/apport/reports-from-hangs updated on 2012-06-29
2405. By Evan on 2012-06-29

Upgrade argv parsing tests with the new '--hanging' option.

2406. By Evan on 2012-06-29

Unit tests save the day. Fix accidentally treating successful loading of the crash report as a failure.

2407. By Evan on 2012-06-29

The PID option may not be set and does not have a default value.

Evan (ev) wrote :

I think this is finally ready to go. I've added some unit tests and corrected the new code where it was breaking existing ones. Let me know if you're happy with the result.

Thanks!

Martin Pitt (pitti) wrote :

Thanks Evan, this workflow seems good to me. I have one more concern:

9 + path = os.path.join(report_dir, '%s.hanging' % pid)

Can we still include the executable name and the uid here, for paranoia's sake? There might be race conditions about the user killing the process himself and then some other process starts and grabs the freed PID, or the hang is detected while the machine is shutting down (and thus all *.hanging files are invalid at the next boot). For addressing the latter I'd actually like to have the *.hanging files in /run/ somewhere, but a plausibility test in the UI (like comparing the .hanging mtime against /proc/uptime) would work fine as well.

Otherwise this looks fine to me, thanks!

review: Needs Fixing
lp:~ev/apport/reports-from-hangs updated on 2012-07-03
2408. By Evan on 2012-07-03

Use the ExecutablePath and uid as part of the .hanging path, to avoid potential race conditions.

2409. By Evan on 2012-07-03

Silently discard .hanging files that were created before a reboot as the pid is no longer valid.

2410. By Evan on 2012-07-03

Make the dialog modal for the hanging application when one is provided.

2411. By Evan on 2012-07-03

Remove double import.

Evan (ev) wrote :

I've made the requested changes and added some extra code to make the apport dialog modal to the hanging application.

Martin Pitt (pitti) wrote :

Thanks, great work! We need to remember to add a gir1.2-wnck build and binary dependency to the packaging.

Please merge to trunk (if all tests succeed).

review: Approve
Martin Pitt (pitti) wrote :

Ah, when you merge, please don't forget to commit the merge together with an appropriate NEWS entry.

Evan (ev) wrote :

All tests passed. Merged in with a NEWS entry.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'apport/fileutils.py'
2--- apport/fileutils.py 2012-06-28 09:37:46 +0000
3+++ apport/fileutils.py 2012-07-03 16:18:21 +0000
4@@ -106,6 +106,19 @@
5 pass
6
7
8+def mark_hanging_process(report, pid):
9+ if 'ExecutablePath' in report:
10+ subject = report['ExecutablePath'].replace('/', '_')
11+ else:
12+ raise ValueError('report does not have the ExecutablePath attribute')
13+
14+ uid = os.getuid()
15+ base = '%s.%s.%s.hanging' % (subject, str(uid), pid)
16+ path = os.path.join(report_dir, base)
17+ with open(path, 'a'):
18+ pass
19+
20+
21 def mark_report_seen(report):
22 '''Mark given report file as seen.'''
23
24
25=== modified file 'apport/ui.py'
26--- apport/ui.py 2012-06-28 09:51:52 +0000
27+++ apport/ui.py 2012-07-03 16:18:21 +0000
28@@ -18,6 +18,8 @@
29 import glob, sys, os.path, optparse, traceback, locale, gettext
30 import errno, zlib
31 import subprocess, threading, webbrowser
32+import signal
33+import time
34
35 import apport, apport.fileutils, apport.REThread
36
37@@ -191,7 +193,12 @@
38 else:
39 reports = apport.fileutils.get_new_reports()
40 for f in reports:
41- self.run_crash(f)
42+ if not self.load_report(f):
43+ continue
44+ if self.report['ProblemType'] == 'Hang':
45+ self.finish_hang(f)
46+ else:
47+ self.run_crash(f)
48 result = True
49
50 return result
51@@ -214,7 +221,7 @@
52 # not there any more? no problem, then it won't be regarded as
53 # "seen" any more anyway
54 pass
55- if not self.load_report(report_file):
56+ if not self.report and not self.load_report(report_file):
57 return
58
59 if 'Ignore' in self.report:
60@@ -308,6 +315,63 @@
61 else:
62 raise
63
64+ def finish_hang(self, f):
65+ '''Finish processing a hanging application after the core pipe handler
66+ has handed the report back.
67+
68+ This will signal to whoopsie that the report needs to be uploaded.
69+ '''
70+ apport.fileutils.mark_report_upload(f)
71+ apport.fileutils.mark_report_seen(f)
72+
73+ def run_hang(self, pid):
74+ '''Report an application hanging.
75+
76+ This will first present a dialog containing the information it can
77+ collect from the running application (everything but the trace) with
78+ the option of terminating or restarting the application, optionally
79+ reporting that this error occurred.
80+
81+ A SIGABRT will then be sent to the process and a series of
82+ noninteractive processes will collect the remaining information and
83+ mark the report for uploading.
84+ '''
85+ self.report = apport.Report('Hang')
86+ self.report.add_proc_info(pid)
87+ self.report.add_package_info()
88+ path = self.report.get('ExecutablePath', '')
89+ self.cur_package = apport.fileutils.find_file_package(path)
90+ self.report.add_os_info()
91+ allowed_to_report = apport.fileutils.allowed_to_report()
92+ response = self.ui_present_report_details(allowed_to_report,
93+ modal_for=pid)
94+ if response['report']:
95+ apport.fileutils.mark_hanging_process(self.report, pid)
96+ os.kill(int(pid), signal.SIGABRT)
97+ else:
98+ os.kill(int(pid), signal.SIGKILL)
99+
100+ if response['restart']:
101+ self.wait_for_pid(pid)
102+ self.restart()
103+
104+ def wait_for_pid(self, pid):
105+ '''waitpid() does not work for non-child processes. Query the process
106+ state in a loop, waiting for "no such process."
107+ '''
108+ while True:
109+ try:
110+ os.kill(int(pid), 0)
111+ except OSError as e:
112+ if e.errno == errno.ESRCH:
113+ break
114+ else:
115+ raise
116+ time.sleep(1)
117+
118+ def kill_segv(self, pid):
119+ os.kill(int(pid), signal.SIGSEGV)
120+
121 def run_report_bug(self, symptom_script=None):
122 '''Report a bug.
123
124@@ -561,6 +625,9 @@
125 if self.options.symptom:
126 self.run_symptom()
127 return True
128+ elif hasattr(self.options, 'pid') and self.options.hanging:
129+ self.run_hang(self.options.pid)
130+ return True
131 elif self.options.filebug:
132 return self.run_report_bug()
133 elif self.options.update_report is not None:
134@@ -652,6 +719,8 @@
135 help=_('Specify package name in --file-bug mode. This is optional if a --pid is specified. (Implied if package name is given as only argument.)'))
136 optparser.add_option('-P', '--pid', type='int',
137 help=_('Specify a running program in --file-bug mode. If this is specified, the bug report will contain more information. (Implied if pid is given as only argument.)'))
138+ optparser.add_option('--hanging', action='store_true', default=False,
139+ help=_('The provided pid is a hanging application.'))
140 optparser.add_option('-c', '--crash-file', metavar='PATH',
141 help=_('Report the crash from given .apport or .crash file instead of the pending ones in %s. (Implied if file is given as only argument.)') % apport.fileutils.report_dir)
142 optparser.add_option('--save', metavar='PATH',
143
144=== modified file 'data/apport'
145--- data/apport 2012-06-28 09:37:46 +0000
146+++ data/apport 2012-07-03 16:18:21 +0000
147@@ -329,6 +329,15 @@
148 error_log('could not determine ExecutablePath, aborting')
149 sys.exit(1)
150
151+ subject = info['ExecutablePath'].replace('/', '_')
152+ base = '%s.%s.%s.hanging' % (subject, str(pidstat.st_uid), pid)
153+ hanging = os.path.join(apport.fileutils.report_dir, base)
154+
155+ if os.path.exists(hanging):
156+ if (os.stat('/proc/uptime').st_ctime < os.stat(hanging).st_mtime):
157+ info['ProblemType'] = 'Hang'
158+ os.unlink(hanging)
159+
160 if 'InterpreterPath' in info:
161 error_log('script: %s, interpreted by %s (command line "%s")' %
162 (info['ExecutablePath'], info['InterpreterPath'],
163
164=== modified file 'gtk/apport-gtk'
165--- gtk/apport-gtk 2012-06-21 13:38:27 +0000
166+++ gtk/apport-gtk 2012-07-03 16:18:21 +0000
167@@ -13,8 +13,7 @@
168
169 import os.path, sys, subprocess, os, re
170
171-from gi.repository import GObject
172-from gi.repository import GLib
173+from gi.repository import GObject, GLib, Wnck, GdkX11, Gdk
174 try:
175 from gi.repository import Gtk
176 except RuntimeError as e:
177@@ -25,7 +24,17 @@
178 import apport
179 from apport import unicode_gettext as _
180 import apport.ui
181-from gi.repository import Gdk
182+
183+def find_xid_for_pid (pid):
184+ '''Return the X11 Window (xid) for the supplied process ID.'''
185+
186+ pid = int(pid)
187+ screen = Wnck.Screen.get_default()
188+ screen.force_update()
189+ for window in screen.get_windows():
190+ if window.get_pid() == pid:
191+ return window.get_xid()
192+ return None
193
194 class GTKUserInterface(apport.ui.UserInterface):
195 '''GTK UserInterface.'''
196@@ -175,7 +184,16 @@
197 self.w('send_error_report').set_active(True)
198 self.w('send_error_report').hide()
199
200- def ui_present_report_details(self, allowed_to_report=True):
201+ def set_modal_for(self, xid):
202+ gdk_window = self.w('dialog_crash_new')
203+ gdk_window.realize()
204+ gdk_window = gdk_window.get_window()
205+ gdk_display = GdkX11.X11Display.get_default()
206+ foreign = GdkX11.X11Window.foreign_new_for_display(gdk_display, xid)
207+ gdk_window.set_transient_for(foreign)
208+ gdk_window.set_modal_hint(True)
209+
210+ def ui_present_report_details(self, allowed_to_report=True, modal_for=None):
211 icon = None
212 self.collect_called = False
213 report_type = self.report.get('ProblemType')
214@@ -195,7 +213,30 @@
215 self.w('continue_button').set_label(_('Continue'))
216
217 self.w('cancel_button').hide()
218- if not self.report_file:
219+ if modal_for is not None:
220+ xid = find_xid_for_pid(modal_for)
221+ if xid:
222+ self.set_modal_for(xid)
223+
224+ if report_type == 'Hang':
225+ self.w('ignore_future_problems').set_active(False)
226+ self.w('ignore_future_problems').hide()
227+ self.w('closed_button').show()
228+ self.w('closed_button').set_label(_('Force Closed'))
229+ self.w('continue_button').set_label(_('Relaunch'))
230+ self.w('subtitle_label').hide()
231+ self.desktop_info = self.get_desktop_entry()
232+ if self.desktop_info:
233+ icon = self.desktop_info.get('icon')
234+ name = self.desktop_info['name']
235+ name = GLib.markup_escape_text(name)
236+ title = _('The application %s has stopped responding.') % name
237+ else:
238+ icon = 'distributor-logo'
239+ name = os.path.basename(self.report['ExecutablePath'])
240+ title = _('The program "%s" has stopped responding.') % name
241+ self.w('title_label').set_label('<big><b>%s</b></big>' % title)
242+ elif not self.report_file:
243 self.setup_bug_report()
244 elif report_type == 'KernelCrash' or report_type == 'KernelOops':
245 self.w('ignore_future_problems').set_active(False)
246
247=== modified file 'test/test_fileutils.py'
248--- test/test_fileutils.py 2012-06-28 09:37:46 +0000
249+++ test/test_fileutils.py 2012-07-03 16:18:21 +0000
250@@ -113,6 +113,23 @@
251 self.assertEqual(apport.fileutils.seen_report(r), True)
252 self.assertEqual(set(apport.fileutils.get_new_reports()), nr)
253
254+ def test_mark_hanging_process(self):
255+ '''mark_hanging_process()'''
256+ pr = problem_report.ProblemReport()
257+ pr['ExecutablePath'] = '/bin/bash'
258+ apport.fileutils.mark_hanging_process(pr, '1')
259+ uid = str(os.getuid())
260+ base = '_bin_bash.%s.1.hanging' % uid
261+ expected = os.path.join(apport.fileutils.report_dir, base)
262+ self.assertTrue(os.path.exists(expected))
263+
264+ def test_mark_report_upload(self):
265+ '''mark_report_upload()'''
266+ report = os.path.join(apport.fileutils.report_dir, 'report.crash')
267+ apport.fileutils.mark_report_upload(report)
268+ expected = os.path.join(apport.fileutils.report_dir, 'report.upload')
269+ self.assertTrue(os.path.exists(expected))
270+
271 def test_get_all_reports(self):
272 '''get_all_reports()'''
273
274
275=== modified file 'test/test_ui.py'
276--- test/test_ui.py 2012-06-28 09:37:46 +0000
277+++ test/test_ui.py 2012-07-03 16:18:21 +0000
278@@ -183,7 +183,7 @@
279 self.ui = None
280 self.report_file.close()
281
282- self.assertEqual(subprocess.call(['pidof', '/bin/yes']), 1, 'no stray test processes')
283+ self.assertEqual(subprocess.call(['pidof', '/usr/bin/yes']), 1, 'no stray test processes')
284
285 # clean up apport report from _gen_test_crash()
286 for f in glob.glob('/var/crash/_usr_bin_yes.*.crash'):
287@@ -1711,7 +1711,7 @@
288 _chk('apport-gtk', None,
289 {'filebug': False, 'package': None, 'pid': None, 'crash_file':
290 None, 'symptom': None, 'update_report': None, 'save': None,
291- 'window': False, 'tag': []})
292+ 'window': False, 'tag': [], 'hanging': False})
293 # updating report not allowed without args
294 self.assertRaises(SystemExit, _chk, 'apport-collect', None, {})
295
296@@ -1719,7 +1719,7 @@
297 _chk('apport-kde', 'coreutils',
298 {'filebug': True, 'package': 'coreutils', 'pid': None,
299 'crash_file': None, 'symptom': None, 'update_report': None,
300- 'save': None, 'window': False, 'tag': []})
301+ 'save': None, 'window': False, 'tag': [], 'hanging': False})
302
303 # symptom is preferred over package
304 f = open(os.path.join(apport.ui.symptom_script_dir, 'coreutils.py'), 'w')
305@@ -1731,13 +1731,13 @@
306 _chk('apport-cli', 'coreutils',
307 {'filebug': True, 'package': None, 'pid': None, 'crash_file':
308 None, 'symptom': 'coreutils', 'update_report': None, 'save':
309- None, 'window': False, 'tag': []})
310+ None, 'window': False, 'tag': [], 'hanging': False})
311
312 # PID
313 _chk('apport-cli', '1234', {'filebug': True, 'package': None,
314 'pid': '1234', 'crash_file': None, 'symptom': None,
315 'update_report': None, 'save': None, 'window': False,
316- 'tag': []})
317+ 'tag': [], 'hanging': False})
318
319 # .crash/.apport files; check correct handling of spaces
320 for suffix in ('.crash', '.apport'):
321@@ -1745,14 +1745,14 @@
322 'package': None, 'pid': None,
323 'crash_file': '/tmp/f oo' + suffix, 'symptom': None,
324 'update_report': None, 'save': None, 'window': False,
325- 'tag': []})
326+ 'tag': [], 'hanging': False})
327
328 # executable
329 _chk('apport-cli', '/usr/bin/tail', {'filebug': True,
330 'package': 'coreutils',
331 'pid': None, 'crash_file': None, 'symptom': None,
332 'update_report': None, 'save': None, 'window': False,
333- 'tag': []})
334+ 'tag': [], 'hanging': False})
335
336 # update existing report
337 _chk('apport-collect', '1234', {'filebug': False, 'package': None,
338@@ -1783,7 +1783,7 @@
339 #
340 _chk([], {'filebug': True, 'package': None, 'pid': None, 'crash_file':
341 None, 'symptom': None, 'update_report': None, 'save': None,
342- 'window': False, 'tag': []})
343+ 'window': False, 'tag': [], 'hanging': False})
344
345 #
346 # single arguments
347@@ -1793,7 +1793,7 @@
348 _chk(['coreutils'], {'filebug': True, 'package': 'coreutils', 'pid':
349 None, 'crash_file': None, 'symptom': None,
350 'update_report': None, 'save': None, 'window':
351- False, 'tag': []})
352+ False, 'tag': [], 'hanging': False})
353
354 # symptom (preferred over package)
355 f = open(os.path.join(apport.ui.symptom_script_dir, 'coreutils.py'), 'w')
356@@ -1805,26 +1805,27 @@
357 _chk(['coreutils'], {'filebug': True, 'package': None, 'pid': None,
358 'crash_file': None, 'symptom': 'coreutils',
359 'update_report': None, 'save': None, 'window':
360- False, 'tag': []})
361+ False, 'tag': [], 'hanging': False})
362 os.unlink(os.path.join(apport.ui.symptom_script_dir, 'coreutils.py'))
363
364 # PID
365 _chk(['1234'], {'filebug': True, 'package': None, 'pid': '1234',
366 'crash_file': None, 'symptom': None, 'update_report':
367- None, 'save': None, 'window': False, 'tag': []})
368+ None, 'save': None, 'window': False, 'tag': [],
369+ 'hanging': False})
370
371 # .crash/.apport files; check correct handling of spaces
372 for suffix in ('.crash', '.apport'):
373 _chk(['/tmp/f oo' + suffix],
374 {'filebug': False, 'package': None, 'pid': None, 'crash_file':
375 '/tmp/f oo' + suffix, 'symptom': None, 'update_report': None,
376- 'save': None, 'window': False, 'tag': []})
377+ 'save': None, 'window': False, 'tag': [], 'hanging': False})
378
379 # executable name
380 _chk(['/usr/bin/tail'],
381 {'filebug': True, 'package': 'coreutils', 'pid': None,
382 'crash_file': None, 'symptom': None, 'update_report': None,
383- 'save': None, 'window': False, 'tag': []})
384+ 'save': None, 'window': False, 'tag': [], 'hanging': False})
385
386 #
387 # supported options
388@@ -1834,17 +1835,19 @@
389 _chk(['--save', 'foo.apport', 'coreutils'],
390 {'filebug': True, 'package': 'coreutils', 'pid': None,
391 'crash_file': None, 'symptom': None, 'update_report': None,
392- 'save': 'foo.apport', 'window': False, 'tag': []})
393+ 'save': 'foo.apport', 'window': False, 'tag': [],
394+ 'hanging': False})
395
396 # --tag
397 _chk(['--tag', 'foo', 'coreutils'],
398 {'filebug': True, 'package': 'coreutils', 'pid': None,
399 'crash_file': None, 'symptom': None, 'update_report': None,
400- 'save': None, 'window': False, 'tag': ['foo']})
401+ 'save': None, 'window': False, 'tag': ['foo'], 'hanging': False})
402 _chk(['--tag', 'foo', '--tag', 'bar', 'coreutils'],
403 {'filebug': True, 'package': 'coreutils', 'pid': None,
404 'crash_file': None, 'symptom': None, 'update_report': None,
405- 'save': None, 'window': False, 'tag': ['foo', 'bar']})
406+ 'save': None, 'window': False, 'tag': ['foo', 'bar'],
407+ 'hanging': False})
408
409 def test_can_examine_locally_crash(self):
410 '''can_examine_locally() for a crash report'''
411@@ -1948,5 +1951,20 @@
412 'genericname[de]': 'Übersetzer',
413 'exec': 'gedit %U'})
414
415+ def test_wait_for_pid(self):
416+ # fork a test process
417+ pid = os.fork()
418+ if pid == 0:
419+ os.dup2(os.open('/dev/null', os.O_WRONLY), sys.stdout.fileno())
420+ os.execv('/usr/bin/yes', ['yes'])
421+ assert False, 'Could not execute /usr/bin/yes'
422+
423+ time.sleep(0.5)
424+ os.kill(pid, signal.SIGKILL)
425+ os.waitpid(pid, 0)
426+ self.ui.wait_for_pid(pid)
427+
428+ self.assertRaises(OSError, self.ui.wait_for_pid, '1')
429+
430
431 unittest.main()
432
433=== modified file 'test/test_ui_gtk.py'
434--- test/test_ui_gtk.py 2012-06-28 09:37:46 +0000
435+++ test/test_ui_gtk.py 2012-07-03 16:18:21 +0000
436@@ -227,6 +227,44 @@
437 self.assertTrue(self.app.w('ignore_future_problems').get_label().endswith(
438 'of this program version'))
439
440+ def test_hang_layout(self):
441+ '''
442+ +-----------------------------------------------------------------+
443+ | [ apport ] The application Apport has stopped responding. |
444+ | |
445+ | [x] Send an error report to help fix this problem. |
446+ | |
447+ | [ Show Details ] [ Force Closed ] [ Relaunch ] |
448+ +-----------------------------------------------------------------+
449+ '''
450+ self.app.report['ProblemType'] = 'Hang'
451+ self.app.report['ProcCmdline'] = 'apport-bug apport'
452+ self.app.report['Package'] = 'apport 1.2.3~0ubuntu1'
453+ with tempfile.NamedTemporaryFile() as fp:
454+ fp.write(b'''[Desktop Entry]
455+Version=1.0
456+Name=Apport
457+Type=Application''')
458+ fp.flush()
459+ self.app.report['DesktopFile'] = fp.name
460+ GLib.idle_add(Gtk.main_quit)
461+ self.app.ui_present_report_details(True)
462+ self.assertEqual(self.app.w('dialog_crash_new').get_title(), self.distro)
463+ self.assertEqual(self.app.w('title_label').get_text(),
464+ _('The application Apport has stopped responding.'))
465+ send_error_report = self.app.w('send_error_report')
466+ self.assertTrue(send_error_report.get_property('visible'))
467+ self.assertTrue(send_error_report.get_active())
468+ self.assertTrue(self.app.w('show_details').get_property('visible'))
469+ self.assertTrue(self.app.w('continue_button').get_property('visible'))
470+ self.assertEqual(self.app.w('continue_button').get_label(),
471+ _('Relaunch'))
472+ self.assertTrue(self.app.w('closed_button').get_property('visible'))
473+ self.assertEqual(self.app.w('closed_button').get_label(),
474+ _('Force Closed'))
475+ self.assertFalse(self.app.w('subtitle_label').get_property('visible'))
476+ self.assertFalse(self.app.w('ignore_future_problems').get_property('visible'))
477+
478 def test_system_crash_layout(self):
479 '''
480 +---------------------------------------------------------------+

Subscribers

People subscribed via source and target branches