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