frappy/secop/states.py

193 lines
6.6 KiB
Python

#!/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 <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""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