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