From 9fe028435334a555f43a6d198a0174b4c4de299e Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Thu, 14 Apr 2022 08:06:10 -0600 Subject: Implement a new subprocess wrapper Uses subprocess.run and lets it parent do much more of the work, particularly the communicate-wait-output collection. Now returns the CompletedProcess instance that sp.run does. This routine should really live in SCons.Util, as it is not specifically tied to Action. Unfortunately, in trying to supply something sane in case env['ENV'] is not set up (can that still happen?) it calls SCons.Action.get_default_ENV which, if accessed from Util, gets us into the dreaded import loop problem. Signed-off-by: Mats Wichmann --- CHANGES.txt | 7 +++ SCons/Action.py | 103 ++++++++++++++++++++++++++++++++++++++++- SCons/ActionTests.py | 14 ++---- SCons/Environment.py | 24 ++++------ SCons/EnvironmentTests.py | 2 +- SCons/Platform/aix.py | 19 ++++---- SCons/Tool/MSCommon/common.py | 54 +++++++++------------ SCons/Tool/clang.py | 32 ++++++------- SCons/Tool/clangxx.py | 32 ++++++------- SCons/Tool/docbook/__init__.py | 1 - SCons/Tool/gcc.py | 35 ++++++-------- SCons/Tool/swig.py | 18 +++---- SCons/Tool/xgettext.py | 14 +++--- test/fixture/mygcc.py | 12 +++++ 14 files changed, 222 insertions(+), 145 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ee08ab7..2b19c3e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -96,6 +96,13 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER than source). Docs updated. Fixes #4326 and #4327. - Cleaned up dblite module (checker warnings, etc.). - Some cleanup in the FortranCommon tool. + - Rewrite the internal _subproc routine - a new scons_subproc_run() now + makes use of Python's subprocess.run in a more natural way, getting + around some of the issues with attempted context manager use, fetching + output, etc. - we let the subprocess module do the hard work, + since it's well debugged and supported. _subproc is no longer + called by internal code, but remains in place in case there are builds + which call to it (even though it was never "published API"). RELEASE 4.5.2 - Sun, 21 Mar 2023 14:08:29 -0700 diff --git a/SCons/Action.py b/SCons/Action.py index 8748985..110d415 100644 --- a/SCons/Action.py +++ b/SCons/Action.py @@ -824,13 +824,112 @@ def _resolve_shell_env(env, target, source): return ENV -def _subproc(scons_env, cmd, error: str='ignore', **kw): - """Wrapper for subprocess which pulls from construction env. +def scons_subproc_run( + scons_env, *args, error: str = None, **kwargs +) -> subprocess.CompletedProcess: + """Run a command with arguments using an SCons execution environment. + + Does an underlyng call to :func:`subprocess.run` to run a command and + returns a :class:`subprocess.CompletedProcess` instance with the results. + Use when an external command needs to run in an "SCons context" - + that is, with a crafted execution environment, rather than the user's + existing environment. A supplied :keyword:`env` keyword argument + is used to set up the environment to pass to the call; if omitted, + *scons_env* is used to derive a suitable default. Certain other + keyword arguments are examined before passing them through in the call. + The legacy :keyword:`error` keyword is remapped to :keyword:`check`. + The caller is responsible for setting up the appropriate keyword + arguments for :func:`subprocess.run`. + + A subset of interesting kerword arguments follows; see the Python + documentation of :mod:`subprocess` for a complete list. + + Keyword Arguments: + stdout: (and *stderr*, *stdin*) if set to :const:`subprocess.PIPE`. + send input to or collect output from the relevant stream in + the subprocess; the default ``None`` does no redirection + (i.e. output or errors may go to the console or log file, + but is not captured); if set to :const:`subprocess.DEVNULL` + they are explicitly thrown away. :keyword:`capture_output` is a + synonym for setting both :keyword:`stdout` and :keyword:`stderr` + to :const:`~subprocess.PIPE`. + text: open *stdin*, *stdout*, *stderr* in text mode. Default + is binary mode. :keyword:`universal_newlines` is a synonym - + note :keyword:`text` is not understood before Python 3.7. + encoding: specifies an encoding. Changes to text mode. + errors: specified error handling. Changes to text mode. + input: a byte sequence to be passed to *stdin*, unless text + mode is enabled, in which case it must be a string. + shell: if true, the command is executed through the shell. + check: if true and the subprocess exits with a non-zero exit + code, raise a :exc:`subprocess.CalledProcessError` exception. + Otherwise (the default) in case of an :exc:`OSError`, report the + exit code in the :class:`~Subprocess.CompletedProcess` instance. + + .. versionadded:: 4.5 + """ + # Figure out the execution environment to use + ENV = kwargs.get('env', None) + if ENV is None: + ENV = get_default_ENV(scons_env) + kwargs['env'] = SCons.Util.sanitize_shell_env(ENV) + + # backwards-compat with _subproc: accept 'error', map it to + # ``check`` and remove, since it would not be recognized by run() + check = kwargs.get('check') + if check and error: + raise ValueError('error and check arguments may not both be used.') + if check is None: + check = False # always supply some value for check + if error is not None: + if error == 'raise': + check = True + del kwargs['error'] + kwargs['check'] = check + + # Ensure that the ENV values are all strings: + new_env = {} + for key, value in ENV.items(): + if is_List(value): + # If the value is a list, then we assume it is a path list, + # because that's a pretty common list-like value to stick + # in an environment variable: + value = SCons.Util.flatten_sequence(value) + new_env[key] = os.pathsep.join(str(v) for v in value) + else: + # If it's not a list type, "convert" it to str. This is + # harmless if it's already a string, but gets us the right + # thing for Dir and File instances and will produce something + # reasonable for just about everything else: + new_env[key] = str(value) + kwargs['env'] = new_env + + # Some tools/tests expect no failures for things like missing files + # unless raise/check is set. If set, let subprocess.run go ahead and + # kill things, else catch and construct a dummy CompletedProcess instance. + if check: + cp = subprocess.run(*args, **kwargs) + else: + try: + cp = subprocess.run(*args, **kwargs) + except OSError as exc: + argline = ' '.join(*args) + cp = subprocess.CompletedProcess(argline, 1) + cp.stdout = "" + cp.stderr = "" + + return cp + + +def _subproc(scons_env, cmd, error='ignore', **kw): + """Wrapper for subprocess.Popen which pulls from construction env. Use for calls to subprocess which need to interpolate values from an SCons construction environment into the environment passed to subprocess. Adds an an error-handling argument. Adds ability to specify std{in,out,err} with "'devnull'" tag. + + .. deprecated:: 4.5 """ # TODO: just uses subprocess.DEVNULL now, we can drop the "devnull" # string now - it is a holdover from Py2, which didn't have DEVNULL. diff --git a/SCons/ActionTests.py b/SCons/ActionTests.py index 846762e..7239f4f 100644 --- a/SCons/ActionTests.py +++ b/SCons/ActionTests.py @@ -41,7 +41,7 @@ import os import sys import types import unittest -import subprocess +from subprocess import PIPE import SCons.Action import SCons.Environment @@ -2327,17 +2327,9 @@ class ObjectContentsTestCase(unittest.TestCase): self.assertEqual(c, expected[sys.version_info[:2]]) def test_uncaught_exception_bubbles(self): - """Test that _subproc bubbles uncaught exceptions""" - + """Test that scons_subproc_run bubbles uncaught exceptions""" try: - pobj = SCons.Action._subproc( - Environment(), - None, - stdin='devnull', - stderr='devnull', - stdout=subprocess.PIPE, - ) - pobj.wait() + cp = SCons.Action.scons_subproc_run(Environment(), None, stdout=PIPE) except EnvironmentError: pass except Exception: diff --git a/SCons/Environment.py b/SCons/Environment.py index 4e5601e..d29f874 100644 --- a/SCons/Environment.py +++ b/SCons/Environment.py @@ -36,6 +36,7 @@ import sys import re import shlex from collections import UserDict, deque +from subprocess import PIPE, DEVNULL import SCons.Action import SCons.Builder @@ -770,14 +771,11 @@ class SubstitutionEnvironment: Raises: OSError: if the external command returned non-zero exit status. """ - - import subprocess - # common arguments kw = { - "stdin": "devnull", - "stdout": subprocess.PIPE, - "stderr": subprocess.PIPE, + "stdin": DEVNULL, + "stdout": PIPE, + "stderr": PIPE, "universal_newlines": True, } # if the command is a list, assume it's been quoted @@ -785,14 +783,12 @@ class SubstitutionEnvironment: if not is_List(command): kw["shell"] = True # run constructed command - p = SCons.Action._subproc(self, command, **kw) - out, err = p.communicate() - status = p.wait() - if err: - sys.stderr.write("" + err) - if status: - raise OSError("'%s' exited %d" % (command, status)) - return out + cp = SCons.Action.scons_subproc_run(self, command, **kw) + if cp.stderr: + sys.stderr.write(cp.stderr) + if cp.returncode: + raise OSError(f'{command!r} exited {cp.returncode}') + return cp.stdout def AddMethod(self, function, name=None) -> None: diff --git a/SCons/EnvironmentTests.py b/SCons/EnvironmentTests.py index e7cb99f..2f96679 100644 --- a/SCons/EnvironmentTests.py +++ b/SCons/EnvironmentTests.py @@ -649,7 +649,7 @@ sys.exit(0) try: env.backtick(cmd) except OSError as e: - assert str(e) == "'%s' exited 1" % cmd, str(e) + assert str(e) == f'{cmd!r} exited 1', str(e) else: self.fail("did not catch expected OSError") diff --git a/SCons/Platform/aix.py b/SCons/Platform/aix.py index 3afe506..1136226 100644 --- a/SCons/Platform/aix.py +++ b/SCons/Platform/aix.py @@ -28,7 +28,7 @@ will usually be imported through the generic SCons.Platform.Platform() selection method. """ -import subprocess +from subprocess import PIPE from . import posix @@ -47,22 +47,21 @@ def get_xlc(env, xlc=None, packages=[]): xlc = xlc[0] for package in packages: # find the installed filename, which may be a symlink as well - pipe = SCons.Action._subproc(env, ['lslpp', '-fc', package], - stdin = 'devnull', - stderr = 'devnull', - universal_newlines=True, - stdout = subprocess.PIPE) + cp = SCons.Action.scons_subproc_run( + env, ['lslpp', '-fc', package], universal_newlines=True, stdout=PIPE + ) # output of lslpp is something like this: # #Path:Fileset:File # /usr/lib/objrepos:vac.C 6.0.0.0:/usr/vac/exe/xlCcpp # /usr/lib/objrepos:vac.C 6.0.0.0:/usr/vac/bin/xlc_r -> /usr/vac/bin/xlc - for line in pipe.stdout: + for line in cp.stdout.splitlines(): if xlcPath: - continue # read everything to let lslpp terminate + continue # read everything to let lslpp terminate fileset, filename = line.split(':')[1:3] filename = filename.split()[0] - if ('/' in xlc and filename == xlc) \ - or ('/' not in xlc and filename.endswith('/' + xlc)): + if ('/' in xlc and filename == xlc) or ( + '/' not in xlc and filename.endswith('/' + xlc) + ): xlcVersion = fileset.split()[1] xlcPath, sep, xlc = filename.rpartition('/') return (xlcPath, xlc, xlcVersion) diff --git a/SCons/Tool/MSCommon/common.py b/SCons/Tool/MSCommon/common.py index 0cadc10..aa4b356 100644 --- a/SCons/Tool/MSCommon/common.py +++ b/SCons/Tool/MSCommon/common.py @@ -297,49 +297,39 @@ def get_output(vcbat, args=None, env=None): if args: debug("Calling '%s %s'", vcbat, args) - popen = SCons.Action._subproc(env, - '"%s" %s & set' % (vcbat, args), - stdin='devnull', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + cp = SCons.Action.scons_subproc_run( + env, + '"%s" %s & set' % (vcbat, args), + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) else: debug("Calling '%s'", vcbat) - popen = SCons.Action._subproc(env, - '"%s" & set' % vcbat, - stdin='devnull', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - # Use the .stdout and .stderr attributes directly because the - # .communicate() method uses the threading module on Windows - # and won't work under Pythons not built with threading. - with popen.stdout: - stdout = popen.stdout.read() - with popen.stderr: - stderr = popen.stderr.read() + cp = SCons.Action.scons_subproc_run( + env, + '"%s" & set' % vcbat, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) # Extra debug logic, uncomment if necessary - # debug('stdout:%s', stdout) - # debug('stderr:%s', stderr) + # debug('stdout:%s', cp.stdout) + # debug('stderr:%s', cp.stderr) # Ongoing problems getting non-corrupted text led to this # changing to "oem" from "mbcs" - the scripts run presumably # attached to a console, so some particular rules apply. - # Unfortunately, "oem" not defined in Python 3.5, so get another way - if sys.version_info.major == 3 and sys.version_info.minor < 6: - from ctypes import windll - - OEM = "cp{}".format(windll.kernel32.GetConsoleOutputCP()) - else: - OEM = "oem" - if stderr: + OEM = "oem" + if cp.stderr: # TODO: find something better to do with stderr; # this at least prevents errors from getting swallowed. - sys.stderr.write(stderr.decode(OEM)) - if popen.wait() != 0: - raise IOError(stderr.decode(OEM)) + sys.stderr.write(cp.stderr.decode(OEM)) + if cp.returncode != 0: + raise IOError(cp.stderr.decode(OEM)) - return stdout.decode(OEM) + return cp.stdout.decode(OEM) KEEPLIST = ( diff --git a/SCons/Tool/clang.py b/SCons/Tool/clang.py index 1cef0ad..3b9488f 100644 --- a/SCons/Tool/clang.py +++ b/SCons/Tool/clang.py @@ -33,7 +33,7 @@ selection method. import os import re -import subprocess +from subprocess import DEVNULL, PIPE import SCons.Util import SCons.Tool.cc @@ -50,11 +50,12 @@ def generate(env) -> None: if env['PLATFORM'] == 'win32': # Ensure that we have a proper path for clang - clang = SCons.Tool.find_program_path(env, compilers[0], - default_paths=get_clang_install_dirs(env['PLATFORM'])) + clang = SCons.Tool.find_program_path( + env, compilers[0], default_paths=get_clang_install_dirs(env['PLATFORM']) + ) if clang: clang_bin_dir = os.path.dirname(clang) - env.AppendENVPath('PATH', clang_bin_dir) + env.AppendENVPath("PATH", clang_bin_dir) # Set-up ms tools paths msvc_setup_env_once(env) @@ -67,24 +68,19 @@ def generate(env) -> None: # determine compiler version if env['CC']: - # pipe = SCons.Action._subproc(env, [env['CC'], '-dumpversion'], - pipe = SCons.Action._subproc(env, [env['CC'], '--version'], - stdin='devnull', - stderr='devnull', - stdout=subprocess.PIPE) - if pipe.wait() != 0: return - # clang -dumpversion is of no use - with pipe.stdout: - line = pipe.stdout.readline() - line = line.decode() - match = re.search(r'clang +version +([0-9]+(?:\.[0-9]+)+)', line) - if match: - env['CCVERSION'] = match.group(1) + kw = { + 'stdout': PIPE, + 'stderr': DEVNULL, + 'universal_newlines': True, + } + cp = SCons.Action.scons_subproc_run(env, [env['CC'], '-dumpversion'], **kw) + line = cp.stdout + if line: + env['CCVERSION'] = line env['CCDEPFLAGS'] = '-MMD -MF ${TARGET}.d' env["NINJA_DEPFILE_PARSE_FORMAT"] = 'clang' - def exists(env): return env.Detect(compilers) diff --git a/SCons/Tool/clangxx.py b/SCons/Tool/clangxx.py index 5f8202c..9659fe4 100644 --- a/SCons/Tool/clangxx.py +++ b/SCons/Tool/clangxx.py @@ -33,7 +33,7 @@ selection method. import os.path import re -import subprocess +from subprocess import DEVNULL, PIPE import SCons.Tool import SCons.Util @@ -50,7 +50,7 @@ def generate(env) -> None: SCons.Tool.cxx.generate(env) - env['CXX'] = env.Detect(compilers) or 'clang++' + env['CXX'] = env.Detect(compilers) or 'clang++' # platform specific settings if env['PLATFORM'] == 'aix': @@ -63,7 +63,9 @@ def generate(env) -> None: env['SHOBJSUFFIX'] = '.pic.o' elif env['PLATFORM'] == 'win32': # Ensure that we have a proper path for clang++ - clangxx = SCons.Tool.find_program_path(env, compilers[0], default_paths=get_clang_install_dirs(env['PLATFORM'])) + clangxx = SCons.Tool.find_program_path( + env, compilers[0], default_paths=get_clang_install_dirs(env['PLATFORM']) + ) if clangxx: clangxx_bin_dir = os.path.dirname(clangxx) env.AppendENVPath('PATH', clangxx_bin_dir) @@ -71,23 +73,17 @@ def generate(env) -> None: # Set-up ms tools paths msvc_setup_env_once(env) - # determine compiler version if env['CXX']: - pipe = SCons.Action._subproc(env, [env['CXX'], '--version'], - stdin='devnull', - stderr='devnull', - stdout=subprocess.PIPE) - if pipe.wait() != 0: - return - - # clang -dumpversion is of no use - with pipe.stdout: - line = pipe.stdout.readline() - line = line.decode() - match = re.search(r'clang +version +([0-9]+(?:\.[0-9]+)+)', line) - if match: - env['CXXVERSION'] = match.group(1) + kw = { + 'stdout': PIPE, + 'stderr': DEVNULL, + 'universal_newlines': True, + } + cp = SCons.Action.scons_subproc_run(env, [env['CXX'], '-dumpversion'], **kw) + line = cp.stdout + if line: + env['CXXVERSION'] = line env['CCDEPFLAGS'] = '-MMD -MF ${TARGET}.d' env["NINJA_DEPFILE_PARSE_FORMAT"] = 'clang' diff --git a/SCons/Tool/docbook/__init__.py b/SCons/Tool/docbook/__init__.py index 54f1883..5403b9f 100644 --- a/SCons/Tool/docbook/__init__.py +++ b/SCons/Tool/docbook/__init__.py @@ -230,7 +230,6 @@ def __xml_scan(node, env, path, arg): # Try to call xsltproc xsltproc = env.subst("$DOCBOOK_XSLTPROC") if xsltproc and xsltproc.endswith('xsltproc'): - # TODO: switch to _subproc or subprocess.run call result = env.backtick(' '.join([xsltproc, xsl_file, str(node)])) depfiles = [x.strip() for x in str(result).splitlines() if x.strip() != "" and not x.startswith(" is executable, and is a GNU compiler (or accepts '--version' at least) + # So to pretend to be gcc, we need to recognize. Parrot what + # test/CC/CCVERSION-fixture/versioned.py does. + if '-dumpversion' in sys.argv: + print('3.9.9') + sys.exit(0) + if '--version' in sys.argv: + print('this is version 2.9.9 with extra text') + sys.exit(0) + compiler = sys.argv[1].encode('utf-8') opts, args = getopt.getopt(sys.argv[2:], 'co:xf:K:') for opt, arg in opts: -- cgit v0.12 From 775a47aecbec0de4e4cdb739501abda8fb492718 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Mon, 13 Feb 2023 13:04:43 -0700 Subject: Tweak scons_subproc_run docstring a bit more [ci skip] Signed-off-by: Mats Wichmann --- SCons/Action.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/SCons/Action.py b/SCons/Action.py index 110d415..2e6ae16 100644 --- a/SCons/Action.py +++ b/SCons/Action.py @@ -833,15 +833,23 @@ def scons_subproc_run( returns a :class:`subprocess.CompletedProcess` instance with the results. Use when an external command needs to run in an "SCons context" - that is, with a crafted execution environment, rather than the user's - existing environment. A supplied :keyword:`env` keyword argument - is used to set up the environment to pass to the call; if omitted, - *scons_env* is used to derive a suitable default. Certain other - keyword arguments are examined before passing them through in the call. - The legacy :keyword:`error` keyword is remapped to :keyword:`check`. - The caller is responsible for setting up the appropriate keyword - arguments for :func:`subprocess.run`. - - A subset of interesting kerword arguments follows; see the Python + existing environment, particularly when you need to collect output + back. Typical case: run a tool's "version" option to find out which + version you have installed. + + If supplied, the :keyword:`env` keyword argument passes an + execution environment to process into appropriate form before it is + supplied to :mod:`subprocess`; if omitted, *scons_env* is used to derive + a suitable default. The other keyword arguments are passed through, + except that SCons legacy :keyword:`error` keyword is remapped to + the subprocess :keyword:`check` keyword. The caller is responsible for + setting up the desired arguments for :func:`subprocess.run`. + + This function retains the legacy behavior of returning something + vaguely usable even in the face of complete failure: it synthesizes + a :class:`~subprocess.CompletedProcess` instance in this case. + + A subset of interesting keyword arguments follows; see the Python documentation of :mod:`subprocess` for a complete list. Keyword Arguments: @@ -880,7 +888,7 @@ def scons_subproc_run( if check and error: raise ValueError('error and check arguments may not both be used.') if check is None: - check = False # always supply some value for check + check = False # always supply some value, to pacify checkers (pylint). if error is not None: if error == 'raise': check = True -- cgit v0.12 From 2f7e50233e7a1cbe85f290ecebb30cefaf74804f Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Sat, 8 Jul 2023 13:58:59 -0600 Subject: New scons_subproc_run: simplify calling in msvc Per a suggestion, do the conditional bit in MSCommon/common.py only for the past that can change, so we can have a single scons_subproc_run call - more readable. Since this didn't make 4.5, change added/deprecated markers to 4.6 Signed-off-by: Mats Wichmann --- SCons/Action.py | 4 ++-- SCons/Tool/MSCommon/common.py | 22 +++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/SCons/Action.py b/SCons/Action.py index 2e6ae16..187e929 100644 --- a/SCons/Action.py +++ b/SCons/Action.py @@ -874,7 +874,7 @@ def scons_subproc_run( Otherwise (the default) in case of an :exc:`OSError`, report the exit code in the :class:`~Subprocess.CompletedProcess` instance. - .. versionadded:: 4.5 + .. versionadded:: 4.6 """ # Figure out the execution environment to use ENV = kwargs.get('env', None) @@ -937,7 +937,7 @@ def _subproc(scons_env, cmd, error='ignore', **kw): subprocess. Adds an an error-handling argument. Adds ability to specify std{in,out,err} with "'devnull'" tag. - .. deprecated:: 4.5 + .. deprecated:: 4.6 """ # TODO: just uses subprocess.DEVNULL now, we can drop the "devnull" # string now - it is a holdover from Py2, which didn't have DEVNULL. diff --git a/SCons/Tool/MSCommon/common.py b/SCons/Tool/MSCommon/common.py index aa4b356..a3daff6 100644 --- a/SCons/Tool/MSCommon/common.py +++ b/SCons/Tool/MSCommon/common.py @@ -29,9 +29,9 @@ import copy import json import os import re -import subprocess import sys from contextlib import suppress +from subprocess import DEVNULL, PIPE from pathlib import Path import SCons.Util @@ -297,22 +297,14 @@ def get_output(vcbat, args=None, env=None): if args: debug("Calling '%s %s'", vcbat, args) - cp = SCons.Action.scons_subproc_run( - env, - '"%s" %s & set' % (vcbat, args), - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) + cmd_str = '"%s" %s & set' % (vcbat, args) else: debug("Calling '%s'", vcbat) - cp = SCons.Action.scons_subproc_run( - env, - '"%s" & set' % vcbat, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) + cmd_str = '"%s" & set' % vcbat + + cp = SCons.Action.scons_subproc_run( + env, cmd_str, stdin=DEVNULL, stdout=PIPE, stderr=PIPE, + ) # Extra debug logic, uncomment if necessary # debug('stdout:%s', cp.stdout) -- cgit v0.12