diff --git a/docs/changelog/2335.bugfix.rst b/docs/changelog/2335.bugfix.rst new file mode 100644 index 000000000..205e17e08 --- /dev/null +++ b/docs/changelog/2335.bugfix.rst @@ -0,0 +1 @@ +Fix the incorrect operation when ``setuptools`` plugins output something into ``stdout``. diff --git a/src/virtualenv/discovery/cached_py_info.py b/src/virtualenv/discovery/cached_py_info.py index 4e1d976ff..7eb4161f7 100644 --- a/src/virtualenv/discovery/cached_py_info.py +++ b/src/virtualenv/discovery/cached_py_info.py @@ -8,8 +8,10 @@ import logging import os +import random import sys from collections import OrderedDict +from string import ascii_lowercase, ascii_uppercase, digits from virtualenv.app_data import AppDataDisabled from virtualenv.discovery.py_info import PythonInfo @@ -85,10 +87,27 @@ def _get_via_file_cache(cls, app_data, path, exe, env): return py_info +COOKIE_LENGTH = 32 # type: int + + +def gen_cookie(): + return "".join(random.choice("".join((ascii_lowercase, ascii_uppercase, digits))) for _ in range(COOKIE_LENGTH)) + + def _run_subprocess(cls, exe, app_data, env): py_info_script = Path(os.path.abspath(__file__)).parent / "py_info.py" + # Cookies allow to split the serialized stdout output generated by the script collecting the info from the output + # generated by something else. The right way to deal with it is to create an anonymous pipe and pass its descriptor + # to the child and output to it. But AFAIK all of them are either not cross-platform or too big to implement and are + # not in the stdlib. So the easiest and the shortest way I could mind is just using the cookies. + # We generate pseudorandom cookies because it easy to implement and avoids breakage from outputting modules source + # code, i.e. by debug output libraries. We reverse the cookies to avoid breakages resulting from variable values + # appearing in debug output. + + start_cookie = gen_cookie() + end_cookie = gen_cookie() with app_data.ensure_extracted(py_info_script) as py_info_script: - cmd = [exe, str(py_info_script)] + cmd = [exe, str(py_info_script), start_cookie, end_cookie] # prevent sys.prefix from leaking into the child process - see https://bugs.python.org/issue22490 env = env.copy() env.pop("__PYVENV_LAUNCHER__", None) @@ -108,6 +127,26 @@ def _run_subprocess(cls, exe, app_data, env): out, err, code = "", os_error.strerror, os_error.errno result, failure = None, None if code == 0: + out_starts = out.find(start_cookie[::-1]) + + if out_starts > -1: + pre_cookie = out[:out_starts] + + if pre_cookie: + sys.stdout.write(pre_cookie) + + out = out[out_starts + COOKIE_LENGTH :] + + out_ends = out.find(end_cookie[::-1]) + + if out_ends > -1: + post_cookie = out[out_ends + COOKIE_LENGTH :] + + if post_cookie: + sys.stdout.write(post_cookie) + + out = out[:out_ends] + result = cls._from_json(out) result.executable = exe # keep original executable as this may contain initialization code else: diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 144a1d1aa..09a3dc14d 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -524,4 +524,21 @@ def _possible_base(self): if __name__ == "__main__": # dump a JSON representation of the current python # noinspection PyProtectedMember - print(PythonInfo()._to_json()) + argv = sys.argv[1:] + + if len(argv) >= 1: + start_cookie = argv[0] + argv = argv[1:] + else: + start_cookie = "" + + if len(argv) >= 1: + end_cookie = argv[0] + argv = argv[1:] + else: + end_cookie = "" + + sys.argv = sys.argv[:1] + argv + + info = PythonInfo()._to_json() + sys.stdout.write("".join((start_cookie[::-1], info, end_cookie[::-1])))