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 11import argparse 12import dbus 13import dbus.mainloop.glib 14import gobject 15import imp 16import json 17import logging 18import os 19import os.path 20import signal 21import sys 22import testing 23import traceback 24 25import logging_setup 26import modem_cdma 27import modem_3gpp 28import modemmanager 29import sim 30import state_machine_factory as smf 31 32import common 33from autotest_lib.client.cros.cellular import mm1_constants 34 35# Flags used by pseudomodem modules only that are defined below in 36# ParserArguments. 37CLI_FLAG = '--cli' 38EXIT_ERROR_FILE_FLAG = '--exit-error-file' 39 40class 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. 288def 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 447def 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. 483def _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 490def _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 526def 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 539def 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 568if __name__ == '__main__': 569 main() 570