Merge pull request #706 from ZyX-I/better-errors

Better error handling
This commit is contained in:
ZyX-I 2014-01-26 10:43:16 -08:00
commit a4e8f36f36
4 changed files with 96 additions and 12 deletions

View File

@ -10,10 +10,43 @@ from powerline.lib.config import ConfigLoader
from threading import Lock, Event from threading import Lock, Event
try:
from __builtin__ import unicode
except ImportError:
unicode = str # NOQA
DEFAULT_SYSTEM_CONFIG_DIR = None DEFAULT_SYSTEM_CONFIG_DIR = None
def safe_unicode(s):
'''Return unicode instance without raising an exception.
'''
try:
try:
return unicode(s)
except UnicodeDecodeError:
try:
return unicode(s, 'utf-8')
except TypeError:
return unicode(str(s), 'utf-8')
except Exception as e:
return safe_unicode(e)
class FailedUnicode(unicode):
'''Builtin ``unicode`` (``str`` in python 3) subclass indicating fatal
error.
If your code for some reason wants to determine whether `.render()` method
failed it should check returned string for being a FailedUnicode instance.
Alternatively you could subclass Powerline and override `.render()` method
to do what you like in place of catching the exception and returning
FailedUnicode.
'''
pass
def find_config_file(search_paths, config_file): def find_config_file(search_paths, config_file):
config_file += '.json' config_file += '.json'
for path in search_paths: for path in search_paths:
@ -61,6 +94,29 @@ class PowerlineLogger(object):
self._log('debug', msg, *args, **kwargs) self._log('debug', msg, *args, **kwargs)
_fallback_logger = None
def _get_fallback_logger():
global _fallback_logger
if _fallback_logger:
return _fallback_logger
log_format = '%(asctime)s:%(levelname)s:%(message)s'
formatter = logging.Formatter(log_format)
level = logging.WARNING
handler = logging.StreamHandler()
handler.setLevel(level)
handler.setFormatter(formatter)
logger = logging.getLogger('powerline')
logger.setLevel(level)
logger.addHandler(handler)
_fallback_logger = PowerlineLogger(None, logger, '_fallback_')
return _fallback_logger
class Powerline(object): class Powerline(object):
'''Main powerline class, entrance point for all powerline uses. Sets '''Main powerline class, entrance point for all powerline uses. Sets
powerline up and loads the configuration. powerline up and loads the configuration.
@ -233,7 +289,7 @@ class Powerline(object):
try: try:
Renderer = __import__(self.renderer_module, fromlist=['renderer']).renderer Renderer = __import__(self.renderer_module, fromlist=['renderer']).renderer
except Exception as e: except Exception as e:
self.pl.exception('Failed to import renderer module: {0}', str(e)) self.exception('Failed to import renderer module: {0}', str(e))
sys.exit(1) sys.exit(1)
# Renderer updates configuration file via segments .startup thus it # Renderer updates configuration file via segments .startup thus it
@ -242,7 +298,7 @@ class Powerline(object):
try: try:
renderer = Renderer(**self.renderer_options) renderer = Renderer(**self.renderer_options)
except Exception as e: except Exception as e:
self.pl.exception('Failed to construct renderer object: {0}', str(e)) self.exception('Failed to construct renderer object: {0}', str(e))
if not hasattr(self, 'renderer'): if not hasattr(self, 'renderer'):
raise raise
else: else:
@ -360,16 +416,34 @@ class Powerline(object):
try: try:
self.create_renderer(**create_renderer_kwargs) self.create_renderer(**create_renderer_kwargs)
except Exception as e: except Exception as e:
self.pl.exception('Failed to create renderer: {0}', str(e)) self.exception('Failed to create renderer: {0}', str(e))
finally: if hasattr(self, 'renderer'):
self.create_renderer_kwargs.clear() with self.cr_kwargs_lock:
self.create_renderer_kwargs.clear()
else:
raise
else:
with self.cr_kwargs_lock:
self.create_renderer_kwargs.clear()
def render(self, *args, **kwargs): def render(self, *args, **kwargs):
'''Update/create renderer if needed and pass all arguments further to '''Update/create renderer if needed and pass all arguments further to
``self.renderer.render()``. ``self.renderer.render()``.
''' '''
self.update_renderer() try:
return self.renderer.render(*args, **kwargs) self.update_renderer()
return self.renderer.render(*args, **kwargs)
except Exception as e:
try:
self.exception('Failed to render: {0}', str(e))
except Exception as e:
# Updates e variable to new value, masking previous one.
# Normally it is the same exception (due to raise in case pl is
# unset), but it may also show error in logger. Note that latter
# is not logged by logger for obvious reasons, thus this also
# prevents us from seeing logger traceback.
pass
return FailedUnicode(safe_unicode(e))
def shutdown(self): def shutdown(self):
'''Shut down all background threads. Must be run only prior to exiting '''Shut down all background threads. Must be run only prior to exiting
@ -410,3 +484,9 @@ class Powerline(object):
def __exit__(self, *args): def __exit__(self, *args):
self.shutdown() self.shutdown()
def exception(self, msg, *args, **kwargs):
if 'prefix' not in kwargs:
kwargs['prefix'] = 'powerline'
pl = getattr(self, 'pl', None) or _get_fallback_logger()
return pl.exception(msg, *args, **kwargs)

