• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 - The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import shlex
16import signal
17import time
18
19from acts.libs.proc import job
20
21
22class ShellCommand(object):
23    """Wraps basic commands that tend to be tied very closely to a shell.
24
25    This class is a wrapper for running basic shell commands through
26    any object that has a run command. Basic shell functionality for managing
27    the system, programs, and files in wrapped within this class.
28
29    Note: At the moment this only works with the ssh runner.
30    """
31
32    def __init__(self, runner, working_dir=None):
33        """Creates a new shell command invoker.
34
35        Args:
36            runner: The object that will run the shell commands.
37            working_dir: The directory that all commands should work in,
38                         if none then the runners enviroment default is used.
39        """
40        self._runner = runner
41        self._working_dir = working_dir
42
43    def run(self, command, timeout=60):
44        """Runs a generic command through the runner.
45
46        Takes the command and prepares it to be run in the target shell using
47        this objects settings.
48
49        Args:
50            command: The command to run.
51            timeout: How long to wait for the command (in seconds).
52
53        Returns:
54            A CmdResult object containing the results of the shell command.
55
56        Raises:
57            job.Error: When the command executed but had an error.
58        """
59        if self._working_dir:
60            command_str = 'cd %s; %s' % (self._working_dir, command)
61        else:
62            command_str = command
63
64        return self._runner.run(command_str, timeout=timeout)
65
66    def is_alive(self, identifier):
67        """Checks to see if a program is alive.
68
69        Checks to see if a program is alive on the shells enviroment. This can
70        be used to check on generic programs, or a specific program using
71        a pid.
72
73        Args:
74            identifier: string or int, Used to identify the program to check.
75                        if given an int then it is assumed to be a pid. If
76                        given a string then it will be used as a search key
77                        to compare on the running processes.
78        Returns:
79            True if a process was found running, false otherwise.
80        """
81        try:
82            if isinstance(identifier, str):
83                self.run('ps aux | grep -v grep | grep %s' % identifier)
84            elif isinstance(identifier, int):
85                self.signal(identifier, 0)
86            else:
87                raise ValueError('Bad type was given for identifier')
88
89            return True
90        except job.Error:
91            return False
92
93    def get_pids(self, identifier):
94        """Gets the pids of a program.
95
96        Searches for a program with a specific name and grabs the pids for all
97        programs that match.
98
99        Args:
100            identifier: A search term that identifies the program.
101
102        Returns: An array of all pids that matched the identifier, or None
103                  if no pids were found.
104        """
105        try:
106            result = self.run('ps aux | grep -v grep | grep %s' % identifier)
107        except job.Error as e:
108            if e.result.exit_status == 1:
109                # Grep returns exit status 1 when no lines are selected. This is
110                # an expected return code.
111                return
112            raise e
113
114        lines = result.stdout.splitlines()
115
116        # The expected output of the above command is like so:
117        # bob    14349  0.0  0.0  34788  5552 pts/2    Ss   Oct10   0:03 bash
118        # bob    52967  0.0  0.0  34972  5152 pts/4    Ss   Oct10   0:00 bash
119        # Where the format is:
120        # USER    PID  ...
121        for line in lines:
122            pieces = line.split()
123            try:
124                yield int(pieces[1])
125            except StopIteration:
126                return
127
128    def search_file(self, search_string, file_name):
129        """Searches through a file for a string.
130
131        Args:
132            search_string: The string or pattern to look for.
133            file_name: The name of the file to search.
134
135        Returns:
136            True if the string or pattern was found, False otherwise.
137        """
138        try:
139            self.run('grep %s %s' % (shlex.quote(search_string), file_name))
140            return True
141        except job.Error:
142            return False
143
144    def read_file(self, file_name):
145        """Reads a file through the shell.
146
147        Args:
148            file_name: The name of the file to read.
149
150        Returns:
151            A string of the files contents.
152        """
153        return self.run('cat %s' % file_name).stdout
154
155    def write_file(self, file_name, data):
156        """Writes a block of data to a file through the shell.
157
158        Args:
159            file_name: The name of the file to write to.
160            data: The string of data to write.
161        """
162        return self.run('echo %s > %s' % (shlex.quote(data), file_name))
163
164    def append_file(self, file_name, data):
165        """Appends a block of data to a file through the shell.
166
167        Args:
168            file_name: The name of the file to write to.
169            data: The string of data to write.
170        """
171        return self.run('echo %s >> %s' % (shlex.quote(data), file_name))
172
173    def touch_file(self, file_name):
174        """Creates a file through the shell.
175
176        Args:
177            file_name: The name of the file to create.
178        """
179        self.write_file(file_name, '')
180
181    def delete_file(self, file_name):
182        """Deletes a file through the shell.
183
184        Args:
185            file_name: The name of the file to delete.
186        """
187        try:
188            self.run('rm -r %s' % file_name)
189        except job.Error as e:
190            if 'No such file or directory' in e.result.stderr:
191                return
192
193            raise
194
195    def kill(self, identifier, timeout=10):
196        """Kills a program or group of programs through the shell.
197
198        Kills all programs that match an identifier through the shell. This
199        will send an increasing queue of kill signals to all programs
200        that match the identifier until either all are dead or the timeout
201        finishes.
202
203        Programs are guaranteed to be killed after running this command.
204
205        Args:
206            identifier: A string used to identify the program.
207            timeout: The time to wait for all programs to die. Each signal will
208                     take an equal portion of this time.
209        """
210        if isinstance(identifier, int):
211            pids = [identifier]
212        else:
213            pids = list(self.get_pids(identifier))
214
215        signal_queue = [signal.SIGINT, signal.SIGTERM, signal.SIGKILL]
216
217        signal_duration = timeout / len(signal_queue)
218        for sig in signal_queue:
219            for pid in pids:
220                try:
221                    self.signal(pid, sig)
222                except job.Error:
223                    pass
224
225            start_time = time.time()
226            while pids and time.time() - start_time < signal_duration:
227                time.sleep(0.1)
228                pids = [pid for pid in pids if self.is_alive(pid)]
229
230            if not pids:
231                break
232
233    def signal(self, pid, sig):
234        """Sends a specific signal to a program.
235
236        Args:
237            pid: The process id of the program to kill.
238            sig: The signal to send.
239
240        Raises:
241            job.Error: Raised when the signal fail to reach
242                       the specified program.
243        """
244        self.run('kill -%d %d' % (sig, pid))
245