Source code for helper.controller

"""
Helper Controller Class

"""
import logging
import os
import platform
import signal
import sys
import time

from helper import config
from helper import __version__

LOGGER = logging.getLogger(__name__)


[docs]class Controller(object): """Extend this class to implement your core application controller. Key methods to implement are Controller.setup, Controller.process and Controller.cleanup. If you do not want to use the sleep/wake structure but rather something like a blocking IOLoop, overwrite the Controller.run method. """ APPNAME = sys.argv[0].split(os.sep)[-1] VERSION = __version__ #: When shutting down, how long should sleeping block the interpreter while #: waiting for the state to indicate the class is no longer active. SLEEP_UNIT = 0.5 #: How often should :meth:`Controller.process` be invoked WAKE_INTERVAL = 60 #: Initializing state is only set during initial object creation STATE_INITIALIZING = 0x01 #: When helper has set the signal timer and is paused, it will be in the #: sleeping state. STATE_SLEEPING = 0x02 #: The idle state is available to implementing classes to indicate that #: while they are not actively performing tasks, they are not sleeping. #: Objects in the idle state can be shutdown immediately. STATE_IDLE = 0x03 #: The active state should be set whenever the implementing class is #: performing a task that can not be interrupted. STATE_ACTIVE = 0x04 #: The stop requested state is set when a signal is received indicating the #: process should stop. The app will invoke the :meth:`Controller.stop` #: method which will wait for the process state to change from STATE_ACTIVE STATE_STOP_REQUESTED = 0x05 #: Once the application has started to shutdown, it will set the state to #: stopping and then invoke the :meth:`Controller.stopping` method. STATE_STOPPING = 0x06 #: Once the application has fully stopped, the state is set to stopped. STATE_STOPPED = 0x07 # For reverse lookup _STATES = {0x01: 'Initializing', 0x02: 'Sleeping', 0x03: 'Idle', 0x04: 'Active', 0x05: 'Stop Requested', 0x06: 'Stopping', 0x07: 'Stopped'} # Default state _state = None def __init__(self, args, operating_system): """Create an instance of the controller passing in the debug flag, the options and arguments from the cli parser. :param argparse.Namespace args: Command line arguments :param str operating_system: Operating system name from helper.platform """ self.set_state(self.STATE_INITIALIZING) self.args = args self.debug = args.foreground try: self.config = config.Config(args.config) except ValueError: sys.exit(1) self.logging_config = config.LoggingConfig(self.config.logging, self.debug) self.operating_system = operating_system
[docs] def cleanup(self): """Override this method to cleanly shutdown the application.""" LOGGER.debug('Unextended %s.cleanup() method', self.__class__.__name__)
[docs] def configuration_reloaded(self): """Override to provide any steps when the configuration is reloaded.""" LOGGER.debug('Unextended %s.configuration_reloaded() method', self.__class__.__name__)
@property def current_state(self): """Property method that return the string description of the runtime state. :rtype: str """ return self._STATES[self._state] @property def is_active(self): """Property method that returns a bool specifying if the process is currently active. :rtype: bool """ return self._state == self.STATE_ACTIVE @property def is_idle(self): """Property method that returns a bool specifying if the process is currently idle. :rtype: bool """ return self._state == self.STATE_IDLE @property def is_initializing(self): """Property method that returns a bool specifying if the process is currently initializing. :rtype: bool """ return self._state == self.STATE_INITIALIZING @property def is_running(self): """Property method that returns a bool specifying if the process is currently running. This will return true if the state is active, idle or initializing. :rtype: bool """ return self._state in [self.STATE_ACTIVE, self.STATE_IDLE, self.STATE_INITIALIZING] @property def is_sleeping(self): """Property method that returns a bool specifying if the process is currently sleeping. :rtype: bool """ return self._state == self.STATE_SLEEPING @property def is_stopped(self): """Property method that returns a bool specifying if the process is stopped. :rtype: bool """ return self._state == self.STATE_STOPPED @property def is_stopping(self): """Property method that returns a bool specifying if the process is stopping. :rtype: bool """ return self._state == self.STATE_STOPPING @property def is_waiting_to_stop(self): """Property method that returns a bool specifying if the process is waiting for the current process to finish so it can stop. :rtype: bool """ return self._state == self.STATE_STOP_REQUESTED
[docs] def on_sighup(self, signum_unused, frame_unused): """Called when SIGHUP is received, shutdown internal runtime state, reloads configuration and then calls Controller.run(). Can be extended to implement other behaviors. """ LOGGER.info('Received SIGHUP') if self.config.reload(): LOGGER.info('Configuration reloaded') if self.logging_config.update(self.config.logging, self.debug): LOGGER.info('Logging configuration updated') self.configuration_reloaded() if self.is_sleeping: signal.pause()
[docs] def on_sigterm(self, signum_unused, frame_unused): """Called when SIGTERM is received, calling self.stop(). Override to implement a different behavior. """ LOGGER.info('Received SIGTERM, initiating shutdown') self.stop()
[docs] def on_sigusr1(self, signum_unused, frame_unused): """Called when SIGUSR1 is received, does not have any attached behavior. Override to implement a behavior for this signal. """ LOGGER.info('Received SIGUSR1')
[docs] def on_sigusr2(self, signum_unused, frame_unused): """Called when SIGUSR2 is received, does not have any attached behavior. Override to implement a behavior for this signal. """ LOGGER.info('Received SIGUSR2')
[docs] def process(self): """To be implemented by the extending class. Is called after every sleep interval in the main application loop. """ raise NotImplementedError
[docs] def run(self): """The core method for starting the application. Will setup logging, toggle the runtime state flag, block on loop, then call shutdown. Redefine this method if you intend to use an IO Loop or some other long running process. """ LOGGER.info('%s v%s started', self.APPNAME, self.VERSION) self.setup() self.process() signal.signal(signal.SIGALRM, self._wake) self._sleep() while self.is_running or self.is_sleeping: signal.pause()
[docs] def start(self): """Important: Do not extend this method, rather redefine Controller.run """ self.setup_signals() self.run()
[docs] def set_state(self, state): """Set the runtime state of the Controller. Use the internal constants to ensure proper state values: - :attr:`Controller.STATE_INITIALIZING` - :attr:`Controller.STATE_ACTIVE` - :attr:`Controller.STATE_IDLE` - :attr:`Controller.STATE_SLEEPING` - :attr:`Controller.STATE_STOP_REQUESTED` - :attr:`Controller.STATE_STOPPING` - :attr:`Controller.STATE_STOPPED` :param int state: The runtime state :raises: ValueError """ LOGGER.debug('Attempting to set state to %s', self._STATES.get(state, state)) if state == self._state: LOGGER.debug('Ignoring request to set state to current state: %s', self._STATES[state]) return if state not in self._STATES.keys(): raise ValueError('Invalid Runtime State') if self.is_waiting_to_stop and state not in [self.STATE_STOPPING, self.STATE_STOPPED]: LOGGER.warning('Attempt to set invalid state while waiting to ' 'shutdown: %s ', self._STATES[state]) return # Validate the next state for a shutting down process if self.is_stopping and state != self.STATE_STOPPED: LOGGER.warning('Attempt to set invalid post shutdown state: %s', self._STATES[state]) return # Validate the next state for a running process if self.is_running and state not in [self.STATE_ACTIVE, self.STATE_IDLE, self.STATE_SLEEPING, self.STATE_STOP_REQUESTED, self.STATE_STOPPING]: LOGGER.warning('Attempt to set invalid post running state: %s', self._STATES[state]) return # Validate the next state for a sleeping process if self.is_sleeping and state not in [self.STATE_ACTIVE, self.STATE_IDLE, self.STATE_STOP_REQUESTED, self.STATE_STOPPING]: LOGGER.warning('Attempt to set invalid post sleeping state: %s', self._STATES[state]) return # Set the value self._state = state # Log the change LOGGER.debug('Runtime state changed to %s', self._STATES[self._state])
[docs] def setup(self): """Override to provide any required setup steps.""" LOGGER.debug('Unextended %s.setup() method', self.__class__.__name__)
[docs] def setup_signals(self): signal.signal(signal.SIGHUP, self.on_sighup) signal.signal(signal.SIGTERM, self.on_sigterm) signal.signal(signal.SIGUSR1, self.on_sigusr1) signal.signal(signal.SIGUSR2, self.on_sigusr2)
[docs] def shutdown(self): """Override to provide any required shutdown steps.""" LOGGER.debug('Unextended %s.shutdown() method', self.__class__.__name__)
[docs] def stop(self): """Override to implement shutdown steps.""" LOGGER.info('Attempting to stop the process') self.set_state(self.STATE_STOP_REQUESTED) # Clear out the timer signal.setitimer(signal.ITIMER_PROF, 0, 0) # Call shutdown for classes to add shutdown steps self.shutdown() # Wait for the current run to finish while self.is_running and self.is_waiting_to_stop: LOGGER.info('Waiting for the process to finish') time.sleep(self.SLEEP_UNIT) # Change the state to shutting down if not self.is_stopping: self.set_state(self.STATE_STOPPING) # Call a method that may be overwritten to cleanly shutdown self.cleanup() # Change our state self._stopped()
@property def system_platform(self): """Return a tuple containing the operating system, python implementation (CPython, pypy, etc), and python version. :rtype: tuple(str, str, str) """ return (self.operating_system, platform.python_implementation(), platform.python_version()) @property def wake_interval(self): """Property method that returns the wake interval in seconds. :rtype: int """ return (self.config.application.get('wake_interval') or self.WAKE_INTERVAL) def _sleep(self): """Setup the next alarm to fire and then wait for it to fire.""" # Make sure that the application is not shutting down before sleeping if self.is_stopping: LOGGER.debug('Not sleeping, application is trying to shutdown') return # Set the signal timer signal.setitimer(signal.ITIMER_REAL, self.wake_interval, 0) # Toggle that we are running self.set_state(self.STATE_SLEEPING) def _stopped(self): """Sets the state back to idle when shutdown steps are complete.""" LOGGER.debug('Application stopped') self.set_state(self.STATE_STOPPED) def _wake(self, _signal, _frame): """Fired every time the alarm is signaled. If the app is not shutting or shutdown, it will attempt to process. :param int _signal: The signal number :param frame _frame: The stack frame when received """ LOGGER.debug('Application woke up') # Only run the code path if it's not shutting down or shutdown if not any([self.is_stopping, self.is_stopped, self.is_idle]): # Note that we're running self.set_state(self.STATE_ACTIVE) # Process actions for the application self.process() # Exit out if the app is waiting to stop if self.is_waiting_to_stop: return self.set_state(self.STATE_STOPPING) # Wait until the process is to be woken again self._sleep() else: LOGGER.info('Exiting wake interval without sleeping again')