1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import dbus
6import dbus.service
7import gobject
8import logging
9
10import pm_errors
11import pm_constants
12import utils
13
14from autotest_lib.client.cros.cellular import mm1_constants
15
16class StateMachine(dbus.service.Object):
17    """
18    StateMachine is the abstract base class for the complex state machines
19    that are involved in the pseudo modem manager.
20
21    Every state transition is managed by a function that has been mapped to a
22    specific modem state. For example, the method that handles the case where
23    the modem is in the ENABLED state would look like:
24
25        def _HandleEnabledState(self):
26            # Do stuff.
27
28    The correct method will be dynamically located and executed by the step
29    function according to the dictionary returned by the subclass'
30    implementation of StateMachine._GetModemStateFunctionMap.
31
32    Using the StateMachine in |interactive| mode:
33    In interactive mode, the state machine object exposes a dbus object under
34    the object path |pm_constants.TESTING_PATH|/|self._GetIsmObjectName()|,
35    where |self._GetIsmObjectName()| returns the dbus object name to be used.
36
37    In this mode, the state machine waits for a dbus method call
38    |pm_constants.I_TESTING_ISM|.|Advance| when a state transition is possible
39    before actually executing the transition.
40
41    """
42    def __init__(self, modem):
43        super(StateMachine, self).__init__(None, None)
44        self._modem = modem
45        self._started = False
46        self._done = False
47        self._interactive = False
48        self._trans_func_map = self._GetModemStateFunctionMap()
49
50
51    def __exit__(self):
52        self.remove_from_connection()
53
54
55    @property
56    def cancelled(self):
57        """
58        @returns: True, if the state machine has been cancelled or has
59                transitioned to a terminal state. False, otherwise.
60
61        """
62        return self._done
63
64
65    def Cancel(self):
66        """
67        Tells the state machine to stop transitioning to further states.
68
69        """
70        self._done = True
71
72
73    def EnterInteractiveMode(self, bus):
74        """
75        Run this machine in interactive mode.
76
77        This function must be called before |Start|. In this mode, the machine
78        waits for an |Advance| call before each step.
79
80        @param bus: The bus on which the testing interface must be exported.
81
82        """
83        if not bus:
84            self.warning('Cannot enter interactive mode without a |bus|.')
85            return
86
87        self._interactive = True
88        self._ism_object_path = '/'.join([pm_constants.TESTING_PATH,
89                                          self._GetIsmObjectName()])
90        self.add_to_connection(bus, self._ism_object_path)
91        self._interactive = True
92        self._waiting_for_advance = False
93        logging.info('Running state machine in interactive mode')
94        logging.info('Exported test object at %s', self._ism_object_path)
95
96
97    def Start(self):
98        """ Start the state machine. """
99        self.Step()
100
101
102    @utils.log_dbus_method()
103    @dbus.service.method(pm_constants.I_TESTING_ISM, out_signature='b')
104    def Advance(self):
105        """
106        Advance a step on a state machine running in interactive mode.
107
108        @returns: True if the state machine was advanced. False otherwise.
109        @raises: TestError if called on a non-interactive state machine.
110
111        """
112        if not self._interactive:
113            raise pm_errors.TestError(
114                    'Can not advance a non-interactive state machine')
115
116        if not self._waiting_for_advance:
117            logging.warning('%s received an unexpected advance request',
118                            self._GetIsmObjectName())
119            return False
120        logging.info('%s state machine advancing', self._GetIsmObjectName())
121        self._waiting_for_advance = False
122        if not self._next_transition(self):
123            self._done = True
124        self._ScheduleNextStep()
125        return True
126
127
128    @dbus.service.signal(pm_constants.I_TESTING_ISM)
129    def Waiting(self):
130        """
131        Signal sent out by an interactive machine when it is waiting for remote
132        dbus call  on the |Advance| function.
133
134        """
135        logging.info('%s state machine waiting', self._GetIsmObjectName())
136
137
138    @utils.log_dbus_method()
139    @dbus.service.method(pm_constants.I_TESTING_ISM, out_signature='b')
140    def IsWaiting(self):
141        """
142        Determine whether the state machine is waiting for user action.
143
144        @returns: True if machine is waiting for |Advance| call.
145
146        """
147        return self._waiting_for_advance
148
149
150    def Step(self):
151        """
152        Executes the next corresponding state transition based on the modem
153        state.
154
155        """
156        logging.info('StateMachine: Step')
157        if self._done:
158            logging.info('StateMachine: Terminating.')
159            return
160
161        if not self._started:
162            if not self._ShouldStartStateMachine():
163                logging.info('StateMachine cannot start.')
164                return
165            self._started = True
166
167        state = self._GetCurrentState()
168        func = self._trans_func_map.get(state, self._GetDefaultHandler())
169        if not self._interactive:
170            if func and func(self):
171                self._ScheduleNextStep()
172            else:
173                self._done = True
174            return
175
176        assert not self._waiting_for_advance
177        if func:
178            self._next_transition = func
179            self._waiting_for_advance = True
180            self.Waiting()  # Wait for user to |Advance| the machine.
181        else:
182            self._done = True
183
184
185    def _ScheduleNextStep(self):
186        """
187        Schedules the next state transition to execute on the idle loop.
188        subclasses can override this method to implement custom logic, such as
189        delays.
190
191        """
192        gobject.idle_add(StateMachine.Step, self)
193
194
195    def _GetIsmObjectName(self):
196        """
197        The name of the dbus object exposed by this object with |I_TESTING_ISM|
198        interface.
199
200        By default, this is the name of the most concrete class of the object.
201
202        """
203        return self.__class__.__name__
204
205
206    def _GetDefaultHandler(self):
207        """
208        Returns the function to handle a modem state, for which the value
209        returned by StateMachine._GetModemStateFunctionMap is None. The
210        returned function's signature must match:
211
212            StateMachine -> Boolean
213
214        This function by default returns None. If no function exists to handle
215        a modem state, the default behavior is to terminate the state machine.
216
217        """
218        return None
219
220
221    def _GetModemStateFunctionMap(self):
222        """
223        Returns a mapping from modem states to corresponding transition
224        functions to execute. The returned function's signature must match:
225
226            StateMachine -> Boolean
227
228        The first argument to the function is a state machine, which will
229        typically be passed a value of |self|. The return value, if True,
230        indicates that the state machine should keep executing further state
231        transitions. A return value of False indicates that the state machine
232        will transition to a terminal state.
233
234        This method must be implemented by a subclass. Subclasses can further
235        override this method to provide custom functionality.
236
237        """
238        raise NotImplementedError()
239
240
241    def _ShouldStartStateMachine(self):
242        """
243        This method will be called when the state machine is in a starting
244        state. This method should return True, if the state machine can
245        successfully begin its state transitions, False if it should not
246        proceed. This method can also raise an exception in the failure case.
247
248        In the success case, this method should also execute any necessary
249        initialization steps.
250
251        This method must be implemented by a subclass. Subclasses can
252        further override this method to provide custom functionality.
253
254        """
255        raise NotImplementedError()
256
257
258    def _GetCurrentState(self):
259        """
260        Get the current state of the state machine.
261
262        This method is called to get the current state of the machine when
263        deciding what the next transition should be.
264        By default, the state machines are tied to the modem state, and this
265        function simply returns the modem state.
266
267        Subclasses can override this function to use custom states in the state
268        machine.
269
270        @returns: The modem state.
271
272        """
273        return self._modem.Get(mm1_constants.I_MODEM, 'State')
274