Home | History | Annotate | Download | only in pseudomodem
      1 #!/usr/bin/env python
      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 is the entry point for pseudomodem. Though honestly, I can't think
      7 # of any case when you want to use this module directly. Instead, use the
      8 # |pseudomodem_context| module that provides a way to launch pseudomodem in a
      9 # child process.
     10 
     11 import argparse
     12 import dbus
     13 import dbus.mainloop.glib
     14 import gobject
     15 import imp
     16 import json
     17 import logging
     18 import os
     19 import os.path
     20 import signal
     21 import sys
     22 import testing
     23 import traceback
     24 
     25 import logging_setup
     26 import modem_cdma
     27 import modem_3gpp
     28 import modemmanager
     29 import sim
     30 import state_machine_factory as smf
     31 
     32 import common
     33 from autotest_lib.client.cros.cellular import mm1_constants
     34 
     35 # Flags used by pseudomodem modules only that are defined below in
     36 # ParserArguments.
     37 CLI_FLAG = '--cli'
     38 EXIT_ERROR_FILE_FLAG = '--exit-error-file'
     39 
     40 class PseudoModemManager(object):
     41     """
     42     The main class to be used to launch the pseudomodem.
     43 
     44     There should be only one instance of this class that orchestrates
     45     pseudomodem.
     46 
     47     """
     48 
     49     def Setup(self, opts):
     50         """
     51         Call |Setup| to prepare pseudomodem to be launched.
     52 
     53         @param opts: The options accepted by pseudomodem. See top level function
     54                 |ParseArguments| for details.
     55 
     56         """
     57         self._opts = opts
     58 
     59         self._in_exit_sequence = False
     60         self._manager = None
     61         self._modem = None
     62         self._state_machine_factory = None
     63         self._sim = None
     64         self._mainloop = None
     65 
     66         self._dbus_loop = dbus.mainloop.glib.DBusGMainLoop()
     67         self._bus = dbus.SystemBus(private=True, mainloop=self._dbus_loop)
     68         self._bus_name = dbus.service.BusName(mm1_constants.I_MODEM_MANAGER,
     69                                               self._bus)
     70         logging.info('Exported dbus service with well known name: |%s|',
     71                      self._bus_name.get_name())
     72 
     73         self._SetupPseudomodemParts()
     74         logging.info('Pseudomodem setup completed!')
     75 
     76 
     77     def StartBlocking(self):
     78         """
     79         Start pseudomodem operation.
     80 
     81         This call blocks untill |GracefulExit| is called from some other
     82         context.
     83 
     84         """
     85         self._mainloop = gobject.MainLoop()
     86         self._mainloop.run()
     87 
     88 
     89     def GracefulExit(self):
     90         """ Stop pseudomodem operation and clean up. """
     91         if self._in_exit_sequence:
     92             logging.debug('Already exiting.')
     93             return
     94 
     95         self._in_exit_sequence = True
     96         logging.info('pseudomodem shutdown sequence initiated...')
     97         # Guard each step by its own try...catch, because we want to attempt
     98         # each step irrespective of whether the earlier ones succeeded.
     99         try:
    100             if self._manager:
    101                 self._manager.Remove(self._modem)
    102         except Exception as e:
    103             logging.warning('Error while exiting: %s', repr(e))
    104         try:
    105             if self._mainloop:
    106                 self._mainloop.quit()
    107         except Exception as e:
    108             logging.warning('Error while exiting: %s', repr(e))
    109 
    110         logging.info('pseudomodem: Bye! Bye!')
    111 
    112 
    113     def _SetupPseudomodemParts(self):
    114         """
    115         Contructs all pseudomodem objects, but does not start operation.
    116 
    117         Three main objects are created: the |Modem|, the |Sim|, and the
    118         |StateMachineFactory|. This objects may be instantiations of the default
    119         classes, or of user provided classes, depending on options provided.
    120 
    121         """
    122         self._ReadCustomParts()
    123 
    124         use_3gpp = (self._opts.family == '3GPP')
    125 
    126         if not self._modem and not self._state_machine_factory:
    127             self._state_machine_factory = smf.StateMachineFactory()
    128             logging.info('Created default state machine factory.')
    129 
    130         if use_3gpp and not self._sim:
    131             self._sim = sim.SIM(sim.SIM.Carrier('test'),
    132                                 mm1_constants.MM_MODEM_ACCESS_TECHNOLOGY_GSM,
    133                                 locked=self._opts.locked)
    134             logging.info('Created default 3GPP SIM.')
    135 
    136         # Store this constant here because the variable name is too long.
    137         network_available = dbus.types.UInt32(
    138                 mm1_constants.MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE)
    139         if not self._modem:
    140             if use_3gpp:
    141                 technology_gsm = dbus.types.UInt32(
    142                         mm1_constants.MM_MODEM_ACCESS_TECHNOLOGY_GSM)
    143                 networks = [modem_3gpp.Modem3gpp.GsmNetwork(
    144                         'Roaming Network Long ' + str(i),
    145                         'Roaming Network Short ' + str(i),
    146                         '00100' + str(i + 1),
    147                         network_available,
    148                         technology_gsm)
    149                         for i in xrange(self._opts.roaming_networks)]
    150                 # TODO(armansito): Support "not activated" initialization option
    151                 # for 3GPP carriers.
    152                 self._modem = modem_3gpp.Modem3gpp(
    153                         self._state_machine_factory,
    154                         roaming_networks=networks)
    155                 logging.info('Created default 3GPP modem.')
    156             else:
    157                 self._modem = modem_cdma.ModemCdma(
    158                         self._state_machine_factory,
    159                         modem_cdma.ModemCdma.CdmaNetwork(
    160                                 activated=self._opts.activated))
    161                 logging.info('Created default CDMA modem.')
    162 
    163         # Everyone gets the |_bus|, woohoo!
    164         self._manager = modemmanager.ModemManager(self._bus)
    165         self._modem.SetBus(self._bus)  # Also sets it on StateMachineFactory.
    166         self._manager.Add(self._modem)
    167 
    168         # Unfortunately, setting the SIM has to be deferred until everyone has
    169         # their BUS set. |self._sim| exists if the user provided one, or if the
    170         # modem family is |3GPP|.
    171         if self._sim:
    172             self._modem.SetSIM(self._sim)
    173 
    174         # The testing interface can be brought up now that we have the bus.
    175         self._testing_object = testing.Testing(self._modem, self._bus)
    176 
    177 
    178     def _ReadCustomParts(self):
    179         """
    180         Loads user provided implementations of pseudomodem objects.
    181 
    182         The user can provide their own implementations of the |Modem|, |Sim| or
    183         |StateMachineFactory| classes.
    184 
    185         """
    186         if not self._opts.test_module:
    187             return
    188 
    189         test_module = self._LoadCustomPartsModule(self._opts.test_module)
    190 
    191         if self._opts.test_modem_class:
    192             self._modem = self._CreateCustomObject(test_module,
    193                                                    self._opts.test_modem_class,
    194                                                    self._opts.test_modem_arg)
    195 
    196         if self._opts.test_sim_class:
    197             self._sim = self._CreateCustomObject(test_module,
    198                                                  self._opts.test_sim_class,
    199                                                  self._opts.test_sim_arg)
    200 
    201         if self._opts.test_state_machine_factory_class:
    202             if self._opts.test_modem_class:
    203                 logging.warning(
    204                         'User provided a |Modem| implementation as well as a '
    205                         '|StateMachineFactory|. Ignoring the latter.')
    206             else:
    207                 self._state_machine_factory = self._CreateCustomObject(
    208                         test_module,
    209                         self._opts.test_state_machine_factory_class,
    210                         self._opts.test_state_machine_factory_arg)
    211 
    212 
    213     def _CreateCustomObject(self, test_module, class_name, arg_file_name):
    214         """
    215         Create the custom object specified by test.
    216 
    217         @param test_module: The loaded module that implemets the custom object.
    218         @param class_name: Name of the class implementing the custom object.
    219         @param arg_file_name: Absolute path to file containing list of arguments
    220                 taken by |test_module|.|class_name| constructor in json.
    221         @returns: A brand new object of the custom type.
    222         @raises: AttributeError if the class definition is not found;
    223                 ValueError if |arg_file| does not contain valid json
    224                 representaiton of a python list.
    225                 Other errors may be raised during object creation.
    226 
    227         """
    228         arg = None
    229         if arg_file_name:
    230             arg_file = open(arg_file_name, 'rb')
    231             try:
    232                 arg = json.load(arg_file)
    233             finally:
    234                 arg_file.close()
    235             if not isinstance(arg, list):
    236                 raise ValueError('Argument must be a python list.')
    237 
    238         class_def = getattr(test_module, class_name)
    239         try:
    240             if arg:
    241                 logging.debug('Loading test class %s%s',
    242                               class_name, str(arg))
    243                 return class_def(*arg)
    244             else:
    245                 logging.debug('Loading test class %s', class_def)
    246                 return class_def()
    247         except Exception as e:
    248             logging.error('Exception raised when instantiating class %s: %s',
    249                           class_name, str(e))
    250             raise
    251 
    252 
    253     def _LoadCustomPartsModule(self, module_abs_path):
    254         """
    255         Loads the given file as a python module.
    256 
    257         The loaded module *is* added to |sys.modules|.
    258 
    259         @param module_abs_path: Absolute path to the file to be loaded.
    260         @returns: The loaded module.
    261         @raises: ImportError if the module can not be loaded, or if another
    262                  module with the same name is already loaded.
    263 
    264         """
    265         path, name = os.path.split(module_abs_path)
    266         name, _ = os.path.splitext(name)
    267 
    268         if name in sys.modules:
    269             raise ImportError('A module named |%s| is already loaded.' %
    270                               name)
    271 
    272         logging.debug('Loading module %s from %s', name, path)
    273         module_file, filepath, data = imp.find_module(name, [path])
    274         try:
    275             module = imp.load_module(name, module_file, filepath, data)
    276         except Exception as e:
    277             logging.error(
    278                     'Exception raised when loading test module from %s: %s',
    279                     module_abs_path, str(e))
    280             raise
    281         finally:
    282             module_file.close()
    283         return module
    284 
    285 
    286 # ##############################################################################
    287 # Public static functions.
    288 def ParseArguments(arg_string=None):
    289     """
    290     The main argument parser.
    291 
    292     Pseudomodem is a command line tool.
    293     Since pseudomodem is a highly customizable tool, the command line arguments
    294     are expected to be quite complex.
    295     We use argparse to keep the command line options easy to use.
    296 
    297     @param arg_string: If not None, the string to parse. If none, |sys.argv| is
    298             used to obtain the argument string.
    299     @returns: The parsed options object.
    300 
    301     """
    302     parser = argparse.ArgumentParser(
    303             description="Run pseudomodem to simulate a modem using the "
    304                         "modemmanager-next DBus interface.")
    305 
    306     parser.add_argument(
    307             CLI_FLAG,
    308             action='store_true',
    309             default=False,
    310             help='Launch the command line interface in foreground to interact '
    311                  'with the launched pseudomodem process. This argument is used '
    312                  'by |pseudomodem_context|. pseudomodem itself ignores it.')
    313     parser.add_argument(
    314             EXIT_ERROR_FILE_FLAG,
    315             default=None,
    316             help='If provided, full path to file to which pseudomodem should '
    317                  'dump the error condition before exiting, in case of a crash. '
    318                  'The file is not created if it does not already exist.')
    319 
    320     modem_arguments = parser.add_argument_group(
    321             title='Modem options',
    322             description='Options to customize the modem exported.')
    323     modem_arguments.add_argument(
    324             '--family', '-f',
    325             choices=['3GPP', 'CDMA'],
    326             default='3GPP')
    327 
    328     gsm_arguments = parser.add_argument_group(
    329             title='3GPP options',
    330             description='Options specific to 3GPP modems. [Only make sense '
    331                         'when modem family is 3GPP]')
    332 
    333     gsm_arguments.add_argument(
    334             '--roaming-networks', '-r',
    335             type=_NonNegInt,
    336             default=0,
    337             metavar='<# networks>',
    338             help='Number of roaming networks available')
    339 
    340     cdma_arguments = parser.add_argument_group(
    341             title='CDMA options',
    342             description='Options specific to CDMA modems. [Only make sense '
    343                         'when modem family is CDMA]')
    344 
    345     sim_arguments = parser.add_argument_group(
    346             title='SIM options',
    347             description='Options to customize the SIM in the modem. [Only make '
    348                         'sense when modem family is 3GPP]')
    349     sim_arguments.add_argument(
    350             '--activated',
    351             type=bool,
    352             default=True,
    353             help='Determine whether the SIM is activated')
    354     sim_arguments.add_argument(
    355             '--locked', '-l',
    356             type=bool,
    357             default=False,
    358             help='Determine whether the SIM is in locked state')
    359 
    360     testing_arguments = parser.add_argument_group(
    361             title='Testing interface options',
    362             description='Options to modify how the tests or user interacts '
    363                         'with pseudomodem')
    364     testing_arguments = parser.add_argument(
    365             '--interactive-state-machines-all',
    366             type=bool,
    367             default=False,
    368             help='Launch all state machines in interactive mode.')
    369     testing_arguments = parser.add_argument(
    370             '--interactive-state-machine',
    371             type=str,
    372             default=None,
    373             help='Launch the specified state machine in interactive mode. May '
    374                  'be repeated to specify multiple machines.')
    375 
    376     customize_arguments = parser.add_argument_group(
    377             title='Customizable modem options',
    378             description='Options to customize the emulated modem.')
    379     customize_arguments.add_argument(
    380             '--test-module',
    381             type=str,
    382             default=None,
    383             metavar='CUSTOM_MODULE',
    384             help='Absolute path to the module with custom definitions.')
    385     customize_arguments.add_argument(
    386             '--test-modem-class',
    387             type=str,
    388             default=None,
    389             metavar='MODEM_CLASS',
    390             help='Name of the class in CUSTOM_MODULE that implements the modem '
    391                  'to load.')
    392     customize_arguments.add_argument(
    393             '--test-modem-arg',
    394             type=str,
    395             default=None,
    396             help='Absolute path to the json description of argument list '
    397                  'taken by MODEM_CLASS.')
    398     customize_arguments.add_argument(
    399             '--test-sim-class',
    400             type=str,
    401             default=None,
    402             metavar='SIM_CLASS',
    403             help='Name of the class in CUSTOM_MODULE that implements the SIM '
    404                  'to load.')
    405     customize_arguments.add_argument(
    406             '--test-sim-arg',
    407             type=str,
    408             default=None,
    409             help='Aboslute path to the json description of argument list '
    410                  'taken by SIM_CLASS')
    411     customize_arguments.add_argument(
    412             '--test-state-machine-factory-class',
    413             type=str,
    414             default=None,
    415             metavar='SMF_CLASS',
    416             help='Name of the class in CUSTOM_MODULE that impelements the '
    417                  'state machine factory to load. Only used if MODEM_CLASS is '
    418                  'not provided.')
    419     customize_arguments.add_argument(
    420             '--test-state-machine-factory-arg',
    421             type=str,
    422             default=None,
    423             help='Absolute path to the json description of argument list '
    424                  'taken by SMF_CLASS')
    425 
    426     opts = parser.parse_args(arg_string)
    427 
    428     # Extra sanity checks.
    429     if opts.family == 'CDMA' and opts.roaming_networks > 0:
    430         raise argparse.ArgumentTypeError('CDMA networks do not support '
    431                                          'roaming networks.')
    432 
    433     test_objects = (opts.test_modem_class or
    434                     opts.test_sim_class or
    435                     opts.test_state_machine_factory_class)
    436     if not opts.test_module and test_objects:
    437         raise argparse.ArgumentTypeError('test_module is required with any '
    438                                          'other customization arguments.')
    439 
    440     if opts.test_modem_class and opts.test_state_machine_factory_class:
    441         logging.warning('test-state-machine-factory-class will be ignored '
    442                         'because test-modem-class was provided.')
    443 
    444     return opts
    445 
    446 
    447 def ExtractExitError(dump_file_path):
    448     """
    449     Gets the exit error left behind by a crashed pseudomodem.
    450 
    451     If there is a file at |dump_file_path|, extracts the error and the traceback
    452     left behind by the child process. This function is intended to be used by
    453     the launching process to parse the error file left behind by pseudomodem.
    454 
    455     @param dump_file_path: Full path to the file to read.
    456     @returns: (error_reason, error_traceback)
    457             error_reason: str. The one line reason for error that should be
    458                     used to raise exceptions.
    459             error_traceback: A list of str. This is the traceback left
    460                     behind by the child process, if any. May be [].
    461 
    462     """
    463     error_reason = 'No detailed reason found :('
    464     error_traceback = []
    465     if dump_file_path:
    466         try:
    467             dump_file = open(dump_file_path, 'rb')
    468             error_reason = dump_file.readline().strip()
    469             error_traceback = dump_file.readlines()
    470             dump_file.close()
    471         except OSError as e:
    472             logging.error('Could not open dump file %s: %s',
    473                           dump_file_path, str(e))
    474     return error_reason, error_traceback
    475 
    476 
    477 # The single global instance of PseudoModemManager.
    478 _pseudo_modem_manager = None
    479 
    480 
    481 # ##############################################################################
    482 # Private static functions.
    483 def _NonNegInt(value):
    484     value = int(value)
    485     if value < 0:
    486         raise argparse.ArgumentTypeError('%s is not a non-negative int' % value)
    487     return value
    488 
    489 
    490 def _DumpExitError(dump_file_path, exc):
    491     """
    492     Dump information about the raised exception in the exit error file.
    493 
    494     Format of file dumped:
    495     - First line is the reason for the crash.
    496     - Subsequent lines are the traceback from the exception raised.
    497 
    498     We expect the file to exist, because we want the launching context (that
    499     will eventually read the error dump) to create and own the file.
    500 
    501     @param dump_file_path: Full path to file to which we should dump.
    502     @param exc: The exception raised.
    503 
    504     """
    505     if not dump_file_path:
    506         return
    507 
    508     if not os.path.isfile(dump_file_path):
    509         logging.error('File |%s| does not exist. Can not dump exit error.',
    510                       dump_file_path)
    511         return
    512 
    513     try:
    514         dump_file = open(dump_file_path, 'wb')
    515     except IOError as e:
    516         logging.error('Could not open file |%s| to dump exit error. '
    517                       'Exception raised when opening file: %s',
    518                       dump_file_path, str(e))
    519         return
    520 
    521     dump_file.write(str(exc) + '\n')
    522     dump_file.writelines(traceback.format_exc())
    523     dump_file.close()
    524 
    525 
    526 def sig_handler(signum, frame):
    527     """
    528     Top level signal handler to handle user interrupt.
    529 
    530     @param signum: The signal received.
    531     @param frame: Ignored.
    532     """
    533     global _pseudo_modem_manager
    534     logging.debug('Signal handler called with signal %d', signum)
    535     if _pseudo_modem_manager:
    536         _pseudo_modem_manager.GracefulExit()
    537 
    538 
    539 def main():
    540     """
    541     This is the entry point for raw pseudomodem.
    542 
    543     You should not be running this module as a script. If you're trying to run
    544     pseudomodem from the command line, see |pseudomodem_context| module.
    545 
    546     """
    547     global _pseudo_modem_manager
    548 
    549     logging_setup.SetupLogging()
    550 
    551     logging.info('Pseudomodem commandline: [%s]', str(sys.argv))
    552     opts = ParseArguments()
    553 
    554     signal.signal(signal.SIGINT, sig_handler)
    555     signal.signal(signal.SIGTERM, sig_handler)
    556 
    557     try:
    558         _pseudo_modem_manager = PseudoModemManager()
    559         _pseudo_modem_manager.Setup(opts)
    560         _pseudo_modem_manager.StartBlocking()
    561     except Exception as e:
    562         logging.error('Caught exception at top level: %s', str(e))
    563         _DumpExitError(opts.exit_error_file, e)
    564         _pseudo_modem_manager.GracefulExit()
    565         raise
    566 
    567 
    568 if __name__ == '__main__':
    569     main()
    570