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