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