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