You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
16 KiB
433 lines
16 KiB
# 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)
|