• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7import argparse
8import dbus
9import logging
10import os
11import signal
12import sys
13import traceback
14
15import at_transceiver
16import global_state
17import modem_configuration
18import task_loop
19import wardmodem_exceptions as wme
20
21import common
22from autotest_lib.client.bin import utils
23from autotest_lib.client.common_lib import error
24from autotest_lib.client.cros.cellular import net_interface
25
26STATE_MACHINE_DIR_NAME = 'state_machines'
27
28class 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
321class 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
697SIGNAL_TO_NAMES_DICT = \
698        dict((getattr(signal, n), n)
699             for n in dir(signal) if n.startswith('SIG') and '_' not in n)
700
701def 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
719def 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
745if __name__ == '__main__':
746    main()
747