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 15 import logging 16 import os 17 import shlex 18 import sys 19 import time 20 21 if os.name == 'posix' and sys.version_info[0] < 3: 22 import subprocess32 as subprocess 23 DEVNULL = open(os.devnull, 'wb') 24 else: 25 import subprocess 26 # Only exists in python3.3 27 from subprocess import DEVNULL 28 29 30 class Error(Exception): 31 """Indicates that a command failed, is fatal to the test unless caught.""" 32 33 def __init__(self, result): 34 super(Error, self).__init__(result) 35 self.result = result 36 37 38 class TimeoutError(Error): 39 """Thrown when a BackgroundJob times out on wait.""" 40 41 42 class Result(object): 43 """Command execution result. 44 45 Contains information on subprocess execution after it has exited. 46 47 Attributes: 48 command: An array containing the command and all arguments that 49 was executed. 50 exit_status: Integer exit code of the process. 51 stdout_raw: The raw bytes output from standard out. 52 stderr_raw: The raw bytes output from standard error 53 duration: How long the process ran for. 54 did_timeout: True if the program timed out and was killed. 55 """ 56 57 @property 58 def stdout(self): 59 """String representation of standard output.""" 60 if not self._stdout_str: 61 self._stdout_str = self._raw_stdout.decode(encoding=self._encoding) 62 self._stdout_str = self._stdout_str.strip() 63 return self._stdout_str 64 65 @property 66 def stderr(self): 67 """String representation of standard error.""" 68 if not self._stderr_str: 69 self._stderr_str = self._raw_stderr.decode(encoding=self._encoding) 70 self._stderr_str = self._stderr_str.strip() 71 return self._stderr_str 72 73 def __init__(self, 74 command=[], 75 stdout=bytes(), 76 stderr=bytes(), 77 exit_status=None, 78 duration=0, 79 did_timeout=False, 80 encoding='utf-8'): 81 """ 82 Args: 83 command: The command that was run. This will be a list containing 84 the executed command and all args. 85 stdout: The raw bytes that standard output gave. 86 stderr: The raw bytes that standard error gave. 87 exit_status: The exit status of the command. 88 duration: How long the command ran. 89 did_timeout: True if the command timed out. 90 encoding: The encoding standard that the program uses. 91 """ 92 self.command = command 93 self.exit_status = exit_status 94 self._raw_stdout = stdout 95 self._raw_stderr = stderr 96 self._stdout_str = None 97 self._stderr_str = None 98 self._encoding = encoding 99 self.duration = duration 100 self.did_timeout = did_timeout 101 102 def __repr__(self): 103 return ('job.Result(command=%r, stdout=%r, stderr=%r, exit_status=%r, ' 104 'duration=%r, did_timeout=%r, encoding=%r)') % ( 105 self.command, self._raw_stdout, self._raw_stderr, 106 self.exit_status, self.duration, self.did_timeout, 107 self._encoding) 108 109 110 def run(command, 111 timeout=60, 112 ignore_status=False, 113 env=None, 114 io_encoding='utf-8'): 115 """Execute a command in a subproccess and return its output. 116 117 Commands can be either shell commands (given as strings) or the 118 path and arguments to an executable (given as a list). This function 119 will block until the subprocess finishes or times out. 120 121 Args: 122 command: The command to execute. Can be either a string or a list. 123 timeout: number seconds to wait for command to finish. 124 ignore_status: bool True to ignore the exit code of the remote 125 subprocess. Note that if you do ignore status codes, 126 you should handle non-zero exit codes explicitly. 127 env: dict enviroment variables to setup on the remote host. 128 io_encoding: str unicode encoding of command output. 129 130 Returns: 131 A job.Result containing the results of the ssh command. 132 133 Raises: 134 job.TimeoutError: When the remote command took to long to execute. 135 Error: When the ssh connection failed to be created. 136 CommandError: Ssh worked, but the command had an error executing. 137 """ 138 start_time = time.time() 139 proc = subprocess.Popen( 140 command, 141 env=env, 142 stdout=subprocess.PIPE, 143 stderr=subprocess.PIPE, 144 shell=not isinstance(command, list)) 145 # Wait on the process terminating 146 timed_out = False 147 out = bytes() 148 err = bytes() 149 try: 150 (out, err) = proc.communicate(timeout=timeout) 151 except subprocess.TimeoutExpired: 152 timed_out = True 153 proc.kill() 154 proc.wait() 155 156 result = Result( 157 command=command, 158 stdout=out, 159 stderr=err, 160 exit_status=proc.returncode, 161 duration=time.time() - start_time, 162 encoding=io_encoding, 163 did_timeout=timed_out) 164 logging.debug(result) 165 166 if timed_out: 167 logging.error("Command %s with %s timeout setting timed out", command, 168 timeout) 169 raise TimeoutError(result) 170 171 if not ignore_status and proc.returncode != 0: 172 raise Error(result) 173 174 return result 175 176 177 def run_async(command, env=None): 178 """Execute a command in a subproccess asynchronously. 179 180 It is the callers responsibility to kill/wait on the resulting 181 subprocess.Popen object. 182 183 Commands can be either shell commands (given as strings) or the 184 path and arguments to an executable (given as a list). This function 185 will not block. 186 187 Args: 188 command: The command to execute. Can be either a string or a list. 189 env: dict enviroment variables to setup on the remote host. 190 191 Returns: 192 A subprocess.Popen object representing the created subprocess. 193 194 """ 195 proc = subprocess.Popen( 196 command, 197 env=env, 198 preexec_fn=os.setpgrp, 199 shell=not isinstance(command, list), 200 stdout=DEVNULL, 201 stderr=subprocess.STDOUT) 202 logging.debug("command %s started with pid %s", command, proc.pid) 203 return proc 204 205