blob: e80d6dc8c3c1e45aadb7c73742b7a61677a150f5 [file] [log] [blame]
# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This module helps launch pseudomodem as a subprocess. It helps with the
# initial setup of pseudomodem, as well as ensures proper cleanup.
# For details about the options accepted by pseudomodem, please check the
# |pseudomodem| module.
# This module also doubles as the python entry point to run pseudomodem from the
# command line. To avoid confusion, please use the shell script run_pseudomodem
# to run pseudomodem from command line.
import dbus
import json
import logging
import os
import pwd
import signal
import stat
import sys
import subprocess
import tempfile
import common
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.cros import service_stopper
from autotest_lib.client.cros.cellular import mm1_constants
from autotest_lib.client.cros.cellular import net_interface
import pm_constants
import pseudomodem
# TODO(pprabhu) Move this to the right utils file.
# pprabhu: I haven't yet figured out which of the myriad utils files I should
# update. There is an implementation of |nuke_subprocess| that does not take
# timeout_hint_seconds in common_lib/utils.py, but |poll_for_condition|
# is not available there.
def nuke_subprocess(subproc, timeout_hint_seconds=0):
"""
Attempt to kill the given subprocess via an escalating series of signals.
Between each attempt, the process is given |timeout_hint_seconds| to clean
up. So, the function may take up to 3 * |timeout_hint_seconds| time to
finish.
@param subproc: The python subprocess to nuke.
@param timeout_hint_seconds: The time to wait between successive attempts.
@returns: The result from the subprocess, None if we failed to kill it.
"""
# check if the subprocess is still alive, first
if subproc.poll() is not None:
return subproc.poll()
signal_queue = [signal.SIGINT, signal.SIGTERM, signal.SIGKILL]
for sig in signal_queue:
logging.info('Nuking %s with %s', subproc.pid, sig)
utils.signal_pid(subproc.pid, sig)
try:
utils.poll_for_condition(
lambda: subproc.poll() is not None,
timeout=timeout_hint_seconds)
return subproc.poll()
except utils.TimeoutError:
pass
return None
class PseudoModemManagerContextException(Exception):
""" Exception class for exceptions raised by PseudoModemManagerContext. """
pass
class PseudoModemManagerContext(object):
"""
A context to launch pseudomodem in background.
Tests should use |PeudoModemManagerContext| to launch pseudomodem. It is
intended to be used with the |with| clause like so:
with PseudoModemManagerContext(...):
# Run test
pseudomodem will be launch in a subprocess safely when entering the |with|
block, and cleaned up when exiting.
"""
SHORT_TIMEOUT_SECONDS = 4
# Some actions are dependent on hardware cooperating. We need to wait longer
# for these. Try to minimize using this constant.
WAIT_FOR_HARDWARE_TIMEOUT_SECONDS = 12
TEMP_FILE_PREFIX = 'pseudomodem_'
REAL_MANAGER_SERVICES = ['modemmanager', 'cromo']
REAL_MANAGER_PROCESSES = ['ModemManager', 'cromo']
TEST_OBJECT_ARG_FLAGS = ['test-modem-arg',
'test-sim-arg',
'test-state-machine-factory-arg']
def __init__(self,
use_pseudomodem,
flags_map=None,
block_output=True,
bus=None):
"""
@param use_pseudomodem: This flag can be used to treat pseudomodem as a
no-op. When |True|, pseudomodem is launched as expected. When
|False|, this operation is a no-op, and pseudomodem will not be
launched.
@param flags_map: This is a map of pseudomodem arguments. See
|pseudomodem| module for the list of supported arguments. For
example, to launch pseudomodem with a modem of family 3GPP, use:
with PseudoModemManager(True, flags_map={'family' : '3GPP}):
# Do stuff
@param block_output: If True, output from the pseudomodem process is not
piped to stdout. This is the default.
@param bus: A handle to the dbus.SystemBus. If you use dbus in your
tests, you should obtain a handle to the bus and pass it in
here. Not doing so can cause incompatible mainloop settings in
the dbus module.
"""
self._use_pseudomodem = use_pseudomodem
self._block_output = block_output
self._temp_files = []
self.cmd_line_flags = self._ConvertMapToFlags(flags_map if flags_map
else {})
self._service_stopper = service_stopper.ServiceStopper(
self.REAL_MANAGER_SERVICES)
self._net_interface = None
self._null_pipe = None
self._exit_error_file_path = None
self._pseudomodem_process = None
self._bus = bus
if not self._bus:
# Currently, the glib mainloop, or a wrapper thereof are the only
# mainloops we ever use with dbus. So, it's a comparatively safe bet
# to set that up as the mainloop here.
# Ideally, if a test wants to use dbus, it should pass us its own
# bus.
dbus_loop = dbus.mainloop.glib.DBusGMainLoop()
self._bus = dbus.SystemBus(private=True, mainloop=dbus_loop)
@property
def cmd_line_flags(self):
""" The command line flags that will be passed to pseudomodem. """
return self._cmd_line_flags
@cmd_line_flags.setter
def cmd_line_flags(self, val):
"""
Set the command line flags to be passed to pseudomodem.
@param val: The flags.
"""
logging.info('Command line flags for pseudomodem set to: |%s|', val)
self._cmd_line_flags = val
def __enter__(self):
return self.Start()
def __exit__(self, *args):
return self.Stop(*args)
def Start(self):
""" Start the context. This launches pseudomodem. """
if not self._use_pseudomodem:
return self
self._CheckPseudoModemArguments()
self._service_stopper.stop_services()
self._WaitForRealModemManagersToDie()
self._net_interface = net_interface.PseudoNetInterface()
self._net_interface.Setup()
toplevel = os.path.dirname(os.path.realpath(__file__))
cmd = [os.path.join(toplevel, 'pseudomodem.py')]
cmd = cmd + self.cmd_line_flags
fd, self._exit_error_file_path = self._CreateTempFile()
os.close(fd) # We don't need the fd.
cmd = cmd + [pseudomodem.EXIT_ERROR_FILE_FLAG,
self._exit_error_file_path]
# Setup health checker for child process.
signal.signal(signal.SIGCHLD, self._SigchldHandler)
if self._block_output:
self._null_pipe = open(os.devnull, 'w')
self._pseudomodem_process = subprocess.Popen(
cmd,
preexec_fn=PseudoModemManagerContext._SetUserModem,
close_fds=True,
stdout=self._null_pipe,
stderr=self._null_pipe)
else:
self._pseudomodem_process = subprocess.Popen(
cmd,
preexec_fn=PseudoModemManagerContext._SetUserModem,
close_fds=True)
self._EnsurePseudoModemUp()
return self
def Stop(self, *args):
""" Exit the context. This terminates pseudomodem. """
if not self._use_pseudomodem:
return
# Remove health check on child process.
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
if self._pseudomodem_process:
if self._pseudomodem_process.poll() is None:
if (nuke_subprocess(self._pseudomodem_process,
self.SHORT_TIMEOUT_SECONDS) is
None):
logging.warning('Failed to clean up the launched '
'pseudomodem process')
self._pseudomodem_process = None
if self._null_pipe:
self._null_pipe.close()
self._null_pipe = None
if self._net_interface:
self._net_interface.Teardown()
self._net_interface = None
self._DeleteTempFiles()
self._service_stopper.restore_services()
def _ConvertMapToFlags(self, flags_map):
"""
Convert the argument map given to the context to flags for pseudomodem.
@param flags_map: A map of flags. The keys are the names of the flags
accepted by pseudomodem. The value, if not None, is the value
for that flag. We do not support |None| as the value for a flag.
@returns: the list of flags to pass to pseudomodem.
"""
cmd_line_flags = []
for key, value in flags_map.iteritems():
cmd_line_flags.append('--' + key)
if key in self.TEST_OBJECT_ARG_FLAGS:
cmd_line_flags.append(self._DumpArgToFile(value))
elif value:
cmd_line_flags.append(value)
return cmd_line_flags
def _DumpArgToFile(self, arg):
"""
Dump a given python list to a temp file in json format.
This is used to pass arguments to custom objects from tests that
are to be instantiated by pseudomodem. The argument must be a list. When
running pseudomodem, this list will be unpacked to get the arguments.
@returns: Absolute path to the tempfile created.
"""
fd, arg_file_path = self._CreateTempFile()
arg_file = os.fdopen(fd, 'wb')
json.dump(arg, arg_file)
arg_file.close()
return arg_file_path
def _WaitForRealModemManagersToDie(self):
"""
Wait for real modem managers to quit. Die otherwise.
Sometimes service stopper does not kill ModemManager process, if it is
launched by something other than upstart. We want to ensure that the
process is dead before continuing.
This method can block for up to a minute. Sometimes, ModemManager can
take up to a 10 seconds to die after service stopper has stopped it. We
wait for it to clean up before concluding that the process is here to
stay.
@raises: PseudoModemManagerContextException if a modem manager process
does not quit in a reasonable amount of time.
"""
def _IsProcessRunning(process):
try:
utils.run('pgrep -x %s' % process)
return True
except error.CmdError:
return False
for manager in self.REAL_MANAGER_PROCESSES:
try:
utils.poll_for_condition(
lambda:not _IsProcessRunning(manager),
timeout=self.WAIT_FOR_HARDWARE_TIMEOUT_SECONDS)
except utils.TimeoutError:
err_msg = ('%s is still running. '
'It may interfere with pseudomodem.' %
manager)
logging.error(err_msg)
raise PseudoModemManagerContextException(err_msg)
def _CheckPseudoModemArguments(self):
"""
Parse the given pseudomodem arguments.
By parsing the arguments in the context, we can provide early feedback
about incorrect arguments.
"""
pseudomodem.ParseArguments(self.cmd_line_flags)
@staticmethod
def _SetUserModem():
"""
Set the unix user of the calling process to |modem|.
This functions is called by the launched subprocess so that pseudomodem
can be launched as the |modem| user.
On encountering an error, this method will terminate the process.
"""
try:
pwd_data = pwd.getpwnam(pm_constants.MM1_USER)
except KeyError as e:
logging.error('Could not find uid for user %s [%s]',
pm_constants.MM1_USER, str(e))
sys.exit(1)
logging.debug('Setting UID to %d', pwd_data.pw_uid)
try:
os.setuid(pwd_data.pw_uid)
except OSError as e:
logging.error('Could not set uid to %d [%s]',
pwd_data.pw_uid, str(e))
sys.exit(1)
def _EnsurePseudoModemUp(self):
""" Makes sure that pseudomodem in child process is ready. """
def _LivenessCheck():
try:
testing_object = self._bus.get_object(
mm1_constants.I_MODEM_MANAGER,
pm_constants.TESTING_PATH)
return testing_object.IsAlive(
dbus_interface=pm_constants.I_TESTING)
except dbus.DBusException as e:
logging.debug('LivenessCheck: No luck yet. (%s)', str(e))
return False
utils.poll_for_condition(
_LivenessCheck,
timeout=self.SHORT_TIMEOUT_SECONDS,
exception=PseudoModemManagerContextException(
'pseudomodem did not initialize properly.'))
def _CreateTempFile(self):
"""
Creates a tempfile such that the child process can read/write it.
The file path is stored in a list so that the file can be deleted later
using |_DeleteTempFiles|.
@returns: (fd, arg_file_path)
fd: A file descriptor for the created file.
arg_file_path: Full path of the created file.
"""
fd, arg_file_path = tempfile.mkstemp(prefix=self.TEMP_FILE_PREFIX)
self._temp_files.append(arg_file_path)
# Set file permissions so that pseudomodem process can read/write it.
cur_mod = os.stat(arg_file_path).st_mode
os.chmod(arg_file_path,
cur_mod | stat.S_IRGRP | stat.S_IROTH | stat.S_IWGRP |
stat.S_IWOTH)
return fd, arg_file_path
def _DeleteTempFiles(self):
""" Deletes all temp files created by this context. """
for file_path in self._temp_files:
try:
os.remove(file_path)
except OSError as e:
logging.warning('Failed to delete temp file: %s (error %s)',
file_path, str(e))
def _SigchldHandler(self, signum, frame):
"""
Signal handler for SIGCHLD.
This is setup while the pseudomodem subprocess is running. A call to
this signal handler may signify early termination of the subprocess.
@param signum: The signal number.
@param frame: Ignored.
"""
if not self._pseudomodem_process:
# We can receive a SIGCHLD even before the setup of the child
# process is complete.
return
if self._pseudomodem_process.poll() is not None:
# See if child process left detailed error report
error_reason, error_traceback = pseudomodem.ExtractExitError(
self._exit_error_file_path)
logging.error('pseudomodem child process quit early!')
logging.error('Reason: %s', error_reason)
for line in error_traceback:
logging.error('Traceback: %s', line.strip())
raise PseudoModemManagerContextException(
'pseudomodem quit early! (%s)' %
error_reason)