1# Copyright (c) 2014 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# This module helps launch pseudomodem as a subprocess. It helps with the 6# initial setup of pseudomodem, as well as ensures proper cleanup. 7# For details about the options accepted by pseudomodem, please check the 8# |pseudomodem| module. 9# This module also doubles as the python entry point to run pseudomodem from the 10# command line. To avoid confusion, please use the shell script run_pseudomodem 11# to run pseudomodem from command line. 12 13import dbus 14import json 15import logging 16import os 17import pwd 18import signal 19import stat 20import sys 21import subprocess 22import tempfile 23 24import common 25from autotest_lib.client.bin import utils 26from autotest_lib.client.common_lib import error 27from autotest_lib.client.cros import service_stopper 28from autotest_lib.client.cros.cellular import mm1_constants 29from autotest_lib.client.cros.cellular import net_interface 30 31import pm_constants 32import pseudomodem 33 34# TODO(pprabhu) Move this to the right utils file. 35# pprabhu: I haven't yet figured out which of the myriad utils files I should 36# update. There is an implementation of |nuke_subprocess| that does not take 37# timeout_hint_seconds in common_lib/base_utils.py, but |poll_for_condition| 38# is not available there. 39def nuke_subprocess(subproc, timeout_hint_seconds=0): 40 """ 41 Attempt to kill the given subprocess via an escalating series of signals. 42 43 Between each attempt, the process is given |timeout_hint_seconds| to clean 44 up. So, the function may take up to 3 * |timeout_hint_seconds| time to 45 finish. 46 47 @param subproc: The python subprocess to nuke. 48 @param timeout_hint_seconds: The time to wait between successive attempts. 49 @returns: The result from the subprocess, None if we failed to kill it. 50 51 """ 52 # check if the subprocess is still alive, first 53 if subproc.poll() is not None: 54 return subproc.poll() 55 56 signal_queue = [signal.SIGINT, signal.SIGTERM, signal.SIGKILL] 57 for sig in signal_queue: 58 logging.info('Nuking %s with %s', subproc.pid, sig) 59 utils.signal_pid(subproc.pid, sig) 60 try: 61 utils.poll_for_condition( 62 lambda: subproc.poll() is not None, 63 timeout=timeout_hint_seconds) 64 return subproc.poll() 65 except utils.TimeoutError: 66 pass 67 return None 68 69 70class PseudoModemManagerContextException(Exception): 71 """ Exception class for exceptions raised by PseudoModemManagerContext. """ 72 pass 73 74 75class PseudoModemManagerContext(object): 76 """ 77 A context to launch pseudomodem in background. 78 79 Tests should use |PeudoModemManagerContext| to launch pseudomodem. It is 80 intended to be used with the |with| clause like so: 81 82 with PseudoModemManagerContext(...): 83 # Run test 84 85 pseudomodem will be launch in a subprocess safely when entering the |with| 86 block, and cleaned up when exiting. 87 88 """ 89 SHORT_TIMEOUT_SECONDS = 4 90 # Some actions are dependent on hardware cooperating. We need to wait longer 91 # for these. Try to minimize using this constant. 92 WAIT_FOR_HARDWARE_TIMEOUT_SECONDS = 12 93 TEMP_FILE_PREFIX = 'pseudomodem_' 94 REAL_MANAGER_SERVICES = ['modemmanager', 'cromo'] 95 REAL_MANAGER_PROCESSES = ['ModemManager', 'cromo'] 96 TEST_OBJECT_ARG_FLAGS = ['test-modem-arg', 97 'test-sim-arg', 98 'test-state-machine-factory-arg'] 99 100 def __init__(self, 101 use_pseudomodem, 102 flags_map=None, 103 block_output=True, 104 bus=None): 105 """ 106 @param use_pseudomodem: This flag can be used to treat pseudomodem as a 107 no-op. When |True|, pseudomodem is launched as expected. When 108 |False|, this operation is a no-op, and pseudomodem will not be 109 launched. 110 @param flags_map: This is a map of pseudomodem arguments. See 111 |pseudomodem| module for the list of supported arguments. For 112 example, to launch pseudomodem with a modem of family 3GPP, use: 113 with PseudoModemManager(True, flags_map={'family' : '3GPP}): 114 # Do stuff 115 @param block_output: If True, output from the pseudomodem process is not 116 piped to stdout. This is the default. 117 @param bus: A handle to the dbus.SystemBus. If you use dbus in your 118 tests, you should obtain a handle to the bus and pass it in 119 here. Not doing so can cause incompatible mainloop settings in 120 the dbus module. 121 122 """ 123 self._use_pseudomodem = use_pseudomodem 124 self._block_output = block_output 125 126 self._temp_files = [] 127 self.cmd_line_flags = self._ConvertMapToFlags(flags_map if flags_map 128 else {}) 129 self._service_stopper = service_stopper.ServiceStopper( 130 self.REAL_MANAGER_SERVICES) 131 self._net_interface = None 132 self._null_pipe = None 133 self._exit_error_file_path = None 134 self._pseudomodem_process = None 135 136 self._bus = bus 137 if not self._bus: 138 # Currently, the glib mainloop, or a wrapper thereof are the only 139 # mainloops we ever use with dbus. So, it's a comparatively safe bet 140 # to set that up as the mainloop here. 141 # Ideally, if a test wants to use dbus, it should pass us its own 142 # bus. 143 dbus_loop = dbus.mainloop.glib.DBusGMainLoop() 144 self._bus = dbus.SystemBus(private=True, mainloop=dbus_loop) 145 146 147 @property 148 def cmd_line_flags(self): 149 """ The command line flags that will be passed to pseudomodem. """ 150 return self._cmd_line_flags 151 152 153 @cmd_line_flags.setter 154 def cmd_line_flags(self, val): 155 """ 156 Set the command line flags to be passed to pseudomodem. 157 158 @param val: The flags. 159 160 """ 161 logging.info('Command line flags for pseudomodem set to: |%s|', val) 162 self._cmd_line_flags = val 163 164 165 def __enter__(self): 166 return self.Start() 167 168 169 def __exit__(self, *args): 170 return self.Stop(*args) 171 172 173 def Start(self): 174 """ Start the context. This launches pseudomodem. """ 175 if not self._use_pseudomodem: 176 return self 177 178 self._CheckPseudoModemArguments() 179 180 self._service_stopper.stop_services() 181 self._WaitForRealModemManagersToDie() 182 183 self._net_interface = net_interface.PseudoNetInterface() 184 self._net_interface.Setup() 185 186 toplevel = os.path.dirname(os.path.realpath(__file__)) 187 cmd = [os.path.join(toplevel, 'pseudomodem.py')] 188 cmd = cmd + self.cmd_line_flags 189 190 fd, self._exit_error_file_path = self._CreateTempFile() 191 os.close(fd) # We don't need the fd. 192 cmd = cmd + [pseudomodem.EXIT_ERROR_FILE_FLAG, 193 self._exit_error_file_path] 194 195 # Setup health checker for child process. 196 signal.signal(signal.SIGCHLD, self._SigchldHandler) 197 198 if self._block_output: 199 self._null_pipe = open(os.devnull, 'w') 200 self._pseudomodem_process = subprocess.Popen( 201 cmd, 202 preexec_fn=PseudoModemManagerContext._SetUserModem, 203 close_fds=True, 204 stdout=self._null_pipe, 205 stderr=self._null_pipe) 206 else: 207 self._pseudomodem_process = subprocess.Popen( 208 cmd, 209 preexec_fn=PseudoModemManagerContext._SetUserModem, 210 close_fds=True) 211 self._EnsurePseudoModemUp() 212 return self 213 214 215 def Stop(self, *args): 216 """ Exit the context. This terminates pseudomodem. """ 217 if not self._use_pseudomodem: 218 return 219 220 # Remove health check on child process. 221 signal.signal(signal.SIGCHLD, signal.SIG_DFL) 222 223 if self._pseudomodem_process: 224 if self._pseudomodem_process.poll() is None: 225 if (nuke_subprocess(self._pseudomodem_process, 226 self.SHORT_TIMEOUT_SECONDS) is 227 None): 228 logging.warning('Failed to clean up the launched ' 229 'pseudomodem process') 230 self._pseudomodem_process = None 231 232 if self._null_pipe: 233 self._null_pipe.close() 234 self._null_pipe = None 235 236 if self._net_interface: 237 self._net_interface.Teardown() 238 self._net_interface = None 239 240 self._DeleteTempFiles() 241 self._service_stopper.restore_services() 242 243 244 def _ConvertMapToFlags(self, flags_map): 245 """ 246 Convert the argument map given to the context to flags for pseudomodem. 247 248 @param flags_map: A map of flags. The keys are the names of the flags 249 accepted by pseudomodem. The value, if not None, is the value 250 for that flag. We do not support |None| as the value for a flag. 251 @returns: the list of flags to pass to pseudomodem. 252 253 """ 254 cmd_line_flags = [] 255 for key, value in flags_map.iteritems(): 256 cmd_line_flags.append('--' + key) 257 if key in self.TEST_OBJECT_ARG_FLAGS: 258 cmd_line_flags.append(self._DumpArgToFile(value)) 259 elif value: 260 cmd_line_flags.append(value) 261 return cmd_line_flags 262 263 264 def _DumpArgToFile(self, arg): 265 """ 266 Dump a given python list to a temp file in json format. 267 268 This is used to pass arguments to custom objects from tests that 269 are to be instantiated by pseudomodem. The argument must be a list. When 270 running pseudomodem, this list will be unpacked to get the arguments. 271 272 @returns: Absolute path to the tempfile created. 273 274 """ 275 fd, arg_file_path = self._CreateTempFile() 276 arg_file = os.fdopen(fd, 'wb') 277 json.dump(arg, arg_file) 278 arg_file.close() 279 return arg_file_path 280 281 282 def _WaitForRealModemManagersToDie(self): 283 """ 284 Wait for real modem managers to quit. Die otherwise. 285 286 Sometimes service stopper does not kill ModemManager process, if it is 287 launched by something other than upstart. We want to ensure that the 288 process is dead before continuing. 289 290 This method can block for up to a minute. Sometimes, ModemManager can 291 take up to a 10 seconds to die after service stopper has stopped it. We 292 wait for it to clean up before concluding that the process is here to 293 stay. 294 295 @raises: PseudoModemManagerContextException if a modem manager process 296 does not quit in a reasonable amount of time. 297 """ 298 def _IsProcessRunning(process): 299 try: 300 utils.run('pgrep -x %s' % process) 301 return True 302 except error.CmdError: 303 return False 304 305 for manager in self.REAL_MANAGER_PROCESSES: 306 try: 307 utils.poll_for_condition( 308 lambda:not _IsProcessRunning(manager), 309 timeout=self.WAIT_FOR_HARDWARE_TIMEOUT_SECONDS) 310 except utils.TimeoutError: 311 err_msg = ('%s is still running. ' 312 'It may interfere with pseudomodem.' % 313 manager) 314 logging.error(err_msg) 315 raise PseudoModemManagerContextException(err_msg) 316 317 318 def _CheckPseudoModemArguments(self): 319 """ 320 Parse the given pseudomodem arguments. 321 322 By parsing the arguments in the context, we can provide early feedback 323 about incorrect arguments. 324 325 """ 326 pseudomodem.ParseArguments(self.cmd_line_flags) 327 328 329 @staticmethod 330 def _SetUserModem(): 331 """ 332 Set the unix user of the calling process to |modem|. 333 334 This functions is called by the launched subprocess so that pseudomodem 335 can be launched as the |modem| user. 336 On encountering an error, this method will terminate the process. 337 338 """ 339 try: 340 pwd_data = pwd.getpwnam(pm_constants.MM1_USER) 341 except KeyError as e: 342 logging.error('Could not find uid for user %s [%s]', 343 pm_constants.MM1_USER, str(e)) 344 sys.exit(1) 345 346 logging.debug('Setting UID to %d', pwd_data.pw_uid) 347 try: 348 os.setuid(pwd_data.pw_uid) 349 except OSError as e: 350 logging.error('Could not set uid to %d [%s]', 351 pwd_data.pw_uid, str(e)) 352 sys.exit(1) 353 354 355 def _EnsurePseudoModemUp(self): 356 """ Makes sure that pseudomodem in child process is ready. """ 357 def _LivenessCheck(): 358 try: 359 testing_object = self._bus.get_object( 360 mm1_constants.I_MODEM_MANAGER, 361 pm_constants.TESTING_PATH) 362 return testing_object.IsAlive( 363 dbus_interface=pm_constants.I_TESTING) 364 except dbus.DBusException as e: 365 logging.debug('LivenessCheck: No luck yet. (%s)', str(e)) 366 return False 367 368 utils.poll_for_condition( 369 _LivenessCheck, 370 timeout=self.SHORT_TIMEOUT_SECONDS, 371 exception=PseudoModemManagerContextException( 372 'pseudomodem did not initialize properly.')) 373 374 375 def _CreateTempFile(self): 376 """ 377 Creates a tempfile such that the child process can read/write it. 378 379 The file path is stored in a list so that the file can be deleted later 380 using |_DeleteTempFiles|. 381 382 @returns: (fd, arg_file_path) 383 fd: A file descriptor for the created file. 384 arg_file_path: Full path of the created file. 385 386 """ 387 fd, arg_file_path = tempfile.mkstemp(prefix=self.TEMP_FILE_PREFIX) 388 self._temp_files.append(arg_file_path) 389 # Set file permissions so that pseudomodem process can read/write it. 390 cur_mod = os.stat(arg_file_path).st_mode 391 os.chmod(arg_file_path, 392 cur_mod | stat.S_IRGRP | stat.S_IROTH | stat.S_IWGRP | 393 stat.S_IWOTH) 394 return fd, arg_file_path 395 396 397 def _DeleteTempFiles(self): 398 """ Deletes all temp files created by this context. """ 399 for file_path in self._temp_files: 400 try: 401 os.remove(file_path) 402 except OSError as e: 403 logging.warning('Failed to delete temp file: %s (error %s)', 404 file_path, str(e)) 405 406 407 def _SigchldHandler(self, signum, frame): 408 """ 409 Signal handler for SIGCHLD. 410 411 This is setup while the pseudomodem subprocess is running. A call to 412 this signal handler may signify early termination of the subprocess. 413 414 @param signum: The signal number. 415 @param frame: Ignored. 416 417 """ 418 if not self._pseudomodem_process: 419 # We can receive a SIGCHLD even before the setup of the child 420 # process is complete. 421 return 422 if self._pseudomodem_process.poll() is not None: 423 # See if child process left detailed error report 424 error_reason, error_traceback = pseudomodem.ExtractExitError( 425 self._exit_error_file_path) 426 logging.error('pseudomodem child process quit early!') 427 logging.error('Reason: %s', error_reason) 428 for line in error_traceback: 429 logging.error('Traceback: %s', line.strip()) 430 raise PseudoModemManagerContextException( 431 'pseudomodem quit early! (%s)' % 432 error_reason) 433