Home | History | Annotate | Download | only in wardmodem
      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