+ add on_error, on_restart etc. to states.py + add n_retry argument to mercury change/multichange
177 lines
5.9 KiB
Python
177 lines
5.9 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.errors import ProgrammingError
|
|
from secop.lib.newstatemachine import StateMachine, Retry, Finish, Start, Stop
|
|
|
|
|
|
class status_code:
|
|
"""decorator for state methods"""
|
|
def __init__(self, code, text=None):
|
|
self.code = code
|
|
self.text = text
|
|
|
|
def __set_name__(self, owner, name):
|
|
if not issubclass(owner, HasStates):
|
|
raise ProgrammingError('when using decorator "status_code", %s must inherit HasStates' % owner.__name__)
|
|
self.cls = owner
|
|
self.name = name
|
|
if 'statusMap' not in owner.__dict__:
|
|
# we need a copy on each inheritance level
|
|
owner.statusMap = owner.statusMap.copy()
|
|
owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text
|
|
setattr(owner, name, self.func) # replace with original method
|
|
|
|
def __call__(self, func):
|
|
self.func = func
|
|
return self
|
|
|
|
|
|
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 = {}
|
|
|
|
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.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__
|
|
status = self.statusMap.get(name)
|
|
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 doPoll(self):
|
|
super().doPoll()
|
|
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 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
|