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