Mini Shell
# Copyright (C) 2009 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/>.
#
# Author(s): Chris Lumens <clumens@redhat.com>
#
from meh import MAIN_RESPONSE_DEBUG, MAIN_RESPONSE_SAVE, MAIN_RESPONSE_SHELL, MAIN_RESPONSE_QUIT, DumpInfo, ExceptionInfo
import bdb
import os
import sys
import report
import traceback
import subprocess
import gettext
_ = lambda x: gettext.translation("python-meh", fallback=True).gettext(x) if x != "" else ""
class NoNetwork(Exception):
def __init__(self, msg=""):
Exception.__init__(self)
self.msg = msg
def __str__(self):
return "No network is available"
class ExceptionHandler(object):
def __init__(self, confObj, intfClass, exnClass):
"""Create a new ExceptionHandler instance. Subclasses of this one may
be created, provided they are careful to provide all the methods
this one does. Instance attributes:
conf -- A filled in Config instance. At least the programName
and programVersion are required.
exnClass -- An instance of ExceptionDump or a subclass of it. This
is required to know how to represent the Python
exception internally.
intfClass-- An instance of AbstractIntf. This is required to know
what UI classes to use.
"""
self.conf = confObj
self.exnClass = exnClass
self.intf = intfClass
self._exitcode = 10
self._exn = None
self.exnFile = None
self.exnText = ""
def _setExitCode(self, code):
self._exitcode = code
exitcode = property(lambda s: s._exitcode,
lambda s, v: s._setExitCode(v))
def _setExn(self, e):
self._exn = e
exn = property(lambda s: s._exn,
lambda s, v: s._setExn(v))
def handleException(self, dump_info):
"""This is the main exception handling entry point. When Python
gets an exception it doesn't know how to handle, this method will
be called. It then saves the traceback and displays the main
dialog asking the user what to do next. Once this method is
called, there's no good way to go back to what you were doing
before.
All arguments are passed in from the handler created by calling
self.install(). This method should not usually be overridden by
a subclass.
:type dump_info: an instance of the meh.DumpInfo class
"""
responseHash = { MAIN_RESPONSE_QUIT: self.runQuit,
MAIN_RESPONSE_DEBUG: self.runDebug,
MAIN_RESPONSE_SAVE: self.runSave,
MAIN_RESPONSE_SHELL: self.runShell,
}
# Quit if we got an exception when running pdb.
if isinstance(dump_info.exc_info.value, bdb.BdbQuit):
sys.exit(self.exitcode)
# Restore original exception handler.
sys.excepthook = sys.__excepthook__
self.preWriteHook(dump_info)
# Save the exception to the filesystem first.
self.exn = self.exnClass(dump_info.exc_info, self.conf)
(fobj, self.exnFile) = self.openFile()
self.exnText = self.exn.traceback_and_object_dump(dump_info.object)
fobj.write(self.exnText)
fobj.close()
self.postWriteHook(dump_info)
# Run initial UI screen, asking whether to save/debug/quit.
while True:
win = self.intf.mainExceptionWindow(str(self.exn), self.exnText)
if not win:
self.runQuit(dump_info.exc_info)
rc = win.run()
win.destroy()
try:
responseHash[rc](dump_info.exc_info)
except KeyError:
# This happens if (for example) the user hits Escape instead
# of pressing a button.
continue
win.destroy()
def preWriteHook(self, dump_info):
"""Subclasses may supply a function with this name that will be
called immediately before the traceback is written to disk in
order to have any sort of special pre-write processing that needs
to be done.
:type dump_info: an instance of the meh.DumpInfo class
"""
pass
def postWriteHook(self, dump_info):
"""Subclasses may supply a function with this name that will be
called immediately after the traceback is written to disk, but
immediately before the UI is run. This is to provide a place for
any special handling to happen once there is a file on disk.
:type dump_info: an instance of the meh.DumpInfo class
"""
pass
def install(self, obj):
"""Install ourselves as the top level exception handler with Python.
If this method is not called after an ExceptionHandler instance is
created, none of the rest of this code will ever be called.
obj -- A Python object that may be dumped to a file when the
exception is saved. This should be something like the top
level object in a program.
"""
sys.excepthook = lambda ty, value, tb: \
self.handleException(DumpInfo(ExceptionInfo(ty, value, tb), obj))
def openFile(self):
"""Create a randomly named output file to write the exception dump to.
This requires a programName be set in the Config instance. The
return value is a (file descriptor, path) pair. The file must be
closed by the caller when writing is done. Subclasses should not
override this method unless they know what they're doing.
"""
import tempfile
(fd, path) = tempfile.mkstemp("", "%s-tb-" % self.conf.programName, "/tmp")
fo = os.fdopen(fd, "w")
return (fo, path)
### Methods called by the primary exception handling dialog.
def runQuit(self, exc_info):
"""This method is called when the "Exit" button is clicked. It may
be overridden by a subclass, but the basic behavior of eventually
quitting should be preserved.
:type exc_info: an instance of the meh.ExceptionInfo class
"""
sys.exit(self.exitcode)
def runDebug(self, exc_info):
"""This method is called when the "Debug" button is clicked. It may
be overridden by a subclass if specialized behavior is required to
enter debug mode.
:type exc_info: an instance of the meh.ExceptionInfo class
"""
print("")
print(_("Use 'continue' command to quit the debugger and get back to "
"the main menu"))
import pdb
pdb.post_mortem(exc_info.stack)
#no need to quit here, let's just get back to the main dialog
def runShell(self, exc_info):
"""This method is called when user requests to run a shell. It may
be overridden by a subclass if specialized behavior is required to
run shell (e.g. TTY switching).
:type exc_info: an instance of the meh.ExceptionInfo class
"""
print("")
print(_("Exit the shell to get back to the main menu"))
proc = subprocess.Popen(["bash", "--login"], shell=True, cwd="/")
proc.wait()
def runSave(self, exc_info):
"""This method is called when the "Save" button is clicked. It may
be overridden by a subclass, but that's going to be a lot of work.
:type exc_info: an instance of the meh.ExceptionInfo class
"""
params = dict()
params.update(self.exn.environment_info)
pkg_info = params.pop("pkg_info", None)
#if rpmdb queries fail (anaconda), use self.conf.* values
if "component" not in params:
params["component"] = self.conf.programName
if pkg_info:
params["pkg_name"] = pkg_info.name
params["pkg_version"] = pkg_info.version
params["pkg_release"] = pkg_info.release
params["pkg_epoch"] = pkg_info.epoch
params["pkg_arch"] = pkg_info.arch
params["package"] = "%s-%s-%s.%s" % (pkg_info.name, pkg_info.version,
pkg_info.release, pkg_info.arch)
else:
params["pkg_name"] = self.conf.programName
parts = self.conf.programVersion.rsplit("-", 1)
if len(parts) == 2:
version, release = parts
else:
version = parts[0]
# not given, make something up
release = "1"
params["pkg_version"] = version
params["pkg_release"] = release
# we don't support specifying epoch in the config object and only
# the anaconda which is not using epochs doesn't have the RPMdb to
# query
params["pkg_epoch"] = "0"
params["pkg_arch"] = self.conf.programArch
params["package"] = "{0.programName}-{0.programVersion}".format(
self.conf)
params["hashmarkername"] = self.conf.programName
params["duphash"] = self.exn.hash
params["reason"] = self.exn.desc
params["description"] = "The following was filed automatically by %s:\n%s" \
% (self.conf.programName, str(self.exn))
tb_item_name = "%s-tb" % self.conf.programName
params[tb_item_name] = self.exnText
# save data generated by callbacks
for (item_name, (callback, _anc)) in self.conf.callbackDict.items():
try:
ret = callback()
if ret:
params[item_name] = ret
except Exception as exc: # pylint: disable=broad-except
params[item_name] = "Caused error: %s" % exc
for fpath in self.conf.fileList:
try:
with open(fpath, "r") as fobj:
# would be better to use the full path here, but libreport
# doesn't allow '/' characters in the item name
filename = os.path.basename(fpath)
if filename not in params:
params[filename] = fobj.read()
else:
params[filename+"_file"] = fobj.read()
except: # pylint: disable=bare-except
#skip files we cannot read
continue
# tell ABRT some more things it requires
params["analyzer"] = "Python"
params["backtrace"] = "".join(traceback.format_exception(exc_info.type,
exc_info.value,
exc_info.stack))
signature = report.createPythonUnhandledExceptionSignature(**params)
# We don't want to automatically quit here since the user may wish to
# save somewhere else, debug, etc.
self.intf.saveExceptionWindow(signature)