1351 lines
50 KiB
Python
Executable file
1351 lines
50 KiB
Python
Executable file
#!/usr/bin/python
|
|
"""
|
|
A class and functions used for running and controlling child processes.
|
|
|
|
@copyright: 2008-2009 Red Hat Inc.
|
|
"""
|
|
|
|
import os, sys, pty, select, termios, fcntl
|
|
|
|
|
|
# The following helper functions are shared by the server and the client.
|
|
|
|
def _lock(filename):
|
|
if not os.path.exists(filename):
|
|
open(filename, "w").close()
|
|
fd = os.open(filename, os.O_RDWR)
|
|
fcntl.lockf(fd, fcntl.LOCK_EX)
|
|
return fd
|
|
|
|
|
|
def _unlock(fd):
|
|
fcntl.lockf(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
|
|
|
|
def _locked(filename):
|
|
try:
|
|
fd = os.open(filename, os.O_RDWR)
|
|
except:
|
|
return False
|
|
try:
|
|
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except:
|
|
os.close(fd)
|
|
return True
|
|
fcntl.lockf(fd, fcntl.LOCK_UN)
|
|
os.close(fd)
|
|
return False
|
|
|
|
|
|
def _wait(filename):
|
|
fd = _lock(filename)
|
|
_unlock(fd)
|
|
|
|
|
|
def _get_filenames(base_dir, id):
|
|
return [os.path.join(base_dir, s + id) for s in
|
|
"shell-pid-", "status-", "output-", "inpipe-",
|
|
"lock-server-running-", "lock-client-starting-"]
|
|
|
|
|
|
def _get_reader_filename(base_dir, id, reader):
|
|
return os.path.join(base_dir, "outpipe-%s-%s" % (reader, id))
|
|
|
|
|
|
# The following is the server part of the module.
|
|
|
|
if __name__ == "__main__":
|
|
id = sys.stdin.readline().strip()
|
|
echo = sys.stdin.readline().strip() == "True"
|
|
readers = sys.stdin.readline().strip().split(",")
|
|
command = sys.stdin.readline().strip() + " && echo %s > /dev/null" % id
|
|
|
|
# Define filenames to be used for communication
|
|
base_dir = "/tmp/kvm_spawn"
|
|
(shell_pid_filename,
|
|
status_filename,
|
|
output_filename,
|
|
inpipe_filename,
|
|
lock_server_running_filename,
|
|
lock_client_starting_filename) = _get_filenames(base_dir, id)
|
|
|
|
# Populate the reader filenames list
|
|
reader_filenames = [_get_reader_filename(base_dir, id, reader)
|
|
for reader in readers]
|
|
|
|
# Set $TERM = dumb
|
|
os.putenv("TERM", "dumb")
|
|
|
|
(shell_pid, shell_fd) = pty.fork()
|
|
if shell_pid == 0:
|
|
# Child process: run the command in a subshell
|
|
os.execv("/bin/sh", ["/bin/sh", "-c", command])
|
|
else:
|
|
# Parent process
|
|
lock_server_running = _lock(lock_server_running_filename)
|
|
|
|
# Set terminal echo on/off and disable pre- and post-processing
|
|
attr = termios.tcgetattr(shell_fd)
|
|
attr[0] &= ~termios.INLCR
|
|
attr[0] &= ~termios.ICRNL
|
|
attr[0] &= ~termios.IGNCR
|
|
attr[1] &= ~termios.OPOST
|
|
if echo:
|
|
attr[3] |= termios.ECHO
|
|
else:
|
|
attr[3] &= ~termios.ECHO
|
|
termios.tcsetattr(shell_fd, termios.TCSANOW, attr)
|
|
|
|
# Open output file
|
|
output_file = open(output_filename, "w")
|
|
# Open input pipe
|
|
os.mkfifo(inpipe_filename)
|
|
inpipe_fd = os.open(inpipe_filename, os.O_RDWR)
|
|
# Open output pipes (readers)
|
|
reader_fds = []
|
|
for filename in reader_filenames:
|
|
os.mkfifo(filename)
|
|
reader_fds.append(os.open(filename, os.O_RDWR))
|
|
|
|
# Write shell PID to file
|
|
file = open(shell_pid_filename, "w")
|
|
file.write(str(shell_pid))
|
|
file.close()
|
|
|
|
# Print something to stdout so the client can start working
|
|
print "Server %s ready" % id
|
|
sys.stdout.flush()
|
|
|
|
# Initialize buffers
|
|
buffers = ["" for reader in readers]
|
|
|
|
# Read from child and write to files/pipes
|
|
while True:
|
|
check_termination = False
|
|
# Make a list of reader pipes whose buffers are not empty
|
|
fds = [fd for (i, fd) in enumerate(reader_fds) if buffers[i]]
|
|
# Wait until there's something to do
|
|
r, w, x = select.select([shell_fd, inpipe_fd], fds, [], 0.5)
|
|
# If a reader pipe is ready for writing --
|
|
for (i, fd) in enumerate(reader_fds):
|
|
if fd in w:
|
|
bytes_written = os.write(fd, buffers[i])
|
|
buffers[i] = buffers[i][bytes_written:]
|
|
# If there's data to read from the child process --
|
|
if shell_fd in r:
|
|
try:
|
|
data = os.read(shell_fd, 16384)
|
|
except OSError:
|
|
data = ""
|
|
if not data:
|
|
check_termination = True
|
|
# Remove carriage returns from the data -- they often cause
|
|
# trouble and are normally not needed
|
|
data = data.replace("\r", "")
|
|
output_file.write(data)
|
|
output_file.flush()
|
|
for i in range(len(readers)):
|
|
buffers[i] += data
|
|
# If os.read() raised an exception or there was nothing to read --
|
|
if check_termination or shell_fd not in r:
|
|
pid, status = os.waitpid(shell_pid, os.WNOHANG)
|
|
if pid:
|
|
status = os.WEXITSTATUS(status)
|
|
break
|
|
# If there's data to read from the client --
|
|
if inpipe_fd in r:
|
|
data = os.read(inpipe_fd, 1024)
|
|
os.write(shell_fd, data)
|
|
|
|
# Write the exit status to a file
|
|
file = open(status_filename, "w")
|
|
file.write(str(status))
|
|
file.close()
|
|
|
|
# Wait for the client to finish initializing
|
|
_wait(lock_client_starting_filename)
|
|
|
|
# Delete FIFOs
|
|
for filename in reader_filenames + [inpipe_filename]:
|
|
try:
|
|
os.unlink(filename)
|
|
except OSError:
|
|
pass
|
|
|
|
# Close all files and pipes
|
|
output_file.close()
|
|
os.close(inpipe_fd)
|
|
for fd in reader_fds:
|
|
os.close(fd)
|
|
|
|
_unlock(lock_server_running)
|
|
exit(0)
|
|
|
|
|
|
# The following is the client part of the module.
|
|
|
|
import subprocess, time, signal, re, threading, logging
|
|
import virt_utils
|
|
|
|
|
|
class ExpectError(Exception):
|
|
def __init__(self, patterns, output):
|
|
Exception.__init__(self, patterns, output)
|
|
self.patterns = patterns
|
|
self.output = output
|
|
|
|
def _pattern_str(self):
|
|
if len(self.patterns) == 1:
|
|
return "pattern %r" % self.patterns[0]
|
|
else:
|
|
return "patterns %r" % self.patterns
|
|
|
|
def __str__(self):
|
|
return ("Unknown error occurred while looking for %s (output: %r)" %
|
|
(self._pattern_str(), self.output))
|
|
|
|
|
|
class ExpectTimeoutError(ExpectError):
|
|
def __str__(self):
|
|
return ("Timeout expired while looking for %s (output: %r)" %
|
|
(self._pattern_str(), self.output))
|
|
|
|
|
|
class ExpectProcessTerminatedError(ExpectError):
|
|
def __init__(self, patterns, status, output):
|
|
ExpectError.__init__(self, patterns, output)
|
|
self.status = status
|
|
|
|
def __str__(self):
|
|
return ("Process terminated while looking for %s "
|
|
"(status: %s, output: %r)" % (self._pattern_str(),
|
|
self.status, self.output))
|
|
|
|
|
|
class ShellError(Exception):
|
|
def __init__(self, cmd, output):
|
|
Exception.__init__(self, cmd, output)
|
|
self.cmd = cmd
|
|
self.output = output
|
|
|
|
def __str__(self):
|
|
return ("Could not execute shell command %r (output: %r)" %
|
|
(self.cmd, self.output))
|
|
|
|
|
|
class ShellTimeoutError(ShellError):
|
|
def __str__(self):
|
|
return ("Timeout expired while waiting for shell command to "
|
|
"complete: %r (output: %r)" % (self.cmd, self.output))
|
|
|
|
|
|
class ShellProcessTerminatedError(ShellError):
|
|
# Raised when the shell process itself (e.g. ssh, netcat, telnet)
|
|
# terminates unexpectedly
|
|
def __init__(self, cmd, status, output):
|
|
ShellError.__init__(self, cmd, output)
|
|
self.status = status
|
|
|
|
def __str__(self):
|
|
return ("Shell process terminated while waiting for command to "
|
|
"complete: %r (status: %s, output: %r)" %
|
|
(self.cmd, self.status, self.output))
|
|
|
|
|
|
class ShellCmdError(ShellError):
|
|
# Raised when a command executed in a shell terminates with a nonzero
|
|
# exit code (status)
|
|
def __init__(self, cmd, status, output):
|
|
ShellError.__init__(self, cmd, output)
|
|
self.status = status
|
|
|
|
def __str__(self):
|
|
return ("Shell command failed: %r (status: %s, output: %r)" %
|
|
(self.cmd, self.status, self.output))
|
|
|
|
|
|
class ShellStatusError(ShellError):
|
|
# Raised when the command's exit status cannot be obtained
|
|
def __str__(self):
|
|
return ("Could not get exit status of command: %r (output: %r)" %
|
|
(self.cmd, self.output))
|
|
|
|
|
|
def run_bg(command, termination_func=None, output_func=None, output_prefix="",
|
|
timeout=1.0):
|
|
"""
|
|
Run command as a subprocess. Call output_func with each line of output
|
|
from the subprocess (prefixed by output_prefix). Call termination_func
|
|
when the subprocess terminates. Return when timeout expires or when the
|
|
subprocess exits -- whichever occurs first.
|
|
|
|
@brief: Run a subprocess in the background and collect its output and
|
|
exit status.
|
|
|
|
@param command: The shell command to execute
|
|
@param termination_func: A function to call when the process terminates
|
|
(should take an integer exit status parameter)
|
|
@param output_func: A function to call with each line of output from
|
|
the subprocess (should take a string parameter)
|
|
@param output_prefix: A string to pre-pend to each line of the output,
|
|
before passing it to stdout_func
|
|
@param timeout: Time duration (in seconds) to wait for the subprocess to
|
|
terminate before returning
|
|
|
|
@return: A Tail object.
|
|
"""
|
|
process = Tail(command=command,
|
|
termination_func=termination_func,
|
|
output_func=output_func,
|
|
output_prefix=output_prefix)
|
|
|
|
end_time = time.time() + timeout
|
|
while time.time() < end_time and process.is_alive():
|
|
time.sleep(0.1)
|
|
|
|
return process
|
|
|
|
|
|
def run_fg(command, output_func=None, output_prefix="", timeout=1.0):
|
|
"""
|
|
Run command as a subprocess. Call output_func with each line of output
|
|
from the subprocess (prefixed by prefix). Return when timeout expires or
|
|
when the subprocess exits -- whichever occurs first. If timeout expires
|
|
and the subprocess is still running, kill it before returning.
|
|
|
|
@brief: Run a subprocess in the foreground and collect its output and
|
|
exit status.
|
|
|
|
@param command: The shell command to execute
|
|
@param output_func: A function to call with each line of output from
|
|
the subprocess (should take a string parameter)
|
|
@param output_prefix: A string to pre-pend to each line of the output,
|
|
before passing it to stdout_func
|
|
@param timeout: Time duration (in seconds) to wait for the subprocess to
|
|
terminate before killing it and returning
|
|
|
|
@return: A 2-tuple containing the exit status of the process and its
|
|
STDOUT/STDERR output. If timeout expires before the process
|
|
terminates, the returned status is None.
|
|
"""
|
|
process = run_bg(command, None, output_func, output_prefix, timeout)
|
|
output = process.get_output()
|
|
if process.is_alive():
|
|
status = None
|
|
else:
|
|
status = process.get_status()
|
|
process.close()
|
|
return (status, output)
|
|
|
|
|
|
class Spawn:
|
|
"""
|
|
This class is used for spawning and controlling a child process.
|
|
|
|
A new instance of this class can either run a new server (a small Python
|
|
program that reads output from the child process and reports it to the
|
|
client and to a text file) or attach to an already running server.
|
|
When a server is started it runs the child process.
|
|
The server writes output from the child's STDOUT and STDERR to a text file.
|
|
The text file can be accessed at any time using get_output().
|
|
In addition, the server opens as many pipes as requested by the client and
|
|
writes the output to them.
|
|
The pipes are requested and accessed by classes derived from Spawn.
|
|
These pipes are referred to as "readers".
|
|
The server also receives input from the client and sends it to the child
|
|
process.
|
|
An instance of this class can be pickled. Every derived class is
|
|
responsible for restoring its own state by properly defining
|
|
__getinitargs__().
|
|
|
|
The first named pipe is used by _tail(), a function that runs in the
|
|
background and reports new output from the child as it is produced.
|
|
The second named pipe is used by a set of functions that read and parse
|
|
output as requested by the user in an interactive manner, similar to
|
|
pexpect.
|
|
When unpickled it automatically
|
|
resumes _tail() if needed.
|
|
"""
|
|
|
|
def __init__(self, command=None, id=None, auto_close=False, echo=False,
|
|
linesep="\n"):
|
|
"""
|
|
Initialize the class and run command as a child process.
|
|
|
|
@param command: Command to run, or None if accessing an already running
|
|
server.
|
|
@param id: ID of an already running server, if accessing a running
|
|
server, or None if starting a new one.
|
|
@param auto_close: If True, close() the instance automatically when its
|
|
reference count drops to zero (default False).
|
|
@param echo: Boolean indicating whether echo should be initially
|
|
enabled for the pseudo terminal running the subprocess. This
|
|
parameter has an effect only when starting a new server.
|
|
@param linesep: Line separator to be appended to strings sent to the
|
|
child process by sendline().
|
|
"""
|
|
self.id = id or virt_utils.generate_random_string(8)
|
|
|
|
# Define filenames for communication with server
|
|
base_dir = "/tmp/kvm_spawn"
|
|
try:
|
|
os.makedirs(base_dir)
|
|
except:
|
|
pass
|
|
(self.shell_pid_filename,
|
|
self.status_filename,
|
|
self.output_filename,
|
|
self.inpipe_filename,
|
|
self.lock_server_running_filename,
|
|
self.lock_client_starting_filename) = _get_filenames(base_dir,
|
|
self.id)
|
|
|
|
# Remember some attributes
|
|
self.auto_close = auto_close
|
|
self.echo = echo
|
|
self.linesep = linesep
|
|
|
|
# Make sure the 'readers' and 'close_hooks' attributes exist
|
|
if not hasattr(self, "readers"):
|
|
self.readers = []
|
|
if not hasattr(self, "close_hooks"):
|
|
self.close_hooks = []
|
|
|
|
# Define the reader filenames
|
|
self.reader_filenames = dict(
|
|
(reader, _get_reader_filename(base_dir, self.id, reader))
|
|
for reader in self.readers)
|
|
|
|
# Let the server know a client intends to open some pipes;
|
|
# if the executed command terminates quickly, the server will wait for
|
|
# the client to release the lock before exiting
|
|
lock_client_starting = _lock(self.lock_client_starting_filename)
|
|
|
|
# Start the server (which runs the command)
|
|
if command:
|
|
sub = subprocess.Popen("%s %s" % (sys.executable, __file__),
|
|
shell=True,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
# Send parameters to the server
|
|
sub.stdin.write("%s\n" % self.id)
|
|
sub.stdin.write("%s\n" % echo)
|
|
sub.stdin.write("%s\n" % ",".join(self.readers))
|
|
sub.stdin.write("%s\n" % command)
|
|
# Wait for the server to complete its initialization
|
|
while not "Server %s ready" % self.id in sub.stdout.readline():
|
|
pass
|
|
|
|
# Open the reading pipes
|
|
self.reader_fds = {}
|
|
try:
|
|
assert(_locked(self.lock_server_running_filename))
|
|
for reader, filename in self.reader_filenames.items():
|
|
self.reader_fds[reader] = os.open(filename, os.O_RDONLY)
|
|
except:
|
|
pass
|
|
|
|
# Allow the server to continue
|
|
_unlock(lock_client_starting)
|
|
|
|
|
|
# The following two functions are defined to make sure the state is set
|
|
# exclusively by the constructor call as specified in __getinitargs__().
|
|
|
|
def __getstate__(self):
|
|
pass
|
|
|
|
|
|
def __setstate__(self, state):
|
|
pass
|
|
|
|
|
|
def __getinitargs__(self):
|
|
# Save some information when pickling -- will be passed to the
|
|
# constructor upon unpickling
|
|
return (None, self.id, self.auto_close, self.echo, self.linesep)
|
|
|
|
|
|
def __del__(self):
|
|
if self.auto_close:
|
|
self.close()
|
|
|
|
|
|
def _add_reader(self, reader):
|
|
"""
|
|
Add a reader whose file descriptor can be obtained with _get_fd().
|
|
Should be called before __init__(). Intended for use by derived
|
|
classes.
|
|
|
|
@param reader: The name of the reader.
|
|
"""
|
|
if not hasattr(self, "readers"):
|
|
self.readers = []
|
|
self.readers.append(reader)
|
|
|
|
|
|
def _add_close_hook(self, hook):
|
|
"""
|
|
Add a close hook function to be called when close() is called.
|
|
The function will be called after the process terminates but before
|
|
final cleanup. Intended for use by derived classes.
|
|
|
|
@param hook: The hook function.
|
|
"""
|
|
if not hasattr(self, "close_hooks"):
|
|
self.close_hooks = []
|
|
self.close_hooks.append(hook)
|
|
|
|
|
|
def _get_fd(self, reader):
|
|
"""
|
|
Return an open file descriptor corresponding to the specified reader
|
|
pipe. If no such reader exists, or the pipe could not be opened,
|
|
return None. Intended for use by derived classes.
|
|
|
|
@param reader: The name of the reader.
|
|
"""
|
|
return self.reader_fds.get(reader)
|
|
|
|
|
|
def get_id(self):
|
|
"""
|
|
Return the instance's id attribute, which may be used to access the
|
|
process in the future.
|
|
"""
|
|
return self.id
|
|
|
|
|
|
def get_pid(self):
|
|
"""
|
|
Return the PID of the process.
|
|
|
|
Note: this may be the PID of the shell process running the user given
|
|
command.
|
|
"""
|
|
try:
|
|
file = open(self.shell_pid_filename, "r")
|
|
pid = int(file.read())
|
|
file.close()
|
|
return pid
|
|
except:
|
|
return None
|
|
|
|
|
|
def get_status(self):
|
|
"""
|
|
Wait for the process to exit and return its exit status, or None
|
|
if the exit status is not available.
|
|
"""
|
|
_wait(self.lock_server_running_filename)
|
|
try:
|
|
file = open(self.status_filename, "r")
|
|
status = int(file.read())
|
|
file.close()
|
|
return status
|
|
except:
|
|
return None
|
|
|
|
|
|
def get_output(self):
|
|
"""
|
|
Return the STDOUT and STDERR output of the process so far.
|
|
"""
|
|
try:
|
|
file = open(self.output_filename, "r")
|
|
output = file.read()
|
|
file.close()
|
|
return output
|
|
except:
|
|
return ""
|
|
|
|
|
|
def is_alive(self):
|
|
"""
|
|
Return True if the process is running.
|
|
"""
|
|
return _locked(self.lock_server_running_filename)
|
|
|
|
|
|
def close(self, sig=signal.SIGKILL):
|
|
"""
|
|
Kill the child process if it's alive and remove temporary files.
|
|
|
|
@param sig: The signal to send the process when attempting to kill it.
|
|
"""
|
|
# Kill it if it's alive
|
|
if self.is_alive():
|
|
virt_utils.kill_process_tree(self.get_pid(), sig)
|
|
# Wait for the server to exit
|
|
_wait(self.lock_server_running_filename)
|
|
# Call all cleanup routines
|
|
for hook in self.close_hooks:
|
|
hook(self)
|
|
# Close reader file descriptors
|
|
for fd in self.reader_fds.values():
|
|
try:
|
|
os.close(fd)
|
|
except:
|
|
pass
|
|
self.reader_fds = {}
|
|
# Remove all used files
|
|
for filename in (_get_filenames("/tmp/kvm_spawn", self.id) +
|
|
self.reader_filenames.values()):
|
|
try:
|
|
os.unlink(filename)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def set_linesep(self, linesep):
|
|
"""
|
|
Sets the line separator string (usually "\\n").
|
|
|
|
@param linesep: Line separator string.
|
|
"""
|
|
self.linesep = linesep
|
|
|
|
|
|
def send(self, str=""):
|
|
"""
|
|
Send a string to the child process.
|
|
|
|
@param str: String to send to the child process.
|
|
"""
|
|
try:
|
|
fd = os.open(self.inpipe_filename, os.O_RDWR)
|
|
os.write(fd, str)
|
|
os.close(fd)
|
|
except:
|
|
pass
|
|
|
|
|
|
def sendline(self, str=""):
|
|
"""
|
|
Send a string followed by a line separator to the child process.
|
|
|
|
@param str: String to send to the child process.
|
|
"""
|
|
self.send(str + self.linesep)
|
|
|
|
|
|
_thread_kill_requested = False
|
|
|
|
def kill_tail_threads():
|
|
"""
|
|
Kill all Tail threads.
|
|
|
|
After calling this function no new threads should be started.
|
|
"""
|
|
global _thread_kill_requested
|
|
_thread_kill_requested = True
|
|
for t in threading.enumerate():
|
|
if hasattr(t, "name") and t.name.startswith("tail_thread"):
|
|
t.join(10)
|
|
_thread_kill_requested = False
|
|
|
|
|
|
class Tail(Spawn):
|
|
"""
|
|
This class runs a child process in the background and sends its output in
|
|
real time, line-by-line, to a callback function.
|
|
|
|
See Spawn's docstring.
|
|
|
|
This class uses a single pipe reader to read data in real time from the
|
|
child process and report it to a given callback function.
|
|
When the child process exits, its exit status is reported to an additional
|
|
callback function.
|
|
|
|
When this class is unpickled, it automatically resumes reporting output.
|
|
"""
|
|
|
|
def __init__(self, command=None, id=None, auto_close=False, echo=False,
|
|
linesep="\n", termination_func=None, termination_params=(),
|
|
output_func=None, output_params=(), output_prefix=""):
|
|
"""
|
|
Initialize the class and run command as a child process.
|
|
|
|
@param command: Command to run, or None if accessing an already running
|
|
server.
|
|
@param id: ID of an already running server, if accessing a running
|
|
server, or None if starting a new one.
|
|
@param auto_close: If True, close() the instance automatically when its
|
|
reference count drops to zero (default False).
|
|
@param echo: Boolean indicating whether echo should be initially
|
|
enabled for the pseudo terminal running the subprocess. This
|
|
parameter has an effect only when starting a new server.
|
|
@param linesep: Line separator to be appended to strings sent to the
|
|
child process by sendline().
|
|
@param termination_func: Function to call when the process exits. The
|
|
function must accept a single exit status parameter.
|
|
@param termination_params: Parameters to send to termination_func
|
|
before the exit status.
|
|
@param output_func: Function to call whenever a line of output is
|
|
available from the STDOUT or STDERR streams of the process.
|
|
The function must accept a single string parameter. The string
|
|
does not include the final newline.
|
|
@param output_params: Parameters to send to output_func before the
|
|
output line.
|
|
@param output_prefix: String to prepend to lines sent to output_func.
|
|
"""
|
|
# Add a reader and a close hook
|
|
self._add_reader("tail")
|
|
self._add_close_hook(Tail._join_thread)
|
|
|
|
# Init the superclass
|
|
Spawn.__init__(self, command, id, auto_close, echo, linesep)
|
|
|
|
# Remember some attributes
|
|
self.termination_func = termination_func
|
|
self.termination_params = termination_params
|
|
self.output_func = output_func
|
|
self.output_params = output_params
|
|
self.output_prefix = output_prefix
|
|
|
|
# Start the thread in the background
|
|
self.tail_thread = None
|
|
if termination_func or output_func:
|
|
self._start_thread()
|
|
|
|
|
|
def __getinitargs__(self):
|
|
return Spawn.__getinitargs__(self) + (self.termination_func,
|
|
self.termination_params,
|
|
self.output_func,
|
|
self.output_params,
|
|
self.output_prefix)
|
|
|
|
|
|
def set_termination_func(self, termination_func):
|
|
"""
|
|
Set the termination_func attribute. See __init__() for details.
|
|
|
|
@param termination_func: Function to call when the process terminates.
|
|
Must take a single parameter -- the exit status.
|
|
"""
|
|
self.termination_func = termination_func
|
|
if termination_func and not self.tail_thread:
|
|
self._start_thread()
|
|
|
|
|
|
def set_termination_params(self, termination_params):
|
|
"""
|
|
Set the termination_params attribute. See __init__() for details.
|
|
|
|
@param termination_params: Parameters to send to termination_func
|
|
before the exit status.
|
|
"""
|
|
self.termination_params = termination_params
|
|
|
|
|
|
def set_output_func(self, output_func):
|
|
"""
|
|
Set the output_func attribute. See __init__() for details.
|
|
|
|
@param output_func: Function to call for each line of STDOUT/STDERR
|
|
output from the process. Must take a single string parameter.
|
|
"""
|
|
self.output_func = output_func
|
|
if output_func and not self.tail_thread:
|
|
self._start_thread()
|
|
|
|
|
|
def set_output_params(self, output_params):
|
|
"""
|
|
Set the output_params attribute. See __init__() for details.
|
|
|
|
@param output_params: Parameters to send to output_func before the
|
|
output line.
|
|
"""
|
|
self.output_params = output_params
|
|
|
|
|
|
def set_output_prefix(self, output_prefix):
|
|
"""
|
|
Set the output_prefix attribute. See __init__() for details.
|
|
|
|
@param output_prefix: String to pre-pend to each line sent to
|
|
output_func (see set_output_callback()).
|
|
"""
|
|
self.output_prefix = output_prefix
|
|
|
|
|
|
def _tail(self):
|
|
def print_line(text):
|
|
# Pre-pend prefix and remove trailing whitespace
|
|
text = self.output_prefix + text.rstrip()
|
|
# Pass text to output_func
|
|
try:
|
|
params = self.output_params + (text,)
|
|
self.output_func(*params)
|
|
except TypeError:
|
|
pass
|
|
|
|
try:
|
|
fd = self._get_fd("tail")
|
|
buffer = ""
|
|
while True:
|
|
global _thread_kill_requested
|
|
if _thread_kill_requested:
|
|
return
|
|
try:
|
|
# See if there's any data to read from the pipe
|
|
r, w, x = select.select([fd], [], [], 0.05)
|
|
except:
|
|
break
|
|
if fd in r:
|
|
# Some data is available; read it
|
|
new_data = os.read(fd, 1024)
|
|
if not new_data:
|
|
break
|
|
buffer += new_data
|
|
# Send the output to output_func line by line
|
|
# (except for the last line)
|
|
if self.output_func:
|
|
lines = buffer.split("\n")
|
|
for line in lines[:-1]:
|
|
print_line(line)
|
|
# Leave only the last line
|
|
last_newline_index = buffer.rfind("\n")
|
|
buffer = buffer[last_newline_index+1:]
|
|
else:
|
|
# No output is available right now; flush the buffer
|
|
if buffer:
|
|
print_line(buffer)
|
|
buffer = ""
|
|
# The process terminated; print any remaining output
|
|
if buffer:
|
|
print_line(buffer)
|
|
# Get the exit status, print it and send it to termination_func
|
|
status = self.get_status()
|
|
if status is None:
|
|
return
|
|
print_line("(Process terminated with status %s)" % status)
|
|
try:
|
|
params = self.termination_params + (status,)
|
|
self.termination_func(*params)
|
|
except TypeError:
|
|
pass
|
|
finally:
|
|
self.tail_thread = None
|
|
|
|
|
|
def _start_thread(self):
|
|
self.tail_thread = threading.Thread(target=self._tail,
|
|
name="tail_thread_%s" % self.id)
|
|
self.tail_thread.start()
|
|
|
|
|
|
def _join_thread(self):
|
|
# Wait for the tail thread to exit
|
|
# (it's done this way because self.tail_thread may become None at any
|
|
# time)
|
|
t = self.tail_thread
|
|
if t:
|
|
t.join()
|
|
|
|
|
|
class Expect(Tail):
|
|
"""
|
|
This class runs a child process in the background and provides expect-like
|
|
services.
|
|
|
|
It also provides all of Tail's functionality.
|
|
"""
|
|
|
|
def __init__(self, command=None, id=None, auto_close=True, echo=False,
|
|
linesep="\n", termination_func=None, termination_params=(),
|
|
output_func=None, output_params=(), output_prefix=""):
|
|
"""
|
|
Initialize the class and run command as a child process.
|
|
|
|
@param command: Command to run, or None if accessing an already running
|
|
server.
|
|
@param id: ID of an already running server, if accessing a running
|
|
server, or None if starting a new one.
|
|
@param auto_close: If True, close() the instance automatically when its
|
|
reference count drops to zero (default False).
|
|
@param echo: Boolean indicating whether echo should be initially
|
|
enabled for the pseudo terminal running the subprocess. This
|
|
parameter has an effect only when starting a new server.
|
|
@param linesep: Line separator to be appended to strings sent to the
|
|
child process by sendline().
|
|
@param termination_func: Function to call when the process exits. The
|
|
function must accept a single exit status parameter.
|
|
@param termination_params: Parameters to send to termination_func
|
|
before the exit status.
|
|
@param output_func: Function to call whenever a line of output is
|
|
available from the STDOUT or STDERR streams of the process.
|
|
The function must accept a single string parameter. The string
|
|
does not include the final newline.
|
|
@param output_params: Parameters to send to output_func before the
|
|
output line.
|
|
@param output_prefix: String to prepend to lines sent to output_func.
|
|
"""
|
|
# Add a reader
|
|
self._add_reader("expect")
|
|
|
|
# Init the superclass
|
|
Tail.__init__(self, command, id, auto_close, echo, linesep,
|
|
termination_func, termination_params,
|
|
output_func, output_params, output_prefix)
|
|
|
|
|
|
def __getinitargs__(self):
|
|
return Tail.__getinitargs__(self)
|
|
|
|
|
|
def read_nonblocking(self, timeout=None):
|
|
"""
|
|
Read from child until there is nothing to read for timeout seconds.
|
|
|
|
@param timeout: Time (seconds) to wait before we give up reading from
|
|
the child process, or None to use the default value.
|
|
"""
|
|
if timeout is None:
|
|
timeout = 0.1
|
|
fd = self._get_fd("expect")
|
|
data = ""
|
|
while True:
|
|
try:
|
|
r, w, x = select.select([fd], [], [], timeout)
|
|
except:
|
|
return data
|
|
if fd in r:
|
|
new_data = os.read(fd, 1024)
|
|
if not new_data:
|
|
return data
|
|
data += new_data
|
|
else:
|
|
return data
|
|
|
|
|
|
def match_patterns(self, str, patterns):
|
|
"""
|
|
Match str against a list of patterns.
|
|
|
|
Return the index of the first pattern that matches a substring of str.
|
|
None and empty strings in patterns are ignored.
|
|
If no match is found, return None.
|
|
|
|
@param patterns: List of strings (regular expression patterns).
|
|
"""
|
|
for i in range(len(patterns)):
|
|
if not patterns[i]:
|
|
continue
|
|
if re.search(patterns[i], str):
|
|
return i
|
|
|
|
|
|
def read_until_output_matches(self, patterns, filter=lambda x: x,
|
|
timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Read using read_nonblocking until a match is found using match_patterns,
|
|
or until timeout expires. Before attempting to search for a match, the
|
|
data is filtered using the filter function provided.
|
|
|
|
@brief: Read from child using read_nonblocking until a pattern
|
|
matches.
|
|
@param patterns: List of strings (regular expression patterns)
|
|
@param filter: Function to apply to the data read from the child before
|
|
attempting to match it against the patterns (should take and
|
|
return a string)
|
|
@param timeout: The duration (in seconds) to wait until a match is
|
|
found
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
@return: Tuple containing the match index and the data read so far
|
|
@raise ExpectTimeoutError: Raised if timeout expires
|
|
@raise ExpectProcessTerminatedError: Raised if the child process
|
|
terminates while waiting for output
|
|
@raise ExpectError: Raised if an unknown error occurs
|
|
"""
|
|
fd = self._get_fd("expect")
|
|
o = ""
|
|
end_time = time.time() + timeout
|
|
while True:
|
|
try:
|
|
r, w, x = select.select([fd], [], [],
|
|
max(0, end_time - time.time()))
|
|
except (select.error, TypeError):
|
|
break
|
|
if not r:
|
|
raise ExpectTimeoutError(patterns, o)
|
|
# Read data from child
|
|
data = self.read_nonblocking(internal_timeout)
|
|
if not data:
|
|
break
|
|
# Print it if necessary
|
|
if print_func:
|
|
for line in data.splitlines():
|
|
print_func(line)
|
|
# Look for patterns
|
|
o += data
|
|
match = self.match_patterns(filter(o), patterns)
|
|
if match is not None:
|
|
return match, o
|
|
|
|
# Check if the child has terminated
|
|
if virt_utils.wait_for(lambda: not self.is_alive(), 5, 0, 0.1):
|
|
raise ExpectProcessTerminatedError(patterns, self.get_status(), o)
|
|
else:
|
|
# This shouldn't happen
|
|
raise ExpectError(patterns, o)
|
|
|
|
|
|
def read_until_last_word_matches(self, patterns, timeout=60,
|
|
internal_timeout=None, print_func=None):
|
|
"""
|
|
Read using read_nonblocking until the last word of the output matches
|
|
one of the patterns (using match_patterns), or until timeout expires.
|
|
|
|
@param patterns: A list of strings (regular expression patterns)
|
|
@param timeout: The duration (in seconds) to wait until a match is
|
|
found
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
@return: A tuple containing the match index and the data read so far
|
|
@raise ExpectTimeoutError: Raised if timeout expires
|
|
@raise ExpectProcessTerminatedError: Raised if the child process
|
|
terminates while waiting for output
|
|
@raise ExpectError: Raised if an unknown error occurs
|
|
"""
|
|
def get_last_word(str):
|
|
if str:
|
|
return str.split()[-1]
|
|
else:
|
|
return ""
|
|
|
|
return self.read_until_output_matches(patterns, get_last_word,
|
|
timeout, internal_timeout,
|
|
print_func)
|
|
|
|
|
|
def read_until_last_line_matches(self, patterns, timeout=60,
|
|
internal_timeout=None, print_func=None):
|
|
"""
|
|
Read using read_nonblocking until the last non-empty line of the output
|
|
matches one of the patterns (using match_patterns), or until timeout
|
|
expires. Return a tuple containing the match index (or None if no match
|
|
was found) and the data read so far.
|
|
|
|
@brief: Read using read_nonblocking until the last non-empty line
|
|
matches a pattern.
|
|
|
|
@param patterns: A list of strings (regular expression patterns)
|
|
@param timeout: The duration (in seconds) to wait until a match is
|
|
found
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
@return: A tuple containing the match index and the data read so far
|
|
@raise ExpectTimeoutError: Raised if timeout expires
|
|
@raise ExpectProcessTerminatedError: Raised if the child process
|
|
terminates while waiting for output
|
|
@raise ExpectError: Raised if an unknown error occurs
|
|
"""
|
|
def get_last_nonempty_line(str):
|
|
nonempty_lines = [l for l in str.splitlines() if l.strip()]
|
|
if nonempty_lines:
|
|
return nonempty_lines[-1]
|
|
else:
|
|
return ""
|
|
|
|
return self.read_until_output_matches(patterns, get_last_nonempty_line,
|
|
timeout, internal_timeout,
|
|
print_func)
|
|
|
|
|
|
class ShellSession(Expect):
|
|
"""
|
|
This class runs a child process in the background. It it suited for
|
|
processes that provide an interactive shell, such as SSH and Telnet.
|
|
|
|
It provides all services of Expect and Tail. In addition, it
|
|
provides command running services, and a utility function to test the
|
|
process for responsiveness.
|
|
"""
|
|
|
|
def __init__(self, command=None, id=None, auto_close=True, echo=False,
|
|
linesep="\n", termination_func=None, termination_params=(),
|
|
output_func=None, output_params=(), output_prefix="",
|
|
prompt=r"[\#\$]\s*$", status_test_command="echo $?"):
|
|
"""
|
|
Initialize the class and run command as a child process.
|
|
|
|
@param command: Command to run, or None if accessing an already running
|
|
server.
|
|
@param id: ID of an already running server, if accessing a running
|
|
server, or None if starting a new one.
|
|
@param auto_close: If True, close() the instance automatically when its
|
|
reference count drops to zero (default True).
|
|
@param echo: Boolean indicating whether echo should be initially
|
|
enabled for the pseudo terminal running the subprocess. This
|
|
parameter has an effect only when starting a new server.
|
|
@param linesep: Line separator to be appended to strings sent to the
|
|
child process by sendline().
|
|
@param termination_func: Function to call when the process exits. The
|
|
function must accept a single exit status parameter.
|
|
@param termination_params: Parameters to send to termination_func
|
|
before the exit status.
|
|
@param output_func: Function to call whenever a line of output is
|
|
available from the STDOUT or STDERR streams of the process.
|
|
The function must accept a single string parameter. The string
|
|
does not include the final newline.
|
|
@param output_params: Parameters to send to output_func before the
|
|
output line.
|
|
@param output_prefix: String to prepend to lines sent to output_func.
|
|
@param prompt: Regular expression describing the shell's prompt line.
|
|
@param status_test_command: Command to be used for getting the last
|
|
exit status of commands run inside the shell (used by
|
|
cmd_status_output() and friends).
|
|
"""
|
|
# Init the superclass
|
|
Expect.__init__(self, command, id, auto_close, echo, linesep,
|
|
termination_func, termination_params,
|
|
output_func, output_params, output_prefix)
|
|
|
|
# Remember some attributes
|
|
self.prompt = prompt
|
|
self.status_test_command = status_test_command
|
|
|
|
|
|
def __getinitargs__(self):
|
|
return Expect.__getinitargs__(self) + (self.prompt,
|
|
self.status_test_command)
|
|
|
|
|
|
def set_prompt(self, prompt):
|
|
"""
|
|
Set the prompt attribute for later use by read_up_to_prompt.
|
|
|
|
@param: String that describes the prompt contents.
|
|
"""
|
|
self.prompt = prompt
|
|
|
|
|
|
def set_status_test_command(self, status_test_command):
|
|
"""
|
|
Set the command to be sent in order to get the last exit status.
|
|
|
|
@param status_test_command: Command that will be sent to get the last
|
|
exit status.
|
|
"""
|
|
self.status_test_command = status_test_command
|
|
|
|
|
|
def is_responsive(self, timeout=5.0):
|
|
"""
|
|
Return True if the process responds to STDIN/terminal input.
|
|
|
|
Send a newline to the child process (e.g. SSH or Telnet) and read some
|
|
output using read_nonblocking().
|
|
If all is OK, some output should be available (e.g. the shell prompt).
|
|
In that case return True. Otherwise return False.
|
|
|
|
@param timeout: Time duration to wait before the process is considered
|
|
unresponsive.
|
|
"""
|
|
# Read all output that's waiting to be read, to make sure the output
|
|
# we read next is in response to the newline sent
|
|
self.read_nonblocking(timeout=0)
|
|
# Send a newline
|
|
self.sendline()
|
|
# Wait up to timeout seconds for some output from the child
|
|
end_time = time.time() + timeout
|
|
while time.time() < end_time:
|
|
time.sleep(0.5)
|
|
if self.read_nonblocking(timeout=0).strip():
|
|
return True
|
|
# No output -- report unresponsive
|
|
return False
|
|
|
|
|
|
def read_up_to_prompt(self, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Read using read_nonblocking until the last non-empty line of the output
|
|
matches the prompt regular expression set by set_prompt, or until
|
|
timeout expires.
|
|
|
|
@brief: Read using read_nonblocking until the last non-empty line
|
|
matches the prompt.
|
|
|
|
@param timeout: The duration (in seconds) to wait until a match is
|
|
found
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being
|
|
read (should take a string parameter)
|
|
|
|
@return: The data read so far
|
|
@raise ExpectTimeoutError: Raised if timeout expires
|
|
@raise ExpectProcessTerminatedError: Raised if the shell process
|
|
terminates while waiting for output
|
|
@raise ExpectError: Raised if an unknown error occurs
|
|
"""
|
|
m, o = self.read_until_last_line_matches([self.prompt], timeout,
|
|
internal_timeout, print_func)
|
|
return o
|
|
|
|
|
|
def cmd_output(self, cmd, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Send a command and return its output.
|
|
|
|
@param cmd: Command to send (must not contain newline characters)
|
|
@param timeout: The duration (in seconds) to wait for the prompt to
|
|
return
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
|
|
@return: The output of cmd
|
|
@raise ShellTimeoutError: Raised if timeout expires
|
|
@raise ShellProcessTerminatedError: Raised if the shell process
|
|
terminates while waiting for output
|
|
@raise ShellError: Raised if an unknown error occurs
|
|
"""
|
|
def remove_command_echo(str, cmd):
|
|
if str and str.splitlines()[0] == cmd:
|
|
str = "".join(str.splitlines(True)[1:])
|
|
return str
|
|
|
|
def remove_last_nonempty_line(str):
|
|
return "".join(str.rstrip().splitlines(True)[:-1])
|
|
|
|
logging.debug("Sending command: %s" % cmd)
|
|
self.read_nonblocking(timeout=0)
|
|
self.sendline(cmd)
|
|
try:
|
|
o = self.read_up_to_prompt(timeout, internal_timeout, print_func)
|
|
except ExpectError, e:
|
|
o = remove_command_echo(e.output, cmd)
|
|
if isinstance(e, ExpectTimeoutError):
|
|
raise ShellTimeoutError(cmd, o)
|
|
elif isinstance(e, ExpectProcessTerminatedError):
|
|
raise ShellProcessTerminatedError(cmd, e.status, o)
|
|
else:
|
|
raise ShellError(cmd, o)
|
|
|
|
# Remove the echoed command and the final shell prompt
|
|
return remove_last_nonempty_line(remove_command_echo(o, cmd))
|
|
|
|
|
|
def cmd_status_output(self, cmd, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Send a command and return its exit status and output.
|
|
|
|
@param cmd: Command to send (must not contain newline characters)
|
|
@param timeout: The duration (in seconds) to wait for the prompt to
|
|
return
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
|
|
@return: A tuple (status, output) where status is the exit status and
|
|
output is the output of cmd
|
|
@raise ShellTimeoutError: Raised if timeout expires
|
|
@raise ShellProcessTerminatedError: Raised if the shell process
|
|
terminates while waiting for output
|
|
@raise ShellStatusError: Raised if the exit status cannot be obtained
|
|
@raise ShellError: Raised if an unknown error occurs
|
|
"""
|
|
o = self.cmd_output(cmd, timeout, internal_timeout, print_func)
|
|
try:
|
|
# Send the 'echo $?' (or equivalent) command to get the exit status
|
|
s = self.cmd_output(self.status_test_command, 10, internal_timeout)
|
|
except ShellError:
|
|
raise ShellStatusError(cmd, o)
|
|
|
|
# Get the first line consisting of digits only
|
|
digit_lines = [l for l in s.splitlines() if l.strip().isdigit()]
|
|
if digit_lines:
|
|
return int(digit_lines[0].strip()), o
|
|
else:
|
|
raise ShellStatusError(cmd, o)
|
|
|
|
|
|
def cmd_status(self, cmd, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Send a command and return its exit status.
|
|
|
|
@param cmd: Command to send (must not contain newline characters)
|
|
@param timeout: The duration (in seconds) to wait for the prompt to
|
|
return
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
|
|
@return: The exit status of cmd
|
|
@raise ShellTimeoutError: Raised if timeout expires
|
|
@raise ShellProcessTerminatedError: Raised if the shell process
|
|
terminates while waiting for output
|
|
@raise ShellStatusError: Raised if the exit status cannot be obtained
|
|
@raise ShellError: Raised if an unknown error occurs
|
|
"""
|
|
s, o = self.cmd_status_output(cmd, timeout, internal_timeout,
|
|
print_func)
|
|
return s
|
|
|
|
|
|
def cmd(self, cmd, timeout=60, internal_timeout=None, print_func=None):
|
|
"""
|
|
Send a command and return its output. If the command's exit status is
|
|
nonzero, raise an exception.
|
|
|
|
@param cmd: Command to send (must not contain newline characters)
|
|
@param timeout: The duration (in seconds) to wait for the prompt to
|
|
return
|
|
@param internal_timeout: The timeout to pass to read_nonblocking
|
|
@param print_func: A function to be used to print the data being read
|
|
(should take a string parameter)
|
|
|
|
@return: The output of cmd
|
|
@raise ShellTimeoutError: Raised if timeout expires
|
|
@raise ShellProcessTerminatedError: Raised if the shell process
|
|
terminates while waiting for output
|
|
@raise ShellError: Raised if the exit status cannot be obtained or if
|
|
an unknown error occurs
|
|
@raise ShellStatusError: Raised if the exit status cannot be obtained
|
|
@raise ShellError: Raised if an unknown error occurs
|
|
@raise ShellCmdError: Raised if the exit status is nonzero
|
|
"""
|
|
s, o = self.cmd_status_output(cmd, timeout, internal_timeout,
|
|
print_func)
|
|
if s != 0:
|
|
raise ShellCmdError(cmd, s, o)
|
|
return o
|
|
|
|
|
|
def get_command_output(self, cmd, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Alias for cmd_output() for backward compatibility.
|
|
"""
|
|
return self.cmd_output(cmd, timeout, internal_timeout, print_func)
|
|
|
|
|
|
def get_command_status_output(self, cmd, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Alias for cmd_status_output() for backward compatibility.
|
|
"""
|
|
return self.cmd_status_output(cmd, timeout, internal_timeout,
|
|
print_func)
|
|
|
|
|
|
def get_command_status(self, cmd, timeout=60, internal_timeout=None,
|
|
print_func=None):
|
|
"""
|
|
Alias for cmd_status() for backward compatibility.
|
|
"""
|
|
return self.cmd_status(cmd, timeout, internal_timeout, print_func)
|