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