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