blob: c5d8d3aec4821fb30a72b0d3e9cd888115cd7247 [file] [log] [blame]
# 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)