Message ID | eac379742371cccb0422d947d6e8083002fafda1.1248102188.git.mgoldish@redhat.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Ok, I have made minor remarks for the first version of this module http://codereview.appspot.com/79042/diff/1/4 and Michael either commented or addressed the questions. I am going to commit this new module. Thanks for your work, Michael! On Mon, Jul 20, 2009 at 12:07 PM, Michael Goldish<mgoldish@redhat.com> wrote: > This module is intended to be used for controlling all child processes in KVM > tests: both QEMU processes and SSH/SCP/Telnet processes. Processes started with > this module keep running and can be interacted with even after the parent > process exits. > > The current run_bg() utility tracks a child process as long as the parent > process is running. When the parent process exits, the tracking thread > terminates and cannot resume when needed. > > Currently SSH/SCP/Telnet communication is handled by kvm_utils.kvm_spawn, which > does not allow the child process to run after the parent process exits. Thus, > open SSH/SCP/Telnet sessions cannot be reused by tests following the one in > which they are opened. > > The new module provides a solution to these two problems, and also saves some > code by reusing common code required both for QEMU processes and SSH/SCP/Telnet > processes. > > Signed-off-by: Michael Goldish <mgoldish@redhat.com> > --- > Â client/tests/kvm/kvm_subprocess.py | 1146 ++++++++++++++++++++++++++++++++++++ > Â 1 files changed, 1146 insertions(+), 0 deletions(-) > Â create mode 100644 client/tests/kvm/kvm_subprocess.py > > diff --git a/client/tests/kvm/kvm_subprocess.py b/client/tests/kvm/kvm_subprocess.py > new file mode 100644 > index 0000000..413bdaa > --- /dev/null > +++ b/client/tests/kvm/kvm_subprocess.py > @@ -0,0 +1,1146 @@ > +#!/usr/bin/python > +import sys, subprocess, pty, select, os, time, signal, re, termios, fcntl > +import threading, logging, commands > +import common, kvm_utils > + > +""" > +A class and functions used for running and controlling child processes. > + > +@copyright: 2008-2009 Red Hat Inc. > +""" > + > + > +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 kvm_tail object. > + Â Â """ > + Â Â process = kvm_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) > + > + > +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)) > + > + > +class kvm_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 kvm_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, 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 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 kvm_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.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("python %s" % __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 > + Â Â Â Â Â Â sub.stdout.readline() > + > + Â Â Â Â # 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.echo, self.linesep) > + > + > + Â Â 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_shell_pid(self): > + Â Â Â Â """ > + Â Â Â Â Return the PID of the subshell process, or None if not available. > + Â Â Â Â The subshell is the shell that runs the command. > + Â Â Â Â """ > + Â Â Â Â try: > + Â Â Â Â Â Â file = open(self.shell_pid_filename, "r") > + Â Â Â Â Â Â pid = int(file.read()) > + Â Â Â Â Â Â file.close() > + Â Â Â Â Â Â return pid > + Â Â Â Â except: > + Â Â Â Â Â Â return None > + > + > + Â Â def get_pid(self, index=0): > + Â Â Â Â """ > + Â Â Â Â Try to get and return the PID of a child process of the subshell. > + Â Â Â Â This is usually the PID of the process executed in the subshell. > + Â Â Â Â There are 3 exceptions: > + Â Â Â Â Â Â - If the subshell couldn't start the process for some reason, no > + Â Â Â Â Â Â Â PID can be returned. > + Â Â Â Â Â Â - If the subshell is running several processes in parallel, > + Â Â Â Â Â Â Â multiple PIDs can be returned. Â Use the index parameter in this > + Â Â Â Â Â Â Â case. > + Â Â Â Â Â Â - Before starting the process, after the process has terminated, > + Â Â Â Â Â Â Â or while running shell code that doesn't start any processes -- > + Â Â Â Â Â Â Â no PID can be returned. > + > + Â Â Â Â @param index: The index of the child process whose PID is requested. > + Â Â Â Â Â Â Â Â Normally this should remain 0. > + Â Â Â Â @return: The PID of the child process, or None if none could be found. > + Â Â Â Â """ > + Â Â Â Â parent_pid = self.get_shell_pid() > + Â Â Â Â if not parent_pid: > + Â Â Â Â Â Â return None > + Â Â Â Â pids = commands.getoutput("ps --ppid %d -o pid=" % parent_pid).split() > + Â Â Â Â try: > + Â Â Â Â Â Â return int(pids[index]) > + Â Â Â Â 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. > + Â Â Â Â """ > + Â Â Â Â pid = self.get_shell_pid() > + Â Â Â Â # See if the PID exists > + Â Â Â Â try: > + Â Â Â Â Â Â os.kill(pid, 0) > + Â Â Â Â except: > + Â Â Â Â Â Â return False > + Â Â Â Â # Make sure the PID belongs to the original process > + Â Â Â Â filename = "/proc/%d/cmdline" % pid > + Â Â Â Â try: > + Â Â Â Â Â Â file = open(filename, "r") > + Â Â Â Â Â Â cmdline = file.read() > + Â Â Â Â Â Â file.close() > + Â Â Â Â except: > + Â Â Â Â Â Â # If we couldn't find the file for some reason, skip the check > + Â Â Â Â Â Â return True > + Â Â Â Â if self.id in cmdline: > + Â Â Â Â Â Â return True > + Â Â Â Â return False > + > + > + Â Â def close(self, sig=signal.SIGTERM): > + Â Â Â Â """ > + Â Â Â Â 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(): > + Â Â Â Â Â Â try: > + Â Â Â Â Â Â Â Â os.kill(self.get_shell_pid(), sig) > + Â Â Â Â Â Â except: > + Â Â Â Â Â Â Â Â pass > + Â Â Â Â # Wait for the server to exit > + Â Â Â Â _wait(self.lock_server_running_filename) > + Â Â Â Â # Call all cleanup routines > + Â Â Â Â for hook in self.close_hooks: > + Â Â Â Â Â Â hook() > + Â Â Â Â # Close reader file descriptors > + Â Â Â Â for fd in self.reader_fds.values(): > + Â Â Â Â Â Â try: > + Â Â Â Â Â Â Â Â os.close(fd) > + Â Â Â Â Â Â except: > + Â Â Â Â Â Â Â Â pass > + Â Â Â Â # 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) > + > + > +class kvm_tail(kvm_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 kvm_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, echo=False, linesep="\n", > + Â Â Â Â Â Â Â Â termination_func=None, output_func=None, 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 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 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_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(self._join_thread) > + > + Â Â Â Â # Init the superclass > + Â Â Â Â kvm_spawn.__init__(self, command, id, echo, linesep) > + > + Â Â Â Â # Remember some attributes > + Â Â Â Â self.termination_func = termination_func > + Â Â Â Â self.output_func = output_func > + Â Â Â Â self.output_prefix = output_prefix > + > + Â Â Â Â # Start the thread in the background > + Â Â Â Â self.tail_thread = threading.Thread(None, self._tail) > + Â Â Â Â self.tail_thread.start() > + > + > + Â Â def __getinitargs__(self): > + Â Â Â Â return kvm_spawn.__getinitargs__(self) + (self.termination_func, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â self.output_func, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â 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 > + > + > + Â Â 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 > + > + > + Â Â 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() > + Â Â Â Â Â Â # Sanitize text > + Â Â Â Â Â Â text = text.decode("utf-8", "replace") > + Â Â Â Â Â Â # Pass it to output_func > + Â Â Â Â Â Â try: > + Â Â Â Â Â Â Â Â self.output_func(text) > + Â Â Â Â Â Â except TypeError: > + Â Â Â Â Â Â Â Â pass > + > + Â Â Â Â fd = self._get_fd("tail") > + Â Â Â Â buffer = "" > + Â Â Â Â while True: > + Â Â Â Â Â Â 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: > + Â Â Â Â Â Â self.termination_func(status) > + Â Â Â Â except TypeError: > + Â Â Â Â Â Â pass > + > + > + Â Â def _join_thread(self): > + Â Â Â Â # Wait for the tail thread to exit > + Â Â Â Â if self.tail_thread: > + Â Â Â Â Â Â self.tail_thread.join() > + > + > +class kvm_expect(kvm_tail): > + Â Â """ > + Â Â This class runs a child process in the background and provides expect-like > + Â Â services. > + > + Â Â It also provides all of kvm_tail's functionality. > + Â Â """ > + > + Â Â def __init__(self, command=None, id=None, echo=False, linesep="\n", > + Â Â Â Â Â Â Â Â termination_func=None, output_func=None, 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 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 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_prefix: String to prepend to lines sent to output_func. > + Â Â Â Â """ > + Â Â Â Â # Add a reader > + Â Â Â Â self._add_reader("expect") > + > + Â Â Â Â # Init the superclass > + Â Â Â Â kvm_tail.__init__(self, command, id, echo, linesep, > + Â Â Â Â Â Â Â Â Â Â Â Â Â termination_func, output_func, output_prefix) > + > + > + Â Â def __getinitargs__(self): > + Â Â Â Â return kvm_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=30.0, 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 (or None if no match was > + Â Â Â Â Â Â Â Â found) and the data read so far. > + Â Â Â Â """ > + Â Â Â Â match = None > + Â Â Â Â data = "" > + > + Â Â Â Â end_time = time.time() + timeout > + Â Â Â Â while time.time() < end_time: > + Â Â Â Â Â Â # Read data from child > + Â Â Â Â Â Â newdata = self.read_nonblocking(internal_timeout) > + Â Â Â Â Â Â # Print it if necessary > + Â Â Â Â Â Â if print_func and newdata: > + Â Â Â Â Â Â Â Â str = newdata > + Â Â Â Â Â Â Â Â if str.endswith("\n"): > + Â Â Â Â Â Â Â Â Â Â str = str[:-1] > + Â Â Â Â Â Â Â Â for line in str.split("\n"): > + Â Â Â Â Â Â Â Â Â Â print_func(line.decode("utf-8", "replace")) > + Â Â Â Â Â Â data += newdata > + > + Â Â Â Â Â Â done = False > + Â Â Â Â Â Â # Look for patterns > + Â Â Â Â Â Â match = self.match_patterns(filter(data), patterns) > + Â Â Â Â Â Â if match is not None: > + Â Â Â Â Â Â Â Â done = True > + Â Â Â Â Â Â # Check if child has died > + Â Â Â Â Â Â if not self.is_alive(): > + Â Â Â Â Â Â Â Â logging.debug("Process terminated with status %s" % self.get_status()) > + Â Â Â Â Â Â Â Â done = True > + Â Â Â Â Â Â # Are we done? > + Â Â Â Â Â Â if done: break > + > + Â Â Â Â # Print some debugging info > + Â Â Â Â if match is None and (self.is_alive() or self.get_status() != 0): > + Â Â Â Â Â Â logging.debug("Timeout elapsed or process terminated. Output:" + > + Â Â Â Â Â Â Â Â Â Â Â Â Â kvm_utils.format_str_for_message(data.strip())) > + > + Â Â Â Â return (match, data) > + > + > + Â Â def read_until_last_word_matches(self, patterns, timeout=30.0, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â 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 (or None if no match was > + Â Â Â Â Â Â Â Â found) and the data read so far. > + Â Â Â Â """ > + Â Â Â Â 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=30.0, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â 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) > + Â Â Â Â """ > + Â Â Â Â 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 kvm_shell_session(kvm_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 kvm_expect and kvm_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, echo=False, linesep="\n", > + Â Â Â Â Â Â Â Â termination_func=None, output_func=None, 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 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 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_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 > + Â Â Â Â Â Â Â Â get_command_status_output() and friends). > + Â Â Â Â """ > + Â Â Â Â # Init the superclass > + Â Â Â Â kvm_expect.__init__(self, command, id, echo, linesep, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â termination_func, output_func, output_prefix) > + > + Â Â Â Â # Remember some attributes > + Â Â Â Â self.prompt = prompt > + Â Â Â Â self.status_test_command = status_test_command > + > + > + Â Â def __getinitargs__(self): > + Â Â Â Â return kvm_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.1) > + Â Â Â Â # 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=30.0, 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: A tuple containing True/False indicating whether the prompt > + Â Â Â Â Â Â Â Â was found, and the data read so far. > + Â Â Â Â """ > + Â Â Â Â (match, output) = self.read_until_last_line_matches([self.prompt], > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â internal_timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â print_func) > + Â Â Â Â return (match is not None, output) > + > + > + Â Â def get_command_status_output(self, command, timeout=30.0, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â internal_timeout=None, print_func=None): > + Â Â Â Â """ > + Â Â Â Â Send a command and return its exit status and output. > + > + Â Â Â Â @param command: Command to send (must not contain newline characters) > + Â Â Â Â @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 (status, output) where status is the exit status or > + Â Â Â Â Â Â Â Â None if no exit status is available (e.g. timeout elapsed), and > + Â Â Â Â Â Â Â Â output is the output of command. > + Â Â Â Â """ > + Â Â Â Â 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]) > + > + Â Â Â Â # Print some debugging info > + Â Â Â Â logging.debug("Sending command: %s" % command) > + > + Â Â Â Â # Read everything that's waiting to be read > + Â Â Â Â self.read_nonblocking(0.1) > + > + Â Â Â Â # Send the command and get its output > + Â Â Â Â self.sendline(command) > + Â Â Â Â (match, output) = self.read_up_to_prompt(timeout, internal_timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â print_func) > + Â Â Â Â # Remove the echoed command from the output > + Â Â Â Â output = remove_command_echo(output, command) > + Â Â Â Â # If the prompt was not found, return the output so far > + Â Â Â Â if not match: > + Â Â Â Â Â Â return (None, output) > + Â Â Â Â # Remove the final shell prompt from the output > + Â Â Â Â output = remove_last_nonempty_line(output) > + > + Â Â Â Â # Send the 'echo ...' command to get the last exit status > + Â Â Â Â self.sendline(self.status_test_command) > + Â Â Â Â (match, status) = self.read_up_to_prompt(10.0, internal_timeout) > + Â Â Â Â if not match: > + Â Â Â Â Â Â return (None, output) > + Â Â Â Â status = remove_command_echo(status, self.status_test_command) > + Â Â Â Â status = remove_last_nonempty_line(status) > + Â Â Â Â # Get the first line consisting of digits only > + Â Â Â Â digit_lines = [l for l in status.splitlines() if l.strip().isdigit()] > + Â Â Â Â if not digit_lines: > + Â Â Â Â Â Â return (None, output) > + Â Â Â Â status = int(digit_lines[0].strip()) > + > + Â Â Â Â # Print some debugging info > + Â Â Â Â if status != 0: > + Â Â Â Â Â Â logging.debug("Command failed; status: %d, output:%s", status, > + Â Â Â Â Â Â Â Â Â Â Â Â Â kvm_utils.format_str_for_message(output.strip())) > + > + Â Â Â Â return (status, output) > + > + > + Â Â def get_command_status(self, command, timeout=30.0, internal_timeout=None, > + Â Â Â Â Â Â Â Â Â Â Â Â Â print_func=None): > + Â Â Â Â """ > + Â Â Â Â Send a command and return its exit status. > + > + Â Â Â Â @param command: Command to send > + Â Â Â Â @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: Exit status or None if no exit status is available (e.g. > + Â Â Â Â Â Â Â Â timeout elapsed). > + Â Â Â Â """ > + Â Â Â Â (status, output) = self.get_command_status_output(command, timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â internal_timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â print_func) > + Â Â Â Â return status > + > + > + Â Â def get_command_output(self, command, timeout=30.0, internal_timeout=None, > + Â Â Â Â Â Â Â Â Â Â Â Â Â print_func=None): > + Â Â Â Â """ > + Â Â Â Â Send a command and return its output. > + > + Â Â Â Â @param command: Command to send > + Â Â Â Â @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) > + Â Â Â Â """ > + Â Â Â Â (status, output) = self.get_command_status_output(command, timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â internal_timeout, > + Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â print_func) > + Â Â Â Â return output > + > + > +# The following is the server part of the module. > + > +def _server_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 "hello" > + Â Â Â Â sys.stdout.flush() > + > + Â Â Â Â # Initialize buffers > + Â Â Â Â buffers = ["" for reader in readers] > + > + Â Â Â Â # Read from child and write to files/pipes > + Â Â Â Â while True: > + Â Â Â Â Â Â # 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, []) > + Â Â Â Â Â Â # 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: > + Â Â Â Â Â Â Â Â Â Â break > + Â Â Â Â Â Â Â Â # 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 there's data to read from the client -- > + Â Â Â Â Â Â if inpipe_fd in r: > + Â Â Â Â Â Â Â Â data = os.read(inpipe_fd, 1024) > + Â Â Â Â Â Â Â Â os.write(shell_fd, data) > + > + Â Â Â Â # Wait for the shell process to exit and get its exit status > + Â Â Â Â status = os.waitpid(shell_pid, 0)[1] > + Â Â Â Â status = os.WEXITSTATUS(status) > + Â Â Â Â 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) > + > + > +if __name__ == "__main__": > + Â Â _server_main() > -- > 1.5.4.1 > > _______________________________________________ > Autotest mailing list > Autotest@test.kernel.org > http://test.kernel.org/cgi-bin/mailman/listinfo/autotest >
diff --git a/client/tests/kvm/kvm_subprocess.py b/client/tests/kvm/kvm_subprocess.py new file mode 100644 index 0000000..413bdaa --- /dev/null +++ b/client/tests/kvm/kvm_subprocess.py @@ -0,0 +1,1146 @@ +#!/usr/bin/python +import sys, subprocess, pty, select, os, time, signal, re, termios, fcntl +import threading, logging, commands +import common, kvm_utils + +""" +A class and functions used for running and controlling child processes. + +@copyright: 2008-2009 Red Hat Inc. +""" + + +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 kvm_tail object. + """ + process = kvm_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) + + +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)) + + +class kvm_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 kvm_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, 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 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 kvm_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.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("python %s" % __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 + sub.stdout.readline() + + # 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.echo, self.linesep) + + + 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_shell_pid(self): + """ + Return the PID of the subshell process, or None if not available. + The subshell is the shell that runs the command. + """ + try: + file = open(self.shell_pid_filename, "r") + pid = int(file.read()) + file.close() + return pid + except: + return None + + + def get_pid(self, index=0): + """ + Try to get and return the PID of a child process of the subshell. + This is usually the PID of the process executed in the subshell. + There are 3 exceptions: + - If the subshell couldn't start the process for some reason, no + PID can be returned. + - If the subshell is running several processes in parallel, + multiple PIDs can be returned. Use the index parameter in this + case. + - Before starting the process, after the process has terminated, + or while running shell code that doesn't start any processes -- + no PID can be returned. + + @param index: The index of the child process whose PID is requested. + Normally this should remain 0. + @return: The PID of the child process, or None if none could be found. + """ + parent_pid = self.get_shell_pid() + if not parent_pid: + return None + pids = commands.getoutput("ps --ppid %d -o pid=" % parent_pid).split() + try: + return int(pids[index]) + 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. + """ + pid = self.get_shell_pid() + # See if the PID exists + try: + os.kill(pid, 0) + except: + return False + # Make sure the PID belongs to the original process + filename = "/proc/%d/cmdline" % pid + try: + file = open(filename, "r") + cmdline = file.read() + file.close() + except: + # If we couldn't find the file for some reason, skip the check + return True + if self.id in cmdline: + return True + return False + + + def close(self, sig=signal.SIGTERM): + """ + 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(): + try: + os.kill(self.get_shell_pid(), sig) + except: + pass + # Wait for the server to exit + _wait(self.lock_server_running_filename) + # Call all cleanup routines + for hook in self.close_hooks: + hook() + # Close reader file descriptors + for fd in self.reader_fds.values(): + try: + os.close(fd) + except: + pass + # 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) + + +class kvm_tail(kvm_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 kvm_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, echo=False, linesep="\n", + termination_func=None, output_func=None, 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 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 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_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(self._join_thread) + + # Init the superclass + kvm_spawn.__init__(self, command, id, echo, linesep) + + # Remember some attributes + self.termination_func = termination_func + self.output_func = output_func + self.output_prefix = output_prefix + + # Start the thread in the background + self.tail_thread = threading.Thread(None, self._tail) + self.tail_thread.start() + + + def __getinitargs__(self): + return kvm_spawn.__getinitargs__(self) + (self.termination_func, + self.output_func, + 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 + + + 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 + + + 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() + # Sanitize text + text = text.decode("utf-8", "replace") + # Pass it to output_func + try: + self.output_func(text) + except TypeError: + pass + + fd = self._get_fd("tail") + buffer = "" + while True: + 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: + self.termination_func(status) + except TypeError: + pass + + + def _join_thread(self): + # Wait for the tail thread to exit + if self.tail_thread: + self.tail_thread.join() + + +class kvm_expect(kvm_tail): + """ + This class runs a child process in the background and provides expect-like + services. + + It also provides all of kvm_tail's functionality. + """ + + def __init__(self, command=None, id=None, echo=False, linesep="\n", + termination_func=None, output_func=None, 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 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 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_prefix: String to prepend to lines sent to output_func. + """ + # Add a reader + self._add_reader("expect") + + # Init the superclass + kvm_tail.__init__(self, command, id, echo, linesep, + termination_func, output_func, output_prefix) + + + def __getinitargs__(self): + return kvm_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=30.0, 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 (or None if no match was + found) and the data read so far. + """ + match = None + data = "" + + end_time = time.time() + timeout + while time.time() < end_time: + # Read data from child + newdata = self.read_nonblocking(internal_timeout) + # Print it if necessary + if print_func and newdata: + str = newdata + if str.endswith("\n"): + str = str[:-1] + for line in str.split("\n"): + print_func(line.decode("utf-8", "replace")) + data += newdata + + done = False + # Look for patterns + match = self.match_patterns(filter(data), patterns) + if match is not None: + done = True + # Check if child has died + if not self.is_alive(): + logging.debug("Process terminated with status %s" % self.get_status()) + done = True + # Are we done? + if done: break + + # Print some debugging info + if match is None and (self.is_alive() or self.get_status() != 0): + logging.debug("Timeout elapsed or process terminated. Output:" + + kvm_utils.format_str_for_message(data.strip())) + + return (match, data) + + + def read_until_last_word_matches(self, patterns, timeout=30.0, + 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 (or None if no match was + found) and the data read so far. + """ + 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=30.0, + 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) + """ + 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 kvm_shell_session(kvm_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 kvm_expect and kvm_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, echo=False, linesep="\n", + termination_func=None, output_func=None, 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 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 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_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 + get_command_status_output() and friends). + """ + # Init the superclass + kvm_expect.__init__(self, command, id, echo, linesep, + termination_func, output_func, output_prefix) + + # Remember some attributes + self.prompt = prompt + self.status_test_command = status_test_command + + + def __getinitargs__(self): + return kvm_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.1) + # 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=30.0, 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: A tuple containing True/False indicating whether the prompt + was found, and the data read so far. + """ + (match, output) = self.read_until_last_line_matches([self.prompt], + timeout, + internal_timeout, + print_func) + return (match is not None, output) + + + def get_command_status_output(self, command, timeout=30.0, + internal_timeout=None, print_func=None): + """ + Send a command and return its exit status and output. + + @param command: Command to send (must not contain newline characters) + @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 (status, output) where status is the exit status or + None if no exit status is available (e.g. timeout elapsed), and + output is the output of command. + """ + 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]) + + # Print some debugging info + logging.debug("Sending command: %s" % command) + + # Read everything that's waiting to be read + self.read_nonblocking(0.1) + + # Send the command and get its output + self.sendline(command) + (match, output) = self.read_up_to_prompt(timeout, internal_timeout, + print_func) + # Remove the echoed command from the output + output = remove_command_echo(output, command) + # If the prompt was not found, return the output so far + if not match: + return (None, output) + # Remove the final shell prompt from the output + output = remove_last_nonempty_line(output) + + # Send the 'echo ...' command to get the last exit status + self.sendline(self.status_test_command) + (match, status) = self.read_up_to_prompt(10.0, internal_timeout) + if not match: + return (None, output) + status = remove_command_echo(status, self.status_test_command) + status = remove_last_nonempty_line(status) + # Get the first line consisting of digits only + digit_lines = [l for l in status.splitlines() if l.strip().isdigit()] + if not digit_lines: + return (None, output) + status = int(digit_lines[0].strip()) + + # Print some debugging info + if status != 0: + logging.debug("Command failed; status: %d, output:%s", status, + kvm_utils.format_str_for_message(output.strip())) + + return (status, output) + + + def get_command_status(self, command, timeout=30.0, internal_timeout=None, + print_func=None): + """ + Send a command and return its exit status. + + @param command: Command to send + @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: Exit status or None if no exit status is available (e.g. + timeout elapsed). + """ + (status, output) = self.get_command_status_output(command, timeout, + internal_timeout, + print_func) + return status + + + def get_command_output(self, command, timeout=30.0, internal_timeout=None, + print_func=None): + """ + Send a command and return its output. + + @param command: Command to send + @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) + """ + (status, output) = self.get_command_status_output(command, timeout, + internal_timeout, + print_func) + return output + + +# The following is the server part of the module. + +def _server_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 "hello" + sys.stdout.flush() + + # Initialize buffers + buffers = ["" for reader in readers] + + # Read from child and write to files/pipes + while True: + # 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, []) + # 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: + break + # 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 there's data to read from the client -- + if inpipe_fd in r: + data = os.read(inpipe_fd, 1024) + os.write(shell_fd, data) + + # Wait for the shell process to exit and get its exit status + status = os.waitpid(shell_pid, 0)[1] + status = os.WEXITSTATUS(status) + 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) + + +if __name__ == "__main__": + _server_main()
This module is intended to be used for controlling all child processes in KVM tests: both QEMU processes and SSH/SCP/Telnet processes. Processes started with this module keep running and can be interacted with even after the parent process exits. The current run_bg() utility tracks a child process as long as the parent process is running. When the parent process exits, the tracking thread terminates and cannot resume when needed. Currently SSH/SCP/Telnet communication is handled by kvm_utils.kvm_spawn, which does not allow the child process to run after the parent process exits. Thus, open SSH/SCP/Telnet sessions cannot be reused by tests following the one in which they are opened. The new module provides a solution to these two problems, and also saves some code by reusing common code required both for QEMU processes and SSH/SCP/Telnet processes. Signed-off-by: Michael Goldish <mgoldish@redhat.com> --- client/tests/kvm/kvm_subprocess.py | 1146 ++++++++++++++++++++++++++++++++++++ 1 files changed, 1146 insertions(+), 0 deletions(-) create mode 100644 client/tests/kvm/kvm_subprocess.py