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