/
exception.py
412 lines (336 loc) · 16.7 KB
/
exception.py
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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
#
# exception.py - general exception formatting and saving
#
# Copyright (C) 2000-2013 Red Hat, Inc.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import errno
import glob
import gi
import os
import re
import shutil
import sys
import time
import traceback
import blivet.errors
from meh import Config
from meh.dump import ReverseExceptionDump
from meh.handler import ExceptionHandler
from pyanaconda import kickstart
from pyanaconda.core import util
from pyanaconda.core.async_utils import run_in_loop
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.constants import THREAD_EXCEPTION_HANDLING_TEST, IPMI_FAILED
from pyanaconda.core.product import get_product_is_final_release
from pyanaconda.errors import NonInteractiveError
from pyanaconda.core.i18n import _
from pyanaconda.modules.common.errors.storage import UnusableStorageError
from pyanaconda.core.threads import thread_manager
from pyanaconda.ui.communication import hubQ
from simpleline import App
from simpleline.event_loop.signals import ExceptionSignal
from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)
class AnacondaReverseExceptionDump(ReverseExceptionDump):
@property
def desc(self):
"""
When traceback will be part of the exception message split the
description from traceback. Description is used in name of the
bug in Bugzilla.
This is useful when saving exception in exception handler and
raising this exception elsewhere (subprocess exception).
:return: Exception description (bug name)
:rtype: str
"""
if self.type and self.value:
parsed_exc = traceback.format_exception_only(self.type, self.value)[0].split("\nTraceback")
description = parsed_exc[0]
# TODO: remove when fixed (#1277422)
# Use only first line of description (because of libreport bug - reported)
description = description.split("\n")[0]
return description.strip()
else:
return ""
class AnacondaExceptionHandler(ExceptionHandler):
def __init__(self, confObj, intfClass, exnClass, tty_num, gui_lock, interactive):
"""
:see: python-meh's ExceptionHandler
:param tty_num: the number of tty the interface is running on
"""
super().__init__(confObj, intfClass, exnClass)
self._gui_lock = gui_lock
self._intf_tty_num = tty_num
self._interactive = interactive
def _main_loop_handleException(self, dump_info):
"""
Helper method with one argument only so that it can be registered
with run_in_loop to run on idle or called from a handler.
:type dump_info: an instance of the meh.DumpInfo class
"""
ty = dump_info.exc_info.type
value = dump_info.exc_info.value
if (issubclass(ty, blivet.errors.StorageError) and value.hardware_fault) \
or (issubclass(ty, OSError) and value.errno == errno.EIO):
# hardware fault or '[Errno 5] Input/Output error'
hw_error_msg = _("The installation was stopped due to what "
"seems to be a problem with your hardware. "
"The exact error message is:\n\n%s.\n\n "
"The installer will now terminate.") % str(value)
self.intf.messageWindow(_("Hardware error occurred"), hw_error_msg)
self._run_kickstart_scripts(dump_info)
util.ipmi_report(IPMI_FAILED)
sys.exit(1)
elif isinstance(value, UnusableStorageError):
self._run_kickstart_scripts(dump_info)
util.ipmi_report(IPMI_FAILED)
sys.exit(1)
elif isinstance(value, NonInteractiveError):
self._run_kickstart_scripts(dump_info)
util.ipmi_report(IPMI_FAILED)
sys.exit(1)
else:
# This will call postWriteHook.
super().handleException(dump_info)
return False
def handleException(self, dump_info):
"""
Our own handleException method doing some additional stuff before
calling the original python-meh's one.
:type dump_info: an instance of the meh.DumpInfo class
:see: python-meh's ExceptionHandler.handleException
"""
log.debug("running handleException")
# don't try and attach empty or non-existent files (#2185827)
self.conf.fileList = [
fn for fn in self.conf.fileList if os.path.exists(fn) and os.path.getsize(fn) > 0
]
exception_lines = traceback.format_exception(*dump_info.exc_info)
log.critical("\n".join(exception_lines))
ty = dump_info.exc_info.type
value = dump_info.exc_info.value
try:
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
# XXX: Gtk stopped raising RuntimeError if it fails to
# initialize. Horay! But will it stay like this? Let's be
# cautious and raise the exception on our own to work in both
# cases
initialized = Gtk.init_check(None)[0]
if not initialized:
raise RuntimeError()
# Attempt to grab the GUI initializing lock, do not block
if not self._gui_lock.acquire(False):
# the graphical interface is running, don't crash it by
# running another one potentially from a different thread
log.debug("Gtk running, queuing exception handler to the main loop")
run_in_loop(self._main_loop_handleException, dump_info)
else:
log.debug("Gtk not running, starting Gtk and running exception handler in it")
self._main_loop_handleException(dump_info)
except (RuntimeError, ImportError, ValueError):
log.debug("Gtk cannot be initialized")
# X not running (Gtk cannot be initialized)
if thread_manager.in_main_thread():
log.debug("In the main thread, running exception handler")
if issubclass(ty, NonInteractiveError) or not self._interactive:
if issubclass(ty, NonInteractiveError):
cmdline_error_msg = _("\nThe installation was stopped due to an "
"error which occurred while running in "
"non-interactive cmdline mode. Since there "
"cannot be any questions in cmdline mode, edit "
"your kickstart file and retry installation. "
"\nThe exact error message is: \n\n%s. \n\nThe "
"installer will now terminate.") % str(value)
else:
cmdline_error_msg = _("\nRunning in cmdline mode, no interactive "
"debugging allowed.\nThe exact error message is: "
"\n\n%s.\n\nThe installer will now terminate."
) % str(value)
# since there is no UI in cmdline mode and it is completely
# non-interactive, we can't show a message window asking the user
# to acknowledge the error; instead, print the error out and sleep
# for a few seconds before exiting the installer
print(cmdline_error_msg, flush=True)
self._run_kickstart_scripts(dump_info)
util.ipmi_report(IPMI_FAILED)
time.sleep(180)
sys.exit(1)
else:
print("\nAn unknown error has occured, look at the "
"/tmp/anaconda-tb* file(s) for more details")
# in the main thread, run exception handler
self._main_loop_handleException(dump_info)
else:
log.debug("In a non-main thread, sending a message with exception data")
# not in the main thread, just send message with exception
# data and let message handler run the exception handler in
# the main thread
exc_info = dump_info.exc_info
# new Simpleline package is now used in TUI. Look if Simpleline is
# initialized or if this is some fallback from GTK or other stuff.
if App.is_initialized():
# if Simpleline is initialized enqueue exception there
loop = App.get_event_loop()
loop.enqueue_signal(ExceptionSignal(App.get_scheduler(), exception_info=exc_info))
else:
hubQ.send_exception((exc_info.type,
exc_info.value,
exc_info.stack))
def postWriteHook(self, dump_info):
# See if there is a /root present in the root path and put exception there as well
if os.access(conf.target.system_root + "/root", os.X_OK):
try:
dest = conf.target.system_root + "/root/%s" % os.path.basename(self.exnFile)
shutil.copyfile(self.exnFile, dest)
except (shutil.Error, OSError):
log.error("Failed to copy %s to %s/root", self.exnFile, conf.target.system_root)
# run kickstart traceback scripts (if necessary)
self._run_kickstart_scripts(dump_info)
util.ipmi_report(IPMI_FAILED)
def _run_kickstart_scripts(self, dump_info):
"""Run the %traceback and %onerror kickstart scripts."""
anaconda = dump_info.object
try:
util.runOnErrorScripts(anaconda.ksdata.scripts)
kickstart.runTracebackScripts(anaconda.ksdata.scripts)
# pylint: disable=bare-except
# ruff: noqa: E722
except:
pass
def runDebug(self, exc_info):
if conf.system.can_switch_tty and self._intf_tty_num != 1:
util.vtActivate(1)
os.open("/dev/console", os.O_RDWR) # reclaim stdin
os.dup2(0, 1) # reclaim stdout
os.dup2(0, 2) # reclaim stderr
# ^
# |
# +------ dup2 is magic, I tells ya!
# bring back the echo
import termios
si = sys.stdin.fileno()
attr = termios.tcgetattr(si)
attr[3] = attr[3] & termios.ECHO
termios.tcsetattr(si, termios.TCSADRAIN, attr)
print("\nEntering debugger...")
print("Use 'continue' command to quit the debugger and get back to the main window")
import pdb
pdb.post_mortem(exc_info.stack)
if conf.system.can_switch_tty and self._intf_tty_num != 1:
util.vtActivate(self._intf_tty_num)
def initExceptionHandling(anaconda):
file_list = ["/tmp/anaconda.log", "/tmp/packaging.log",
"/tmp/program.log", "/tmp/storage.log",
"/tmp/dnf.librepo.log", "/tmp/hawkey.log",
"/tmp/lvm.log", conf.target.system_root + "/root/install.log",
"/proc/cmdline", "/root/lorax-packages.log",
"/tmp/blivet-gui-utils.log", "/tmp/dbus.log"]
if os.path.exists("/tmp/syslog"):
file_list.extend(["/tmp/syslog"])
if anaconda.opts and anaconda.opts.ksfile:
file_list.extend([anaconda.opts.ksfile])
config = Config(programName="anaconda",
programVersion=util.get_anaconda_version_string(),
programArch=os.uname()[4],
attrSkipList=["_intf._actions",
"_intf._currentAction._xklwrapper",
"_intf._currentAction._spokes[\"KeyboardSpoke\"]._xkl_wrapper",
"_intf._currentAction._storage_playground",
"_intf._currentAction._spokes[\"CustomPartitioningSpoke\"]._storage_playground",
"_intf._currentAction.language.translations",
"_intf._currentAction.language.locales",
"_intf._currentAction._spokes[\"PasswordSpoke\"]._oldweak",
"_intf._currentAction._spokes[\"PasswordSpoke\"]._password",
"_intf._currentAction._spokes[\"UserSpoke\"]._password",
"_intf._currentAction._spokes[\"UserSpoke\"]._oldweak",
"_intf.storage.bootloader.password",
"_intf.storage.data",
"_intf.storage.ksdata",
"_intf.data",
"_bootloader.encrypted_password",
"_bootloader.password",
"payload._groups"],
localSkipList=["passphrase", "password", "_oldweak", "_password", "try_passphrase"],
fileList=file_list)
config.register_callback("lsblk_output", lsblk_callback, attchmnt_only=False)
config.register_callback("nmcli_dev_list", nmcli_dev_list_callback,
attchmnt_only=True)
# provide extra information for libreport
config.register_callback("type", lambda: "anaconda", attchmnt_only=True)
config.register_callback("addons", list_addons_callback, attchmnt_only=False)
if "/tmp/syslog" not in file_list:
# no syslog, grab output from journalctl and put it also to the
# anaconda-tb file
config.register_callback("journalctl", journalctl_callback, attchmnt_only=False)
if not get_product_is_final_release():
config.register_callback("release_type", lambda: "pre-release", attchmnt_only=True)
handler = AnacondaExceptionHandler(config, anaconda.intf.meh_interface,
AnacondaReverseExceptionDump, anaconda.intf.tty_num,
anaconda.gui_initialized, anaconda.interactive_mode)
handler.install(anaconda)
return config
def lsblk_callback():
"""Callback to get info about block devices."""
options = "NAME,SIZE,OWNER,GROUP,MODE,FSTYPE,LABEL,UUID,PARTUUID,FSAVAIL,FSUSE%,MOUNTPOINT"
return util.execWithCapture("lsblk", ["--bytes", "-o", options])
def nmcli_dev_list_callback():
"""Callback to get info about network devices."""
return util.execWithCapture("nmcli", ["device", "show"])
def journalctl_callback():
"""Callback to get logs from journalctl."""
# regex to filter log messages from anaconda's process (we have that in our
# logs)
anaconda_log_line = re.compile(r"\[%d\]:" % os.getpid())
ret = ""
for line in util.execReadlines("journalctl", ["-b"]):
if anaconda_log_line.search(line) is None:
# not an anaconda's message
ret += line + "\n"
return ret
def list_addons_callback():
"""
Callback to get info about the addons potentially affecting Anaconda's
behaviour.
"""
# list available addons and take their package names
addon_pkgs = glob.glob("/usr/share/anaconda/addons/*")
return ", ".join(addon.rsplit("/", 1)[1] for addon in addon_pkgs)
def test_exception_handling():
"""
Function that can be used for testing exception handling in anaconda. It
tries to prepare a worst case scenario designed from bugs seen so far.
"""
# XXX: this is a huge hack, but probably the only way, how we can get
# "unique" stack and thus unique hash and new bugreport
def raise_exception(msg, non_ascii):
timestamp = str(time.time()).split(".", 1)[0]
code = """
def f%s(msg, non_ascii):
raise RuntimeError(msg)
f%s(msg, non_ascii)
""" % (timestamp, timestamp)
eval(compile(code, "str_eval", "exec")) # pylint: disable=eval-used
# test non-ascii characters dumping
non_ascii = '\u0159'
msg = "NOTABUG: testing exception handling"
# raise exception from a separate thread
thread_manager.add_thread(
name=THREAD_EXCEPTION_HANDLING_TEST,
target=raise_exception,
args=(msg, non_ascii)
)