| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import dbus |
| import dbus.service |
| import gobject |
| import logging |
| |
| import pm_errors |
| import pm_constants |
| import utils |
| |
| from autotest_lib.client.cros.cellular import mm1_constants |
| |
| class StateMachine(dbus.service.Object): |
| """ |
| StateMachine is the abstract base class for the complex state machines |
| that are involved in the pseudo modem manager. |
| |
| Every state transition is managed by a function that has been mapped to a |
| specific modem state. For example, the method that handles the case where |
| the modem is in the ENABLED state would look like: |
| |
| def _HandleEnabledState(self): |
| # Do stuff. |
| |
| The correct method will be dynamically located and executed by the step |
| function according to the dictionary returned by the subclass' |
| implementation of StateMachine._GetModemStateFunctionMap. |
| |
| Using the StateMachine in |interactive| mode: |
| In interactive mode, the state machine object exposes a dbus object under |
| the object path |pm_constants.TESTING_PATH|/|self._GetIsmObjectName()|, |
| where |self._GetIsmObjectName()| returns the dbus object name to be used. |
| |
| In this mode, the state machine waits for a dbus method call |
| |pm_constants.I_TESTING_ISM|.|Advance| when a state transition is possible |
| before actually executing the transition. |
| |
| """ |
| def __init__(self, modem): |
| super(StateMachine, self).__init__(None, None) |
| self._modem = modem |
| self._started = False |
| self._done = False |
| self._interactive = False |
| self._trans_func_map = self._GetModemStateFunctionMap() |
| |
| |
| def __exit__(self): |
| self.remove_from_connection() |
| |
| |
| @property |
| def cancelled(self): |
| """ |
| @returns: True, if the state machine has been cancelled or has |
| transitioned to a terminal state. False, otherwise. |
| |
| """ |
| return self._done |
| |
| |
| def Cancel(self): |
| """ |
| Tells the state machine to stop transitioning to further states. |
| |
| """ |
| self._done = True |
| |
| |
| def EnterInteractiveMode(self, bus): |
| """ |
| Run this machine in interactive mode. |
| |
| This function must be called before |Start|. In this mode, the machine |
| waits for an |Advance| call before each step. |
| |
| @param bus: The bus on which the testing interface must be exported. |
| |
| """ |
| if not bus: |
| self.warning('Cannot enter interactive mode without a |bus|.') |
| return |
| |
| self._interactive = True |
| self._ism_object_path = '/'.join([pm_constants.TESTING_PATH, |
| self._GetIsmObjectName()]) |
| self.add_to_connection(bus, self._ism_object_path) |
| self._interactive = True |
| self._waiting_for_advance = False |
| logging.info('Running state machine in interactive mode') |
| logging.info('Exported test object at %s', self._ism_object_path) |
| |
| |
| def Start(self): |
| """ Start the state machine. """ |
| self.Step() |
| |
| |
| @utils.log_dbus_method() |
| @dbus.service.method(pm_constants.I_TESTING_ISM, out_signature='b') |
| def Advance(self): |
| """ |
| Advance a step on a state machine running in interactive mode. |
| |
| @returns: True if the state machine was advanced. False otherwise. |
| @raises: TestError if called on a non-interactive state machine. |
| |
| """ |
| if not self._interactive: |
| raise pm_errors.TestError( |
| 'Can not advance a non-interactive state machine') |
| |
| if not self._waiting_for_advance: |
| logging.warning('%s received an unexpected advance request', |
| self._GetIsmObjectName()) |
| return False |
| logging.info('%s state machine advancing', self._GetIsmObjectName()) |
| self._waiting_for_advance = False |
| if not self._next_transition(self): |
| self._done = True |
| self._ScheduleNextStep() |
| return True |
| |
| |
| @dbus.service.signal(pm_constants.I_TESTING_ISM) |
| def Waiting(self): |
| """ |
| Signal sent out by an interactive machine when it is waiting for remote |
| dbus call on the |Advance| function. |
| |
| """ |
| logging.info('%s state machine waiting', self._GetIsmObjectName()) |
| |
| |
| @utils.log_dbus_method() |
| @dbus.service.method(pm_constants.I_TESTING_ISM, out_signature='b') |
| def IsWaiting(self): |
| """ |
| Determine whether the state machine is waiting for user action. |
| |
| @returns: True if machine is waiting for |Advance| call. |
| |
| """ |
| return self._waiting_for_advance |
| |
| |
| def Step(self): |
| """ |
| Executes the next corresponding state transition based on the modem |
| state. |
| |
| """ |
| logging.info('StateMachine: Step') |
| if self._done: |
| logging.info('StateMachine: Terminating.') |
| return |
| |
| if not self._started: |
| if not self._ShouldStartStateMachine(): |
| logging.info('StateMachine cannot start.') |
| return |
| self._started = True |
| |
| state = self._GetCurrentState() |
| func = self._trans_func_map.get(state, self._GetDefaultHandler()) |
| if not self._interactive: |
| if func and func(self): |
| self._ScheduleNextStep() |
| else: |
| self._done = True |
| return |
| |
| assert not self._waiting_for_advance |
| if func: |
| self._next_transition = func |
| self._waiting_for_advance = True |
| self.Waiting() # Wait for user to |Advance| the machine. |
| else: |
| self._done = True |
| |
| |
| def _ScheduleNextStep(self): |
| """ |
| Schedules the next state transition to execute on the idle loop. |
| subclasses can override this method to implement custom logic, such as |
| delays. |
| |
| """ |
| gobject.idle_add(StateMachine.Step, self) |
| |
| |
| def _GetIsmObjectName(self): |
| """ |
| The name of the dbus object exposed by this object with |I_TESTING_ISM| |
| interface. |
| |
| By default, this is the name of the most concrete class of the object. |
| |
| """ |
| return self.__class__.__name__ |
| |
| |
| def _GetDefaultHandler(self): |
| """ |
| Returns the function to handle a modem state, for which the value |
| returned by StateMachine._GetModemStateFunctionMap is None. The |
| returned function's signature must match: |
| |
| StateMachine -> Boolean |
| |
| This function by default returns None. If no function exists to handle |
| a modem state, the default behavior is to terminate the state machine. |
| |
| """ |
| return None |
| |
| |
| def _GetModemStateFunctionMap(self): |
| """ |
| Returns a mapping from modem states to corresponding transition |
| functions to execute. The returned function's signature must match: |
| |
| StateMachine -> Boolean |
| |
| The first argument to the function is a state machine, which will |
| typically be passed a value of |self|. The return value, if True, |
| indicates that the state machine should keep executing further state |
| transitions. A return value of False indicates that the state machine |
| will transition to a terminal state. |
| |
| This method must be implemented by a subclass. Subclasses can further |
| override this method to provide custom functionality. |
| |
| """ |
| raise NotImplementedError() |
| |
| |
| def _ShouldStartStateMachine(self): |
| """ |
| This method will be called when the state machine is in a starting |
| state. This method should return True, if the state machine can |
| successfully begin its state transitions, False if it should not |
| proceed. This method can also raise an exception in the failure case. |
| |
| In the success case, this method should also execute any necessary |
| initialization steps. |
| |
| This method must be implemented by a subclass. Subclasses can |
| further override this method to provide custom functionality. |
| |
| """ |
| raise NotImplementedError() |
| |
| |
| def _GetCurrentState(self): |
| """ |
| Get the current state of the state machine. |
| |
| This method is called to get the current state of the machine when |
| deciding what the next transition should be. |
| By default, the state machines are tied to the modem state, and this |
| function simply returns the modem state. |
| |
| Subclasses can override this function to use custom states in the state |
| machine. |
| |
| @returns: The modem state. |
| |
| """ |
| return self._modem.Get(mm1_constants.I_MODEM, 'State') |