|
| 1 | +import sys |
| 2 | +from collections import namedtuple |
| 3 | +from enum import IntEnum |
| 4 | + |
| 5 | + |
| 6 | +class ProcessState(IntEnum): |
| 7 | + """Oredered enum to make state checks easy. |
| 8 | +
|
| 9 | + For example Alive can be determined using >= ProcessState.ALIVE, |
| 10 | + which will return True if the state is READY as well as ALIVE. |
| 11 | + """ |
| 12 | + NOT_STARTED = 0 |
| 13 | + STARTED = 1 |
| 14 | + ERROR = 2 |
| 15 | + STOPPING = 3 |
| 16 | + ALIVE = 4 |
| 17 | + READY = 5 |
| 18 | + |
| 19 | + |
| 20 | +# Process state change callback mappings. |
| 21 | +_STATUS_CALLBACKS = [ |
| 22 | + 'on_started', |
| 23 | + 'on_alive', |
| 24 | + 'on_ready', |
| 25 | + 'on_error', |
| 26 | + 'on_stopping', |
| 27 | +] |
| 28 | + |
| 29 | +# namedtuple defaults only available on 3.7 and later python versions |
| 30 | +if sys.version_info < (3, 7): |
| 31 | + StatusCallbackMap = namedtuple('CallbackMap', _STATUS_CALLBACKS) |
| 32 | + StatusCallbackMap.__new__.__defaults__ = (None,) * 5 |
| 33 | +else: |
| 34 | + StatusCallbackMap = namedtuple( |
| 35 | + 'CallbackMap', |
| 36 | + _STATUS_CALLBACKS, |
| 37 | + defaults=(None,) * len(_STATUS_CALLBACKS), |
| 38 | + ) |
| 39 | + |
| 40 | + |
| 41 | +class ProcessStatus: |
| 42 | + """Process status tracker. |
| 43 | +
|
| 44 | + The class tracks process status and execute callback methods on |
| 45 | + state changes as well as replies to messagebus queries of the |
| 46 | + process status. |
| 47 | +
|
| 48 | + Args: |
| 49 | + name (str): process name, will be used to create the messagebus |
| 50 | + messagetype "mycroft.{name}...". |
| 51 | + bus (MessageBusClient): Connection to the Mycroft messagebus. |
| 52 | + callback_map (StatusCallbackMap): optionally, status callbacks for the |
| 53 | + various status changes. |
| 54 | + """ |
| 55 | + |
| 56 | + def __init__(self, name, bus=None, callback_map=None, namespace="mycroft"): |
| 57 | + self.name = name |
| 58 | + self._namespace = namespace |
| 59 | + self.callbacks = callback_map or StatusCallbackMap() |
| 60 | + self.state = ProcessState.NOT_STARTED |
| 61 | + |
| 62 | + # Messagebus connection |
| 63 | + self.bus = None |
| 64 | + if bus: |
| 65 | + self.bind(bus) |
| 66 | + |
| 67 | + def bind(self, bus): |
| 68 | + self.bus = bus |
| 69 | + self._register_handlers() |
| 70 | + |
| 71 | + def _register_handlers(self): |
| 72 | + """Register messagebus handlers for status queries.""" |
| 73 | + self.bus.on(f'{self._namespace}.{self.name}.is_alive', self.check_alive) |
| 74 | + self.bus.on(f'{self._namespace}.{self.name}.is_ready', self.check_ready) |
| 75 | + |
| 76 | + # The next one is for backwards compatibility |
| 77 | + self.bus.on(f'mycroft.{self.name}.all_loaded', self.check_ready) |
| 78 | + |
| 79 | + def check_alive(self, message=None): |
| 80 | + """Respond to is_alive status request. |
| 81 | +
|
| 82 | + Args: |
| 83 | + message: Optional message to respond to, if omitted no message |
| 84 | + is sent. |
| 85 | +
|
| 86 | + Returns: |
| 87 | + bool, True if process is alive. |
| 88 | + """ |
| 89 | + is_alive = self.state >= ProcessState.ALIVE |
| 90 | + |
| 91 | + if message: |
| 92 | + status = {'status': is_alive} |
| 93 | + self.bus.emit(message.response(data=status)) |
| 94 | + |
| 95 | + return is_alive |
| 96 | + |
| 97 | + def check_ready(self, message=None): |
| 98 | + """Respond to all_loaded status request. |
| 99 | +
|
| 100 | + Args: |
| 101 | + message: Optional message to respond to, if omitted no message |
| 102 | + is sent. |
| 103 | +
|
| 104 | + Returns: |
| 105 | + bool, True if process is ready. |
| 106 | + """ |
| 107 | + is_ready = self.state >= ProcessState.READY |
| 108 | + if message: |
| 109 | + status = {'status': is_ready} |
| 110 | + self.bus.emit(message.response(data=status)) |
| 111 | + |
| 112 | + return is_ready |
| 113 | + |
| 114 | + def set_started(self): |
| 115 | + """Process is started.""" |
| 116 | + self.state = ProcessState.STARTED |
| 117 | + if self.callbacks.on_started: |
| 118 | + self.callbacks.on_started() |
| 119 | + |
| 120 | + def set_alive(self): |
| 121 | + """Basic loading is done.""" |
| 122 | + self.state = ProcessState.ALIVE |
| 123 | + if self.callbacks.on_alive: |
| 124 | + self.callbacks.on_alive() |
| 125 | + |
| 126 | + def set_ready(self): |
| 127 | + """All loading is done.""" |
| 128 | + self.state = ProcessState.READY |
| 129 | + if self.callbacks.on_ready: |
| 130 | + self.callbacks.on_ready() |
| 131 | + |
| 132 | + def set_stopping(self): |
| 133 | + """Process shutdown has started.""" |
| 134 | + self.state = ProcessState.STOPPING |
| 135 | + if self.callbacks.on_stopping: |
| 136 | + self.callbacks.on_stopping() |
| 137 | + |
| 138 | + def set_error(self, err=''): |
| 139 | + """An error has occured and the process is non-functional.""" |
| 140 | + # Intentionally leave is_started True |
| 141 | + self.state = ProcessState.ERROR |
| 142 | + if self.callbacks.on_error: |
| 143 | + self.callbacks.on_error(err) |
0 commit comments