blob: eb1526ac26f38e6856cffa4d1fa0fd31cea1ea60 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import getpass
import logging
import os
import pipes
import subprocess
import types
from StringIO import StringIO
PIPE = subprocess.PIPE
# File descriptor for /dev/null.
dev_null = None
def WrapLines(data):
'''Returns a function that returns a list of all lines in data.'''
def Wrapper(strip=False):
ret = StringIO(data).readlines()
if strip:
ret = [x.strip() for x in ret]
return ret
return Wrapper
def OpenDevNull():
'''Opens and returns a readable/writable file pointing to /dev/null.
The file object may be reused.
'''
global dev_null # pylint: disable=W0603
if not dev_null:
# There is a possible race condition here, but it is extremely
# unlikely and won't hurt anyway (we'll just have multiple files
# pointing at /dev/null).
dev_null = open(os.devnull, 'r+')
return dev_null
def Spawn(args, **kwargs):
'''
Popen wrapper with extra functionality:
- Sets close_fds to True by default. (You may still set
close_fds=False to leave all fds open.)
- Provides a consistent interface to functionality like the call,
check_call, and check_output functions in subprocess.
To get a command's output, logging stderr if the process fails:
# Doesn't check retcode
Spawn(['cmd'], read_stdout=True, log_stderr_on_error=True).stdout_data
# Throws CalledProcessError on error
Spawn(['cmd'], read_stdout=True, log_stderr_on_error=True,
check_call=True).stdout_data
To get a command's stdout and stderr, without checking the retcode:
stdout, stderr = Spawn(
['cmd'], read_stdout=True, read_stderr=True).communicate()
Args:
log: Do a logging.info before running the command, or to any
logging object to call its info method.
stdout: Same as subprocess.Popen, but may be set to DEV_NULL to discard
all stdout.
stderr: Same as subprocess.Popen, but may be set to DEV_NULL to discard
all stderr.
call: Wait for the command to complete.
check_call: Wait for the command to complete, throwing an
exception if it fails. This implies call=True. This may be either
True to signify that any non-zero exit status is failure, or a function
that takes a returncode and returns True if that returncode is
considered OK (e.g., lambda returncode: returncode in [0,1]).
check_output: Wait for the command to complete, throwing an
exception if it fails, and saves the contents to the return
object's stdout_data attribute. Implies check_call=True and
read_stdout=True.
log_stderr_on_error: Log stderr only if the command fails.
Implies read_stderr=True and call=True.
read_stdout: Wait for the command to complete, saving the contents
to the return object's stdout_data attribute. This implies
call=True and stdout=PIPE.
ignore_stdout: Ignore stdout.
read_stderr: Wait for the command to complete, saving the contents
to the return object's stderr_data attribute. This implies
call=True and stderr=PIPE.
ignore_stderr: Ignore stderr.
sudo: Prepend sudo to arguments if user is not root.
Returns/Raises:
Same as Popen.
'''
kwargs.setdefault('close_fds', True)
logger = logging
log = kwargs.pop('log', False)
if kwargs.get('shell'):
args_to_log = args
else:
args_to_log = ' '.join(pipes.quote(arg) for arg in args)
if log:
if log != True:
logger = log
logger.info('Running command: "%s"', args_to_log)
call = kwargs.pop('call', False)
check_call = kwargs.pop('check_call', False)
check_output = kwargs.pop('check_output', False)
read_stdout = kwargs.pop('read_stdout', False)
ignore_stdout = kwargs.pop('ignore_stdout', False)
read_stderr = kwargs.pop('read_stderr', False)
ignore_stderr = kwargs.pop('ignore_stderr', False)
log_stderr_on_error = kwargs.pop('log_stderr_on_error', False)
sudo = kwargs.pop('sudo', False)
if sudo and getpass.getuser() != 'root':
if kwargs.pop('shell', False):
args = ['sudo', 'sh', '-c', args]
else:
args = ['sudo'] + args
if ignore_stdout:
assert not read_stdout
assert not kwargs.get('stdout')
kwargs['stdout'] = OpenDevNull()
if ignore_stderr:
assert not read_stderr
assert not log_stderr_on_error
assert not kwargs.get('stderr')
kwargs['stderr'] = OpenDevNull()
if check_output:
check_call = check_call or True
read_stdout = True
if check_call:
call = True
if log_stderr_on_error:
read_stderr = True
if read_stdout:
call = True
assert kwargs.get('stdout') in [None, PIPE]
kwargs['stdout'] = PIPE
if read_stderr:
call = True
assert kwargs.get('stderr') in [None, PIPE]
kwargs['stderr'] = PIPE
if call and (not read_stdout) and kwargs.get('stdout') == PIPE:
raise ValueError('Cannot use call=True argument with stdout=PIPE, '
'since OS buffers may get filled up')
if call and (not read_stderr) and kwargs.get('stderr') == PIPE:
raise ValueError('Cannot use call=True argument with stderr=PIPE, '
'since OS buffers may get filled up')
process = subprocess.Popen(args, **kwargs)
process.stdout_data = None
process.stdout_lines = None
process.stderr_data = None
process.stderr_lines = None
if call:
if read_stdout or read_stderr:
stdout, stderr = process.communicate()
if read_stdout:
process.stdout_data = stdout
process.stdout_lines = WrapLines(process.stdout_data)
if read_stderr:
process.stderr_data = stderr
process.stderr_lines = WrapLines(process.stderr_data)
process.communicate = (
lambda: (process.stdout_data, process.stderr_data))
else:
# No need to communicate; just wait
process.wait()
if type(check_call) == types.FunctionType:
failed = not check_call(process.returncode)
else:
failed = process.returncode != 0
if failed:
if log or log_stderr_on_error:
message = 'Exit code %d from command: "%s"' % (
process.returncode, args_to_log)
if log_stderr_on_error:
message += '; stderr: """\n%s\n"""' % process.stderr_data
logger.error(message)
if check_call:
raise subprocess.CalledProcessError(process.returncode, args)
return process