#!/usr/bin/env python
# PySys System Test Framework, Copyright (C) 2006-2020 M.B. Grieve
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""
Process execution and monitoring implementations.
"""
from pysys.constants import *
# set the modules to import when imported the pysys.process package
__all__ = [ "helper",
"monitor",
"monitorimpl",
"user",
"Process"]
# add to the __path__ to import the platform specific process.helper module
dirname = __path__[0]
if IS_WINDOWS:
__path__.append(os.path.join(dirname, "plat-win32"))
else:
__path__.append(os.path.join(dirname, "plat-unix"))
import os.path, time, threading, sys, locale
import logging
if sys.version_info[0] == 2:
import Queue
else:
import queue as Queue
from pysys.constants import *
from pysys.exceptions import *
from pysys.utils.pycompat import *
def _stringToUnicode(s):
""" Converts a unicode string or a utf-8 bit string into a unicode string.
@deprecated: for internal use only, will be removed in future.
"""
if not PY2: return s
if isinstance(s, unicode):
return s
else:
return unicode(s, "utf8")
log = logging.getLogger('pysys.process')
[docs]class Process(object):
"""Represents a process that PySys has started (or can start).
Instances of a platform-specific implementation of this interface are returned
by `pysys.process.user.ProcessUser.startProcess`.
:ivar str ~.command: The full path to the executable.
:ivar list[str] ~.arguments: A list of arguments to the command.
:ivar dict(str,str) ~.environs: A dictionary of environment variables (key, value) for the process context execution.
Use unicode strings rather than byte strings if possible; on Python 2 byte strings are converted
automatically to unicode using utf-8.
:ivar str ~.workingDir: The working directory for the process
:ivar ~.state: The state of the process.
:vartype state: `pysys.constants.FOREGROUND` or `pysys.constants.BACKGROUND`
:ivar int ~.timeout: The time in seconds for a foreground process to complete.
:ivar str ~.stdout: The full path to the filename to write the stdout of the process, or None for no stderr stream.
:ivar str ~.stderr: The full path to the filename to write the stderr of the process, or None for no stderr stream.
:ivar str ~.displayName: Display name for this process (defaults to the basename if not explicitly specified). The
display name is returned by calling ``str()`` on this instance. The display name and pid are returned by
``repr()``.
:ivar str expectedExitStatus: The condition string used to determine whether the exit status/code
returned by the process is correct, for example '==0'.
:ivar int ~.pid: The process id for a running or complete process (as set by the OS), or None if it is not yet started.
:ivar int ~.exitStatus: The process exit status for a completed process (for many processes 0 represents success),
or None if it has not yet completed.
:ivar dict[str,obj] ~.info: A mutable dictionary of user-supplied information that was passed into startProcess,
for example port numbers, log file paths etc.
"""
def __init__(self, command, arguments, environs, workingDir, state, timeout, stdout=None, stderr=None, displayName=None,
expectedExitStatus=None, info={}):
self.displayName = displayName if displayName else os.path.basename(command)
self.info = info
self.command = command
self.arguments = arguments
self.environs = {}
for key in environs: self.environs[_stringToUnicode(key)] = _stringToUnicode(environs[key])
self.workingDir = workingDir
self.state = state
self.timeout = timeout
self.expectedExitStatus = expectedExitStatus
# 'publicly' available data attributes set on execution
self.pid = None
self.exitStatus = None
# these may be further updated by the subclass
self.stdout = stdout
self.stderr = stderr
# catch these common mistakes
assert os.path.isdir(self.workingDir), 'Working directory for %s does not exist: %s'%(self.displayName, self.stdout)
if self.stdout: assert os.path.dirname(self.stdout)==self.workingDir or os.path.isdir(os.path.dirname(self.stdout)), 'Parent directory for stdout does not exist: %s'%self.stdout
# print process debug information
log.debug("Process parameters for executable %s" % os.path.basename(self.command))
log.debug(" command : %s", self.command)
for a in self.arguments: log.debug(" argument : %s", a)
log.debug(" working dir : %s", self.workingDir)
log.debug(" stdout : %s", stdout)
log.debug(" stderr : %s", stderr)
keys=list(self.environs.keys())
keys.sort()
for e in keys: log.debug(" environment : %s=%s", e, self.environs[e])
if info: log.debug(" info : %s", info)
# private
self._outQueue = None
def __str__(self): return self.displayName
def __repr__(self): return '%s (pid %s)'%(self.displayName, self.pid)
# these abstract methods must be implemented by subclasses; no need to publically document
def setExitStatus(self): raise Exception('Not implemented')
def startBackgroundProcess(self): raise Exception('Not implemented')
def writeStdin(self): raise Exception('Not implemented')
[docs] def stop(self):
"""Stop a running process.
Does nothing if the process is not running.
@raise pysys.exceptions.ProcessError: Raised if an error occurred whilst trying to stop the process.
"""
raise Exception('Not implemented')
[docs] def signal(self, signal):
"""Send a signal to a running process.
Typically this uses ``os.kill`` to send the signal.
:param int signal: The integer signal to send to the process, e.g. ``process.signal(signal.SIGTERM)``.
@raise pysys.exceptions.ProcessError: Raised if an error occurred whilst trying to signal the process
"""
try:
os.kill(self.pid, signal)
except Exception:
raise ProcessError("Error sending signal %s to process %r"%(signal, self))
[docs] def write(self, data, addNewLine=True):
"""Write binary data to the stdin of the process.
Note that when the addNewLine argument is set to true, if a new line does not
terminate the input data string, a newline character will be added. If one
already exists a new line character will not be added. Should you explicitly
require to add data without the method appending a new line charater set
addNewLine to false.
:param data: The data to write to the process stdin.
As only binary data can be written to a process stdin,
if a character string rather than a byte object is passed as the data,
it will be automatically converted to a bytes object using the encoding
given by ``locale.getpreferredencoding()``.
:param addNewLine: True if a new line character is to be added to the end of
the data string
"""
if not self.running(): raise Exception('Cannot write to process stdin when it is not running')
if not data: return
if type(data) != binary_type:
data = data.encode(locale.getpreferredencoding())
if addNewLine and not data.endswith(b'\n'): data = data+b'\n'
if self._outQueue == None:
# start thread on demand
self._outQueue = Queue.Queue()
t = threading.Thread(target=self.writeStdin, name='pysys.stdinreader_%s'%str(self))
t.start()
self._outQueue.put(data)
[docs] def running(self):
"""Check to see if a process is running.
:return: True if the process is currently running, False if not.
"""
return self.setExitStatus() is None
[docs] def wait(self, timeout):
"""Wait for a process to complete execution, raising an exception on timeout.
This method provides basic functionality but does not check the exit status or log any messages;
see `pysys.basetest.BaseTest.waitProcess` for a wrapper that adds additional functionality.
Note that this method will not terminate the process if the timeout is exceeded.
:param timeout: The timeout to wait in seconds, for example ``timeout=TIMEOUTS['WaitForProcess']``.
:raise pysys.exceptions.ProcessTimeout: Raised if the timeout is exceeded.
"""
assert timeout > 0, 'timeout must always be specified'
startTime = time.time()
log.debug("Waiting up to %d secs for process %r", timeout, self)
while self.running():
currentTime = time.time()
if currentTime > startTime + timeout:
raise ProcessTimeout('Waiting for completion of %s timed out after %d seconds'%(self, int(timeout)))
time.sleep(0.05)
[docs] def start(self):
"""Start a process using the runtime parameters set at instantiation.
@raise pysys.exceptions.ProcessError: Raised if there is an error creating the process
@raise pysys.exceptions.ProcessTimeout: Raised in the process timed out (foreground process only)
"""
self._outQueue = None # always reset
if self.workingDir and not os.path.isdir(self.workingDir):
raise Exception('Cannot start process %s as workingDir "%s" does not exist'% (self, self.workingDir))
if self.state == FOREGROUND:
self.startBackgroundProcess()
self.wait(self.timeout)
else:
self.startBackgroundProcess()