View File

@ -155,6 +155,10 @@ class ConfigLoader(MultiRunnedThread):
try: try:
self.loaded[path] = deepcopy(self._load(path)) self.loaded[path] = deepcopy(self._load(path))
except Exception as e: except Exception as e:
try:
self.loaded.pop(path)
except KeyError:
pass
self.exception('Error while loading {0}: {1}', path, str(e)) self.exception('Error while loading {0}: {1}', path, str(e))
def run(self): def run(self):

View File

@ -707,13 +707,13 @@ class NetworkLoadSegment(KwThreadedSegment):
total = activity total = activity
interface = name interface = name
if interface in self.interfaces: try:
idata = self.interfaces[interface] idata = self.interfaces[interface]
try: try:
idata['prev'] = idata['last'] idata['prev'] = idata['last']
except KeyError: except KeyError:
pass pass
else: except KeyError:
idata = {} idata = {}
if self.run_once: if self.run_once:
idata['prev'] = (monotonic(), _get_bytes(interface)) idata['prev'] = (monotonic(), _get_bytes(interface))

View File

@ -141,7 +141,7 @@ class TestConfigReload(TestCase):
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents('config', 'themes/test/nonexistent') self.assertAccessEvents('config', 'themes/test/nonexistent')
# It should normally handle file missing error # It should normally handle file missing error
self.assertEqual(p.logger._pop_msgs(), ['exception:test:Failed to create renderer: themes/test/nonexistent']) self.assertEqual(p.logger._pop_msgs(), ['exception:test:powerline:Failed to create renderer: themes/test/nonexistent'])
config['config']['ext']['test']['theme'] = 'default' config['config']['ext']['test']['theme'] = 'default'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
@ -154,7 +154,7 @@ class TestConfigReload(TestCase):
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents('config', 'colorschemes/test/nonexistent') self.assertAccessEvents('config', 'colorschemes/test/nonexistent')
# It should normally handle file missing error # It should normally handle file missing error
self.assertEqual(p.logger._pop_msgs(), ['exception:test:Failed to create renderer: colorschemes/test/nonexistent']) self.assertEqual(p.logger._pop_msgs(), ['exception:test:powerline:Failed to create renderer: colorschemes/test/nonexistent'])
config['config']['ext']['test']['colorscheme'] = '2' config['config']['ext']['test']['colorscheme'] = '2'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
@ -187,7 +187,7 @@ class TestConfigReload(TestCase):
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents('config') self.assertAccessEvents('config')
self.assertIn('exception:test:Failed to create renderer: fcf:colorschemes/test/nonexistentraise', p.logger._pop_msgs()) self.assertIn('exception:test:powerline:Failed to create renderer: fcf:colorschemes/test/nonexistentraise', p.logger._pop_msgs())
config['colorschemes/test/nonexistentraise'] = { config['colorschemes/test/nonexistentraise'] = {
'groups': { 'groups': {