1# Copyright (c) 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""A wrapper for subprocess to make calling shell commands easier.""" 6 7import codecs 8import logging 9import os 10import pipes 11import select 12import signal 13import string 14import StringIO 15import subprocess 16import sys 17import time 18 19from devil import base_error 20 21logger = logging.getLogger(__name__) 22 23_SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./') 24 25# Cache the string-escape codec to ensure subprocess can find it 26# later. Return value doesn't matter. 27codecs.lookup('string-escape') 28 29 30def SingleQuote(s): 31 """Return an shell-escaped version of the string using single quotes. 32 33 Reliably quote a string which may contain unsafe characters (e.g. space, 34 quote, or other special characters such as '$'). 35 36 The returned value can be used in a shell command line as one token that gets 37 to be interpreted literally. 38 39 Args: 40 s: The string to quote. 41 42 Return: 43 The string quoted using single quotes. 44 """ 45 return pipes.quote(s) 46 47 48def DoubleQuote(s): 49 """Return an shell-escaped version of the string using double quotes. 50 51 Reliably quote a string which may contain unsafe characters (e.g. space 52 or quote characters), while retaining some shell features such as variable 53 interpolation. 54 55 The returned value can be used in a shell command line as one token that gets 56 to be further interpreted by the shell. 57 58 The set of characters that retain their special meaning may depend on the 59 shell implementation. This set usually includes: '$', '`', '\', '!', '*', 60 and '@'. 61 62 Args: 63 s: The string to quote. 64 65 Return: 66 The string quoted using double quotes. 67 """ 68 if not s: 69 return '""' 70 elif all(c in _SafeShellChars for c in s): 71 return s 72 else: 73 return '"' + s.replace('"', '\\"') + '"' 74 75 76def ShrinkToSnippet(cmd_parts, var_name, var_value): 77 """Constructs a shell snippet for a command using a variable to shrink it. 78 79 Takes into account all quoting that needs to happen. 80 81 Args: 82 cmd_parts: A list of command arguments. 83 var_name: The variable that holds var_value. 84 var_value: The string to replace in cmd_parts with $var_name 85 86 Returns: 87 A shell snippet that does not include setting the variable. 88 """ 89 def shrink(value): 90 parts = (x and SingleQuote(x) for x in value.split(var_value)) 91 with_substitutions = ('"$%s"' % var_name).join(parts) 92 return with_substitutions or "''" 93 94 return ' '.join(shrink(part) for part in cmd_parts) 95 96 97def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): 98 # preexec_fn isn't supported on windows. 99 if sys.platform == 'win32': 100 close_fds = (stdout is None and stderr is None) 101 preexec_fn = None 102 else: 103 close_fds = True 104 preexec_fn = lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL) 105 106 return subprocess.Popen( 107 args=args, cwd=cwd, stdout=stdout, stderr=stderr, 108 shell=shell, close_fds=close_fds, env=env, preexec_fn=preexec_fn) 109 110 111def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): 112 pipe = Popen(args, stdout=stdout, stderr=stderr, shell=shell, cwd=cwd, 113 env=env) 114 pipe.communicate() 115 return pipe.wait() 116 117 118def RunCmd(args, cwd=None): 119 """Opens a subprocess to execute a program and returns its return value. 120 121 Args: 122 args: A string or a sequence of program arguments. The program to execute is 123 the string or the first item in the args sequence. 124 cwd: If not None, the subprocess's current directory will be changed to 125 |cwd| before it's executed. 126 127 Returns: 128 Return code from the command execution. 129 """ 130 logger.info(str(args) + ' ' + (cwd or '')) 131 return Call(args, cwd=cwd) 132 133 134def GetCmdOutput(args, cwd=None, shell=False, env=None): 135 """Open a subprocess to execute a program and returns its output. 136 137 Args: 138 args: A string or a sequence of program arguments. The program to execute is 139 the string or the first item in the args sequence. 140 cwd: If not None, the subprocess's current directory will be changed to 141 |cwd| before it's executed. 142 shell: Whether to execute args as a shell command. 143 env: If not None, a mapping that defines environment variables for the 144 subprocess. 145 146 Returns: 147 Captures and returns the command's stdout. 148 Prints the command's stderr to logger (which defaults to stdout). 149 """ 150 (_, output) = GetCmdStatusAndOutput(args, cwd, shell, env) 151 return output 152 153 154def _ValidateAndLogCommand(args, cwd, shell): 155 if isinstance(args, basestring): 156 if not shell: 157 raise Exception('string args must be run with shell=True') 158 else: 159 if shell: 160 raise Exception('array args must be run with shell=False') 161 args = ' '.join(SingleQuote(str(c)) for c in args) 162 if cwd is None: 163 cwd = '' 164 else: 165 cwd = ':' + cwd 166 logger.debug('[host]%s> %s', cwd, args) 167 return args 168 169 170def GetCmdStatusAndOutput(args, cwd=None, shell=False, env=None): 171 """Executes a subprocess and returns its exit code and output. 172 173 Args: 174 args: A string or a sequence of program arguments. The program to execute is 175 the string or the first item in the args sequence. 176 cwd: If not None, the subprocess's current directory will be changed to 177 |cwd| before it's executed. 178 shell: Whether to execute args as a shell command. Must be True if args 179 is a string and False if args is a sequence. 180 env: If not None, a mapping that defines environment variables for the 181 subprocess. 182 183 Returns: 184 The 2-tuple (exit code, stdout). 185 """ 186 status, stdout, stderr = GetCmdStatusOutputAndError( 187 args, cwd=cwd, shell=shell, env=env) 188 189 if stderr: 190 logger.critical('STDERR: %s', stderr) 191 logger.debug('STDOUT: %s%s', stdout[:4096].rstrip(), 192 '<truncated>' if len(stdout) > 4096 else '') 193 return (status, stdout) 194 195 196def StartCmd(args, cwd=None, shell=False, env=None): 197 """Starts a subprocess and returns a handle to the process. 198 199 Args: 200 args: A string or a sequence of program arguments. The program to execute is 201 the string or the first item in the args sequence. 202 cwd: If not None, the subprocess's current directory will be changed to 203 |cwd| before it's executed. 204 shell: Whether to execute args as a shell command. Must be True if args 205 is a string and False if args is a sequence. 206 env: If not None, a mapping that defines environment variables for the 207 subprocess. 208 209 Returns: 210 A process handle from subprocess.Popen. 211 """ 212 _ValidateAndLogCommand(args, cwd, shell) 213 return Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 214 shell=shell, cwd=cwd, env=env) 215 216 217def GetCmdStatusOutputAndError(args, cwd=None, shell=False, env=None): 218 """Executes a subprocess and returns its exit code, output, and errors. 219 220 Args: 221 args: A string or a sequence of program arguments. The program to execute is 222 the string or the first item in the args sequence. 223 cwd: If not None, the subprocess's current directory will be changed to 224 |cwd| before it's executed. 225 shell: Whether to execute args as a shell command. Must be True if args 226 is a string and False if args is a sequence. 227 env: If not None, a mapping that defines environment variables for the 228 subprocess. 229 230 Returns: 231 The 3-tuple (exit code, stdout, stderr). 232 """ 233 _ValidateAndLogCommand(args, cwd, shell) 234 pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 235 shell=shell, cwd=cwd, env=env) 236 stdout, stderr = pipe.communicate() 237 return (pipe.returncode, stdout, stderr) 238 239 240class TimeoutError(base_error.BaseError): 241 """Module-specific timeout exception.""" 242 243 def __init__(self, output=None): 244 super(TimeoutError, self).__init__('Timeout') 245 self._output = output 246 247 @property 248 def output(self): 249 return self._output 250 251 252def _IterProcessStdoutFcntl( 253 process, iter_timeout=None, timeout=None, buffer_size=4096, 254 poll_interval=1): 255 """An fcntl-based implementation of _IterProcessStdout.""" 256 # pylint: disable=too-many-nested-blocks 257 import fcntl 258 try: 259 # Enable non-blocking reads from the child's stdout. 260 child_fd = process.stdout.fileno() 261 fl = fcntl.fcntl(child_fd, fcntl.F_GETFL) 262 fcntl.fcntl(child_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 263 264 end_time = (time.time() + timeout) if timeout else None 265 iter_end_time = (time.time() + iter_timeout) if iter_timeout else None 266 267 while True: 268 if end_time and time.time() > end_time: 269 raise TimeoutError() 270 if iter_end_time and time.time() > iter_end_time: 271 yield None 272 iter_end_time = time.time() + iter_timeout 273 274 if iter_end_time: 275 iter_aware_poll_interval = min( 276 poll_interval, 277 max(0, iter_end_time - time.time())) 278 else: 279 iter_aware_poll_interval = poll_interval 280 281 read_fds, _, _ = select.select( 282 [child_fd], [], [], iter_aware_poll_interval) 283 if child_fd in read_fds: 284 data = os.read(child_fd, buffer_size) 285 if not data: 286 break 287 yield data 288 289 if process.poll() is not None: 290 # If process is closed, keep checking for output data (because of timing 291 # issues). 292 while True: 293 read_fds, _, _ = select.select( 294 [child_fd], [], [], iter_aware_poll_interval) 295 if child_fd in read_fds: 296 data = os.read(child_fd, buffer_size) 297 if data: 298 yield data 299 continue 300 break 301 break 302 finally: 303 try: 304 if process.returncode is None: 305 # Make sure the process doesn't stick around if we fail with an 306 # exception. 307 process.kill() 308 except OSError: 309 pass 310 process.wait() 311 312 313def _IterProcessStdoutQueue( 314 process, iter_timeout=None, timeout=None, buffer_size=4096, 315 poll_interval=1): 316 """A Queue.Queue-based implementation of _IterProcessStdout. 317 318 TODO(jbudorick): Evaluate whether this is a suitable replacement for 319 _IterProcessStdoutFcntl on all platforms. 320 """ 321 # pylint: disable=unused-argument 322 import Queue 323 import threading 324 325 stdout_queue = Queue.Queue() 326 327 def read_process_stdout(): 328 # TODO(jbudorick): Pick an appropriate read size here. 329 while True: 330 try: 331 output_chunk = os.read(process.stdout.fileno(), buffer_size) 332 except IOError: 333 break 334 stdout_queue.put(output_chunk, True) 335 if not output_chunk and process.poll() is not None: 336 break 337 338 reader_thread = threading.Thread(target=read_process_stdout) 339 reader_thread.start() 340 341 end_time = (time.time() + timeout) if timeout else None 342 343 try: 344 while True: 345 if end_time and time.time() > end_time: 346 raise TimeoutError() 347 try: 348 s = stdout_queue.get(True, iter_timeout) 349 if not s: 350 break 351 yield s 352 except Queue.Empty: 353 yield None 354 finally: 355 try: 356 if process.returncode is None: 357 # Make sure the process doesn't stick around if we fail with an 358 # exception. 359 process.kill() 360 except OSError: 361 pass 362 process.wait() 363 reader_thread.join() 364 365 366_IterProcessStdout = ( 367 _IterProcessStdoutQueue 368 if sys.platform == 'win32' 369 else _IterProcessStdoutFcntl) 370"""Iterate over a process's stdout. 371 372This is intentionally not public. 373 374Args: 375 process: The process in question. 376 iter_timeout: An optional length of time, in seconds, to wait in 377 between each iteration. If no output is received in the given 378 time, this generator will yield None. 379 timeout: An optional length of time, in seconds, during which 380 the process must finish. If it fails to do so, a TimeoutError 381 will be raised. 382 buffer_size: The maximum number of bytes to read (and thus yield) at once. 383 poll_interval: The length of time to wait in calls to `select.select`. 384 If iter_timeout is set, the remaining length of time in the iteration 385 may take precedence. 386Raises: 387 TimeoutError: if timeout is set and the process does not complete. 388Yields: 389 basestrings of data or None. 390""" 391 392 393def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False, 394 logfile=None, env=None): 395 """Executes a subprocess with a timeout. 396 397 Args: 398 args: List of arguments to the program, the program to execute is the first 399 element. 400 timeout: the timeout in seconds or None to wait forever. 401 cwd: If not None, the subprocess's current directory will be changed to 402 |cwd| before it's executed. 403 shell: Whether to execute args as a shell command. Must be True if args 404 is a string and False if args is a sequence. 405 logfile: Optional file-like object that will receive output from the 406 command as it is running. 407 env: If not None, a mapping that defines environment variables for the 408 subprocess. 409 410 Returns: 411 The 2-tuple (exit code, output). 412 Raises: 413 TimeoutError on timeout. 414 """ 415 _ValidateAndLogCommand(args, cwd, shell) 416 output = StringIO.StringIO() 417 process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE, 418 stderr=subprocess.STDOUT, env=env) 419 try: 420 for data in _IterProcessStdout(process, timeout=timeout): 421 if logfile: 422 logfile.write(data) 423 output.write(data) 424 except TimeoutError: 425 raise TimeoutError(output.getvalue()) 426 427 str_output = output.getvalue() 428 logger.debug('STDOUT+STDERR: %s%s', str_output[:4096].rstrip(), 429 '<truncated>' if len(str_output) > 4096 else '') 430 return process.returncode, str_output 431 432 433def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None, 434 shell=False, env=None, check_status=True): 435 """Executes a subprocess and continuously yields lines from its output. 436 437 Args: 438 args: List of arguments to the program, the program to execute is the first 439 element. 440 iter_timeout: Timeout for each iteration, in seconds. 441 timeout: Timeout for the entire command, in seconds. 442 cwd: If not None, the subprocess's current directory will be changed to 443 |cwd| before it's executed. 444 shell: Whether to execute args as a shell command. Must be True if args 445 is a string and False if args is a sequence. 446 env: If not None, a mapping that defines environment variables for the 447 subprocess. 448 check_status: A boolean indicating whether to check the exit status of the 449 process after all output has been read. 450 Yields: 451 The output of the subprocess, line by line. 452 453 Raises: 454 CalledProcessError if check_status is True and the process exited with a 455 non-zero exit status. 456 """ 457 cmd = _ValidateAndLogCommand(args, cwd, shell) 458 process = Popen(args, cwd=cwd, shell=shell, env=env, 459 stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 460 return _IterCmdOutputLines( 461 process, cmd, iter_timeout=iter_timeout, timeout=timeout, 462 check_status=check_status) 463 464def _IterCmdOutputLines(process, cmd, iter_timeout=None, timeout=None, 465 check_status=True): 466 buffer_output = '' 467 468 iter_end = None 469 cur_iter_timeout = None 470 if iter_timeout: 471 iter_end = time.time() + iter_timeout 472 cur_iter_timeout = iter_timeout 473 474 for data in _IterProcessStdout(process, iter_timeout=cur_iter_timeout, 475 timeout=timeout): 476 if iter_timeout: 477 # Check whether the current iteration has timed out. 478 cur_iter_timeout = iter_end - time.time() 479 if data is None or cur_iter_timeout < 0: 480 yield None 481 iter_end = time.time() + iter_timeout 482 continue 483 else: 484 assert data is not None, ( 485 'Iteration received no data despite no iter_timeout being set. ' 486 'cmd: %s' % cmd) 487 488 # Construct lines to yield from raw data. 489 buffer_output += data 490 has_incomplete_line = buffer_output[-1] not in '\r\n' 491 lines = buffer_output.splitlines() 492 buffer_output = lines.pop() if has_incomplete_line else '' 493 for line in lines: 494 yield line 495 if iter_timeout: 496 iter_end = time.time() + iter_timeout 497 498 if buffer_output: 499 yield buffer_output 500 if check_status and process.returncode: 501 raise subprocess.CalledProcessError(process.returncode, cmd) 502