Hardening against some types of error
[theonering] / src / gvoice / state_machine.py
1 #!/usr/bin/env python
2
3 import logging
4
5 import gobject
6
7 import util.go_utils as gobject_utils
8 import util.coroutines as coroutines
9 import gtk_toolbox
10
11
12 _moduleLogger = logging.getLogger("gvoice.state_machine")
13
14
15 def to_milliseconds(**kwd):
16         if "milliseconds" in kwd:
17                 return kwd["milliseconds"]
18         elif "seconds" in kwd:
19                 return kwd["seconds"] * 1000
20         elif "minutes" in kwd:
21                 return kwd["minutes"] * 1000 * 60
22         elif "hours" in kwd:
23                 return kwd["hours"] * 1000 * 60 * 60
24         raise KeyError("Unknown arg: %r" % kwd)
25
26
27 def to_seconds(**kwd):
28         if "milliseconds" in kwd:
29                 return kwd["milliseconds"] / 1000
30         elif "seconds" in kwd:
31                 return kwd["seconds"]
32         elif "minutes" in kwd:
33                 return kwd["minutes"] * 60
34         elif "hours" in kwd:
35                 return kwd["hours"] * 60 * 60
36         raise KeyError("Unknown arg: %r" % kwd)
37
38
39 class NopStateStrategy(object):
40
41         def __init__(self):
42                 pass
43
44         def initialize_state(self):
45                 pass
46
47         def increment_state(self):
48                 pass
49
50         @property
51         def timeout(self):
52                 return UpdateStateMachine.INFINITE_PERIOD
53
54
55 class ConstantStateStrategy(object):
56
57         def __init__(self, timeout):
58                 assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
59                 self._timeout = timeout
60
61         def initialize_state(self):
62                 pass
63
64         def increment_state(self):
65                 pass
66
67         @property
68         def timeout(self):
69                 return self._timeout
70
71
72 class GeometricStateStrategy(object):
73
74         def __init__(self, init, min, max):
75                 assert 0 < init and init < max and init != UpdateStateMachine.INFINITE_PERIOD
76                 assert 0 < min and min != UpdateStateMachine.INFINITE_PERIOD
77                 assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
78                 self._min = min
79                 self._max = max
80                 self._init = init
81                 self._current = 0
82
83         def initialize_state(self):
84                 self._current = self._min
85
86         def increment_state(self):
87                 if self._max == UpdateStateMachine.INFINITE_PERIOD:
88                         self._current *= 2
89                 else:
90                         self._current = min(2 * self._current, self._max - self._init)
91
92         @property
93         def timeout(self):
94                 timeout = self._init + self._current
95                 return timeout
96
97
98 class StateMachine(object):
99
100         STATE_ACTIVE = 0, "active"
101         STATE_IDLE = 1, "idle"
102         STATE_DND = 2, "dnd"
103
104         def start(self):
105                 raise NotImplementedError("Abstract")
106
107         def stop(self):
108                 raise NotImplementedError("Abstract")
109
110         def close(self):
111                 raise NotImplementedError("Abstract")
112
113         def set_state(self, state):
114                 raise NotImplementedError("Abstract")
115
116         @property
117         def state(self):
118                 raise NotImplementedError("Abstract")
119
120
121 class MasterStateMachine(StateMachine):
122
123         def __init__(self):
124                 self._machines = []
125                 self._state = self.STATE_ACTIVE
126
127         def append_machine(self, machine):
128                 self._machines.append(machine)
129
130         def start(self):
131                 # Confirm we are all on the same page
132                 for machine in self._machines:
133                         machine.set_state(self._state)
134                 for machine in self._machines:
135                         machine.start()
136
137         def stop(self):
138                 for machine in self._machines:
139                         machine.stop()
140
141         def close(self):
142                 for machine in self._machines:
143                         machine.close()
144
145         def set_state(self, state):
146                 self._state = state
147                 for machine in self._machines:
148                         machine.set_state(state)
149
150         @property
151         def state(self):
152                 return self._state
153
154
155 class UpdateStateMachine(StateMachine):
156         # Making sure the it is initialized is finicky, be careful
157
158         INFINITE_PERIOD = -1
159
160         _IS_DAEMON = True
161
162         def __init__(self, updateItems, name=""):
163                 self._name = name
164                 self._updateItems = updateItems
165
166                 self._state = self.STATE_ACTIVE
167                 self._timeoutId = None
168
169                 self._strategies = {}
170                 self._callback = coroutines.func_sink(
171                         coroutines.expand_positional(
172                                 self._request_reset_timers
173                         )
174                 )
175
176         def set_state_strategy(self, state, strategy):
177                 self._strategies[state] = strategy
178
179         def start(self):
180                 assert self._timeoutId is None
181                 for strategy in self._strategies.itervalues():
182                         strategy.initialize_state()
183                 if self._strategy.timeout != self.INFINITE_PERIOD:
184                         self._timeoutId = gobject.idle_add(self._on_timeout)
185                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
186
187         def stop(self):
188                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
189                 self._stop_update()
190
191         def close(self):
192                 assert self._timeoutId is None
193                 self._callback = None
194
195         def set_state(self, newState):
196                 if self._state == newState:
197                         return
198                 oldState = self._state
199                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
200
201                 self._state = newState
202                 self._reset_timers()
203
204         @property
205         def state(self):
206                 return self._state
207
208         def reset_timers(self):
209                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
210                 self._reset_timers()
211
212         @property
213         def request_reset_timers(self):
214                 return self._callback
215
216         @property
217         def _strategy(self):
218                 return self._strategies[self._state]
219
220         @gtk_toolbox.log_exception(_moduleLogger)
221         def _request_reset_timers(self, *args):
222                 self._reset_timers()
223
224         def _schedule_update(self):
225                 assert self._timeoutId is None
226                 self._strategy.increment_state()
227                 nextTimeout = self._strategy.timeout
228                 if nextTimeout != self.INFINITE_PERIOD:
229                         self._timeoutId = gobject_utils.timeout_add_seconds(nextTimeout, self._on_timeout)
230                 _moduleLogger.info("%s Next update in %s seconds" % (self._name, nextTimeout, ))
231
232         def _stop_update(self):
233                 if self._timeoutId is None:
234                         return
235                 gobject.source_remove(self._timeoutId)
236                 self._timeoutId = None
237
238         def _reset_timers(self):
239                 if self._timeoutId is None:
240                         return # not started yet
241                 self._stop_update()
242                 self._strategy.initialize_state()
243                 self._schedule_update()
244
245         @gtk_toolbox.log_exception(_moduleLogger)
246         def _on_timeout(self):
247                 self._timeoutId = None
248                 self._schedule_update()
249                 for item in self._updateItems:
250                         try:
251                                 item.update(force=True)
252                         except Exception:
253                                 _moduleLogger.exception("Update failed for %r" % item)
254                 return False # do not continue