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 5import logging, signal, utils 6 7from autotest_lib.client.bin import test, utils 8from autotest_lib.client.common_lib import error 9from autotest_lib.client.common_lib.cros import chrome 10 11 12class _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 47class 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