import errno
import os
import signal
import six
import subprocess
import time
from paradrop.base.output import out
[docs]def kill(pid, kill_signal=4, timeout=8):
"""
Kill a child process and wait with timeout.
1. Send a SIGTERM signal to the process.
2. Wait up to `kill_signal` seconds for the process to exit.
3. If process is still running, send a SIGKILL signal.
4. Wait up to `timeout` seconds (cumulative with `kill_signal`) for the
process to exit.
Returns True if the process exited before `timeout` seconds elapsed.
"""
os.kill(pid, signal.SIGTERM)
start = time.time()
while (time.time() - start) < kill_signal:
time.sleep(0.1)
try:
# waitpid returns (0, 0) if the process is still running.
# Otherwise, it returns (pid, status).
opid, status = os.waitpid(pid, os.WNOHANG)
if opid == pid:
return True
except OSError as error:
# I think ECHILD means there is no process with the given pid.
if error.errno == errno.ECHILD:
return True
else:
raise error
try:
os.kill(pid, signal.SIGKILL)
except OSError as error:
if error.errno == errno.ESRCH:
# The process exited between the time that we checked it and when
# we tried to send a SIGKILL. This is not an error.
return True
else:
raise error
while (time.time() - start) < timeout:
time.sleep(0.1)
try:
# waitpid returns (0, 0) if the process is still running.
# Otherwise, it returns (pid, status).
opid, status = os.waitpid(pid, os.WNOHANG)
if opid == pid:
return True
except OSError as error:
# I think ECHILD means there is no process with the given pid.
if error.errno == errno.ECHILD:
return True
else:
raise error
return False
[docs]class CommandList(list):
def __contains__(self, s):
"""
Test if the list contains a given string.
"""
return any(s in cmd for prio, cmd in self)
[docs] def append(self, priority, command):
super(CommandList, self).append((priority, command))
[docs] def commands(self):
"""
Iterate over commands in order by priority.
Commands are first sorted by assigned priority. Within each priority
level, the order in which they were added is maintained.
"""
result = list()
for i in range(len(self)):
prio, cmd = self[i]
result.append((prio, i, cmd))
result.sort()
for prio, i, cmd in result:
yield cmd
[docs]class Command(object):
def __init__(self, command, parent=None, ignoreFailure=False):
"""
Construct command object.
command: array of strings specifying command and arguments
Passing a single string is also supported if there are
no spaces within arguments (only between them).
parent: parent object (should be ConfigObject subclass)
"""
self.parent = parent
self.ignoreFailure = ignoreFailure
if type(command) == list:
self.command = [str(v) for v in command]
elif isinstance(command, six.string_types):
self.command = command.split()
# These are set after execute completes.
self.pid = None
self.result = None
def __contains__(self, s):
"""
Test if command contains given string.
Example:
If the cmd.command = ['kill', '1'], then ("kill" in cmd) will return True.
"""
return (s in str(self))
def __str__(self):
return " ".join(self.command)
[docs] def execute(self):
try:
proc = subprocess.Popen(self.command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
self.pid = proc.pid
for line in proc.stdout:
out.verbose("{} {}: {}".format(self.command[0], self.pid, line))
for line in proc.stderr:
out.verbose("{} {}: {}".format(self.command[0], self.pid, line))
self.result = proc.wait()
if self.result == 0:
out.verbose('Command "{}" returned {}\n'.format(
" ".join(self.command), self.result))
else:
out.info('Command "{}" returned {}\n'.format(
" ".join(self.command), self.result))
except Exception as e:
out.info('Command "{}" raised exception {}\n'.format(
" ".join(self.command), e))
self.result = e
if self.parent is not None:
self.parent.executed.append(self)
return (self.ignoreFailure or self.result == 0)
[docs] def success(self):
"""
Returns True if the command was successfully executed.
"""
return (self.ignoreFailure or self.result == 0)
[docs]class ErrorCommand(Command):
"""
Special command object that indicates an error occurred.
"""
def __init__(self, error, parent=None):
super(ErrorCommand, self).__init__([], parent)
self.error = error
[docs] def execute(self):
out.info("An error occurred: {}".format(self.error))
[docs] def success(self):
return False
[docs]class FunctionCommand(Command):
"""
Command that runs a Python function.
"""
def __init__(self, parent, function, *args, **kwargs):
command = [function.__name__]
command.extend(args)
for key, value in six.iteritems(kwargs):
command.append("{}={}".format(key, value))
super(FunctionCommand, self).__init__(command, parent)
self.function = function
self.args = args
self.kwargs = kwargs
self.result = None
[docs] def execute(self):
self.result = self.function(*self.args, **self.kwargs)
[docs]class KillCommand(Command):
"""
Special command object for killing a process
"""
def __init__(self, pid, parent=None):
"""
Create a kill command
The pid argument can either be a real pid (e.g. kill 12345) or a path
to a file containing the pid.
If the pid is coming from a file, it will be resolved at the time that
execute is called. Before that time, the command will be stored
internally as ["kill", "/path/to/file"]. This is not a real command,
but it is meaningful if you print the command object.
"""
# This will not be a valid command if pid is a file path.
command = ["kill", pid]
super(KillCommand, self).__init__(command, parent)
# Is it a numeric pid or a path to a pid file?
try:
self.pid = int(pid)
self.fromFile = False
except ValueError:
self.pid = pid
self.fromFile = True
[docs] def getPid(self):
if self.fromFile:
try:
with open(self.pid, "r") as inputFile:
return int(inputFile.read().strip())
except:
# No pid file --- maybe it was not running?
out.warn("File not found: {}\n".format(self.pid))
return None
else:
return self.pid
[docs] def execute(self):
pid = self.getPid()
if pid is None:
self.result = 0
return True
try:
retval = kill(pid)
self.result = 0
out.info('Command "kill {}" returned {}\n'.format(pid, retval))
except Exception as e:
out.info('Command "kill {}" raised exception {}\n'.format(pid, e))
self.result = e
return (self.result == 0)