Source code for glideinwms.lib.subprocessSupport
# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0
"""Fork a process and run a command
"""
import os
import shlex
import subprocess
from subprocess import CalledProcessError
from . import defaults
# CalledProcessError(self, returncode, cmd, output=None, stderr=None)
# Provides: cmd, returncode, stdout, stderr, output (same as stdout)
# __str__ of this class is not printing the stdout in the error message
[docs]
def iexe_cmd(cmd, useShell=False, stdin_data=None, child_env=None, text=True, encoding=None, timeout=None, log=None):
"""Fork a process and execute cmd
Using `process.communicate()` automatically handling buffers to avoid deadlocks.
Before it had been rewritten to use select to avoid filling up stderr and stdout queues.
The useShell value of True should be used sparingly. It allows for
executing commands that need access to shell features such as pipes,
filename wildcards. Refer to the python manual for more information on
this. When used, the 'cmd' string is not tokenized.
One possible improvement would be to add a function to accept
an array instead of a command string.
Args:
cmd (str): String containing the entire command including all arguments
useShell (bool): if True run the command in a shell (passed to Popen as shell)
stdin_data (str/bytes): Data that will be fed to the command via stdin. It should be bytes if text is False
and encoding is None, str otherwise
child_env (dict): Environment to be set before execution
text (bool): if False, then stdin_data and the return value are bytes instead of str (default: True)
encoding (str|None): encoding to use for the streams. If None (default) and text is True, then the
defaults.BINARY_ENCODING_DEFAULT (utf-8) encoding is used
timeout (None|int): timeout in seconds. No timeout by default
log (logger): optional logger for debug and error messages
Returns:
str/bytes: output of the command. It will be bytes if text is False,
str otherwise
Raises:
subprocess.CalledProcessError: if the subprocess fails (exit status not 0)
RuntimeError: if it fails to invoke the subprocess or the subprocess times out
"""
# TODO: use subprocess.run instead of Pipe
# could this be replaced directly by subprocess run throughout the program?
stdoutdata = stderrdata = ""
if not text:
stdoutdata = stderrdata = b""
else:
if encoding is None:
encoding = defaults.BINARY_ENCODING_DEFAULT
exit_status = 0
try:
# Add in parent process environment, make sure that env overrides parent
if child_env:
for k in os.environ:
if k not in child_env:
child_env[k] = os.environ[k]
# otherwise just use the parent environment
else:
child_env = os.environ
# Tokenize the commandline that should be executed.
if useShell:
command_list = [
"%s" % cmd,
]
else:
command_list = shlex.split(cmd)
# launch process - Converted to using the subprocess module
# when specifying an encoding the streams are text, bytes if encoding is None
process = subprocess.Popen(
command_list,
shell=useShell,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=child_env,
encoding=encoding,
)
if log is not None:
if encoding is None:
encoding = "bytes"
log.debug(f"Spawned subprocess {process.pid} ({encoding}, {timeout}) for {command_list}")
# GOTCHAS:
# 1) stdin should be buffered in memory.
# 2) Python docs suggest not to use communicate if the data size is
# large or unlimited. With large or unlimited stdout and stderr
# communicate at best starts trashing. So far testing for 1000000
# stdout/stderr lines are ok
# 3) Do not use communicate when you are dealing with multiple threads
# or processes at same time. It will serialize the process voiding
# any benefits from multiple processes
try:
stdoutdata, stderrdata = process.communicate(input=stdin_data, timeout=timeout)
except subprocess.TimeoutExpired as e:
process.kill()
stdoutdata, stderrdata = process.communicate()
err_str = "Timeout running '{}'\nStdout:{}\nStderr:{}\nException subprocess.TimeoutExpired:{}".format(
cmd,
stdoutdata,
stderrdata,
e,
)
if log is not None:
log.error(err_str)
raise RuntimeError(err_str)
exit_status = process.returncode
except OSError as e:
err_str = f"Error running '{cmd}'\nStdout:{stdoutdata}\nStderr:{stderrdata}\nException OSError:{e}"
if log is not None:
log.error(err_str)
raise RuntimeError(err_str) from e
if exit_status: # True if exit_status<>0
if log is not None:
log.warning(
f"Command '{cmd}' failed with exit code: {exit_status}\nStdout:{stdoutdata}\nStderr:{stderrdata}"
)
raise CalledProcessError(exit_status, cmd, output="".join(stdoutdata), stderr="".join(stderrdata))
return stdoutdata