1 #!/usr/bin/env python 2 3 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 import argparse 8 import dbus 9 import logging 10 import os 11 import signal 12 import sys 13 import traceback 14 15 import at_transceiver 16 import global_state 17 import modem_configuration 18 import task_loop 19 import wardmodem_exceptions as wme 20 21 import common 22 from autotest_lib.client.bin import utils 23 from autotest_lib.client.common_lib import error 24 from autotest_lib.client.cros.cellular import net_interface 25 26 STATE_MACHINE_DIR_NAME = 'state_machines' 27 28 class WardModem(object): 29 """ 30 The main wardmodem object that replaces a physical modem. 31 32 What it does: 33 - Loads configuration data. 34 - Accepts custom state machines from the test. 35 - Builds objects and ties them together. 36 - Exposes objects for further customization 37 38 What it does not do: 39 - Tweak the different knobs provided by internal objects that it exposes 40 for further customization. 41 That is the responsibility of the WardModemContext. 42 - Care about setting up / tearing down environment. 43 Again, see WardModemContext. 44 """ 45 46 def __init__(self, 47 replaced_modem = None, 48 state_machines = None, 49 modem_at_port_dev_name = None): 50 """ 51 @param replaced_modem: Name of the modem being emulated. If left None, 52 the base modem will be emulated. A list of valid modems can be 53 found in the module modem_configuration 54 55 @param state_machines: Objects of subtypes of StateMachine that override 56 any state machine defined in the configuration files for the 57 same well-known-name. 58 59 @param modem_at_port_dev_name: The full path to the primary AT port of 60 the physical modem. This is needed only if we're running in a 61 mode where we pass on modemmanager commands to the modem. This 62 should be a string of the form '/dev/XXX' 63 64 """ 65 self._logger = logging.getLogger(__name__) 66 67 if not state_machines: 68 state_machines = [] 69 if modem_at_port_dev_name and ( 70 type(modem_at_port_dev_name) is not str or 71 modem_at_port_dev_name[0:5] != '/dev/'): 72 raise wme.WardModemSetupException( 73 'Modem device name must be of the form "/dev/XXX", ' 74 'where XXX is the udev device.') 75 76 # The modem that wardmodem is intended to replace. 77 self._replaced_modem = replaced_modem 78 79 # Pseudo net interface exported to shill. 80 self._net_interface = net_interface.PseudoNetInterface() 81 82 # The internal task loop object. Readable through a property. 83 self._task_loop = task_loop.TaskLoop() 84 85 # The global state object shared by all state machines. 86 self._state = global_state.GlobalState() 87 88 # The configuration object for the replaced modem. 89 self._modem_conf = modem_configuration.ModemConfiguration( 90 replaced_modem) 91 92 self._create_transceiver(modem_at_port_dev_name) 93 self._setup_state_machines(state_machines) 94 95 self._started = False 96 97 98 def start(self): 99 """ 100 Turns on the wardmodem. 101 102 This call is blocking. It will return after |stop| is called. 103 104 """ 105 self._logger.info('Starting wardmodem...') 106 self._net_interface.Setup() 107 self._task_loop.start() 108 109 110 def stop(self): 111 """ 112 Stops wardmodem and cleanup. 113 114 """ 115 # We need to delete a bunch of stuff *before* the task_loop can be 116 # stopped. 117 self._logger.info('Stopping wardmodem.') 118 self._net_interface.Teardown() 119 del self._transceiver 120 os.close(self._wm_at_port) 121 os.close(self._mm_at_port) 122 if self._modem_at_port: 123 os.close(self._modem_at_port) 124 self.task_loop.stop() 125 126 127 @property 128 def modem(self): 129 """ 130 The physical modem being replaced [read-only]. 131 132 @return string representing the replaced modem. 133 134 """ 135 return self._replaced_modem 136 137 138 @property 139 def modem_conf(self): 140 """ 141 The ModemConfiguration object loaded for the replaced modem [read-only]. 142 143 @return A ModemConfiguration object. 144 145 """ 146 return self._modem_conf 147 148 @property 149 def transceiver(self): 150 """ 151 The ATTransceiver that will orchestrate communication [read-only]. 152 153 @return ATTransceiver object. 154 155 """ 156 return self._transceiver 157 158 159 @property 160 def task_loop(self): 161 """ 162 The main loop for asynchronous operations [read-only]. 163 164 @return TaskLoop object. 165 166 """ 167 return self._task_loop 168 169 170 @property 171 def state(self): 172 """ 173 The global state object that must by shared by all state machines. 174 175 @return GlobalState object. 176 177 """ 178 return self._state 179 180 181 @property 182 def mm_at_port_pts_name(self): 183 """ 184 Name of the pty terminal to be used by modemmanager. 185 186 @return A string of the form 'pts/X' where X is the pty number. 187 188 """ 189 fullname = os.ttyname(self._mm_at_port) 190 # fullname is of the form /dev/pts/X where X is a pts number. 191 # We want to return just the pts/X part. 192 assert fullname[0:5] == '/dev/' 193 return fullname[5:] 194 195 196 def _create_transceiver(self, modem_at_port_dev_name): 197 """ 198 Opens a pty pair and initialize ATTransceiver. 199 200 @param modem_at_port_dev_name: The device name of the primary port. 201 202 """ 203 self._modem_at_port = None 204 if modem_at_port_dev_name: 205 try: 206 self._modem_at_port = os.open(modem_at_port_dev_name, 207 os.O_RDWR) 208 except (TypeError, OSError) as e: 209 logging.warning('Could not open modem_port |%s|\nError:\n%s', 210 modem_at_port_dev_name, e) 211 212 self._wm_at_port, self._mm_at_port = os.openpty() 213 self._transceiver = at_transceiver.ATTransceiver(self._wm_at_port, 214 self._modem_conf, 215 self._modem_at_port) 216 217 def _setup_state_machines(self, client_machines): 218 """ 219 Creates the state machines looking at sources in the right order. 220 221 @param client_machines: The client provided state machine objects. 222 223 """ 224 # A local list of state machines created 225 state_machines = [] 226 227 # Create the state machines comprising the wardmodem. 228 # Highest priority is given to the client provided state machines. The 229 # remaining will be instantiated based on |replaced_modem|. 230 for sm in client_machines: 231 if sm.get_well_known_name() in state_machines: 232 raise wme.SetupError('Multiple state machines provided with ' 233 'well-known-name |%s|' % 234 sm.get_well_known_name) 235 state_machines.append(sm.get_well_known_name()) 236 self._transceiver.register_state_machine(sm) 237 self._logger.debug('Added client specified machine {%s --> %s}', 238 sm.get_well_known_name(), 239 sm.__class__.__name__) 240 # Now instantiate modem specific state machines. 241 for sm_module in self._modem_conf.plugin_state_machines: 242 sm = self._create_state_machine(sm_module) 243 if sm.get_well_known_name() not in state_machines: 244 state_machines.append(sm.get_well_known_name()) 245 self._transceiver.register_state_machine(sm) 246 self._logger.debug( 247 'Added modem specific machine {%s --> %s}', 248 sm.get_well_known_name(), 249 sm.__class__.__name__) 250 # Finally instantiate generic state machines. 251 for sm_module in self._modem_conf.base_state_machines: 252 sm = self._create_state_machine(sm_module) 253 if sm.get_well_known_name() not in state_machines: 254 state_machines.append(sm.get_well_known_name()) 255 self._transceiver.register_state_machine(sm) 256 self._logger.debug('Added default machine {%s --> %s}', 257 sm.get_well_known_name(), 258 sm.__class__.__name__) 259 self._logger.info('Loaded state machines: %s', str(state_machines)) 260 261 # Also setup the fallback state machine 262 self._transceiver.register_fallback_state_machine( 263 self._modem_conf.fallback_machine, 264 self._modem_conf.fallback_function) 265 266 267 def _create_state_machine(self, module_name): 268 """ 269 Creates a state machine object given the |module_name|. 270 271 There is a specific naming convention for these state machine 272 definitions. If |module_name| is new_and_shiny_machine, the state 273 machine class must be named NewAndShinyMachine. 274 275 @param module_name: The name of the module from which the state machine 276 should be created. 277 278 @returns An object of type new_and_shiny_machine.NewAndShinyMachine, if 279 it exists. 280 281 @raises WardModemSetupError if |module_name| is malformed or the object 282 creation fails. 283 284 """ 285 # Obtain the name of the state machine class from module_name. 286 # viz, convert my_module_name --> MyModuleName 287 parts = module_name.split('_') 288 parts = [x.title() for x in parts] 289 class_name = ''.join(parts) 290 291 self._import_state_machine_module_as_sm(module_name) 292 return getattr(sm, class_name)( 293 self._state, 294 self._transceiver, 295 self._modem_conf) 296 297 298 def _import_state_machine_module_as_sm(self, module_name): 299 global sm 300 if module_name == 'call_machine': 301 from state_machines import call_machine as sm 302 elif module_name == 'call_machine_e362': 303 from state_machines import call_machine_e362 as sm 304 elif module_name == 'level_indicators_machine': 305 from state_machines import level_indicators_machine as sm 306 elif module_name == 'modem_power_level_machine': 307 from state_machines import modem_power_level_machine as sm 308 elif module_name == 'network_identity_machine': 309 from state_machines import network_identity_machine as sm 310 elif module_name == 'network_operator_machine': 311 from state_machines import network_operator_machine as sm 312 elif module_name == 'network_registration_machine': 313 from state_machines import network_registration_machine as sm 314 elif module_name == 'request_response': 315 from state_machines import request_response as sm 316 else: 317 raise wme.WardModemSetupException('Unknown state machine module: ' 318 '%s' % module_name) 319 320 321 class WardModemContext(object): 322 """ 323 Setup wardmodem according to the options provided. 324 325 This context should be used by everyone to interact with WardModem. 326 This context will 327 (1) Setup wardmodem, setting the correct options on the internals exposed by 328 the wardmodem object. 329 (2) Manage the modemmanager instance during the context's lifetime. 330 331 """ 332 333 MODEMMANAGER_RESTART_TIMEOUT = 60 334 335 def __init__(self, use_wardmodem=True, detach=True, args=None): 336 """ 337 @param use_wardmodem: If False, this context is a no-op. Otherwise, the 338 whole wardmodem magic is done. 339 340 @param detach: A bool flag indicating whether wardmodem should be run in 341 its own process. If True, |start| will return immediately, 342 starting WardModem in its own process; Otherwise, |start| will 343 block until |stop| is called. 344 345 @param args: Options to setup WardModem. This is a list of string 346 command line arguments accepted by the parser defined in 347 |get_option_parser|. 348 TODO(pprabhu) Also except a dict of options to ease 349 customization in tests. 350 351 """ 352 self._logger = logging.getLogger(__name__) 353 self._logger.info('Initializing wardmodem context.') 354 355 self._use_wardmodem = use_wardmodem 356 if not self._use_wardmodem: 357 self._logger.info('WardModemContext directed to do nothing. ' 358 'All wardmodem actions are no-op.') 359 self._logger.debug('........... Welcome to the real world Neo.') 360 return 361 362 self._logger.debug('Wardmodem arguments: detach: %s, args: %s', 363 detach, str(args)) 364 365 self._started = False 366 self._wardmodem = None 367 self._was_mm_running = False 368 self._detach = detach 369 option_parser = self._get_option_parser() 370 371 # XXX:HACK For some reason, parse_args picks up argv when the context is 372 # created by an autotest test. Workaround: stash away the argv. 373 argv_stash = sys.argv 374 sys.argv = ['wardmodem'] 375 self._options = option_parser.parse_args(args) 376 sys.argv = argv_stash 377 378 379 def __enter__(self): 380 self.start() 381 return self 382 383 384 def __exit__(self, type, value, traceback): 385 self.stop() 386 # Don't supress any exceptions raised in the 'with' block 387 return False 388 389 390 def start(self): 391 """ 392 Start the WardModem, setting up the correct environment. 393 394 If |detach| was True, this call will return immediately, running 395 WardModem in its own process; Otherwise, this call will block and only 396 return when |stop| is called. 397 398 """ 399 if not self._use_wardmodem: 400 return 401 402 if self._started: 403 raise wme.WardModemSetupException( 404 'Attempted to re-enter an already active wardmodem ' 405 'context.') 406 407 self._started = True 408 self._wardmodem = WardModem( 409 self._options.modem, 410 modem_at_port_dev_name=self._options.modem_port) 411 if not self._prepare_wardmodem(self._options): 412 raise wme.WardModemSetupException( 413 'Contradictory wardmodem setup options detected.') 414 415 self._prepare_mm() 416 417 if not self._detach: 418 self._wardmodem.start() 419 return 420 421 self._logger.debug('Will fork wardmodem process.') 422 self._child = os.fork() 423 if self._child == 0: 424 # Setup a way to stop the child. 425 def _exit_child(signum, frame): 426 self._logger.info('Signal handler called with signal %s', 427 signum) 428 self._cleanup() 429 os._exit(0) 430 signal.signal(signal.SIGINT, _exit_child) 431 signal.signal(signal.SIGTERM, _exit_child) 432 # In detach mode, all uncaught exceptions raised by wardmodem 433 # will be thrown here. Since this is a child process, they will 434 # be lost. 435 # At least log them before throwing them again, so that we know 436 # something went wrong in wardmodem. 437 try: 438 self._wardmodem.start() 439 except Exception as e: 440 traceback.print_exc() 441 raise 442 443 else: 444 # Wait here for the child to start before continuing. 445 # We will continue once we know that modemmanager process has 446 # detected the wardmodem device, and has exported it on its dbus 447 # interface. 448 utils.poll_for_condition( 449 self._check_for_modem, 450 exception=wme.WardModemSetupException( 451 'Could not cleanly restart modemmanager.'), 452 timeout=self.MODEMMANAGER_RESTART_TIMEOUT, 453 sleep_interval=1) 454 self._logger.debug('Continuing the main process outside ' 455 'wardmodem.') 456 457 458 def stop(self): 459 """ 460 Stops WardModem, restore environment to its previous state. 461 462 """ 463 if not self._use_wardmodem: 464 return 465 466 if not self._started: 467 self._logger.warning('No wardmodem instance running! ' 468 'Nothing to stop.') 469 return 470 471 if self._detach: 472 self._logger.debug('Attempting to kill child wardmodem process.') 473 if self._child != 0: 474 os.kill(self._child, signal.SIGINT) 475 os.waitpid(self._child, 0) 476 self._child = 0 477 self._logger.debug('Finished waiting on child wardmodem process ' 478 'to finish.') 479 else: 480 self._cleanup() 481 self._started = False 482 483 484 def _cleanup(self): 485 # Restore mm before turning off wardmodem. 486 self._restore_mm() 487 self._wardmodem.stop() 488 self._logger.info('Bye, Bye!') 489 490 491 def _prepare_wardmodem(self, options): 492 """ 493 Tweaks the internals exposed by WardModem post-creation according to the 494 options provided. 495 496 @param options: is an object returned by argparse. 497 498 """ 499 if options.modem: 500 if options.pass_through_mode: 501 self._logger.warning('Ignoring "modem" in pass-through-mode.') 502 503 if options.at_terminator: 504 self._wardmodem.transceiver.at_terminator = options.at_terminator 505 506 if options.pass_through_mode: 507 self._wardmodem.transceiver.mode = \ 508 at_transceiver.ATTransceiverMode.PASS_THROUGH 509 510 if options.bridge_mode: 511 self._wardmodem.transceiver.mode = \ 512 at_transceiver.ATTransceiverMode.SPLIT_VERIFY 513 514 if options.modem_port: 515 if not options.pass_through_mode and not options.bridge_mode: 516 self._logger.warning('Ignoring "modem-port" in normal mode.') 517 else: 518 if options.pass_through_mode or options.bridge_mode: 519 self._logger.error('"modem-port" needed in %s mode.' % 520 'bridge-mode' if options.bridge_mode else 521 'pass-through-mode') 522 return False 523 524 if options.fast: 525 if options.pass_through_mode: 526 self._logger.warning('Ignoring "fast" in pass-through-mode') 527 else: 528 self._wardmodem.task_loop.ignore_delays = True 529 530 if options.randomize_delays: 531 if options.fast: 532 self._logger.warning('Ignoring option "randomize-delays" ' 533 '"fast" overrides "randomize-delays".') 534 if options.pass_through_mode: 535 self._logger.warning('Ignoring "randomize-delays" in ' 536 'pass-through-mode') 537 if not options.fast and not options.pass_through_mode: 538 self._wardmodem.task_loop.random_delays = True 539 540 if options.max_randomized_delay: 541 if (options.fast or not options.randomize_delays or 542 options.pass_through_mode): 543 self._logger.warning('Ignoring "max_randomized_delays"') 544 else: 545 self._wardmodem.task_loop.max_random_delay_ms = \ 546 options.max_randomized_delay 547 548 return True 549 550 551 def _prepare_mm(self): 552 """ 553 Starts modemmanager in test mode listening to the WardModem specified 554 pty end-point. 555 556 """ 557 self._was_mm_running = False 558 try: 559 result = utils.run('pgrep ModemManager') 560 if result.stdout: 561 self._was_mm_running = True 562 except error.CmdError: 563 pass 564 try: 565 utils.run('initctl stop modemmanager') 566 except error.CmdError: 567 pass 568 569 mm_opts = '' 570 mm_opts += '--log-level=DEBUG ' 571 mm_opts += '--timestamps ' 572 mm_opts += '--test ' 573 mm_opts += '--debug ' 574 mm_opts += '--test-plugin=' + self._wardmodem.modem_conf.mm_plugin + ' ' 575 mm_opts += '--test-at-port="' + self._wardmodem.mm_at_port_pts_name + \ 576 '" ' 577 mm_opts += '--test-net-port=' + \ 578 net_interface.PseudoNetInterface.IFACE_NAME + ' ' 579 result = utils.run('ModemManager %s &' % mm_opts) 580 self._logger.debug('ModemManager stderr:\n%s', result.stderr) 581 582 583 def _check_for_modem(self): 584 bus = dbus.SystemBus() 585 try: 586 manager = bus.get_object('org.freedesktop.ModemManager1', 587 '/org/freedesktop/ModemManager1') 588 imanager = dbus.Interface(manager, 589 'org.freedesktop.DBus.ObjectManager') 590 modems = imanager.GetManagedObjects() 591 except dbus.exceptions.DBusException as e: 592 # The ObjectManager interface on modemmanager is not up yet. 593 return False 594 # Check if a modem with the test at port has been exported 595 if self._wardmodem.mm_at_port_pts_name in str(modems): 596 return True 597 else: 598 return False 599 600 601 def _restore_mm(self): 602 """ 603 Stops the test instance of modemmanager and restore it to previous 604 state. 605 606 """ 607 result = None 608 try: 609 result = utils.run('pgrep ModemManager') 610 self._logger.warning('ModemManager in test mode still running! ' 611 'Killing it ourselves.') 612 try: 613 utils.run('pkill -9 ModemManager') 614 except error.CmdError: 615 self._logger.warning('Failed to kill test ModemManager.') 616 except error.CmdError: 617 self._logger.debug('As expected: ModemManager in test mode killed.') 618 if self._was_mm_running: 619 try: 620 utils.run('initctl start modemmanager') 621 except error.CmdError: 622 self._logger.warning('Failed to restart modemmanager service.') 623 624 625 def _get_option_parser(self): 626 """ 627 Build an argparse parser to accept options from the user/test to tweak 628 WardModem post-creation. 629 630 """ 631 parser = argparse.ArgumentParser( 632 description='Run the wardmodem modem emulator.') 633 634 modem_group = parser.add_argument_group( 635 'Modem', 636 'Description of the modem to emulate.') 637 modem_group.add_argument( 638 '--modem', 639 help='The modem to emulate.') 640 modem_group.add_argument('--at-terminator', 641 help='The string terminator to use.') 642 643 physical_modem_group = parser.add_argument_group( 644 'Physical modem', 645 'Interaction with the physical modem on-board.') 646 physical_modem_group.add_argument( 647 '--pass-through-mode', 648 default=False, 649 nargs='?', 650 const=True, 651 help='Act as a transparent channel between the modem manager ' 652 'and the physical modem. "--modem-port" option required.') 653 physical_modem_group.add_argument( 654 '--bridge-mode', 655 default=False, 656 nargs='?', 657 const=True, 658 help='Should we also setup a bridge with the real modem? Note ' 659 'that the responses generated by wardmodem state machines ' 660 'take precedence over those received from the physical ' 661 'modem. The bridge is used for a soft-verification: A ' 662 'warning is generated if the responses do not match. ' 663 '"--modem-port" option required.') 664 physical_modem_group.add_argument( 665 '--modem-port', 666 help='The primary port used by the real modem. ') 667 668 behaviour_group = parser.add_argument_group( 669 'Behaviour', 670 'Tweak the behaviour of running wardmodem.') 671 behaviour_group.add_argument( 672 '--fast', 673 default=False, 674 nargs='?', 675 const=True, 676 help='Run the emulator with minimum delay between operations.') 677 behaviour_group.add_argument( 678 '--randomize-delays', 679 default=False, 680 nargs='?', 681 const=True, 682 help='Run emulator with randomized delays between operations.') 683 behaviour_group.add_argument( 684 '--max-randomized-delay', 685 type=int, 686 help='The maximum randomized delay added between operations in ' 687 '"randomize-delays" mode.') 688 689 return parser 690 691 692 # ############################################################################## 693 # Run WardModem as a script. 694 # ############################################################################## 695 _wardmodem_context = None 696 697 SIGNAL_TO_NAMES_DICT = \ 698 dict((getattr(signal, n), n) 699 for n in dir(signal) if n.startswith('SIG') and '_' not in n) 700 701 def exit_wardmodem_script(signum, frame): 702 """ 703 Signal handler to intercept Keyboard interrupt and stop the WardModem. 704 705 @param signum: The signal that was sent to the script 706 707 @param frame: Current stack frame [ignored]. 708 709 """ 710 global _wardmodem_context 711 if signum == signal.SIGINT: 712 logging.info('Captured Ctrl-C. Exiting wardmodem.') 713 _wardmodem_context.stop() 714 else: 715 logging.warning('Captured unexpected signal: %s', 716 SIGNAL_TO_NAMES_DICT.get(signum, str(signum))) 717 718 719 def main(): 720 """ 721 Entry function to wardmodem script. 722 723 """ 724 global _wardmodem_context 725 # HACK: I should not have logged anything before getting here, but 726 # basicConfig wasn't doing anything: So, attempt to clean config. 727 root = logging.getLogger() 728 if root.handlers: 729 for handler in root.handlers: 730 root.removeHandler(handler) 731 logger_format = ('[%(asctime)-15s][%(filename)s:%(lineno)s:%(levelname)s] ' 732 '%(message)s') 733 logging.basicConfig(format=logger_format, 734 level=logging.DEBUG) 735 736 _wardmodem_context = WardModemContext(True, False, sys.argv[1:]) 737 logging.info('\n####################################################\n' 738 'Running wardmodem, hit Ctrl+C to exit.\n' 739 '####################################################\n') 740 741 signal.signal(signal.SIGINT, exit_wardmodem_script) 742 _wardmodem_context.start() 743 744 745 if __name__ == '__main__': 746 main() 747