Home | History | Annotate | Download | only in pseudomodem
      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 
     13 import dbus
     14 import json
     15 import logging
     16 import os
     17 import pwd
     18 import signal
     19 import stat
     20 import sys
     21 import subprocess
     22 import tempfile
     23 
     24 import common
     25 from autotest_lib.client.bin import utils
     26 from autotest_lib.client.common_lib import error
     27 from autotest_lib.client.cros import service_stopper
     28 from autotest_lib.client.cros.cellular import mm1_constants
     29 from autotest_lib.client.cros.cellular import net_interface
     30 
     31 import pm_constants
     32 import 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.
     39 def 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 
     70 class PseudoModemManagerContextException(Exception):
     71     """ Exception class for exceptions raised by PseudoModemManagerContext. """
     72     pass
     73 
     74 
     75 class 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