Home | History | Annotate | Download | only in pseudomodem
      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 
      5 import dbus
      6 import dbus.service
      7 import gobject
      8 import logging
      9 
     10 import pm_errors
     11 import pm_constants
     12 import utils
     13 
     14 from autotest_lib.client.cros.cellular import mm1_constants
     15 
     16 class 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