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