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