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!
This commit is contained in:
blitzmann
2017-11-26 03:53:38 -05:00
parent 1da127c898
commit c000b19986
4 changed files with 179 additions and 293 deletions

114
config.py
View File

@@ -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)

View File

@@ -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))

234
pyfa.py
View File

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

View File

@@ -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)