From c000b19986663b70df1892595ec3b93e2dd1138f Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sun, 26 Nov 2017 03:53:38 -0500 Subject: [PATCH] Finish pulling all the cruft out of pyfa.py relating to logging and error handling. All that is now done in separate areas. Also finally did some major reworking on the error dialog. Now it doesn't spawn a new wxApp and wxFrame for each and every error - attaches to MainFrame and sticks around, having exceptions append to it rather than spawn a new one. In the case that an error happens before MainFrame is available, it spins up a new wxApp. Yay cleanup! --- config.py | 114 +++++++++++++++++++- gui/errorDialog.py | 121 ++++++++------------- pyfa.py | 234 ++++------------------------------------ service/prereqsCheck.py | 3 +- 4 files changed, 179 insertions(+), 293 deletions(-) diff --git a/config.py b/config.py index ed7420dad..d0fd19084 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,8 @@ import os import sys -from logbook import Logger +from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ + StreamHandler, TimedRotatingFileHandler, WARNING pyfalog = Logger(__name__) @@ -30,6 +31,16 @@ savePath = None saveDB = None gameDB = None logPath = None +loggingLevel = None +logging_setup = None + +LOGLEVEL_MAP = { + "critical": CRITICAL, + "error": ERROR, + "warning": WARNING, + "info": INFO, + "debug": DEBUG, +} def isFrozen(): @@ -68,6 +79,7 @@ def defPaths(customSavePath=None): global saveDB global gameDB global saveInRoot + global logPath pyfalog.debug("Configuring Pyfa") @@ -106,6 +118,13 @@ def defPaths(customSavePath=None): if not gameDB: gameDB = os.path.join(pyfaPath, "eve.db") + if debug: + logFile = "pyfa_debug.log" + else: + logFile = "pyfa.log" + + logPath = os.path.join(savePath, logFile) + # DON'T MODIFY ANYTHING BELOW import eos.config @@ -121,3 +140,96 @@ def defPaths(customSavePath=None): # initialize the settings from service.settings import EOSSettings eos.config.settings = EOSSettings.getInstance().EOSSettings # this is kind of confusing, but whatever + +def defLogging(): + global debug + global logPath + global loggingLevel + global logging_setup + + try: + if debug: + logging_setup = NestedSetup([ + # make sure we never bubble up to the stderr handler + # if we run out of setup handling + NullHandler(), + StreamHandler( + sys.stdout, + bubble=False, + level=loggingLevel + ), + TimedRotatingFileHandler( + logPath, + level=0, + backup_count=3, + bubble=True, + date_format='%Y-%m-%d', + ), + ]) + else: + logging_setup = NestedSetup([ + # make sure we never bubble up to the stderr handler + # if we run out of setup handling + NullHandler(), + FingersCrossedHandler( + TimedRotatingFileHandler( + logPath, + level=0, + backup_count=3, + bubble=False, + date_format='%Y-%m-%d', + ), + action_level=ERROR, + buffer_size=1000, + # pull_information=True, + # reset=False, + ) + ]) + except: + print("Critical error attempting to setup logging. Falling back to console only.") + logging_setup = NestedSetup([ + # make sure we never bubble up to the stderr handler + # if we run out of setup handling + NullHandler(), + StreamHandler( + sys.stdout, + bubble=False + ) + ]) + + with logging_setup.threadbound(): + + # Output all stdout (print) messages as warnings + try: + sys.stdout = LoggerWriter(pyfalog.warning) + except: + pyfalog.critical("Cannot redirect. Continuing without writing stdout to log.") + + # Output all stderr (stacktrace) messages as critical + try: + sys.stderr = LoggerWriter(pyfalog.critical) + except: + pyfalog.critical("Cannot redirect. Continuing without writing stderr to log.") + + +class LoggerWriter(object): + def __init__(self, level): + # self.level is really like using log.debug(message) + # at least in my case + self.level = level + + def write(self, message): + # if statement reduces the amount of newlines that are + # printed to the logger + if message.strip() != '': + self.level(message.replace("\n", "")) + + def flush(self): + # create a flush method so things can be flushed when + # the system wants to. Not sure if simply 'printing' + # sys.stderr is the correct way to do it, but it seemed + # to work properly for me. + self.level(sys.stderr) + + + diff --git a/gui/errorDialog.py b/gui/errorDialog.py index b1d972b2b..7188efb59 100644 --- a/gui/errorDialog.py +++ b/gui/errorDialog.py @@ -18,48 +18,50 @@ # =============================================================================== # import platform -# import sys +import sys # -# # noinspection PyPackageRequirements -# import wx -# -# try: -# import config -# except: -# config = None -# -# try: -# import sqlalchemy -# -# sqlalchemy_version = sqlalchemy.__version__ -# except: -# sqlalchemy_version = "Unknown" -# -# try: -# from logbook import __version__ as logbook_version -# except: -# logbook_version = "Unknown" -# -# import wx.lib.agw.hyperlink +# noinspection PyPackageRequirements +import wx +import traceback +import config +from logbook import Logger +from service.prereqsCheck import version_block + +pyfalog = Logger(__name__) -class ErrorFrameHandler(object): - __app = None +class ErrorHandler(object): + __parent = None + __frame = None @classmethod def HandleException(cls, exc_type, exc_value, exc_traceback): - print("Handle excpetion! {}".format(cls.__app)) + with config.logging_setup.threadbound(): + # Print the base level traceback + t = traceback.format_exception(exc_type, exc_value, exc_traceback) + pyfalog.critical("\n\n"+"".join(t)) + + if cls.__parent is None: + app = wx.App(False) + ErrorFrame(None) + app.MainLoop() + sys.exit() + else: + if not cls.__frame: + cls.__frame = ErrorFrame(cls.__parent) + cls.__frame.Show() + cls.__frame.addException("".join(t)) @classmethod - def SetApp(cls, wxApp): - cls.__app = wxApp + def SetParent(cls, parent): + cls.__parent = parent class ErrorFrame(wx.Frame): - def __init__(self, exception=None, tb=None, error_title='Error!'): + def __init__(self, parent=None, error_title='Error!'): v = sys.version_info - wx.Frame.__init__(self, None, id=wx.ID_ANY, title="pyfa error", pos=wx.DefaultPosition, size=wx.Size(500, 600), + wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="pyfa error", pos=wx.DefaultPosition, size=wx.Size(500, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER | wx.STAY_ON_TOP) desc = "pyfa has experienced an unexpected issue. Below is a message that contains crucial\n" \ @@ -96,59 +98,24 @@ class ErrorFrame(wx.Frame): # mainSizer.AddSpacer((0, 5), 0, wx.EXPAND, 5) - errorTextCtrl = wx.TextCtrl(self, wx.ID_ANY, "", wx.DefaultPosition, (-1, 400), wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP) - errorTextCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) - mainSizer.Add(errorTextCtrl, 0, wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, 5) - - # try: - # errorTextCtrl.AppendText("OS version: \t" + str(platform.platform())) - # except: - # errorTextCtrl.AppendText("OS version: Unknown") - # errorTextCtrl.AppendText("\n") - # - # try: - # errorTextCtrl.AppendText("Python: \t" + '{}.{}.{}'.format(v.major, v.minor, v.micro)) - # except: - # errorTextCtrl.AppendText("Python: Unknown") - # errorTextCtrl.AppendText("\n") - # - # try: - # errorTextCtrl.AppendText("wxPython: \t" + wx.VERSION_STRING) - # except: - # errorTextCtrl.AppendText("wxPython: Unknown") - # errorTextCtrl.AppendText("\n") - # - # errorTextCtrl.AppendText("SQLAlchemy: \t" + str(sqlalchemy_version)) - # errorTextCtrl.AppendText("\n") - # - # errorTextCtrl.AppendText("Logbook: \t" + str(logbook_version)) - # errorTextCtrl.AppendText("\n") - # - # try: - # errorTextCtrl.AppendText("pyfa version: {0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion)) - # except: - # errorTextCtrl.AppendText("pyfa version: Unknown") - # errorTextCtrl.AppendText('\n') - # - # errorTextCtrl.AppendText("pyfa root: " + str(config.pyfaPath or "Unknown")) - # errorTextCtrl.AppendText('\n') - # errorTextCtrl.AppendText("save path: " + str(config.savePath or "Unknown")) - # errorTextCtrl.AppendText('\n') - # errorTextCtrl.AppendText("fs encoding: " + str(sys.getfilesystemencoding() or "Unknown")) - # errorTextCtrl.AppendText('\n\n') - - errorTextCtrl.AppendText("EXCEPTION: " + str(exception or "Unknown")) - errorTextCtrl.AppendText('\n\n') - - if tb: - for line in tb: - errorTextCtrl.AppendText(line) - errorTextCtrl.Layout() + self.errorTextCtrl = wx.TextCtrl(self, wx.ID_ANY, version_block.strip(), wx.DefaultPosition, (-1, 400), wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP) + self.errorTextCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) + mainSizer.Add(self.errorTextCtrl, 0, wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, 5) + self.errorTextCtrl.AppendText("\n") + self.errorTextCtrl.Layout() self.SetSizer(mainSizer) mainSizer.Layout() self.Layout() self.Centre(wx.BOTH) + self.Bind(wx.EVT_CLOSE, self.OnClose) self.Show() + + + def OnClose(self, evt): + self.Hide() + + def addException(self, text): + self.errorTextCtrl.AppendText("\n{}\n\n{}".format("#" * 20, text)) \ No newline at end of file diff --git a/pyfa.py b/pyfa.py index 9d6f60afa..4c3509683 100755 --- a/pyfa.py +++ b/pyfa.py @@ -24,19 +24,8 @@ import sys import traceback from optparse import AmbiguousOptionError, BadOptionError, OptionParser from service.prereqsCheck import PreCheckException, PreCheckMessage, version_precheck, version_block -from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, StreamHandler, TimedRotatingFileHandler, WARNING, \ - __version__ as logbook_version - import config -try: - import sqlalchemy -except ImportError: - sqlalchemy = None - -pyfalog = Logger(__name__) - - ascii_text = ''' ++++++++++++++++++++++++++++++++++++++++++++++++++ __ @@ -67,104 +56,9 @@ class PassThroughOptionParser(OptionParser): try: OptionParser._process_args(self, largs, rargs, values) except (BadOptionError, AmbiguousOptionError) as e: - pyfalog.error("Bad startup option passed.") + # pyfalog.error("Bad startup option passed.") largs.append(e.opt_str) -# -# class LoggerWriter(object): -# def __init__(self, level): -# # self.level is really like using log.debug(message) -# # at least in my case -# self.level = level -# -# def write(self, message): -# # if statement reduces the amount of newlines that are -# # printed to the logger -# if message not in {'\n', ' '}: -# self.level(message.replace("\n", "")) -# -# def flush(self): -# # create a flush method so things can be flushed when -# # the system wants to. Not sure if simply 'printing' -# # sys.stderr is the correct way to do it, but it seemed -# # to work properly for me. -# self.level(sys.stderr) -# -# - -def handleGUIException(exc_type, exc_value, exc_traceback): - try: - # Try and import wx in case it's missing. - # noinspection PyPackageRequirements - import wx - from gui.errorDialog import ErrorFrame - except: - # noinspection PyShadowingNames - wx = None - # noinspection PyShadowingNames - ErrorFrame = None - - tb = traceback.format_tb(exc_traceback) - - try: - - # Try and output to our log handler - with logging_setup.threadbound(): - module_list = list(set(sys.modules) & set(globals())) - if module_list: - pyfalog.info("Imported Python Modules:") - for imported_module in module_list: - module_details = sys.modules[imported_module] - pyfalog.info("{0}: {1}", imported_module, getattr(module_details, '__version__', '')) - - pyfalog.critical("Exception in main thread: {0}", str(exc_value)) - # Print the base level traceback - traceback.print_tb(exc_traceback) - - if wx and ErrorFrame: - pyfa_gui = wx.App(False) - if exc_type == PreCheckException: - msgbox = wx.MessageBox(str(exc_value), 'Error', wx.ICON_ERROR | wx.STAY_ON_TOP) - msgbox.ShowModal() - else: - ErrorFrame(exc_value, tb) - - pyfa_gui.MainLoop() - - pyfalog.info("Exiting.") - except Exception as ex: - # Most likely logging isn't available. Try and output to the console - module_list = list(set(sys.modules) & set(globals())) - if module_list: - pyfalog.info("Imported Python Modules:") - for imported_module in module_list: - module_details = sys.modules[imported_module] - print((str(imported_module) + ": " + str(getattr(module_details, '__version__', '')))) - - print(("Exception in main thread: " + str(exc_value))) - traceback.print_tb(exc_traceback) - - if wx and ErrorFrame: - pyfa_gui = wx.App(False) - if exc_type == PreCheckException: - wx.MessageBox(str(exc_value), 'Error', wx.ICON_ERROR | wx.STAY_ON_TOP) - else: - ErrorFrame(exc_value, tb) - - pyfa_gui.MainLoop() - - print("Exiting.") - - finally: - # TODO: Add cleanup when exiting here. - pass - # sys.exit() - -from gui.errorDialog import ErrorFrameHandler - -# Replace the uncaught exception handler with our own handler. -sys.excepthook = ErrorFrameHandler.HandleException - # Parse command line options usage = "usage: %prog [--root]" parser = PassThroughOptionParser(usage=usage) @@ -175,31 +69,27 @@ parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error") (options, args) = parser.parse_args() -# -# if options.logginglevel == "Critical": -# options.logginglevel = CRITICAL -# elif options.logginglevel == "Error": -# options.logginglevel = ERROR -# elif options.logginglevel == "Warning": -# options.logginglevel = WARNING -# elif options.logginglevel == "Info": -# options.logginglevel = INFO -# elif options.logginglevel == "Debug": -# options.logginglevel = DEBUG -# else: -# options.logginglevel = ERROR if __name__ == "__main__": - # Configure paths - print ('starting') - try: + # first and foremost - check required libraries version_precheck() except PreCheckException as ex: + # do not pass GO, go directly to jail (and then die =/) PreCheckMessage(str(ex)) + sys.exit() + # from here, we can assume we have the libraries that we need, including wx import wx + from logbook import Logger + pyfalog = Logger(__name__) + + from gui.errorDialog import ErrorHandler + + # Replace the uncaught exception handler with our own handler. + sys.excepthook = ErrorHandler.HandleException + if options.rootsavedata is True: config.saveInRoot = True @@ -208,108 +98,25 @@ if __name__ == "__main__": options.title = "pyfa %s%s - Python Fitting Assistant" % (config.version, "" if config.tag.lower() != 'git' else " (git)") config.debug = options.debug + config.loggingLevel = config.LOGLEVEL_MAP.get(options.logginglevel.lower(), config.LOGLEVEL_MAP['error']) config.defPaths(options.savepath) + config.defLogging() - # Basic logging initialization + with config.logging_setup.threadbound(): - # Logging levels: - ''' - logbook.CRITICAL - logbook.ERROR - logbook.WARNING - logbook.INFO - logbook.DEBUG - logbook.NOTSET - ''' - - if options.debug: - savePath_filename = "Pyfa_debug.log" - else: - savePath_filename = "Pyfa.log" - - config.logPath = os.path.join(config.savePath, savePath_filename) - - try: - if options.debug: - logging_mode = "Debug" - logging_setup = NestedSetup([ - # make sure we never bubble up to the stderr handler - # if we run out of setup handling - NullHandler(), - StreamHandler( - sys.stdout, - bubble=False, - level=options.logginglevel - ), - TimedRotatingFileHandler( - config.logPath, - level=0, - backup_count=3, - bubble=True, - date_format='%Y-%m-%d', - ), - ]) - else: - logging_mode = "User" - logging_setup = NestedSetup([ - # make sure we never bubble up to the stderr handler - # if we run out of setup handling - NullHandler(), - FingersCrossedHandler( - TimedRotatingFileHandler( - config.logPath, - level=0, - backup_count=3, - bubble=False, - date_format='%Y-%m-%d', - ), - action_level=ERROR, - buffer_size=1000, - # pull_information=True, - # reset=False, - ) - ]) - except: - print("Critical error attempting to setup logging. Falling back to console only.") - logging_mode = "Console Only" - logging_setup = NestedSetup([ - # make sure we never bubble up to the stderr handler - # if we run out of setup handling - NullHandler(), - StreamHandler( - sys.stdout, - bubble=False - ) - ]) - - with logging_setup.threadbound(): pyfalog.info("Starting Pyfa") + pyfalog.info(version_block) - pyfalog.info("Logbook version: {0}", logbook_version) - - pyfalog.info("Running in logging mode: {0}", logging_mode) - # move this to the log set up - if it fails, can't say that we're writing it pyfalog.info("Writing log file to: {0}", config.logPath) - # Output all stdout (print) messages as warnings - # try: - # sys.stdout = LoggerWriter(pyfalog.warning) - # except: - # pyfalog.critical("Cannot redirect. Continuing without writing stdout to log.") - - # Output all stderr (stacktrace) messages as critical - # try: - # sys.stderr = LoggerWriter(pyfalog.critical) - # except: - # pyfalog.critical("Cannot redirect. Continuing without writing stderr to log.") - if hasattr(sys, 'frozen'): pyfalog.info("Running in a frozen state.") else: pyfalog.info("Running in a thawed state.") + # Lets get to the good stuff, shall we? import eos.db - import eos.events # todo: move this to eos initialization + import eos.events # todo: move this to eos initialization? # noinspection PyUnresolvedReferences import service.prefetch # noqa: F401 @@ -323,7 +130,8 @@ if __name__ == "__main__": from gui.mainFrame import MainFrame pyfa = wx.App(False) - MainFrame(options.title) + mf = MainFrame(options.title) + ErrorHandler.SetParent(mf) pyfa.MainLoop() # TODO: Add some thread cleanup code here. Right now we bail, and that can lead to orphaned threads or threads not properly exiting. diff --git a/service/prereqsCheck.py b/service/prereqsCheck.py index 4b781e5f9..ce8b95083 100644 --- a/service/prereqsCheck.py +++ b/service/prereqsCheck.py @@ -20,7 +20,6 @@ class PreCheckMessage(): pass finally: print(msg) - sys.exit() def version_precheck(): global version_block @@ -43,7 +42,7 @@ def version_precheck(): raise Exception() import wx - version_block += "\nwxPython version: {} (wxWidgets {})".format(VERSION_STRING, wx.wxWidgets_version) + version_block += "\nwxPython version: {} ({})".format(VERSION_STRING, wx.wxWidgets_version) except: msg = "pyfa requires wxPython v4.0.0b2+. You can download wxPython from https://wxpython.org/pages/downloads/" raise PreCheckException(msg)