# Ubuntu Testing Automation Harness
# Copyright 2012 Canonical Ltd.
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Process related utilities."""
import errno
import fcntl
import logging
import os
import select
import signal
import StringIO
import subprocess
import sys
import time
import psutil
from utah import logger
[docs]def pid_in_use(pid, arg_patterns=None):
"""Test if the given pid is running.
If arg_patterns is provided then the pid's command line will be compared
to make sure it matches an expected value.
:returns:
proc object if the pid is active and optionally matches one of the
arg_patterns, otherwise None
:rtype: psutil.Process
"""
try:
proc = psutil.Process(pid)
except psutil.error.NoSuchProcess:
return None
cmdline = ' '.join(proc.cmdline)
if arg_patterns:
for p in arg_patterns:
if p in cmdline:
return proc
return None
return proc
[docs]class ProcessChecker(object):
"""Check all running process looking for a given pattern."""
MESSAGE_TEMPLATE = (
"{app} application is running.\n"
"Please stop it before launching utah since "
"it won't interact properly with KVM "
"(Kernel-based Virtual Machine)\n")
[docs] def check_cmdline(self, pattern):
"""Check if the command line of any process matches the given pattern.
:param pattern:
A fragment of the string that should be included in the command
line.
:type pattern: str
:returns:
Whether there's a process command line that matches the given
pattern.
:rtype: bool
"""
pids = psutil.get_pid_list()
cmdlines = []
for pid in pids:
try:
cmdlines.append(' '.join(psutil.Process(pid).cmdline))
except psutil.error.NoSuchProcess:
pass
return any(pattern in cmdline for cmdline in cmdlines)
[docs] def get_error_message(self, app):
"""Return error message for an incompatible application running."""
return self.MESSAGE_TEMPLATE.format(app=app)
[docs]class ProcessRunner(object):
"""Run a command in a subprocess and log output properly.
:param arglist: Argument list as would be passsed to `subprocess.Popen`
:type arglist: list
:attribute output: Command output (both stdout and stderr)
:attribute returncode: Command return code
.. note::
Process is launched when object is instantiated, there isn't any
separate `run` method.
"""
def __init__(self, arglist):
logging.debug('Running command: ' + ' '.join(arglist))
logging.debug('Output follows:')
p = subprocess.Popen(arglist,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = []
while p.poll() is None:
line = p.stdout.readline().strip()
logging.debug(line)
output.append(line)
if p.returncode == 0:
logging.debug('Return code: {}'.format(p.returncode))
else:
logging.warning('Command ({}) failed with return code: {}'
.format(' '.join(arglist), p.returncode))
self.output = '\n'.join(output)
self.returncode = p.returncode
def _get_process_children(pid):
"""Get the children processes of a given one.
:param pid: Process ID of the parent process
:type pid: int
:returns: Process ID for the childern processes
:rtype: list(int)
"""
try:
pids = subprocess.check_output(['ps', '--no-headers', '-o', 'pid',
'--ppid', str(pid)]).split()
return [int(process_id) for process_id in pids]
except subprocess.CalledProcessError:
return []
def _kill(pid, sig):
pids = [pid]
pids.extend(_get_process_children(pid))
for pid in pids:
# process might have died before getting to this line
# so wrap to avoid OSError: no such process
try:
os.kill(pid, sig)
except OSError:
pass
def _handle_fd(buffs, fds, fd, stream):
data = os.read(fd, 256)
if data:
buffs[fd].write(data)
if stream:
logger.log(data, False)
else:
os.close(fd)
fds.remove(fd)
def _rc(pid, timed_out):
"""Determine the exit code of the process and kill it if timed out.
:return: pid's return code or exist status
"""
if timed_out:
_kill(pid, signal.SIGTERM)
_kill(pid, signal.SIGKILL)
status = os.waitpid(pid, 0)
if not os.WIFEXITED(status[1]):
rc = status[1] # todo syslog this?
else:
rc = os.WEXITSTATUS(status[1])
return rc
def _drain(pid, stdout, stderr, deadline, stream):
buff = {
stdout: StringIO.StringIO(),
stderr: StringIO.StringIO(),
}
timed_out = False
fds = [stdout, stderr]
while len(fds):
timeout = 5 # don't just pound the select call
if deadline:
now = time.time()
if now > deadline:
timed_out = True
break
else:
remaining = deadline - now
if remaining < timeout:
timeout = remaining
try:
readable, _, _ = select.select(fds, [], [], timeout)
except select.error as e:
if e.args[0] == errno.EINTR:
continue
raise
for fd in readable:
_handle_fd(buff, fds, fd, stream)
rc = _rc(pid, timed_out)
return (rc, buff[stdout].getvalue(), buff[stderr].getvalue(), timed_out)
def _set_cloexec_flag(fd, cloexec=True):
# this helps ensure the subprocesses will close FD's upon exit
# the floodlight runlist is a great way to validate
old = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
def _pipes():
stdout_r, stdout_w = os.pipe()
stderr_r, stderr_w = os.pipe()
_set_cloexec_flag(stdout_r)
_set_cloexec_flag(stdout_w)
_set_cloexec_flag(stderr_r)
_set_cloexec_flag(stderr_w)
return stdout_r, stdout_w, stderr_r, stderr_w
[docs]def run(cmd, cwd, timeout, stream_syslog):
"""Run a command using fork/exec.
subprocess.* methods, don't provide a good way to do the UNIX classic
fork/exec/select to get stdout/stderr while a process is executing. This
provides a version for UTAH.
:returns: rc, timed_out, stdout, stderr
:rtype: tuple(int, bool, string, string)
"""
stdout_r, stdout_w, stderr_r, stderr_w = _pipes()
pid = os.fork()
if pid == 0:
# setup stdout/stderr
os.close(stdout_r)
os.dup2(stdout_w, 1)
os.close(stderr_r)
os.dup2(stderr_w, 2)
if cwd:
os.chdir(cwd)
os.execv('/bin/sh', ['/bin/sh', '-c', cmd])
print "ERROR os.execv command"
sys.exit(1)
os.close(stdout_w)
os.close(stderr_w)
deadline = 0
if timeout:
deadline = time.time() + timeout
rc, out, err, timed_out = \
_drain(pid, stdout_r, stderr_r, deadline, stream_syslog)
return rc, timed_out, out, err