• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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