When transition between states, start at max rather than min
[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 reinitialize_state(self):
48                 pass
49
50         def increment_state(self):
51                 pass
52
53         @property
54         def timeout(self):
55                 return UpdateStateMachine.INFINITE_PERIOD
56
57         def __repr__(self):
58                 return "NopStateStrategy()"
59
60
61 class ConstantStateStrategy(object):
62
63         def __init__(self, timeout):
64                 assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
65                 self._timeout = timeout
66
67         def initialize_state(self):
68                 pass
69
70         def reinitialize_state(self):
71                 pass
72
73         def increment_state(self):
74                 pass
75
76         @property
77         def timeout(self):
78                 return self._timeout
79
80         def __repr__(self):
81                 return "ConstantStateStrategy(timeout=%r)" % self._timeout
82
83
84 class GeometricStateStrategy(object):
85
86         def __init__(self, init, min, max):
87                 assert 0 < init and init < max or init == UpdateStateMachine.INFINITE_PERIOD
88                 assert 0 < min or min == UpdateStateMachine.INFINITE_PERIOD
89                 assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
90                 self._min = min
91                 self._max = max
92                 self._init = init
93                 self._current = 0
94
95         def initialize_state(self):
96                 self._current = self._max
97
98         def reinitialize_state(self):
99                 self._current = self._min
100
101         def increment_state(self):
102                 if self._current == UpdateStateMachine.INFINITE_PERIOD:
103                         pass
104                 if self._init == UpdateStateMachine.INFINITE_PERIOD:
105                         self._current = UpdateStateMachine.INFINITE_PERIOD
106                 elif self._max == UpdateStateMachine.INFINITE_PERIOD:
107                         self._current *= 2
108                 else:
109                         self._current = min(2 * self._current, self._max - self._init)
110
111         @property
112         def timeout(self):
113                 if UpdateStateMachine.INFINITE_PERIOD in (self._init, self._current):
114                         timeout = UpdateStateMachine.INFINITE_PERIOD
115                 else:
116                         timeout = self._init + self._current
117                 return timeout
118
119         def __repr__(self):
120                 return "GeometricStateStrategy(init=%r, min=%r, max=%r)" % (
121                         self._init, self._min, self._max
122                 )
123
124
125 class StateMachine(object):
126
127         STATE_ACTIVE = 0, "active"
128         STATE_IDLE = 1, "idle"
129         STATE_DND = 2, "dnd"
130
131         def start(self):
132                 raise NotImplementedError("Abstract")
133
134         def stop(self):
135                 raise NotImplementedError("Abstract")
136
137         def close(self):
138                 raise NotImplementedError("Abstract")
139
140         def set_state(self, state):
141                 raise NotImplementedError("Abstract")
142
143         @property
144         def state(self):
145                 raise NotImplementedError("Abstract")
146
147
148 class MasterStateMachine(StateMachine):
149
150         def __init__(self):
151                 self._machines = []
152                 self._state = self.STATE_ACTIVE
153
154         def append_machine(self, machine):
155                 self._machines.append(machine)
156
157         def start(self):
158                 # Confirm we are all on the same page
159                 for machine in self._machines:
160                         machine.set_state(self._state)
161                 for machine in self._machines:
162                         machine.start()
163
164         def stop(self):
165                 for machine in self._machines:
166                         machine.stop()
167
168         def close(self):
169                 for machine in self._machines:
170                         machine.close()
171
172         def set_state(self, state):
173                 self._state = state
174                 for machine in self._machines:
175                         machine.set_state(state)
176
177         @property
178         def state(self):
179                 return self._state
180
181
182 class UpdateStateMachine(StateMachine):
183         # Making sure the it is initialized is finicky, be careful
184
185         INFINITE_PERIOD = -1
186         DEFAULT_MAX_TIMEOUT = to_seconds(hours=24)
187
188         _IS_DAEMON = True
189
190         def __init__(self, updateItems, name="", maxTime = DEFAULT_MAX_TIMEOUT):
191                 self._name = name
192                 self._updateItems = updateItems
193                 self._maxTime = maxTime
194
195                 self._state = self.STATE_ACTIVE
196                 self._timeoutId = None
197
198                 self._strategies = {}
199                 self._callback = coroutines.func_sink(
200                         coroutines.expand_positional(
201                                 self._request_reset_timers
202                         )
203                 )
204
205         def __repr__(self):
206                 return """UpdateStateMachine(
207         name=%r,
208         strategie=%r,
209 )""" % (self._name, self._strategies)
210
211         def set_state_strategy(self, state, strategy):
212                 self._strategies[state] = strategy
213
214         def start(self):
215                 assert self._timeoutId is None
216                 for strategy in self._strategies.itervalues():
217                         strategy.initialize_state()
218                 if self._strategy.timeout != self.INFINITE_PERIOD:
219                         self._timeoutId = gobject.idle_add(self._on_timeout)
220                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
221
222         def stop(self):
223                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
224                 self._stop_update()
225
226         def close(self):
227                 assert self._timeoutId is None
228                 self._callback = None
229
230         def set_state(self, newState):
231                 if self._state == newState:
232                         return
233                 oldState = self._state
234                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
235
236                 self._state = newState
237                 self._reset_timers(initialize=True)
238
239         @property
240         def state(self):
241                 return self._state
242
243         def reset_timers(self):
244                 self._reset_timers()
245
246         @property
247         def request_reset_timers(self):
248                 return self._callback
249
250         @property
251         def _strategy(self):
252                 return self._strategies[self._state]
253
254         @gtk_toolbox.log_exception(_moduleLogger)
255         def _request_reset_timers(self, *args):
256                 self._reset_timers()
257
258         def _reset_timers(self, initialize=False):
259                 if self._timeoutId is None:
260                         return # not started yet
261                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
262                 self._stop_update()
263                 if initialize:
264                         self._strategy.initialize_state()
265                 else:
266                         self._strategy.reinitialize_state()
267                 self._schedule_update()
268
269         def _stop_update(self):
270                 if self._timeoutId is None:
271                         return
272                 gobject.source_remove(self._timeoutId)
273                 self._timeoutId = None
274
275         def _schedule_update(self):
276                 assert self._timeoutId is None
277                 self._strategy.increment_state()
278                 nextTimeout = self._strategy.timeout
279                 if nextTimeout != self.INFINITE_PERIOD and nextTimeout < self._maxTime:
280                         assert 0 < nextTimeout
281                         self._timeoutId = gobject_utils.timeout_add_seconds(nextTimeout, self._on_timeout)
282                         _moduleLogger.info("%s Next update in %s seconds" % (self._name, nextTimeout, ))
283                 else:
284                         _moduleLogger.info("%s No further updates (timeout is %s seconds)" % (self._name, nextTimeout, ))
285
286         @gtk_toolbox.log_exception(_moduleLogger)
287         def _on_timeout(self):
288                 self._timeoutId = None
289                 self._schedule_update()
290                 for item in self._updateItems:
291                         try:
292                                 item.update(force=True)
293                         except Exception:
294                                 _moduleLogger.exception("Update failed for %r" % item)
295                 return False # do not continue