| # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # This module helps launch pseudomodem as a subprocess. It helps with the |
| # initial setup of pseudomodem, as well as ensures proper cleanup. |
| # For details about the options accepted by pseudomodem, please check the |
| # |pseudomodem| module. |
| # This module also doubles as the python entry point to run pseudomodem from the |
| # command line. To avoid confusion, please use the shell script run_pseudomodem |
| # to run pseudomodem from command line. |
| |
| import dbus |
| import json |
| import logging |
| import os |
| import pwd |
| import signal |
| import stat |
| import sys |
| import subprocess |
| import tempfile |
| |
| import common |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.cros import service_stopper |
| from autotest_lib.client.cros.cellular import mm1_constants |
| from autotest_lib.client.cros.cellular import net_interface |
| |
| import pm_constants |
| import pseudomodem |
| |
| # TODO(pprabhu) Move this to the right utils file. |
| # pprabhu: I haven't yet figured out which of the myriad utils files I should |
| # update. There is an implementation of |nuke_subprocess| that does not take |
| # timeout_hint_seconds in common_lib/utils.py, but |poll_for_condition| |
| # is not available there. |
| def nuke_subprocess(subproc, timeout_hint_seconds=0): |
| """ |
| Attempt to kill the given subprocess via an escalating series of signals. |
| |
| Between each attempt, the process is given |timeout_hint_seconds| to clean |
| up. So, the function may take up to 3 * |timeout_hint_seconds| time to |
| finish. |
| |
| @param subproc: The python subprocess to nuke. |
| @param timeout_hint_seconds: The time to wait between successive attempts. |
| @returns: The result from the subprocess, None if we failed to kill it. |
| |
| """ |
| # check if the subprocess is still alive, first |
| if subproc.poll() is not None: |
| return subproc.poll() |
| |
| signal_queue = [signal.SIGINT, signal.SIGTERM, signal.SIGKILL] |
| for sig in signal_queue: |
| logging.info('Nuking %s with %s', subproc.pid, sig) |
| utils.signal_pid(subproc.pid, sig) |
| try: |
| utils.poll_for_condition( |
| lambda: subproc.poll() is not None, |
| timeout=timeout_hint_seconds) |
| return subproc.poll() |
| except utils.TimeoutError: |
| pass |
| return None |
| |
| |
| class PseudoModemManagerContextException(Exception): |
| """ Exception class for exceptions raised by PseudoModemManagerContext. """ |
| pass |
| |
| |
| class PseudoModemManagerContext(object): |
| """ |
| A context to launch pseudomodem in background. |
| |
| Tests should use |PeudoModemManagerContext| to launch pseudomodem. It is |
| intended to be used with the |with| clause like so: |
| |
| with PseudoModemManagerContext(...): |
| # Run test |
| |
| pseudomodem will be launch in a subprocess safely when entering the |with| |
| block, and cleaned up when exiting. |
| |
| """ |
| SHORT_TIMEOUT_SECONDS = 4 |
| # Some actions are dependent on hardware cooperating. We need to wait longer |
| # for these. Try to minimize using this constant. |
| WAIT_FOR_HARDWARE_TIMEOUT_SECONDS = 12 |
| TEMP_FILE_PREFIX = 'pseudomodem_' |
| REAL_MANAGER_SERVICES = ['modemmanager', 'cromo'] |
| REAL_MANAGER_PROCESSES = ['ModemManager', 'cromo'] |
| TEST_OBJECT_ARG_FLAGS = ['test-modem-arg', |
| 'test-sim-arg', |
| 'test-state-machine-factory-arg'] |
| |
| def __init__(self, |
| use_pseudomodem, |
| flags_map=None, |
| block_output=True, |
| bus=None): |
| """ |
| @param use_pseudomodem: This flag can be used to treat pseudomodem as a |
| no-op. When |True|, pseudomodem is launched as expected. When |
| |False|, this operation is a no-op, and pseudomodem will not be |
| launched. |
| @param flags_map: This is a map of pseudomodem arguments. See |
| |pseudomodem| module for the list of supported arguments. For |
| example, to launch pseudomodem with a modem of family 3GPP, use: |
| with PseudoModemManager(True, flags_map={'family' : '3GPP}): |
| # Do stuff |
| @param block_output: If True, output from the pseudomodem process is not |
| piped to stdout. This is the default. |
| @param bus: A handle to the dbus.SystemBus. If you use dbus in your |
| tests, you should obtain a handle to the bus and pass it in |
| here. Not doing so can cause incompatible mainloop settings in |
| the dbus module. |
| |
| """ |
| self._use_pseudomodem = use_pseudomodem |
| self._block_output = block_output |
| |
| self._temp_files = [] |
| self.cmd_line_flags = self._ConvertMapToFlags(flags_map if flags_map |
| else {}) |
| self._service_stopper = service_stopper.ServiceStopper( |
| self.REAL_MANAGER_SERVICES) |
| self._net_interface = None |
| self._null_pipe = None |
| self._exit_error_file_path = None |
| self._pseudomodem_process = None |
| |
| self._bus = bus |
| if not self._bus: |
| # Currently, the glib mainloop, or a wrapper thereof are the only |
| # mainloops we ever use with dbus. So, it's a comparatively safe bet |
| # to set that up as the mainloop here. |
| # Ideally, if a test wants to use dbus, it should pass us its own |
| # bus. |
| dbus_loop = dbus.mainloop.glib.DBusGMainLoop() |
| self._bus = dbus.SystemBus(private=True, mainloop=dbus_loop) |
| |
| |
| @property |
| def cmd_line_flags(self): |
| """ The command line flags that will be passed to pseudomodem. """ |
| return self._cmd_line_flags |
| |
| |
| @cmd_line_flags.setter |
| def cmd_line_flags(self, val): |
| """ |
| Set the command line flags to be passed to pseudomodem. |
| |
| @param val: The flags. |
| |
| """ |
| logging.info('Command line flags for pseudomodem set to: |%s|', val) |
| self._cmd_line_flags = val |
| |
| |
| def __enter__(self): |
| return self.Start() |
| |
| |
| def __exit__(self, *args): |
| return self.Stop(*args) |
| |
| |
| def Start(self): |
| """ Start the context. This launches pseudomodem. """ |
| if not self._use_pseudomodem: |
| return self |
| |
| self._CheckPseudoModemArguments() |
| |
| self._service_stopper.stop_services() |
| self._WaitForRealModemManagersToDie() |
| |
| self._net_interface = net_interface.PseudoNetInterface() |
| self._net_interface.Setup() |
| |
| toplevel = os.path.dirname(os.path.realpath(__file__)) |
| cmd = [os.path.join(toplevel, 'pseudomodem.py')] |
| cmd = cmd + self.cmd_line_flags |
| |
| fd, self._exit_error_file_path = self._CreateTempFile() |
| os.close(fd) # We don't need the fd. |
| cmd = cmd + [pseudomodem.EXIT_ERROR_FILE_FLAG, |
| self._exit_error_file_path] |
| |
| # Setup health checker for child process. |
| signal.signal(signal.SIGCHLD, self._SigchldHandler) |
| |
| if self._block_output: |
| self._null_pipe = open(os.devnull, 'w') |
| self._pseudomodem_process = subprocess.Popen( |
| cmd, |
| preexec_fn=PseudoModemManagerContext._SetUserModem, |
| close_fds=True, |
| stdout=self._null_pipe, |
| stderr=self._null_pipe) |
| else: |
| self._pseudomodem_process = subprocess.Popen( |
| cmd, |
| preexec_fn=PseudoModemManagerContext._SetUserModem, |
| close_fds=True) |
| self._EnsurePseudoModemUp() |
| return self |
| |
| |
| def Stop(self, *args): |
| """ Exit the context. This terminates pseudomodem. """ |
| if not self._use_pseudomodem: |
| return |
| |
| # Remove health check on child process. |
| signal.signal(signal.SIGCHLD, signal.SIG_DFL) |
| |
| if self._pseudomodem_process: |
| if self._pseudomodem_process.poll() is None: |
| if (nuke_subprocess(self._pseudomodem_process, |
| self.SHORT_TIMEOUT_SECONDS) is |
| None): |
| logging.warning('Failed to clean up the launched ' |
| 'pseudomodem process') |
| self._pseudomodem_process = None |
| |
| if self._null_pipe: |
| self._null_pipe.close() |
| self._null_pipe = None |
| |
| if self._net_interface: |
| self._net_interface.Teardown() |
| self._net_interface = None |
| |
| self._DeleteTempFiles() |
| self._service_stopper.restore_services() |
| |
| |
| def _ConvertMapToFlags(self, flags_map): |
| """ |
| Convert the argument map given to the context to flags for pseudomodem. |
| |
| @param flags_map: A map of flags. The keys are the names of the flags |
| accepted by pseudomodem. The value, if not None, is the value |
| for that flag. We do not support |None| as the value for a flag. |
| @returns: the list of flags to pass to pseudomodem. |
| |
| """ |
| cmd_line_flags = [] |
| for key, value in flags_map.iteritems(): |
| cmd_line_flags.append('--' + key) |
| if key in self.TEST_OBJECT_ARG_FLAGS: |
| cmd_line_flags.append(self._DumpArgToFile(value)) |
| elif value: |
| cmd_line_flags.append(value) |
| return cmd_line_flags |
| |
| |
| def _DumpArgToFile(self, arg): |
| """ |
| Dump a given python list to a temp file in json format. |
| |
| This is used to pass arguments to custom objects from tests that |
| are to be instantiated by pseudomodem. The argument must be a list. When |
| running pseudomodem, this list will be unpacked to get the arguments. |
| |
| @returns: Absolute path to the tempfile created. |
| |
| """ |
| fd, arg_file_path = self._CreateTempFile() |
| arg_file = os.fdopen(fd, 'wb') |
| json.dump(arg, arg_file) |
| arg_file.close() |
| return arg_file_path |
| |
| |
| def _WaitForRealModemManagersToDie(self): |
| """ |
| Wait for real modem managers to quit. Die otherwise. |
| |
| Sometimes service stopper does not kill ModemManager process, if it is |
| launched by something other than upstart. We want to ensure that the |
| process is dead before continuing. |
| |
| This method can block for up to a minute. Sometimes, ModemManager can |
| take up to a 10 seconds to die after service stopper has stopped it. We |
| wait for it to clean up before concluding that the process is here to |
| stay. |
| |
| @raises: PseudoModemManagerContextException if a modem manager process |
| does not quit in a reasonable amount of time. |
| """ |
| def _IsProcessRunning(process): |
| try: |
| utils.run('pgrep -x %s' % process) |
| return True |
| except error.CmdError: |
| return False |
| |
| for manager in self.REAL_MANAGER_PROCESSES: |
| try: |
| utils.poll_for_condition( |
| lambda:not _IsProcessRunning(manager), |
| timeout=self.WAIT_FOR_HARDWARE_TIMEOUT_SECONDS) |
| except utils.TimeoutError: |
| err_msg = ('%s is still running. ' |
| 'It may interfere with pseudomodem.' % |
| manager) |
| logging.error(err_msg) |
| raise PseudoModemManagerContextException(err_msg) |
| |
| |
| def _CheckPseudoModemArguments(self): |
| """ |
| Parse the given pseudomodem arguments. |
| |
| By parsing the arguments in the context, we can provide early feedback |
| about incorrect arguments. |
| |
| """ |
| pseudomodem.ParseArguments(self.cmd_line_flags) |
| |
| |
| @staticmethod |
| def _SetUserModem(): |
| """ |
| Set the unix user of the calling process to |modem|. |
| |
| This functions is called by the launched subprocess so that pseudomodem |
| can be launched as the |modem| user. |
| On encountering an error, this method will terminate the process. |
| |
| """ |
| try: |
| pwd_data = pwd.getpwnam(pm_constants.MM1_USER) |
| except KeyError as e: |
| logging.error('Could not find uid for user %s [%s]', |
| pm_constants.MM1_USER, str(e)) |
| sys.exit(1) |
| |
| logging.debug('Setting UID to %d', pwd_data.pw_uid) |
| try: |
| os.setuid(pwd_data.pw_uid) |
| except OSError as e: |
| logging.error('Could not set uid to %d [%s]', |
| pwd_data.pw_uid, str(e)) |
| sys.exit(1) |
| |
| |
| def _EnsurePseudoModemUp(self): |
| """ Makes sure that pseudomodem in child process is ready. """ |
| def _LivenessCheck(): |
| try: |
| testing_object = self._bus.get_object( |
| mm1_constants.I_MODEM_MANAGER, |
| pm_constants.TESTING_PATH) |
| return testing_object.IsAlive( |
| dbus_interface=pm_constants.I_TESTING) |
| except dbus.DBusException as e: |
| logging.debug('LivenessCheck: No luck yet. (%s)', str(e)) |
| return False |
| |
| utils.poll_for_condition( |
| _LivenessCheck, |
| timeout=self.SHORT_TIMEOUT_SECONDS, |
| exception=PseudoModemManagerContextException( |
| 'pseudomodem did not initialize properly.')) |
| |
| |
| def _CreateTempFile(self): |
| """ |
| Creates a tempfile such that the child process can read/write it. |
| |
| The file path is stored in a list so that the file can be deleted later |
| using |_DeleteTempFiles|. |
| |
| @returns: (fd, arg_file_path) |
| fd: A file descriptor for the created file. |
| arg_file_path: Full path of the created file. |
| |
| """ |
| fd, arg_file_path = tempfile.mkstemp(prefix=self.TEMP_FILE_PREFIX) |
| self._temp_files.append(arg_file_path) |
| # Set file permissions so that pseudomodem process can read/write it. |
| cur_mod = os.stat(arg_file_path).st_mode |
| os.chmod(arg_file_path, |
| cur_mod | stat.S_IRGRP | stat.S_IROTH | stat.S_IWGRP | |
| stat.S_IWOTH) |
| return fd, arg_file_path |
| |
| |
| def _DeleteTempFiles(self): |
| """ Deletes all temp files created by this context. """ |
| for file_path in self._temp_files: |
| try: |
| os.remove(file_path) |
| except OSError as e: |
| logging.warning('Failed to delete temp file: %s (error %s)', |
| file_path, str(e)) |
| |
| |
| def _SigchldHandler(self, signum, frame): |
| """ |
| Signal handler for SIGCHLD. |
| |
| This is setup while the pseudomodem subprocess is running. A call to |
| this signal handler may signify early termination of the subprocess. |
| |
| @param signum: The signal number. |
| @param frame: Ignored. |
| |
| """ |
| if not self._pseudomodem_process: |
| # We can receive a SIGCHLD even before the setup of the child |
| # process is complete. |
| return |
| if self._pseudomodem_process.poll() is not None: |
| # See if child process left detailed error report |
| error_reason, error_traceback = pseudomodem.ExtractExitError( |
| self._exit_error_file_path) |
| logging.error('pseudomodem child process quit early!') |
| logging.error('Reason: %s', error_reason) |
| for line in error_traceback: |
| logging.error('Traceback: %s', line.strip()) |
| raise PseudoModemManagerContextException( |
| 'pseudomodem quit early! (%s)' % |
| error_reason) |