1895a5d3a35ab7bac3572819ec364e058d7eda51
[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                 if self._strategy.timeout != self.INFINITE_PERIOD:
176                         self._timeoutId = gobject.idle_add(self._on_timeout)
177                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
178
179         def stop(self):
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                 self._reset_timers()
201
202         @property
203         def request_reset_timers(self):
204                 return self._callback
205
206         @property
207         def _strategy(self):
208                 return self._strategies[self._state]
209
210         @gtk_toolbox.log_exception(_moduleLogger)
211         def _request_reset_timers(self, *args):
212                 self._reset_timers()
213
214         def _set_initial_period(self):
215                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
216
217         def _schedule_update(self):
218                 assert self._timeoutId is None
219                 self._strategy.increment_state()
220                 nextTimeout = self._strategy.timeout
221                 if nextTimeout != self.INFINITE_PERIOD:
222                         self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
223                 _moduleLogger.debug("%s Next update in %s ms" % (self._name, nextTimeout, ))
224
225         def _stop_update(self):
226                 if self._timeoutId is None:
227                         return
228                 gobject.source_remove(self._timeoutId)
229                 self._timeoutId = None
230
231         def _reset_timers(self):
232                 if self._timeoutId is None:
233                         return # not started yet
234                 self._stop_update()
235                 self._strategy.initialize_state()
236                 self._schedule_update()
237
238         @gtk_toolbox.log_exception(_moduleLogger)
239         def _on_timeout(self):
240                 _moduleLogger.info("%s Update" % (self._name))
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