1 # Copyright 2017 - 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 15 import shellescape 16 import signal 17 import time 18 19 from utils import job 20 21 22 class 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 environment default is used. 39 """ 40 self._runner = runner 41 self._working_dir = working_dir 42 43 def run(self, command, timeout=3600, ignore_status=False): 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 ignore_status: Whether or not to throw exception based upon status. 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( 65 command_str, timeout=timeout, ignore_status=ignore_status) 66 67 def is_alive(self, identifier): 68 """Checks to see if a program is alive. 69 70 Checks to see if a program is alive on the shells environment. This can 71 be used to check on generic programs, or a specific program using 72 a pid. 73 74 Args: 75 identifier: string or int, Used to identify the program to check. 76 if given an int then it is assumed to be a pid. If 77 given a string then it will be used as a search key 78 to compare on the running processes. 79 Returns: 80 True if a process was found running, false otherwise. 81 """ 82 try: 83 if isinstance(identifier, str): 84 self.run('ps aux | grep -v grep | grep %s' % identifier) 85 elif isinstance(identifier, int): 86 self.signal(identifier, 0) 87 else: 88 raise ValueError('Bad type was given for identifier') 89 90 return True 91 except job.Error: 92 return False 93 94 def get_command_pids(self, identifier): 95 """Gets the pids of a program, based only upon the starting command 96 97 Searches for a program with a specific name and grabs the pids for all 98 programs that match only the command that started it. The arguments of 99 the command are ignored. 100 101 Args: 102 identifier: The search term that identifies the program. 103 104 Returns: 105 An array of ints of all pids that matched the identifier. 106 """ 107 result = self.run( 108 'ps -C %s --no-heading -o pid:1' % identifier, ignore_status=True) 109 110 # Output looks like pids on separate lines: 111 # 1245 112 # 5001 113 114 pids = result.stdout.splitlines() 115 return [int(pid) for pid in result.stdout.splitlines()] 116 117 def get_pids(self, identifier): 118 """Gets the pids of a program. 119 120 Searches for a program with a specific name and grabs the pids for all 121 programs that match. 122 123 Args: 124 identifier: A search term that identifies the program. 125 126 Returns: An array of all pids that matched the identifier, or None 127 if no pids were found. 128 """ 129 try: 130 result = self.run('ps aux | grep -v grep | grep %s' % identifier) 131 except job.Error: 132 raise StopIteration 133 134 lines = result.stdout.splitlines() 135 136 # The expected output of the above command is like so: 137 # bob 14349 0.0 0.0 34788 5552 pts/2 Ss Oct10 0:03 bash 138 # bob 52967 0.0 0.0 34972 5152 pts/4 Ss Oct10 0:00 bash 139 # Where the format is: 140 # USER PID ... 141 for line in lines: 142 pieces = line.split() 143 yield int(pieces[1]) 144 145 def search_file(self, search_string, file_name): 146 """Searches through a file for a string. 147 148 Args: 149 search_string: The string or pattern to look for. 150 file_name: The name of the file to search. 151 152 Returns: 153 True if the string or pattern was found, False otherwise. 154 """ 155 try: 156 self.run('grep %s %s' % (shellescape.quote(search_string), 157 file_name)) 158 return True 159 except job.Error: 160 return False 161 162 def read_file(self, file_name): 163 """Reads a file through the shell. 164 165 Args: 166 file_name: The name of the file to read. 167 168 Returns: 169 A string of the files contents. 170 """ 171 return self.run('cat %s' % file_name).stdout 172 173 def write_file(self, file_name, data): 174 """Writes a block of data to a file through the shell. 175 176 Args: 177 file_name: The name of the file to write to. 178 data: The string of data to write. 179 """ 180 return self.run('echo %s > %s' % (shellescape.quote(data), file_name)) 181 182 def append_file(self, file_name, data): 183 """Appends a block of data to a file through the shell. 184 185 Args: 186 file_name: The name of the file to write to. 187 data: The string of data to write. 188 """ 189 return self.run('echo %s >> %s' % (shellescape.quote(data), file_name)) 190 191 def touch_file(self, file_name): 192 """Creates a file through the shell. 193 194 Args: 195 file_name: The name of the file to create. 196 """ 197 self.write_file(file_name, '') 198 199 def delete_file(self, file_name): 200 """Deletes a file through the shell. 201 202 Args: 203 file_name: The name of the file to delete. 204 """ 205 try: 206 self.run('rm %s' % file_name) 207 except job.Error as e: 208 if 'No such file or directory' in e.result.stderr: 209 return 210 211 raise 212 213 def kill(self, identifier, timeout=10): 214 """Kills a program or group of programs through the shell. 215 216 Kills all programs that match an identifier through the shell. This 217 will send an increasing queue of kill signals to all programs 218 that match the identifier until either all are dead or the timeout 219 finishes. 220 221 Programs are guaranteed to be killed after running this command. 222 223 Args: 224 identifier: A string used to identify the program. 225 timeout: The time to wait for all programs to die. Each signal will 226 take an equal portion of this time. 227 """ 228 if isinstance(identifier, int): 229 pids = [identifier] 230 else: 231 pids = list(self.get_pids(identifier)) 232 233 signal_queue = [signal.SIGINT, signal.SIGTERM, signal.SIGKILL] 234 235 signal_duration = timeout / len(signal_queue) 236 for sig in signal_queue: 237 for pid in pids: 238 try: 239 self.signal(pid, sig) 240 except job.Error: 241 pass 242 243 start_time = time.time() 244 while pids and time.time() - start_time < signal_duration: 245 time.sleep(0.1) 246 pids = [pid for pid in pids if self.is_alive(pid)] 247 248 if not pids: 249 break 250 251 def signal(self, pid, sig): 252 """Sends a specific signal to a program. 253 254 Args: 255 pid: The process id of the program to kill. 256 sig: The signal to send. 257 258 Raises: 259 job.Error: Raised when the signal fail to reach 260 the specified program. 261 """ 262 self.run('kill -%d %d' % (sig, pid)) 263