Removing a SM hack
[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 and init < max and init != UpdateStateMachine.INFINITE_PERIOD
69                 assert 0 < min and 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 = 0
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, name=""):
155                 self._name = name
156                 self._updateItems = updateItems
157
158                 self._state = self.STATE_ACTIVE
159                 self._timeoutId = None
160
161                 self._strategies = {}
162                 self._callback = coroutines.func_sink(
163                         coroutines.expand_positional(
164                                 self._request_reset_timers
165                         )
166                 )
167
168         def set_state_strategy(self, state, strategy):
169                 self._strategies[state] = strategy
170
171         def start(self):
172                 assert self._timeoutId is None
173                 for strategy in self._strategies.itervalues():
174                         strategy.initialize_state()
175                 self._timeoutId = gobject.idle_add(self._on_timeout)
176                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
177
178         def stop(self):
179                 self._stop_update()
180
181         def close(self):
182                 self._callback = None
183
184         def set_state(self, newState):
185                 if self._state == newState:
186                         return
187                 oldState = self._state
188                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
189
190                 self._state = newState
191                 self._reset_timers()
192
193         @property
194         def state(self):
195                 return self._state
196
197         def reset_timers(self):
198                 self._reset_timers()
199
200         @property
201         def request_reset_timers(self):
202                 return self._callback
203
204         @property
205         def _strategy(self):
206                 return self._strategies[self._state]
207
208         @gtk_toolbox.log_exception(_moduleLogger)
209         def _request_reset_timers(self, *args):
210                 self._reset_timers()
211
212         def _set_initial_period(self):
213                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
214
215         def _schedule_update(self):
216                 assert self._timeoutId is None
217                 self._strategy.increment_state()
218                 nextTimeout = self._strategy.timeout
219                 if nextTimeout != self.INFINITE_PERIOD:
220                         self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
221                 _moduleLogger.info("%s Next update in %s ms" % (self._name, nextTimeout, ))
222
223         def _stop_update(self):
224                 if self._timeoutId is None:
225                         return
226                 gobject.source_remove(self._timeoutId)
227                 self._timeoutId = None
228
229         def _reset_timers(self):
230                 if self._timeoutId is None:
231                         return # not started yet
232                 self._stop_update()
233                 self._strategy.initialize_state()
234                 self._schedule_update()
235
236         @gtk_toolbox.log_exception(_moduleLogger)
237         def _on_timeout(self):
238                 _moduleLogger.debug("%s Update" % (self._name))
239                 for item in self._updateItems:
240                         try:
241                                 item.update(force=True)
242                         except Exception:
243                                 _moduleLogger.exception("Update failed for %r" % item)
244                 self._timeoutId = None
245                 self._schedule_update()
246                 return False # do not continue