Home | History | Annotate | Download | only in login_LogoutProcessCleanup
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging, signal, utils
      6 
      7 from autotest_lib.client.bin import test, utils
      8 from autotest_lib.client.common_lib import error
      9 from autotest_lib.client.common_lib.cros import chrome
     10 
     11 
     12 class _TestProcess:
     13 
     14 
     15     def __init__(self, command, pattern):
     16         self.command = command
     17         self.pattern = pattern
     18         self.pid_su = ''
     19         self.pid_bash = ''
     20 
     21 
     22     def __wait_for_subprocess(self):
     23         """Waits for a subprocess that matches self.pattern."""
     24         def _subprocess_pid(pattern):
     25             pid = utils.system_output('ps -U chronos -o pid,args | grep %s'
     26                                       % pattern, ignore_status=True)
     27             return pid.lstrip().split(' ')[0] if pid else 0
     28 
     29         utils.poll_for_condition(lambda: _subprocess_pid(self.pattern))
     30         self.pid_bash = _subprocess_pid(self.pattern)
     31 
     32 
     33     def run_me_as_chronos(self):
     34         """Runs the command in self.command as user 'chronos'.
     35 
     36         Waits for bash sub-process to start, and fails if this does not happen.
     37 
     38         """
     39         # Start process as user chronos.
     40         self.pid_su = utils.BgJob('su chronos -c "%s"' % self.command)
     41         # Get pid of bash sub-process. Even though utils.BgJob() has exited,
     42         # the su-process may not have created its sub-process yet.
     43         self.__wait_for_subprocess()
     44         return self.pid_bash != ''
     45 
     46 
     47 class login_LogoutProcessCleanup(test.test):
     48     """Tests that all processes owned by chronos are destroyed on logout."""
     49     version = 1
     50 
     51 
     52     def __get_session_manager_pid(self):
     53         """Get the PID of the session manager."""
     54         return utils.system_output('pgrep "^session_manager$"',
     55                                    ignore_status=True)
     56 
     57 
     58     def __get_chronos_pids(self):
     59         """Get a list of all PIDs that are owned by chronos."""
     60         return utils.system_output('pgrep -U chronos',
     61                                    ignore_status=True).splitlines()
     62 
     63 
     64     def __get_stat_fields(self, pid):
     65         """Get a list of strings for the fields in /proc/pid/stat.
     66 
     67         @param pid: process to stat.
     68         """
     69         with open('/proc/%s/stat' % pid) as stat_file:
     70             return stat_file.read().split(' ')
     71 
     72 
     73     def __get_parent_pid(self, pid):
     74         """Get the parent PID of the given process.
     75 
     76         @param pid: process whose parent pid you want to look up.
     77         """
     78         return self.__get_stat_fields(pid)[3]
     79 
     80 
     81     def __is_process_dead(self, pid):
     82         """Check whether or not a process is dead.  Zombies are dead.
     83 
     84         @param pid: process to check on.
     85         """
     86         try:
     87             if self.__get_stat_fields(pid)[2] == 'Z':
     88                 return True
     89         except IOError:
     90             # If the proc entry is gone, it's dead.
     91             return True
     92         return False
     93 
     94 
     95     def __process_has_ancestor(self, pid, ancestor_pid):
     96         """Tests if pid has ancestor_pid anywhere in the process tree.
     97 
     98         @param pid: pid whose ancestry the caller is searching.
     99         @param ancestor_pid: the ancestor to look for.
    100         """
    101         ppid = pid
    102         while not (ppid == ancestor_pid or ppid == '0'):
    103             # This could fail if the process is killed while we are
    104             # looking up the parent.  In that case, treat it as if it
    105             # did not have the ancestor.
    106             try:
    107                 ppid = self.__get_parent_pid(ppid)
    108             except IOError:
    109                 return False
    110         return ppid == ancestor_pid
    111 
    112 
    113     def __has_chronos_processes(self, session_manager_pid):
    114         """Looks for chronos processes not started by the session manager.
    115 
    116         @param session_manager_pid: pid of the session_manager.
    117         """
    118         pids = self.__get_chronos_pids()
    119         for p in pids:
    120             if self.__is_process_dead(p):
    121                 continue
    122             if not self.__process_has_ancestor(p, session_manager_pid):
    123                 logging.info('Found pid (%s) owned by chronos and not '
    124                              'started by the session manager.', p)
    125                 return True
    126         return False
    127 
    128 
    129     def run_once(self):
    130         with chrome.Chrome() as cr:
    131             test_processes = []
    132             test_processes.append(
    133                     _TestProcess('while :; do :; done ; # tst00','bash.*tst00'))
    134             # Create a test command that ignores SIGTERM.
    135             test_processes.append(
    136                     _TestProcess('trap 15; while :; do :; done ; # tst01',
    137                                  'bash.*tst01'))
    138 
    139             for test in test_processes:
    140                 if not test.run_me_as_chronos():
    141                     raise error.TestFail(
    142                             'Did not start: bash %s' % test.command)
    143 
    144             session_manager = self.__get_session_manager_pid()
    145             if not session_manager:
    146                 raise error.TestError('Could not find session manager pid')
    147 
    148             if not self.__has_chronos_processes(session_manager):
    149                 raise error.TestFail(
    150                         'Expected to find processes owned by chronos that were '
    151                         'not started by the session manager while logged in.')
    152 
    153             cpids = self.__get_chronos_pids()
    154 
    155             # Sanity checks: make sure test jobs are in the list and still
    156             # running.
    157             for test in test_processes:
    158                 if cpids.count(test.pid_bash) != 1:
    159                     raise error.TestFail('Job missing (%s - %s)' %
    160                                          (test.pid_bash, test.command))
    161                 if self.__is_process_dead(test.pid_bash):
    162                     raise error.TestFail('Job prematurely dead (%s - %s)' %
    163                                          (test.pid_bash, test.command))
    164 
    165         logging.info('Logged out, searching for processes that should be dead.')
    166 
    167         # Wait until we have a new session manager.  At that point, all
    168         # old processes should be dead.
    169         old_session_manager = session_manager
    170         utils.poll_for_condition(
    171                 lambda: old_session_manager != self.__get_session_manager_pid())
    172         session_manager = self.__get_session_manager_pid()
    173 
    174         # Make sure all pre-logout chronos processes are now dead.
    175         old_pid_count = 0
    176         for p in cpids:
    177             if not self.__is_process_dead(p):
    178                 old_pid_count += 1
    179                 proc_args = utils.system_output('ps -p %s -o args=' % p,
    180                                                 ignore_status=True)
    181                 logging.info('Found pre-logout chronos process pid=%s (%s) '
    182                              'still alive.', p, proc_args)
    183                 # If p is something we started, kill it.
    184                 for test in test_processes:
    185                     if (p == test.pid_su or p == test.pid_bash):
    186                         utils.signal_pid(p, signal.SIGKILL)
    187 
    188         if old_pid_count > 0:
    189             raise error.TestFail('Found %s chronos processes that survived '
    190                                  'logout.' % old_pid_count)
    191