# -*- coding:utf-8 -*-
|
# Copyright 2016 The Android Open Source Project
|
#
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
# you may not use this file except in compliance with the License.
|
# You may obtain a copy of the License at
|
#
|
# http://www.apache.org/licenses/LICENSE-2.0
|
#
|
# Unless required by applicable law or agreed to in writing, software
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# See the License for the specific language governing permissions and
|
# limitations under the License.
|
|
"""Various utility functions."""
|
|
from __future__ import print_function
|
|
import errno
|
import functools
|
import os
|
import signal
|
import subprocess
|
import sys
|
import tempfile
|
import time
|
|
_path = os.path.realpath(__file__ + '/../..')
|
if sys.path[0] != _path:
|
sys.path.insert(0, _path)
|
del _path
|
|
# pylint: disable=wrong-import-position
|
import rh.shell
|
import rh.signals
|
|
|
class CommandResult(object):
|
"""An object to store various attributes of a child process."""
|
|
def __init__(self, cmd=None, error=None, output=None, returncode=None):
|
self.cmd = cmd
|
self.error = error
|
self.output = output
|
self.returncode = returncode
|
|
@property
|
def cmdstr(self):
|
"""Return self.cmd as a nicely formatted string (useful for logs)."""
|
return rh.shell.cmd_to_str(self.cmd)
|
|
|
class RunCommandError(Exception):
|
"""Error caught in RunCommand() method."""
|
|
def __init__(self, msg, result, exception=None):
|
self.msg, self.result, self.exception = msg, result, exception
|
if exception is not None and not isinstance(exception, Exception):
|
raise ValueError('exception must be an exception instance; got %r'
|
% (exception,))
|
Exception.__init__(self, msg)
|
self.args = (msg, result, exception)
|
|
def stringify(self, error=True, output=True):
|
"""Custom method for controlling what is included in stringifying this.
|
|
Each individual argument is the literal name of an attribute
|
on the result object; if False, that value is ignored for adding
|
to this string content. If true, it'll be incorporated.
|
|
Args:
|
error: See comment about individual arguments above.
|
output: See comment about individual arguments above.
|
"""
|
items = [
|
'return code: %s; command: %s' % (
|
self.result.returncode, self.result.cmdstr),
|
]
|
if error and self.result.error:
|
items.append(self.result.error)
|
if output and self.result.output:
|
items.append(self.result.output)
|
if self.msg:
|
items.append(self.msg)
|
return '\n'.join(items)
|
|
def __str__(self):
|
# __str__ needs to return ascii, thus force a conversion to be safe.
|
return self.stringify().decode('utf-8', 'replace').encode(
|
'ascii', 'xmlcharrefreplace')
|
|
def __eq__(self, other):
|
return (type(self) == type(other) and
|
self.args == other.args)
|
|
def __ne__(self, other):
|
return not self.__eq__(other)
|
|
|
class TerminateRunCommandError(RunCommandError):
|
"""We were signaled to shutdown while running a command.
|
|
Client code shouldn't generally know, nor care about this class. It's
|
used internally to suppress retry attempts when we're signaled to die.
|
"""
|
|
|
def sudo_run_command(cmd, user='root', **kwargs):
|
"""Run a command via sudo.
|
|
Client code must use this rather than coming up with their own RunCommand
|
invocation that jams sudo in- this function is used to enforce certain
|
rules in our code about sudo usage, and as a potential auditing point.
|
|
Args:
|
cmd: The command to run. See RunCommand for rules of this argument-
|
SudoRunCommand purely prefixes it with sudo.
|
user: The user to run the command as.
|
kwargs: See RunCommand options, it's a direct pass thru to it.
|
Note that this supports a 'strict' keyword that defaults to True.
|
If set to False, it'll suppress strict sudo behavior.
|
|
Returns:
|
See RunCommand documentation.
|
|
Raises:
|
This function may immediately raise RunCommandError if we're operating
|
in a strict sudo context and the API is being misused.
|
Barring that, see RunCommand's documentation- it can raise the same things
|
RunCommand does.
|
"""
|
sudo_cmd = ['sudo']
|
|
if user == 'root' and os.geteuid() == 0:
|
return run_command(cmd, **kwargs)
|
|
if user != 'root':
|
sudo_cmd += ['-u', user]
|
|
# Pass these values down into the sudo environment, since sudo will
|
# just strip them normally.
|
extra_env = kwargs.pop('extra_env', None)
|
extra_env = {} if extra_env is None else extra_env.copy()
|
|
sudo_cmd.extend('%s=%s' % (k, v) for k, v in extra_env.iteritems())
|
|
# Finally, block people from passing options to sudo.
|
sudo_cmd.append('--')
|
|
if isinstance(cmd, basestring):
|
# We need to handle shell ourselves so the order is correct:
|
# $ sudo [sudo args] -- bash -c '[shell command]'
|
# If we let RunCommand take care of it, we'd end up with:
|
# $ bash -c 'sudo [sudo args] -- [shell command]'
|
shell = kwargs.pop('shell', False)
|
if not shell:
|
raise Exception('Cannot run a string command without a shell')
|
sudo_cmd.extend(['/bin/bash', '-c', cmd])
|
else:
|
sudo_cmd.extend(cmd)
|
|
return run_command(sudo_cmd, **kwargs)
|
|
|
def _kill_child_process(proc, int_timeout, kill_timeout, cmd, original_handler,
|
signum, frame):
|
"""Used as a signal handler by RunCommand.
|
|
This is internal to Runcommand. No other code should use this.
|
"""
|
if signum:
|
# If we've been invoked because of a signal, ignore delivery of that
|
# signal from this point forward. The invoking context of this func
|
# restores signal delivery to what it was prior; we suppress future
|
# delivery till then since this code handles SIGINT/SIGTERM fully
|
# including delivering the signal to the original handler on the way
|
# out.
|
signal.signal(signum, signal.SIG_IGN)
|
|
# Do not trust Popen's returncode alone; we can be invoked from contexts
|
# where the Popen instance was created, but no process was generated.
|
if proc.returncode is None and proc.pid is not None:
|
try:
|
while proc.poll() is None and int_timeout >= 0:
|
time.sleep(0.1)
|
int_timeout -= 0.1
|
|
proc.terminate()
|
while proc.poll() is None and kill_timeout >= 0:
|
time.sleep(0.1)
|
kill_timeout -= 0.1
|
|
if proc.poll() is None:
|
# Still doesn't want to die. Too bad, so sad, time to die.
|
proc.kill()
|
except EnvironmentError as e:
|
print('Ignoring unhandled exception in _kill_child_process: %s' % e,
|
file=sys.stderr)
|
|
# Ensure our child process has been reaped.
|
proc.wait()
|
|
if not rh.signals.relay_signal(original_handler, signum, frame):
|
# Mock up our own, matching exit code for signaling.
|
cmd_result = CommandResult(cmd=cmd, returncode=signum << 8)
|
raise TerminateRunCommandError('Received signal %i' % signum,
|
cmd_result)
|
|
|
class _Popen(subprocess.Popen):
|
"""subprocess.Popen derivative customized for our usage.
|
|
Specifically, we fix terminate/send_signal/kill to work if the child process
|
was a setuid binary; on vanilla kernels, the parent can wax the child
|
regardless, on goobuntu this apparently isn't allowed, thus we fall back
|
to the sudo machinery we have.
|
|
While we're overriding send_signal, we also suppress ESRCH being raised
|
if the process has exited, and suppress signaling all together if the
|
process has knowingly been waitpid'd already.
|
"""
|
|
def send_signal(self, signum):
|
if self.returncode is not None:
|
# The original implementation in Popen allows signaling whatever
|
# process now occupies this pid, even if the Popen object had
|
# waitpid'd. Since we can escalate to sudo kill, we do not want
|
# to allow that. Fixing this addresses that angle, and makes the
|
# API less sucky in the process.
|
return
|
|
try:
|
os.kill(self.pid, signum)
|
except EnvironmentError as e:
|
if e.errno == errno.EPERM:
|
# Kill returns either 0 (signal delivered), or 1 (signal wasn't
|
# delivered). This isn't particularly informative, but we still
|
# need that info to decide what to do, thus error_code_ok=True.
|
ret = sudo_run_command(['kill', '-%i' % signum, str(self.pid)],
|
redirect_stdout=True,
|
redirect_stderr=True, error_code_ok=True)
|
if ret.returncode == 1:
|
# The kill binary doesn't distinguish between permission
|
# denied and the pid is missing. Denied can only occur
|
# under weird grsec/selinux policies. We ignore that
|
# potential and just assume the pid was already dead and
|
# try to reap it.
|
self.poll()
|
elif e.errno == errno.ESRCH:
|
# Since we know the process is dead, reap it now.
|
# Normally Popen would throw this error- we suppress it since
|
# frankly that's a misfeature and we're already overriding
|
# this method.
|
self.poll()
|
else:
|
raise
|
|
|
# pylint: disable=redefined-builtin
|
def run_command(cmd, error_message=None, redirect_stdout=False,
|
redirect_stderr=False, cwd=None, input=None,
|
shell=False, env=None, extra_env=None, ignore_sigint=False,
|
combine_stdout_stderr=False, log_stdout_to_file=None,
|
error_code_ok=False, int_timeout=1, kill_timeout=1,
|
stdout_to_pipe=False, capture_output=False,
|
quiet=False, close_fds=True):
|
"""Runs a command.
|
|
Args:
|
cmd: cmd to run. Should be input to subprocess.Popen. If a string, shell
|
must be true. Otherwise the command must be an array of arguments,
|
and shell must be false.
|
error_message: Prints out this message when an error occurs.
|
redirect_stdout: Returns the stdout.
|
redirect_stderr: Holds stderr output until input is communicated.
|
cwd: The working directory to run this cmd.
|
input: The data to pipe into this command through stdin. If a file object
|
or file descriptor, stdin will be connected directly to that.
|
shell: Controls whether we add a shell as a command interpreter. See cmd
|
since it has to agree as to the type.
|
env: If non-None, this is the environment for the new process.
|
extra_env: If set, this is added to the environment for the new process.
|
This dictionary is not used to clear any entries though.
|
ignore_sigint: If True, we'll ignore signal.SIGINT before calling the
|
child. This is the desired behavior if we know our child will handle
|
Ctrl-C. If we don't do this, I think we and the child will both get
|
Ctrl-C at the same time, which means we'll forcefully kill the child.
|
combine_stdout_stderr: Combines stdout and stderr streams into stdout.
|
log_stdout_to_file: If set, redirects stdout to file specified by this
|
path. If |combine_stdout_stderr| is set to True, then stderr will
|
also be logged to the specified file.
|
error_code_ok: Does not raise an exception when command returns a non-zero
|
exit code. Instead, returns the CommandResult object containing the
|
exit code.
|
int_timeout: If we're interrupted, how long (in seconds) should we give
|
the invoked process to clean up before we send a SIGTERM.
|
kill_timeout: If we're interrupted, how long (in seconds) should we give
|
the invoked process to shutdown from a SIGTERM before we SIGKILL it.
|
stdout_to_pipe: Redirect stdout to pipe.
|
capture_output: Set |redirect_stdout| and |redirect_stderr| to True.
|
quiet: Set |stdout_to_pipe| and |combine_stdout_stderr| to True.
|
close_fds: Whether to close all fds before running |cmd|.
|
|
Returns:
|
A CommandResult object.
|
|
Raises:
|
RunCommandError: Raises exception on error with optional error_message.
|
"""
|
if capture_output:
|
redirect_stdout, redirect_stderr = True, True
|
|
if quiet:
|
stdout_to_pipe, combine_stdout_stderr = True, True
|
|
# Set default for variables.
|
stdout = None
|
stderr = None
|
stdin = None
|
cmd_result = CommandResult()
|
|
# Force the timeout to float; in the process, if it's not convertible,
|
# a self-explanatory exception will be thrown.
|
kill_timeout = float(kill_timeout)
|
|
def _get_tempfile():
|
try:
|
return tempfile.TemporaryFile(bufsize=0)
|
except EnvironmentError as e:
|
if e.errno != errno.ENOENT:
|
raise
|
# This can occur if we were pointed at a specific location for our
|
# TMP, but that location has since been deleted. Suppress that
|
# issue in this particular case since our usage gurantees deletion,
|
# and since this is primarily triggered during hard cgroups
|
# shutdown.
|
return tempfile.TemporaryFile(bufsize=0, dir='/tmp')
|
|
# Modify defaults based on parameters.
|
# Note that tempfiles must be unbuffered else attempts to read
|
# what a separate process did to that file can result in a bad
|
# view of the file.
|
# The Popen API accepts either an int or a file handle for stdout/stderr.
|
# pylint: disable=redefined-variable-type
|
if log_stdout_to_file:
|
stdout = open(log_stdout_to_file, 'w+')
|
elif stdout_to_pipe:
|
stdout = subprocess.PIPE
|
elif redirect_stdout:
|
stdout = _get_tempfile()
|
|
if combine_stdout_stderr:
|
stderr = subprocess.STDOUT
|
elif redirect_stderr:
|
stderr = _get_tempfile()
|
# pylint: enable=redefined-variable-type
|
|
# If subprocesses have direct access to stdout or stderr, they can bypass
|
# our buffers, so we need to flush to ensure that output is not interleaved.
|
if stdout is None or stderr is None:
|
sys.stdout.flush()
|
sys.stderr.flush()
|
|
# If input is a string, we'll create a pipe and send it through that.
|
# Otherwise we assume it's a file object that can be read from directly.
|
if isinstance(input, basestring):
|
stdin = subprocess.PIPE
|
elif input is not None:
|
stdin = input
|
input = None
|
|
if isinstance(cmd, basestring):
|
if not shell:
|
raise Exception('Cannot run a string command without a shell')
|
cmd = ['/bin/bash', '-c', cmd]
|
shell = False
|
elif shell:
|
raise Exception('Cannot run an array command with a shell')
|
|
# If we are using enter_chroot we need to use enterchroot pass env through
|
# to the final command.
|
env = env.copy() if env is not None else os.environ.copy()
|
env.update(extra_env if extra_env else {})
|
|
cmd_result.cmd = cmd
|
|
proc = None
|
# Verify that the signals modules is actually usable, and won't segfault
|
# upon invocation of getsignal. See signals.SignalModuleUsable for the
|
# details and upstream python bug.
|
use_signals = rh.signals.signal_module_usable()
|
try:
|
proc = _Popen(cmd, cwd=cwd, stdin=stdin, stdout=stdout,
|
stderr=stderr, shell=False, env=env,
|
close_fds=close_fds)
|
|
if use_signals:
|
old_sigint = signal.getsignal(signal.SIGINT)
|
if ignore_sigint:
|
handler = signal.SIG_IGN
|
else:
|
handler = functools.partial(
|
_kill_child_process, proc, int_timeout, kill_timeout, cmd,
|
old_sigint)
|
signal.signal(signal.SIGINT, handler)
|
|
old_sigterm = signal.getsignal(signal.SIGTERM)
|
handler = functools.partial(_kill_child_process, proc, int_timeout,
|
kill_timeout, cmd, old_sigterm)
|
signal.signal(signal.SIGTERM, handler)
|
|
try:
|
(cmd_result.output, cmd_result.error) = proc.communicate(input)
|
finally:
|
if use_signals:
|
signal.signal(signal.SIGINT, old_sigint)
|
signal.signal(signal.SIGTERM, old_sigterm)
|
|
if stdout and not log_stdout_to_file and not stdout_to_pipe:
|
# The linter is confused by how stdout is a file & an int.
|
# pylint: disable=maybe-no-member,no-member
|
stdout.seek(0)
|
cmd_result.output = stdout.read()
|
stdout.close()
|
|
if stderr and stderr != subprocess.STDOUT:
|
# The linter is confused by how stderr is a file & an int.
|
# pylint: disable=maybe-no-member,no-member
|
stderr.seek(0)
|
cmd_result.error = stderr.read()
|
stderr.close()
|
|
cmd_result.returncode = proc.returncode
|
|
if not error_code_ok and proc.returncode:
|
msg = 'cwd=%s' % cwd
|
if extra_env:
|
msg += ', extra env=%s' % extra_env
|
if error_message:
|
msg += '\n%s' % error_message
|
raise RunCommandError(msg, cmd_result)
|
except OSError as e:
|
estr = str(e)
|
if e.errno == errno.EACCES:
|
estr += '; does the program need `chmod a+x`?'
|
if error_code_ok:
|
cmd_result = CommandResult(cmd=cmd, error=estr, returncode=255)
|
else:
|
raise RunCommandError(estr, CommandResult(cmd=cmd), exception=e)
|
finally:
|
if proc is not None:
|
# Ensure the process is dead.
|
_kill_child_process(proc, int_timeout, kill_timeout, cmd, None,
|
None, None)
|
|
return cmd_result
|
# pylint: enable=redefined-builtin
|
|
|
def collection(classname, **kwargs):
|
"""Create a new class with mutable named members.
|
|
This is like collections.namedtuple, but mutable. Also similar to the
|
python 3.3 types.SimpleNamespace.
|
|
Example:
|
# Declare default values for this new class.
|
Foo = collection('Foo', a=0, b=10)
|
# Create a new class but set b to 4.
|
foo = Foo(b=4)
|
# Print out a (will be the default 0) and b (will be 4).
|
print('a = %i, b = %i' % (foo.a, foo.b))
|
"""
|
|
def sn_init(self, **kwargs):
|
"""The new class's __init__ function."""
|
# First verify the kwargs don't have excess settings.
|
valid_keys = set(self.__slots__[1:])
|
these_keys = set(kwargs.keys())
|
invalid_keys = these_keys - valid_keys
|
if invalid_keys:
|
raise TypeError('invalid keyword arguments for this object: %r' %
|
invalid_keys)
|
|
# Now initialize this object.
|
for k in valid_keys:
|
setattr(self, k, kwargs.get(k, self.__defaults__[k]))
|
|
def sn_repr(self):
|
"""The new class's __repr__ function."""
|
return '%s(%s)' % (classname, ', '.join(
|
'%s=%r' % (k, getattr(self, k)) for k in self.__slots__[1:]))
|
|
# Give the new class a unique name and then generate the code for it.
|
classname = 'Collection_%s' % classname
|
expr = '\n'.join((
|
'class %(classname)s(object):',
|
' __slots__ = ["__defaults__", "%(slots)s"]',
|
' __defaults__ = {}',
|
)) % {
|
'classname': classname,
|
'slots': '", "'.join(sorted(str(k) for k in kwargs)),
|
}
|
|
# Create the class in a local namespace as exec requires.
|
namespace = {}
|
exec expr in namespace # pylint: disable=exec-used
|
new_class = namespace[classname]
|
|
# Bind the helpers.
|
new_class.__defaults__ = kwargs.copy()
|
new_class.__init__ = sn_init
|
new_class.__repr__ = sn_repr
|
|
return new_class
|