diff --git a/auxilary.py b/auxilary.py index 7823ac9..6b22250 100644 --- a/auxilary.py +++ b/auxilary.py @@ -1,6 +1,11 @@ -import time, psutil, yaml, os +''' +Various helper functions and classes +''' + +import time, yaml, os from subprocess import check_output, DEVNULL, CalledProcessError -from threading import Thread, Event +from threading import Event +from exceptionThreading import ExceptionThread class ConfigFile(): ''' @@ -21,21 +26,7 @@ class ConfigFile(): with open(self._path, 'w') as f: yaml.dump(self._dict, f, default_flow_style=False) -class async: - ''' - Wraps any function in a thread and starts the thread. Intended to be used as - a decorator - ''' - def __init__(self, daemon=False): - self._daemon = daemon - - def __call__(self, f): - def wrapper(*args, **kwargs): - t = Thread(target=f, daemon=self._daemon, args=args, kwargs=kwargs) - t.start() - return wrapper - -class CountdownTimer(Thread): +class CountdownTimer(ExceptionThread): ''' Launches thread which self terminates after some time (given in seconds). Termination triggers some action (a function). Optionally, a sound can be diff --git a/blinkenLights.py b/blinkenLights.py index a069530..123e115 100644 --- a/blinkenLights.py +++ b/blinkenLights.py @@ -1,11 +1,12 @@ import RPi.GPIO as GPIO import time, logging -from threading import Thread, Event +from threading import Event +from exceptionThreading import ExceptionThread from itertools import chain logger = logging.getLogger(__name__) -class Blinkenlights(Thread): +class Blinkenlights(ExceptionThread): def __init__(self, pin, cyclePeriod=2): self._stopper = Event() self._pin = pin diff --git a/exceptionThreading.py b/exceptionThreading.py new file mode 100644 index 0000000..f57b7f7 --- /dev/null +++ b/exceptionThreading.py @@ -0,0 +1,61 @@ +''' +Implementation of exception-aware threads, including a listener to be put in +the top-level thread +''' + +import queue +from threading import Thread, Event + +# Killswitch for exception queue listener. Pass this to top-level thread so it +# can be called as part of clean up code +excStopper = Event() + +# queue to shuttle exceptions from children to top-level thread +_excQueue = queue.Queue() + +def excChildListener(): + ''' + Queue listener to intercept exceptions thrown in child threads that are + exception-aware + ''' + while not excStopper.isSet(): + try: + raise _excQueue.get(True) + _excQueue.task_done() + except queue.Empty: + pass + while 1: + try: + raise _excQueue.get(False) + _excQueue.task_done() + except queue.Empty: + break + +class ExceptionThread(Thread): + ''' + Thread that passes exceptions to a queue, which is handled by + excChildListener in top-level thread + ''' + def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): + super().__init__(group, target, name, args, kwargs, daemon=daemon) + self._queue = _excQueue + + def run(self): + try: + Thread.run(self) + except BaseException as e: + self._queue.put(e) + +class async: + ''' + Wraps any function in an exception-aware thread and starts the thread. + Intended to be used as a decorator + ''' + def __init__(self, daemon=False): + self._daemon = daemon + + def __call__(self, f): + def wrapper(*args, **kwargs): + t = ExceptionThread(target=f, daemon=self._daemon, args=args, kwargs=kwargs) + t.start() + return wrapper diff --git a/gmail.py b/gmail.py index ed72bb5..26d5093 100644 --- a/gmail.py +++ b/gmail.py @@ -1,5 +1,6 @@ import logging, time -from auxilary import async, ConfigFile +from auxilary import ConfigFile +from exceptionThreading import async from smtplib import SMTP from datetime import datetime from email.mime.multipart import MIMEMultipart diff --git a/listeners.py b/listeners.py index 425a469..1bc0e82 100644 --- a/listeners.py +++ b/listeners.py @@ -1,5 +1,5 @@ import logging, os, sys, stat -from threading import Thread +from exceptionThreading import ExceptionThread from evdev import InputDevice, ecodes from select import select from auxilary import waitForPath @@ -7,7 +7,7 @@ import stateMachine logger = logging.getLogger(__name__) -class KeypadListener(Thread): +class KeypadListener(ExceptionThread): def __init__(self, stateMachine, callbackDisarm, callbackArm, soundLib, passwd): ctrlKeys = { 69: 'NUML', 98: '/', 14: 'BS', 96: 'ENTER'} @@ -115,7 +115,7 @@ class KeypadListener(Thread): # TODO: these are not threadsafe # TODO: this code gets really confused if the pipe is deleted -class PipeListener(Thread): +class PipeListener(ExceptionThread): def __init__(self, callback, path): self._path = path diff --git a/main.py b/main.py index 7774238..901a300 100755 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ import RPi.GPIO as GPIO from sharedLogging import unmountGluster # this should be first program module from stateMachine import StateMachine +from exceptionThreading import excChildListener, excStopper logger = logging.getLogger(__name__) @@ -23,6 +24,7 @@ def clean(): logger.critical(traceback.format_exc()) def sigtermHandler(signum, stackFrame): + excStopper.set() logger.info('Caught SIGTERM') raise SystemExit @@ -32,15 +34,14 @@ if __name__ == '__main__': GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) - + stateMachine = StateMachine() # TODO: segfaults are annoying :( #~ signal.signal(signal.SIGSEGV, sig_handler) signal.signal(signal.SIGTERM, sigtermHandler) - while 1: - time.sleep(31536000) + excChildListener() except Exception: logger.critical(traceback.format_exc()) diff --git a/soundLib.py b/soundLib.py index 90521bf..05b001d 100644 --- a/soundLib.py +++ b/soundLib.py @@ -1,9 +1,9 @@ -import logging, os, hashlib, queue, threading, time, psutil +import logging, os, hashlib, queue, time, psutil +from threading import Event +from exceptionThreading import ExceptionThread, async from pygame import mixer from subprocess import call from collections import OrderedDict -from auxilary import async -from queue import Queue logger = logging.getLogger(__name__) @@ -89,8 +89,8 @@ class SoundLib: self.volume = 100 self._applyVolumesToSounds(self.volume) - self._ttsQueue = Queue() - self._stop = threading.Event() + self._ttsQueue = queue.Queue() + self._stop = Event() self._startMonitor() def changeVolume(self, volumeDelta): @@ -187,7 +187,7 @@ class SoundLib: logger.debug('TTS engine received "%s"', text) def _startMonitor(self): - self._thread = t = threading.Thread(target=self._ttsMonitor, daemon=True) + self._thread = t = ExceptionThread(target=self._ttsMonitor, daemon=True) t.start() logger.debug('Starting TTS Queue Monitor') diff --git a/stream.py b/stream.py index 9d26604..13e1279 100644 --- a/stream.py +++ b/stream.py @@ -16,7 +16,8 @@ import gi, time, os, logging from datetime import datetime from threading import Thread, Lock -from auxilary import async, waitForPath, mkdirSafe +from auxilary import waitForPath, mkdirSafe +from exceptionThreading import async from sharedLogging import gluster logger = logging.getLogger(__name__) diff --git a/webInterface.py b/webInterface.py index 6ee387f..ee1d1a6 100644 --- a/webInterface.py +++ b/webInterface.py @@ -5,7 +5,7 @@ from flask_wtf import FlaskForm from wtforms.fields import StringField, SubmitField from wtforms.validators import InputRequired -from auxilary import async +from exceptionThreading import async logger = logging.getLogger(__name__)