#!/usr/bin/env python # -*- coding: utf-8 -*- # ***************************************************************************** # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Module authors: # Markus Zolliker # # ***************************************************************************** """state machine mixin handles status depending on statemachine state """ from secop.core import BUSY, IDLE, ERROR, Parameter, Command, Done from secop.lib.newstatemachine import StateMachine, Retry, Finish, Start, Stop def status_code(code, text=None): """decorator, attaching a status to a state function :param code: the first element of the secop status tuple :param text: the second element of the secop status tuple. if not given, the name of the state function is used (underscores replaced by spaces) :return: the decorator function if a state function has not attached status and is a method of the module running the state machine, the status is inherited from an overridden method, if available a state function without attached status does not change the status, or, if it is used as the start function, BUSY is taken as default status code """ def wrapper(func): func.status = code, func.__name__.replace('_', ' ') if text is None else text return func return wrapper class HasStates: status = Parameter() # make sure this is a parameter all_status_changes = False # when true, send also updates for status changes within a cycle _state_machine = None _status = IDLE, '' statusMap = None def init_state_machine(self, **kwds): self._state_machine = StateMachine( logger=self.log, idle_status=(IDLE, ''), transition=self.state_transition, reset_fast_poll=False, status=(IDLE, ''), **kwds) def initModule(self): super().initModule() self.statusMap = {} self.init_state_machine() def state_transition(self, sm, newstate): """handle status updates""" status = self.get_status(newstate) if sm.next_task: if isinstance(sm.next_task, Stop): if newstate and status is not None: status = status[0], 'stopping (%s)' % status[1] elif newstate: # restart case if status is not None: status = sm.status[0], 'restarting (%s)' % status[1] else: # start case status = self.get_status(sm.next_task.newstate, BUSY) if status: sm.status = status if self.all_status_changes: self.read_status() def get_status(self, statefunc, default_code=None): if statefunc is None: status = self._state_machine.idle_status or (ERROR, 'Finish was returned without final status') else: name = statefunc.__name__ try: # look up in statusMap cache status = self.statusMap[name] except KeyError: # try to get status from method or inherited method cls = type(self) for base in cls.__mro__: try: status = getattr(base, name, None).status break except AttributeError: pass else: status = None # store it in the cache for all further calls self.statusMap[name] = status if status is None and default_code is not None: status = default_code, name.replace('_', ' ') return status def read_status(self): sm = self._state_machine if sm.status == self.status: return Done return sm.status def cycle_machine(self): sm = self._state_machine sm.cycle() if sm.statefunc is None: if sm.reset_fast_poll: sm.reset_fast_poll = False self.setFastPoll(False) self.read_status() def doPoll(self): super().doPoll() self.cycle_machine() def on_cleanup(self, sm): if isinstance(sm.cleanup_reason, Exception): return self.on_error(sm) if isinstance(sm.cleanup_reason, Start): return self.on_restart(sm) if isinstance(sm.cleanup_reason, Stop): return self.on_stop(sm) self.log.error('bad cleanup reason %r', sm.cleanup_reason) def on_error(self, sm): self.log.error('handle error %r', sm.cleanup_reason) self.final_status(ERROR, repr(sm.cleanup_reason)) return None def on_restart(self, sm): return None def on_stop(self, sm): return None def start_machine(self, statefunc, fast_poll=True, **kwds): sm = self._state_machine sm.status = self.get_status(statefunc, BUSY) if sm.statefunc: sm.status = sm.status[0], 'restarting' sm.start(statefunc, cleanup=kwds.pop('cleanup', self.on_cleanup), **kwds) self.read_status() if fast_poll: sm.reset_fast_poll = True self.setFastPoll(True) self.pollInfo.trigger(True) # trigger poller def stop_machine(self, stopped_status=(IDLE, 'stopped')): sm = self._state_machine if sm.is_active: sm.idle_status = stopped_status sm.stop() sm.status = self.get_status(sm.statefunc, sm.status[0])[0], 'stopping' self.read_status() self.pollInfo.trigger(True) # trigger poller @Command def stop(self): self.stop_machine() def final_status(self, code=IDLE, text=''): """final status Usage: return self.final_status('IDLE', 'machine idle') """ sm = self._state_machine sm.idle_status = code, text sm.cleanup = None return Finish