普通文本  |  191行  |  7 KB

# Copyright (c) 2013 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.

import logging, signal

from autotest_lib.client.bin import test, utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import chrome


class _TestProcess:


    def __init__(self, command, pattern):
        self.command = command
        self.pattern = pattern
        self.pid_su = ''
        self.pid_bash = ''


    def __wait_for_subprocess(self):
        """Waits for a subprocess that matches self.pattern."""
        def _subprocess_pid(pattern):
            pid = utils.system_output('ps -U chronos -o pid,args | grep %s'
                                      % pattern, ignore_status=True)
            return pid.lstrip().split(' ')[0] if pid else 0

        utils.poll_for_condition(lambda: _subprocess_pid(self.pattern))
        self.pid_bash = _subprocess_pid(self.pattern)


    def run_me_as_chronos(self):
        """Runs the command in self.command as user 'chronos'.

        Waits for bash sub-process to start, and fails if this does not happen.

        """
        # Start process as user chronos.
        self.pid_su = utils.BgJob('su chronos -c "%s"' % self.command)
        # Get pid of bash sub-process. Even though utils.BgJob() has exited,
        # the su-process may not have created its sub-process yet.
        self.__wait_for_subprocess()
        return self.pid_bash != ''


class login_LogoutProcessCleanup(test.test):
    """Tests that all processes owned by chronos are destroyed on logout."""
    version = 1


    def __get_session_manager_pid(self):
        """Get the PID of the session manager."""
        return utils.system_output('pgrep "^session_manager$"',
                                   ignore_status=True)


    def __get_chronos_pids(self):
        """Get a list of all PIDs that are owned by chronos."""
        return utils.system_output('pgrep -U chronos',
                                   ignore_status=True).splitlines()


    def __get_stat_fields(self, pid):
        """Get a list of strings for the fields in /proc/pid/stat.

        @param pid: process to stat.
        """
        with open('/proc/%s/stat' % pid) as stat_file:
            return stat_file.read().split(' ')


    def __get_parent_pid(self, pid):
        """Get the parent PID of the given process.

        @param pid: process whose parent pid you want to look up.
        """
        return self.__get_stat_fields(pid)[3]


    def __is_process_dead(self, pid):
        """Check whether or not a process is dead.  Zombies are dead.

        @param pid: process to check on.
        """
        try:
            if self.__get_stat_fields(pid)[2] == 'Z':
                return True
        except IOError:
            # If the proc entry is gone, it's dead.
            return True
        return False


    def __process_has_ancestor(self, pid, ancestor_pid):
        """Tests if pid has ancestor_pid anywhere in the process tree.

        @param pid: pid whose ancestry the caller is searching.
        @param ancestor_pid: the ancestor to look for.
        """
        ppid = pid
        while not (ppid == ancestor_pid or ppid == '0'):
            # This could fail if the process is killed while we are
            # looking up the parent.  In that case, treat it as if it
            # did not have the ancestor.
            try:
                ppid = self.__get_parent_pid(ppid)
            except IOError:
                return False
        return ppid == ancestor_pid


    def __has_chronos_processes(self, session_manager_pid):
        """Looks for chronos processes not started by the session manager.

        @param session_manager_pid: pid of the session_manager.
        """
        pids = self.__get_chronos_pids()
        for p in pids:
            if self.__is_process_dead(p):
                continue
            if not self.__process_has_ancestor(p, session_manager_pid):
                logging.info('Found pid (%s) owned by chronos and not '
                             'started by the session manager.', p)
                return True
        return False


    def run_once(self):
        with chrome.Chrome() as cr:
            test_processes = []
            test_processes.append(
                    _TestProcess('while :; do :; done ; # tst00','bash.*tst00'))
            # Create a test command that ignores SIGTERM.
            test_processes.append(
                    _TestProcess('trap 15; while :; do :; done ; # tst01',
                                 'bash.*tst01'))

            for test in test_processes:
                if not test.run_me_as_chronos():
                    raise error.TestFail(
                            'Did not start: bash %s' % test.command)

            session_manager = self.__get_session_manager_pid()
            if not session_manager:
                raise error.TestError('Could not find session manager pid')

            if not self.__has_chronos_processes(session_manager):
                raise error.TestFail(
                        'Expected to find processes owned by chronos that were '
                        'not started by the session manager while logged in.')

            cpids = self.__get_chronos_pids()

            # Sanity checks: make sure test jobs are in the list and still
            # running.
            for test in test_processes:
                if cpids.count(test.pid_bash) != 1:
                    raise error.TestFail('Job missing (%s - %s)' %
                                         (test.pid_bash, test.command))
                if self.__is_process_dead(test.pid_bash):
                    raise error.TestFail('Job prematurely dead (%s - %s)' %
                                         (test.pid_bash, test.command))

        logging.info('Logged out, searching for processes that should be dead.')

        # Wait until we have a new session manager.  At that point, all
        # old processes should be dead.
        old_session_manager = session_manager
        utils.poll_for_condition(
                lambda: old_session_manager != self.__get_session_manager_pid())
        session_manager = self.__get_session_manager_pid()

        # Make sure all pre-logout chronos processes are now dead.
        old_pid_count = 0
        for p in cpids:
            if not self.__is_process_dead(p):
                old_pid_count += 1
                proc_args = utils.system_output('ps -p %s -o args=' % p,
                                                ignore_status=True)
                logging.info('Found pre-logout chronos process pid=%s (%s) '
                             'still alive.', p, proc_args)
                # If p is something we started, kill it.
                for test in test_processes:
                    if (p == test.pid_su or p == test.pid_bash):
                        utils.signal_pid(p, signal.SIGKILL)

        if old_pid_count > 0:
            raise error.TestFail('Found %s chronos processes that survived '
                                 'logout.' % old_pid_count)