From 891ef3eefdf330a2f7fecffc6ac9aba23385a249 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 9 Jan 2021 18:19:04 -0500 Subject: [PATCH 01/62] news fragment --- newsfragments/3588.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3588.minor diff --git a/newsfragments/3588.minor b/newsfragments/3588.minor new file mode 100644 index 000000000..e69de29bb From d78e72595a044a426f07d5b06b4410b89a40b3c9 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 9 Jan 2021 18:19:09 -0500 Subject: [PATCH 02/62] Use SetErrorMode and related constants from pywin32 --- src/allmydata/windows/fixups.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index e7f045b95..a7552b377 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -11,19 +11,19 @@ def initialize(): import codecs, re from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error - from ctypes.wintypes import BOOL, HANDLE, DWORD, UINT, LPWSTR, LPCWSTR, LPVOID + from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID from allmydata.util import log from allmydata.util.encodingutil import canonical_encoding # - SetErrorMode = WINFUNCTYPE( - UINT, UINT, - use_last_error=True - )(("SetErrorMode", windll.kernel32)) - - SEM_FAILCRITICALERRORS = 0x0001 - SEM_NOOPENFILEERRORBOX = 0x8000 + from win32api import ( + SetErrorMode, + ) + from win32con import ( + SEM_FAILCRITICALERRORS, + SEM_NOOPENFILEERRORBOX, + ) SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) From e80bd6894ff823d17d22adb77cc08e05df036913 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 10:39:17 -0500 Subject: [PATCH 03/62] Take a first attempt at testing the argv logic directly --- src/allmydata/test/test_windows.py | 117 +++++++++++++++++++++++++++++ src/allmydata/windows/fixups.py | 63 ++++++++++------ 2 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 src/allmydata/test/test_windows.py diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py new file mode 100644 index 000000000..0eb4de568 --- /dev/null +++ b/src/allmydata/test/test_windows.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Tahoe-LAFS -- secure, distributed storage grid +# +# Copyright © 2020 The Tahoe-LAFS Software Foundation +# +# This file is part of Tahoe-LAFS. +# +# See the docs/about.rst file for licensing information. + +""" +Tests for the ``allmydata.windows``. +""" + +from __future__ import division +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + +from sys import ( + executable, +) +from json import ( + load, +) + +from twisted.python.filepath import ( + FilePath, +) +from twisted.python.runtime import ( + platform, +) + +from testtools import ( + skipUnless, +) + +from testtools.matchers import ( + MatchesAll, + AllMatch, + IsInstance, + Equals, +) + +from hypothesis import ( + given, +) + +from hypothesis.strategies import ( + lists, + text, +) + +from subprocess import ( + check_call, +) + +from .common import ( + SyncTestCase, +) + +from ..windows.fixups import ( + get_argv, +) + +@skipUnless(platform.isWindows()) +class GetArgvTests(SyncTestCase): + """ + Tests for ``get_argv``. + """ + def test_get_argv_return_type(self): + """ + ``get_argv`` returns a list of unicode strings + """ + # We don't know what this process's command line was so we just make + # structural assertions here. + argv = get_argv() + self.assertThat( + argv, + MatchesAll([ + IsInstance(list), + AllMatch(IsInstance(str)), + ]), + ) + + @given(lists(text(max_size=4), max_size=4)) + def test_argv_values(self, argv): + """ + ``get_argv`` returns a list representing the result of tokenizing the + "command line" argument string provided to Windows processes. + """ + save_argv = FilePath(self.mktemp()) + saved_argv_path = FilePath(self.mktemp()) + with open(save_argv.path, "wt") as f: + f.write( + """ + import sys + import json + with open({!r}, "wt") as f: + f.write(json.dumps(sys.argv)) + """.format(saved_argv_path.path), + ) + check_call([ + executable, + save_argv, + ] + argv) + + with open(saved_argv_path, "rt") as f: + saved_argv = load(f) + + self.assertThat( + argv, + Equals(saved_argv), + ) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index a7552b377..2cdb1ad93 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -2,6 +2,43 @@ from __future__ import print_function done = False +def get_argv(): + """ + :return [unicode]: The argument list this process was invoked with, as + unicode. + + Python 2 does not do a good job exposing this information in + ``sys.argv`` on Windows so this code re-retrieves the underlying + information using Windows API calls and massages it into the right + shape. + """ + # + from win32ui import ( + GetCommandLine, + ) + + from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error + from ctypes.wintypes import LPWSTR, LPCWSTR + + # + CommandLineToArgvW = WINFUNCTYPE( + POINTER(LPWSTR), LPCWSTR, POINTER(c_int), + use_last_error=True + )(("CommandLineToArgvW", windll.shell32)) + + argc = c_int(0) + argv_unicode = CommandLineToArgvW(GetCommandLine(), byref(argc)) + if argv_unicode is None: + raise WinError(get_last_error()) + + # Convert it to a normal Python list + return list( + argv_unicode[i] + for i + in range(argc.value) + ) + + def initialize(): global done import sys @@ -10,8 +47,8 @@ def initialize(): done = True import codecs, re - from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error - from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID + from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, get_last_error + from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPVOID from allmydata.util import log from allmydata.util.encodingutil import canonical_encoding @@ -195,23 +232,6 @@ def initialize(): # This works around . - # - GetCommandLineW = WINFUNCTYPE( - LPWSTR, - use_last_error=True - )(("GetCommandLineW", windll.kernel32)) - - # - CommandLineToArgvW = WINFUNCTYPE( - POINTER(LPWSTR), LPCWSTR, POINTER(c_int), - use_last_error=True - )(("CommandLineToArgvW", windll.shell32)) - - argc = c_int(0) - argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) - if argv_unicode is None: - raise WinError(get_last_error()) - # Because of (and similar limitations in # twisted), the 'bin/tahoe' script cannot invoke us with the actual Unicode arguments. # Instead it "mangles" or escapes them using \x7F as an escape character, which we @@ -219,11 +239,12 @@ def initialize(): def unmangle(s): return re.sub(u'\\x7F[0-9a-fA-F]*\\;', lambda m: unichr(int(m.group(0)[1:-1], 16)), s) + argv_unicode = get_argv() try: - argv = [unmangle(argv_unicode[i]).encode('utf-8') for i in xrange(0, argc.value)] + argv = [unmangle(argv_u).encode('utf-8') for argv_u in argv_unicode] except Exception as e: _complain("%s: could not unmangle Unicode arguments.\n%r" - % (sys.argv[0], [argv_unicode[i] for i in xrange(0, argc.value)])) + % (sys.argv[0], argv_unicode)) raise # Take only the suffix with the same number of arguments as sys.argv. From 24f3d74fdf55f4ae4e413e739bd1f63303520910 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 10:48:40 -0500 Subject: [PATCH 04/62] Fix the skip --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 0eb4de568..c1c61696c 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -66,7 +66,7 @@ from ..windows.fixups import ( get_argv, ) -@skipUnless(platform.isWindows()) +@skipUnless(platform.isWindows(), "get_argv is Windows-only") class GetArgvTests(SyncTestCase): """ Tests for ``get_argv``. From 6b621efef27bf73ef763bc49c7e91ebc5e82cb73 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 10:48:49 -0500 Subject: [PATCH 05/62] Turns out there is also CommandLineToArgv just not CommandLineToArgvW, but that's fine. --- src/allmydata/windows/fixups.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 2cdb1ad93..9fb81bdff 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -13,30 +13,12 @@ def get_argv(): shape. """ # - from win32ui import ( + from win32api import ( GetCommandLine, + CommandLineToArgv, ) + return CommandLineToArgv(GetCommandLine()) - from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error - from ctypes.wintypes import LPWSTR, LPCWSTR - - # - CommandLineToArgvW = WINFUNCTYPE( - POINTER(LPWSTR), LPCWSTR, POINTER(c_int), - use_last_error=True - )(("CommandLineToArgvW", windll.shell32)) - - argc = c_int(0) - argv_unicode = CommandLineToArgvW(GetCommandLine(), byref(argc)) - if argv_unicode is None: - raise WinError(get_last_error()) - - # Convert it to a normal Python list - return list( - argv_unicode[i] - for i - in range(argc.value) - ) def initialize(): From b3a6f25c1c486f02e22373b1cc53df57fc2d5c2b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 11:01:30 -0500 Subject: [PATCH 06/62] Python 2 gets an old version with no CommandLineToArgv Thanks. --- src/allmydata/windows/fixups.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 9fb81bdff..2cdb1ad93 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -13,12 +13,30 @@ def get_argv(): shape. """ # - from win32api import ( + from win32ui import ( GetCommandLine, - CommandLineToArgv, ) - return CommandLineToArgv(GetCommandLine()) + from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error + from ctypes.wintypes import LPWSTR, LPCWSTR + + # + CommandLineToArgvW = WINFUNCTYPE( + POINTER(LPWSTR), LPCWSTR, POINTER(c_int), + use_last_error=True + )(("CommandLineToArgvW", windll.shell32)) + + argc = c_int(0) + argv_unicode = CommandLineToArgvW(GetCommandLine(), byref(argc)) + if argv_unicode is None: + raise WinError(get_last_error()) + + # Convert it to a normal Python list + return list( + argv_unicode[i] + for i + in range(argc.value) + ) def initialize(): From a73668a056832c588345be4a9ab832130b5dbf5f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 11:21:13 -0500 Subject: [PATCH 07/62] this doesn't take a list --- src/allmydata/test/test_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index c1c61696c..21932ac48 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -80,10 +80,10 @@ class GetArgvTests(SyncTestCase): argv = get_argv() self.assertThat( argv, - MatchesAll([ + MatchesAll( IsInstance(list), AllMatch(IsInstance(str)), - ]), + ), ) @given(lists(text(max_size=4), max_size=4)) From b02b930eed079d58ef949ecd758db874652ab859 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 11:22:00 -0500 Subject: [PATCH 08/62] do better with paths --- src/allmydata/test/test_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 21932ac48..cd57df690 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -105,10 +105,10 @@ class GetArgvTests(SyncTestCase): ) check_call([ executable, - save_argv, + save_argv.path, ] + argv) - with open(saved_argv_path, "rt") as f: + with open(saved_argv_path.path, "rt") as f: saved_argv = load(f) self.assertThat( From 6091ca2164c299641adcdb86971cef310dcf8957 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 11:23:19 -0500 Subject: [PATCH 09/62] try to get the child source right --- src/allmydata/test/test_windows.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index cd57df690..e13fa9b16 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -26,7 +26,9 @@ from sys import ( from json import ( load, ) - +from textwrap import ( + dedent, +) from twisted.python.filepath import ( FilePath, ) @@ -95,13 +97,13 @@ class GetArgvTests(SyncTestCase): save_argv = FilePath(self.mktemp()) saved_argv_path = FilePath(self.mktemp()) with open(save_argv.path, "wt") as f: - f.write( + f.write(dedent( """ import sys import json with open({!r}, "wt") as f: f.write(json.dumps(sys.argv)) - """.format(saved_argv_path.path), + """.format(saved_argv_path.path)), ) check_call([ executable, From e64a4c64269263b16bfc7048c8f9b6a19d5c30a9 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 19:59:22 -0500 Subject: [PATCH 10/62] Attempt to use a parent-side API that supports unicode properly --- src/allmydata/test/_win_subprocess.py | 159 ++++++++++++++++++++++++++ src/allmydata/test/test_windows.py | 23 ++-- 2 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 src/allmydata/test/_win_subprocess.py diff --git a/src/allmydata/test/_win_subprocess.py b/src/allmydata/test/_win_subprocess.py new file mode 100644 index 000000000..cc66f7552 --- /dev/null +++ b/src/allmydata/test/_win_subprocess.py @@ -0,0 +1,159 @@ +## issue: https://bugs.python.org/issue19264 + +import os +import ctypes +import subprocess +import _subprocess +from ctypes import byref, windll, c_char_p, c_wchar_p, c_void_p, \ + Structure, sizeof, c_wchar, WinError +from ctypes.wintypes import BYTE, WORD, LPWSTR, BOOL, DWORD, LPVOID, \ + HANDLE + + +## +## Types +## + +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +LPCTSTR = c_char_p +LPTSTR = c_wchar_p +LPSECURITY_ATTRIBUTES = c_void_p +LPBYTE = ctypes.POINTER(BYTE) + +class STARTUPINFOW(Structure): + _fields_ = [ + ("cb", DWORD), ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), ("lpTitle", LPWSTR), + ("dwX", DWORD), ("dwY", DWORD), + ("dwXSize", DWORD), ("dwYSize", DWORD), + ("dwXCountChars", DWORD), ("dwYCountChars", DWORD), + ("dwFillAtrribute", DWORD), ("dwFlags", DWORD), + ("wShowWindow", WORD), ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), ("hStdError", HANDLE), + ] + +LPSTARTUPINFOW = ctypes.POINTER(STARTUPINFOW) + + +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ("hProcess", HANDLE), ("hThread", HANDLE), + ("dwProcessId", DWORD), ("dwThreadId", DWORD), + ] + +LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION) + + +class DUMMY_HANDLE(ctypes.c_void_p): + + def __init__(self, *a, **kw): + super(DUMMY_HANDLE, self).__init__(*a, **kw) + self.closed = False + + def Close(self): + if not self.closed: + windll.kernel32.CloseHandle(self) + self.closed = True + + def __int__(self): + return self.value + + +CreateProcessW = windll.kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR, + LPSTARTUPINFOW, LPPROCESS_INFORMATION, +] +CreateProcessW.restype = BOOL + + +## +## Patched functions/classes +## + +def CreateProcess(executable, args, _p_attr, _t_attr, + inherit_handles, creation_flags, env, cwd, + startup_info): + """Create a process supporting unicode executable and args for win32 + + Python implementation of CreateProcess using CreateProcessW for Win32 + + """ + + si = STARTUPINFOW( + dwFlags=startup_info.dwFlags, + wShowWindow=startup_info.wShowWindow, + cb=sizeof(STARTUPINFOW), + ## XXXvlab: not sure of the casting here to ints. + hStdInput=int(startup_info.hStdInput), + hStdOutput=int(startup_info.hStdOutput), + hStdError=int(startup_info.hStdError), + ) + + wenv = None + if env is not None: + ## LPCWSTR seems to be c_wchar_p, so let's say CWSTR is c_wchar + env = (unicode("").join([ + unicode("%s=%s\0") % (k, v) + for k, v in env.items()])) + unicode("\0") + wenv = (c_wchar * len(env))() + wenv.value = env + + pi = PROCESS_INFORMATION() + creation_flags |= CREATE_UNICODE_ENVIRONMENT + + if CreateProcessW(executable, args, None, None, + inherit_handles, creation_flags, + wenv, cwd, byref(si), byref(pi)): + return (DUMMY_HANDLE(pi.hProcess), DUMMY_HANDLE(pi.hThread), + pi.dwProcessId, pi.dwThreadId) + raise WinError() + + +class Popen(subprocess.Popen): + """This superseeds Popen and corrects a bug in cPython 2.7 implem""" + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, to_close, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Code from part of _execute_child from Python 2.7 (9fbb65e) + + There are only 2 little changes concerning the construction of + the the final string in shell mode: we preempt the creation of + the command string when shell is True, because original function + will try to encode unicode args which we want to avoid to be able to + sending it as-is to ``CreateProcess``. + + """ + if not isinstance(args, subprocess.types.StringTypes): + args = subprocess.list2cmdline(args) + + if startupinfo is None: + startupinfo = subprocess.STARTUPINFO() + if shell: + startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = _subprocess.SW_HIDE + comspec = os.environ.get("COMSPEC", unicode("cmd.exe")) + args = unicode('{} /c "{}"').format(comspec, args) + if (_subprocess.GetVersion() >= 0x80000000 or + os.path.basename(comspec).lower() == "command.com"): + w9xpopen = self._find_w9xpopen() + args = unicode('"%s" %s') % (w9xpopen, args) + creationflags |= _subprocess.CREATE_NEW_CONSOLE + + cp = _subprocess.CreateProcess + _subprocess.CreateProcess = CreateProcess + try: + super(Popen, self)._execute_child( + args, executable, + preexec_fn, close_fds, cwd, env, universal_newlines, + startupinfo, creationflags, False, to_close, p2cread, + p2cwrite, c2pread, c2pwrite, errread, errwrite, + ) + finally: + _subprocess.CreateProcess = cp diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index e13fa9b16..bb9aa96bf 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -56,10 +56,6 @@ from hypothesis.strategies import ( text, ) -from subprocess import ( - check_call, -) - from .common import ( SyncTestCase, ) @@ -97,6 +93,10 @@ class GetArgvTests(SyncTestCase): save_argv = FilePath(self.mktemp()) saved_argv_path = FilePath(self.mktemp()) with open(save_argv.path, "wt") as f: + # A simple program to save argv to a file. Using the file saves + # us having to figure out how to reliably get non-ASCII back over + # stdio which may pose an independent set of challenges. At least + # file I/O is relatively simple and well-understood. f.write(dedent( """ import sys @@ -105,11 +105,16 @@ class GetArgvTests(SyncTestCase): f.write(json.dumps(sys.argv)) """.format(saved_argv_path.path)), ) - check_call([ - executable, - save_argv.path, - ] + argv) - + # Python 2.7 doesn't have good options for launching a process with + # non-ASCII in its command line. + from ._win_subprocess import ( + Popen + ) + returncode = Popen([executable, save_argv] + argv).wait() + self.assertThat( + 0, + Equals(returncode), + ) with open(saved_argv_path.path, "rt") as f: saved_argv = load(f) From a21b66e775d4767fbac3272f48ba243e2e13ecdc Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:06:07 -0500 Subject: [PATCH 11/62] FilePath again --- src/allmydata/test/test_windows.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index bb9aa96bf..b0231987d 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -90,9 +90,9 @@ class GetArgvTests(SyncTestCase): ``get_argv`` returns a list representing the result of tokenizing the "command line" argument string provided to Windows processes. """ - save_argv = FilePath(self.mktemp()) + save_argv_path = FilePath(self.mktemp()) saved_argv_path = FilePath(self.mktemp()) - with open(save_argv.path, "wt") as f: + with open(save_argv_path.path, "wt") as f: # A simple program to save argv to a file. Using the file saves # us having to figure out how to reliably get non-ASCII back over # stdio which may pose an independent set of challenges. At least @@ -110,7 +110,7 @@ class GetArgvTests(SyncTestCase): from ._win_subprocess import ( Popen ) - returncode = Popen([executable, save_argv] + argv).wait() + returncode = Popen([executable, save_argv_path.path] + argv).wait() self.assertThat( 0, Equals(returncode), From 18de71666f212788c20f1ef673b259a014118f18 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:10:34 -0500 Subject: [PATCH 12/62] try to work-around bugs in the Popen hotfix --- src/allmydata/test/test_windows.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index b0231987d..ae62af857 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -110,7 +110,10 @@ class GetArgvTests(SyncTestCase): from ._win_subprocess import ( Popen ) - returncode = Popen([executable, save_argv_path.path] + argv).wait() + from subprocess import ( + PIPE, + ) + returncode = Popen([executable, save_argv_path.path] + argv, stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() self.assertThat( 0, Equals(returncode), From 77c9a2c2f55a3c47fc93924c61f039cb5021255a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:13:17 -0500 Subject: [PATCH 13/62] make the failures a little nicer --- src/allmydata/test/test_windows.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index ae62af857..fc40db60c 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -84,14 +84,16 @@ class GetArgvTests(SyncTestCase): ), ) - @given(lists(text(max_size=4), max_size=4)) + @given(lists(text(min_size=1, max_size=4), min_size=1, max_size=4)) def test_argv_values(self, argv): """ ``get_argv`` returns a list representing the result of tokenizing the "command line" argument string provided to Windows processes. """ - save_argv_path = FilePath(self.mktemp()) - saved_argv_path = FilePath(self.mktemp()) + working_path = self.mktemp() + working_path.makedirs() + save_argv_path = working_path.child("script.py") + saved_argv_path = working_path.child("data.json") with open(save_argv_path.path, "wt") as f: # A simple program to save argv to a file. Using the file saves # us having to figure out how to reliably get non-ASCII back over From 360b20a98169663be22771d84d623819beabecd6 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:14:05 -0500 Subject: [PATCH 14/62] FilePath again --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index fc40db60c..63bb3c09e 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -90,7 +90,7 @@ class GetArgvTests(SyncTestCase): ``get_argv`` returns a list representing the result of tokenizing the "command line" argument string provided to Windows processes. """ - working_path = self.mktemp() + working_path = FilePath(self.mktemp()) working_path.makedirs() save_argv_path = working_path.child("script.py") saved_argv_path = working_path.child("data.json") From 28435d65c16dc3a3ac1c3433f461a48384031b2d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:16:25 -0500 Subject: [PATCH 15/62] test the SUT --- src/allmydata/test/test_windows.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 63bb3c09e..c91a2d462 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -101,10 +101,12 @@ class GetArgvTests(SyncTestCase): # file I/O is relatively simple and well-understood. f.write(dedent( """ - import sys + from allmydata.windows.fixups import ( + get_argv, + ) import json with open({!r}, "wt") as f: - f.write(json.dumps(sys.argv)) + f.write(json.dumps(get_argv())) """.format(saved_argv_path.path)), ) # Python 2.7 doesn't have good options for launching a process with From 3bde012ea14f86ff2ec588db997f0dd419b225fe Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:18:00 -0500 Subject: [PATCH 16/62] Create a better expectation If we pass all this stuff to Popen we should expect to see it from get_argv() right? --- src/allmydata/test/test_windows.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index c91a2d462..81638f6e4 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -117,7 +117,8 @@ class GetArgvTests(SyncTestCase): from subprocess import ( PIPE, ) - returncode = Popen([executable, save_argv_path.path] + argv, stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() + argv = [executable, save_argv_path.path] + argv + returncode = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() self.assertThat( 0, Equals(returncode), From b5f0e21ef845cd486d7c1ba767f07612767f1b53 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 20:19:15 -0500 Subject: [PATCH 17/62] testtools convention - actual value comes first --- src/allmydata/test/test_windows.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 81638f6e4..fddb60389 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -120,13 +120,13 @@ class GetArgvTests(SyncTestCase): argv = [executable, save_argv_path.path] + argv returncode = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() self.assertThat( - 0, - Equals(returncode), + returncode, + Equals(0), ) with open(saved_argv_path.path, "rt") as f: saved_argv = load(f) self.assertThat( - argv, - Equals(saved_argv), + saved_argv, + Equals(argv), ) From 30c79bf6787132634ddee2f45ef523c40c8b2abb Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 09:51:36 -0500 Subject: [PATCH 18/62] make sure executable is unicode too, if that matters --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index fddb60389..1d8173de7 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -117,7 +117,7 @@ class GetArgvTests(SyncTestCase): from subprocess import ( PIPE, ) - argv = [executable, save_argv_path.path] + argv + argv = [executable.decode("utf-8"), save_argv_path.path] + argv returncode = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() self.assertThat( returncode, From 42f1930914c7247bfc38d68fa94e17b3e80ee6ef Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 10:08:15 -0500 Subject: [PATCH 19/62] disambiguate this a bit --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 1d8173de7..5aad36c0b 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -128,5 +128,5 @@ class GetArgvTests(SyncTestCase): self.assertThat( saved_argv, - Equals(argv), + Equals([u"expected"] + argv), ) From e2f396445170fa85d499d1b33e7c1d77dd5df16d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 10:09:54 -0500 Subject: [PATCH 20/62] okay this is indeed the expected --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 5aad36c0b..1d8173de7 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -128,5 +128,5 @@ class GetArgvTests(SyncTestCase): self.assertThat( saved_argv, - Equals([u"expected"] + argv), + Equals(argv), ) From 389d70a682825327e6cde32d9d401fb118c90e20 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 10:17:02 -0500 Subject: [PATCH 21/62] see if GetCommandLine() value is interesting --- src/allmydata/test/test_windows.py | 11 ++++++++++- src/allmydata/windows/fixups.py | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 1d8173de7..41a71150d 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -49,6 +49,7 @@ from testtools.matchers import ( from hypothesis import ( given, + note, ) from hypothesis.strategies import ( @@ -118,7 +119,15 @@ class GetArgvTests(SyncTestCase): PIPE, ) argv = [executable.decode("utf-8"), save_argv_path.path] + argv - returncode = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() + p = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE) + p.stdin.close() + stdout = p.stdout.read() + stderr = p.stderr.read() + returncode = p.wait() + + note("stdout: {!r}".format(stdout)) + note("stderr: {!r}".format(stderr)) + self.assertThat( returncode, Equals(0), diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 2cdb1ad93..8537e06b5 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -26,8 +26,12 @@ def get_argv(): use_last_error=True )(("CommandLineToArgvW", windll.shell32)) + import sys + + command_line = GetCommandLine() + print("GetCommandLine() -> {!r}".format(command_line), file=sys.stderr) argc = c_int(0) - argv_unicode = CommandLineToArgvW(GetCommandLine(), byref(argc)) + argv_unicode = CommandLineToArgvW(command_line, byref(argc)) if argv_unicode is None: raise WinError(get_last_error()) From 33f84412b4462f16cdb4115d41e575ca2ecd6f7a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 10:19:17 -0500 Subject: [PATCH 22/62] maybe pywin32 GetCommandLine is not really GetCommandLineW --- src/allmydata/windows/fixups.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 8537e06b5..f91232457 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -12,14 +12,16 @@ def get_argv(): information using Windows API calls and massages it into the right shape. """ - # - from win32ui import ( - GetCommandLine, - ) from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error from ctypes.wintypes import LPWSTR, LPCWSTR + # + GetCommandLineW = WINFUNCTYPE( + LPWSTR, + use_last_error=True + )(("GetCommandLineW", windll.kernel32)) + # CommandLineToArgvW = WINFUNCTYPE( POINTER(LPWSTR), LPCWSTR, POINTER(c_int), @@ -28,8 +30,8 @@ def get_argv(): import sys - command_line = GetCommandLine() - print("GetCommandLine() -> {!r}".format(command_line), file=sys.stderr) + command_line = GetCommandLineW() + print("GetCommandLineW() -> {!r}".format(command_line), file=sys.stderr) argc = c_int(0) argv_unicode = CommandLineToArgvW(command_line, byref(argc)) if argv_unicode is None: From c2e8d94a7389c6f840787e7cabdc125b8501bf58 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 10:24:25 -0500 Subject: [PATCH 23/62] don't fail this test because it is slow --- src/allmydata/test/test_windows.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 41a71150d..1d5c2632d 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -48,6 +48,8 @@ from testtools.matchers import ( ) from hypothesis import ( + HealthCheck, + settings, given, note, ) @@ -85,6 +87,12 @@ class GetArgvTests(SyncTestCase): ), ) + @settings( + # This test runs a child process. This is unavoidably slow and + # variable. Disable the two time-based Hypothesis health checks. + suppress_health_check=[HealthCheck.too_slow], + deadline=None, + ) @given(lists(text(min_size=1, max_size=4), min_size=1, max_size=4)) def test_argv_values(self, argv): """ From 6d499dea53c76de714a70488a776ddd8daf91500 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 10:29:59 -0500 Subject: [PATCH 24/62] exclude nul from the tested argv values --- src/allmydata/test/test_windows.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 1d5c2632d..6e46a7e8f 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -57,6 +57,7 @@ from hypothesis import ( from hypothesis.strategies import ( lists, text, + characters, ) from .common import ( @@ -93,7 +94,23 @@ class GetArgvTests(SyncTestCase): suppress_health_check=[HealthCheck.too_slow], deadline=None, ) - @given(lists(text(min_size=1, max_size=4), min_size=1, max_size=4)) + @given( + lists( + text( + alphabet=characters( + blacklist_categories=('Cs',), + # Windows CommandLine is a null-terminated string, + # analogous to POSIX exec* arguments. So exclude nul from + # our generated arguments. + blacklist_characters=('\x00',), + ), + min_size=1, + max_size=4, + ), + min_size=1, + max_size=4, + ), + ) def test_argv_values(self, argv): """ ``get_argv`` returns a list representing the result of tokenizing the From a0aa3fe2960e64aa8c0717ae277079b473c4c085 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:12:12 -0500 Subject: [PATCH 25/62] try testing UnicodeOutput --- src/allmydata/test/test_windows.py | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 6e46a7e8f..94b9a0f7d 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -29,6 +29,10 @@ from json import ( from textwrap import ( dedent, ) +from subprocess import ( + Popen, +) + from twisted.python.filepath import ( FilePath, ) @@ -56,6 +60,7 @@ from hypothesis import ( from hypothesis.strategies import ( lists, + tuples, text, characters, ) @@ -164,3 +169,54 @@ class GetArgvTests(SyncTestCase): saved_argv, Equals(argv), ) + + +class UnicodeOutputTests(SyncTestCase): + """ + Tests for writing unicode to stdout and stderr. + """ + @given(tuples(characters(), characters())) + def test_write_non_ascii(self, stdout_char, stderr_char): + """ + Non-ASCII unicode characters can be written to stdout and stderr with + automatic UTF-8 encoding. + """ + working_path = FilePath(self.mktemp()) + script = working_path.child("script.py") + script.setContent(dedent( + """ + from future.utils import PY2 + if PY2: + from future.builtins chr + + from allmydata.windows.fixups import initialize + initialize() + + # XXX A shortcoming of the monkey-patch approach is that you'd + # better not iport stdout or stderr before you call initialize. + from sys import argv, stdout, stderr + + stdout.write(chr(int(argv[1]))) + stdout.close() + stderr.write(chr(int(argv[2]))) + stderr.close() + """ + )) + p = Popen([ + executable, + script.path, + str(ord(stdout_char)), + str(ord(stderr_char)), + ]) + stdout = p.stdout.read() + stderr = p.stderr.read() + returncode = p.wait() + + self.assertThat( + (stdout, stderr, returncode), + Equals(( + stdout_char, + stderr_char, + 0, + )), + ) From 08d56c87b417c5c7aac7dd0a8dfa83a5fc33eccd Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:13:45 -0500 Subject: [PATCH 26/62] that was silly --- src/allmydata/test/test_windows.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 94b9a0f7d..ef113d9e8 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -60,7 +60,6 @@ from hypothesis import ( from hypothesis.strategies import ( lists, - tuples, text, characters, ) @@ -175,7 +174,7 @@ class UnicodeOutputTests(SyncTestCase): """ Tests for writing unicode to stdout and stderr. """ - @given(tuples(characters(), characters())) + @given(characters(), characters()) def test_write_non_ascii(self, stdout_char, stderr_char): """ Non-ASCII unicode characters can be written to stdout and stderr with From 221f1640a586781af7833655ce0add7bc6671419 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:14:32 -0500 Subject: [PATCH 27/62] make the container --- src/allmydata/test/test_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index ef113d9e8..02b05e5d1 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -181,6 +181,7 @@ class UnicodeOutputTests(SyncTestCase): automatic UTF-8 encoding. """ working_path = FilePath(self.mktemp()) + working_path.makedirs() script = working_path.child("script.py") script.setContent(dedent( """ From 504b2f5b1f347673fa21424854083e72cfb79b23 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:15:20 -0500 Subject: [PATCH 28/62] get the syntax right --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 02b05e5d1..205311f9a 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -187,7 +187,7 @@ class UnicodeOutputTests(SyncTestCase): """ from future.utils import PY2 if PY2: - from future.builtins chr + from future.builtins import chr from allmydata.windows.fixups import initialize initialize() From 8fa1b6bb1e275f5e72d6fc85984d768acb3e1ddd Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:15:47 -0500 Subject: [PATCH 29/62] make stdout/stderr available --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 205311f9a..89b71eb17 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -207,7 +207,7 @@ class UnicodeOutputTests(SyncTestCase): script.path, str(ord(stdout_char)), str(ord(stderr_char)), - ]) + ], stdout=PIPE, stderr=PIPE) stdout = p.stdout.read() stderr = p.stderr.read() returncode = p.wait() From 23d1d7624254735d99da609de22f3944aaff6020 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:16:23 -0500 Subject: [PATCH 30/62] get the name --- src/allmydata/test/test_windows.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 89b71eb17..a5ceb4883 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -30,6 +30,7 @@ from textwrap import ( dedent, ) from subprocess import ( + PIPE, Popen, ) @@ -144,9 +145,6 @@ class GetArgvTests(SyncTestCase): from ._win_subprocess import ( Popen ) - from subprocess import ( - PIPE, - ) argv = [executable.decode("utf-8"), save_argv_path.path] + argv p = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE) p.stdin.close() From f4a1a6fd97656693b7aebb3263763714397f5e2f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:16:49 -0500 Subject: [PATCH 31/62] get rid of this noise --- src/allmydata/windows/fixups.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index f91232457..3d9dc6afb 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -28,10 +28,7 @@ def get_argv(): use_last_error=True )(("CommandLineToArgvW", windll.shell32)) - import sys - command_line = GetCommandLineW() - print("GetCommandLineW() -> {!r}".format(command_line), file=sys.stderr) argc = c_int(0) argv_unicode = CommandLineToArgvW(command_line, byref(argc)) if argv_unicode is None: From 3adfb2a1089b31b66610264c6b5ad04110383cd6 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:17:57 -0500 Subject: [PATCH 32/62] let it be slow --- src/allmydata/test/test_windows.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index a5ceb4883..857d9d26c 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -73,6 +73,11 @@ from ..windows.fixups import ( get_argv, ) +slow_settings = settings( + suppress_health_check=[HealthCheck.too_slow], + deadline=None, +) + @skipUnless(platform.isWindows(), "get_argv is Windows-only") class GetArgvTests(SyncTestCase): """ @@ -93,12 +98,9 @@ class GetArgvTests(SyncTestCase): ), ) - @settings( - # This test runs a child process. This is unavoidably slow and - # variable. Disable the two time-based Hypothesis health checks. - suppress_health_check=[HealthCheck.too_slow], - deadline=None, - ) + # This test runs a child process. This is unavoidably slow and variable. + # Disable the two time-based Hypothesis health checks. + @slow_settings @given( lists( text( @@ -172,6 +174,7 @@ class UnicodeOutputTests(SyncTestCase): """ Tests for writing unicode to stdout and stderr. """ + @slow_settings @given(characters(), characters()) def test_write_non_ascii(self, stdout_char, stderr_char): """ From a4061619dc5740d961f41a823080c97dc2e2f44d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:32:12 -0500 Subject: [PATCH 33/62] shuffle code around a lot --- src/allmydata/windows/fixups.py | 224 +++++++++++++++++--------------- 1 file changed, 117 insertions(+), 107 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 3d9dc6afb..d7c929de6 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -2,6 +2,40 @@ from __future__ import print_function done = False +from ctypes import WINFUNCTYPE, windll, POINTER, c_int, WinError, byref, get_last_error +from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID + +# +from win32api import ( + SetErrorMode, +) +from win32con import ( + SEM_FAILCRITICALERRORS, + SEM_NOOPENFILEERRORBOX, +) + +# +# BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, +# LPDWORD lpCharsWritten, LPVOID lpReserved); + +WriteConsoleW = WINFUNCTYPE( + BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), LPVOID, + use_last_error=True +)(("WriteConsoleW", windll.kernel32)) + +# +GetCommandLineW = WINFUNCTYPE( + LPWSTR, + use_last_error=True +)(("GetCommandLineW", windll.kernel32)) + +# +CommandLineToArgvW = WINFUNCTYPE( + POINTER(LPWSTR), LPCWSTR, POINTER(c_int), + use_last_error=True +)(("CommandLineToArgvW", windll.shell32)) + + def get_argv(): """ :return [unicode]: The argument list this process was invoked with, as @@ -12,22 +46,6 @@ def get_argv(): information using Windows API calls and massages it into the right shape. """ - - from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, c_int, get_last_error - from ctypes.wintypes import LPWSTR, LPCWSTR - - # - GetCommandLineW = WINFUNCTYPE( - LPWSTR, - use_last_error=True - )(("GetCommandLineW", windll.kernel32)) - - # - CommandLineToArgvW = WINFUNCTYPE( - POINTER(LPWSTR), LPCWSTR, POINTER(c_int), - use_last_error=True - )(("CommandLineToArgvW", windll.shell32)) - command_line = GetCommandLineW() argc = c_int(0) argv_unicode = CommandLineToArgvW(command_line, byref(argc)) @@ -50,20 +68,9 @@ def initialize(): done = True import codecs, re - from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, get_last_error - from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPVOID + from functools import partial from allmydata.util import log - from allmydata.util.encodingutil import canonical_encoding - - # - from win32api import ( - SetErrorMode, - ) - from win32con import ( - SEM_FAILCRITICALERRORS, - SEM_NOOPENFILEERRORBOX, - ) SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) @@ -73,10 +80,12 @@ def initialize(): # which makes for frustrating debugging if stderr is directed to our wrapper. # So be paranoid about catching errors and reporting them to original_stderr, # so that we can at least see them. - def _complain(message): - print(isinstance(message, str) and message or repr(message), file=original_stderr) + def _complain(output_file, message): + print(isinstance(message, str) and message or repr(message), file=output_file) log.msg(message, level=log.WEIRD) + _complain = partial(_complain, original_stderr) + # Work around . codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None) @@ -137,6 +146,9 @@ def initialize(): real_stdout = (old_stdout_fileno == STDOUT_FILENO) real_stderr = (old_stderr_fileno == STDERR_FILENO) + print("real stdout: {}".format(real_stdout)) + print("real stderr: {}".format(real_stderr)) + if real_stdout: hStdout = GetStdHandle(STD_OUTPUT_HANDLE) if not_a_console(hStdout): @@ -148,88 +160,15 @@ def initialize(): real_stderr = False if real_stdout or real_stderr: - # - # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, - # LPDWORD lpCharsWritten, LPVOID lpReserved); - - WriteConsoleW = WINFUNCTYPE( - BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), LPVOID, - use_last_error=True - )(("WriteConsoleW", windll.kernel32)) - - class UnicodeOutput(object): - def __init__(self, hConsole, stream, fileno, name): - self._hConsole = hConsole - self._stream = stream - self._fileno = fileno - self.closed = False - self.softspace = False - self.mode = 'w' - self.encoding = 'utf-8' - self.name = name - if hasattr(stream, 'encoding') and canonical_encoding(stream.encoding) != 'utf-8': - log.msg("%s: %r had encoding %r, but we're going to write UTF-8 to it" % - (name, stream, stream.encoding), level=log.CURIOUS) - self.flush() - - def isatty(self): - return False - def close(self): - # don't really close the handle, that would only cause problems - self.closed = True - def fileno(self): - return self._fileno - def flush(self): - if self._hConsole is None: - try: - self._stream.flush() - except Exception as e: - _complain("%s.flush: %r from %r" % (self.name, e, self._stream)) - raise - - def write(self, text): - try: - if self._hConsole is None: - if isinstance(text, unicode): - text = text.encode('utf-8') - self._stream.write(text) - else: - if not isinstance(text, unicode): - text = str(text).decode('utf-8') - remaining = len(text) - while remaining > 0: - n = DWORD(0) - # There is a shorter-than-documented limitation on the length of the string - # passed to WriteConsoleW (see #1232). - retval = WriteConsoleW(self._hConsole, text, min(remaining, 10000), byref(n), None) - if retval == 0: - raise IOError("WriteConsoleW failed with WinError: %s" % (WinError(get_last_error()),)) - if n.value == 0: - raise IOError("WriteConsoleW returned %r, n.value = 0" % (retval,)) - remaining -= n.value - if remaining == 0: break - text = text[n.value:] - except Exception as e: - _complain("%s.write: %r" % (self.name, e)) - raise - - def writelines(self, lines): - try: - for line in lines: - self.write(line) - except Exception as e: - _complain("%s.writelines: %r" % (self.name, e)) - raise - if real_stdout: - sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO, '') + sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO, '', _complain) else: - sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno, '') + sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno, '', _complain) if real_stderr: - sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO, '') + sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO, '', _complain) else: - sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno, '') + sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno, '', _complain) except Exception as e: _complain("exception %r while fixing up sys.stdout and sys.stderr" % (e,)) @@ -259,3 +198,74 @@ def initialize(): sys.argv = argv[-len(sys.argv):] if sys.argv[0].endswith('.pyscript'): sys.argv[0] = sys.argv[0][:-9] + + +class UnicodeOutput(object): + def __init__(self, hConsole, stream, fileno, name, _complain): + self._hConsole = hConsole + self._stream = stream + self._fileno = fileno + self.closed = False + self.softspace = False + self.mode = 'w' + self.encoding = 'utf-8' + self.name = name + + self._complain = _complain + + from allmydata.util.encodingutil import canonical_encoding + from allmydata.util import log + if hasattr(stream, 'encoding') and canonical_encoding(stream.encoding) != 'utf-8': + log.msg("%s: %r had encoding %r, but we're going to write UTF-8 to it" % + (name, stream, stream.encoding), level=log.CURIOUS) + self.flush() + + def isatty(self): + return False + def close(self): + # don't really close the handle, that would only cause problems + self.closed = True + def fileno(self): + return self._fileno + def flush(self): + if self._hConsole is None: + try: + self._stream.flush() + except Exception as e: + self._complain("%s.flush: %r from %r" % (self.name, e, self._stream)) + raise + + def write(self, text): + try: + if self._hConsole is None: + if isinstance(text, unicode): + text = text.encode('utf-8') + self._stream.write(text) + else: + if not isinstance(text, unicode): + text = str(text).decode('utf-8') + remaining = len(text) + while remaining > 0: + n = DWORD(0) + # There is a shorter-than-documented limitation on the + # length of the string passed to WriteConsoleW (see + # #1232). + retval = WriteConsoleW(self._hConsole, text, min(remaining, 10000), byref(n), None) + if retval == 0: + raise IOError("WriteConsoleW failed with WinError: %s" % (WinError(get_last_error()),)) + if n.value == 0: + raise IOError("WriteConsoleW returned %r, n.value = 0" % (retval,)) + remaining -= n.value + if remaining == 0: break + text = text[n.value:] + except Exception as e: + self._complain("%s.write: %r" % (self.name, e)) + raise + + def writelines(self, lines): + try: + for line in lines: + self.write(line) + except Exception as e: + self._complain("%s.writelines: %r" % (self.name, e)) + raise From 14caaa360c0a2d09303513e267e1e792cb29c43d Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:33:09 -0500 Subject: [PATCH 34/62] different debug --- src/allmydata/windows/fixups.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index d7c929de6..f69a840f8 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -146,17 +146,16 @@ def initialize(): real_stdout = (old_stdout_fileno == STDOUT_FILENO) real_stderr = (old_stderr_fileno == STDERR_FILENO) - print("real stdout: {}".format(real_stdout)) - print("real stderr: {}".format(real_stderr)) - if real_stdout: hStdout = GetStdHandle(STD_OUTPUT_HANDLE) if not_a_console(hStdout): + print("stdout not a console") real_stdout = False if real_stderr: hStderr = GetStdHandle(STD_ERROR_HANDLE) if not_a_console(hStderr): + print("stdout not a console") real_stderr = False if real_stdout or real_stderr: From 4e9bdfeee4963c4b3c2014ca744fa6bc76dcb3cb Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:38:25 -0500 Subject: [PATCH 35/62] please just always work? --- src/allmydata/windows/fixups.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index f69a840f8..df779cab0 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -158,16 +158,15 @@ def initialize(): print("stdout not a console") real_stderr = False - if real_stdout or real_stderr: - if real_stdout: - sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO, '', _complain) - else: - sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno, '', _complain) + if real_stdout: + sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO, '', _complain) + else: + sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno, '', _complain) - if real_stderr: - sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO, '', _complain) - else: - sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno, '', _complain) + if real_stderr: + sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO, '', _complain) + else: + sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno, '', _complain) except Exception as e: _complain("exception %r while fixing up sys.stdout and sys.stderr" % (e,)) From ab1f6f3a595e42f64204455cb4a7890f729b105f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:39:56 -0500 Subject: [PATCH 36/62] clean up this noise --- src/allmydata/windows/fixups.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index df779cab0..9510e5eb6 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -149,13 +149,11 @@ def initialize(): if real_stdout: hStdout = GetStdHandle(STD_OUTPUT_HANDLE) if not_a_console(hStdout): - print("stdout not a console") real_stdout = False if real_stderr: hStderr = GetStdHandle(STD_ERROR_HANDLE) if not_a_console(hStderr): - print("stdout not a console") real_stderr = False if real_stdout: From 112bfaf62597ad1bb4709921859ccc7fb49a4a23 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:42:27 -0500 Subject: [PATCH 37/62] we would like this to be utf-8 corresponding to the inputs --- src/allmydata/test/test_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 857d9d26c..625799bd2 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -209,8 +209,8 @@ class UnicodeOutputTests(SyncTestCase): str(ord(stdout_char)), str(ord(stderr_char)), ], stdout=PIPE, stderr=PIPE) - stdout = p.stdout.read() - stderr = p.stderr.read() + stdout = p.stdout.read().decode("utf-8") + stderr = p.stderr.read().decode("utf-8") returncode = p.wait() self.assertThat( From 1751d682a2c23213a2373fc7e9ba3cc9d48c5f31 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:46:40 -0500 Subject: [PATCH 38/62] is this cool? --- src/allmydata/test/test_windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 625799bd2..02646a32f 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -209,8 +209,8 @@ class UnicodeOutputTests(SyncTestCase): str(ord(stdout_char)), str(ord(stderr_char)), ], stdout=PIPE, stderr=PIPE) - stdout = p.stdout.read().decode("utf-8") - stderr = p.stderr.read().decode("utf-8") + stdout = p.stdout.read().decode("utf-8").replace("\r\n", "\n") + stderr = p.stderr.read().decode("utf-8").replace("\r\n", "\n") returncode = p.wait() self.assertThat( From ad2df670e61dbdee3f477f620c3cd614533a3949 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 11:51:01 -0500 Subject: [PATCH 39/62] try using pywin32 for GetStdHandle --- src/allmydata/windows/fixups.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 9510e5eb6..818551eb4 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -7,7 +7,14 @@ from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID # from win32api import ( + STD_OUTPUT_HANDLE, + STD_ERROR_HANDLE, SetErrorMode, + + # + # HANDLE WINAPI GetStdHandle(DWORD nStdHandle); + # returns INVALID_HANDLE_VALUE, NULL, or a valid handle + GetStdHandle, ) from win32con import ( SEM_FAILCRITICALERRORS, @@ -95,9 +102,6 @@ def initialize(): # and TZOmegaTZIOY # . try: - # - # HANDLE WINAPI GetStdHandle(DWORD nStdHandle); - # returns INVALID_HANDLE_VALUE, NULL, or a valid handle # # # DWORD WINAPI GetFileType(DWORD hFile); @@ -105,14 +109,6 @@ def initialize(): # # BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode); - GetStdHandle = WINFUNCTYPE( - HANDLE, DWORD, - use_last_error=True - )(("GetStdHandle", windll.kernel32)) - - STD_OUTPUT_HANDLE = DWORD(-11) - STD_ERROR_HANDLE = DWORD(-12) - GetFileType = WINFUNCTYPE( DWORD, DWORD, use_last_error=True From dc5ed668158848289e5fa27a5ef834793a4e3b8c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 12:31:08 -0500 Subject: [PATCH 40/62] docstring --- src/allmydata/windows/fixups.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 818551eb4..9249f9d80 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -193,6 +193,11 @@ def initialize(): class UnicodeOutput(object): + """ + ``UnicodeOutput`` is a file-like object that encodes unicode to UTF-8 and + writes it to another file or writes unicode natively to the Windows + console. + """ def __init__(self, hConsole, stream, fileno, name, _complain): self._hConsole = hConsole self._stream = stream From ed713182e77e7d92cf27c80da9c29e72df0f693c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 12:31:13 -0500 Subject: [PATCH 41/62] docstring --- src/allmydata/windows/fixups.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 9249f9d80..56a914c01 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -199,6 +199,20 @@ class UnicodeOutput(object): console. """ def __init__(self, hConsole, stream, fileno, name, _complain): + """ + :param hConsole: ``None`` or a handle on the console to which to write + unicode. Mutually exclusive with ``stream``. + + :param stream: ``None`` or a file-like object to which to write bytes. + + :param fileno: A result to hand back from method of the same name. + + :param name: A human-friendly identifier for this output object. + + :param _complain: A one-argument callable which accepts bytes to be + written when there's a problem. Care should be taken to not make + this do a write on this object. + """ self._hConsole = hConsole self._stream = stream self._fileno = fileno From fd223136db3f275c76673ab46c168d58ed60eec9 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 12:42:52 -0500 Subject: [PATCH 42/62] Avoid breaking non-Windows with test_windows --- src/allmydata/test/test_windows.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 02646a32f..8d8026584 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -69,10 +69,6 @@ from .common import ( SyncTestCase, ) -from ..windows.fixups import ( - get_argv, -) - slow_settings = settings( suppress_health_check=[HealthCheck.too_slow], deadline=None, @@ -87,9 +83,15 @@ class GetArgvTests(SyncTestCase): """ ``get_argv`` returns a list of unicode strings """ + # Hide the ``allmydata.windows.fixups.get_argv`` import here so it + # doesn't cause failures on non-Windows platforms. + from ..windows.fixups import ( + get_argv, + ) + argv = get_argv() + # We don't know what this process's command line was so we just make # structural assertions here. - argv = get_argv() self.assertThat( argv, MatchesAll( From 6de392fd23874703aa2e07283c891113000e01fc Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 12:43:30 -0500 Subject: [PATCH 43/62] blacklist a couple more --- src/allmydata/test/test_python2_regressions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/allmydata/test/test_python2_regressions.py b/src/allmydata/test/test_python2_regressions.py index 84484f1cf..a3547ca27 100644 --- a/src/allmydata/test/test_python2_regressions.py +++ b/src/allmydata/test/test_python2_regressions.py @@ -16,6 +16,8 @@ from testtools.matchers import ( BLACKLIST = { "allmydata.test.check_load", "allmydata.windows.registry", + "allmydata.windows.fixups", + "allmydata.windows._win_subprocess", } From f5bcd272b8eae0fd48a92b1b6b6437cb9d783162 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 12:58:27 -0500 Subject: [PATCH 44/62] skip the other test suite too --- src/allmydata/test/test_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 8d8026584..40ec4889e 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -172,6 +172,7 @@ class GetArgvTests(SyncTestCase): ) +@skipUnless(platform.isWindows(), "intended for Windows-only codepaths") class UnicodeOutputTests(SyncTestCase): """ Tests for writing unicode to stdout and stderr. From f61103aa8009671ce6a6ada948e1e9ad160686ff Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:22:14 -0500 Subject: [PATCH 45/62] spell the module name right --- src/allmydata/test/test_python2_regressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_python2_regressions.py b/src/allmydata/test/test_python2_regressions.py index a3547ca27..bb0cedad7 100644 --- a/src/allmydata/test/test_python2_regressions.py +++ b/src/allmydata/test/test_python2_regressions.py @@ -15,9 +15,9 @@ from testtools.matchers import ( BLACKLIST = { "allmydata.test.check_load", + "allmydata.test._win_subprocess", "allmydata.windows.registry", "allmydata.windows.fixups", - "allmydata.windows._win_subprocess", } From cca0071cbf0e4b874bd9777275d97a6d9636d4b2 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:36:04 -0500 Subject: [PATCH 46/62] these aren't win32 specific --- src/allmydata/windows/fixups.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 56a914c01..e3e11a386 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -43,6 +43,9 @@ CommandLineToArgvW = WINFUNCTYPE( )(("CommandLineToArgvW", windll.shell32)) +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + def get_argv(): """ :return [unicode]: The argument list this process was invoked with, as @@ -137,8 +140,6 @@ def initialize(): if hasattr(sys.stderr, 'fileno'): old_stderr_fileno = sys.stderr.fileno() - STDOUT_FILENO = 1 - STDERR_FILENO = 2 real_stdout = (old_stdout_fileno == STDOUT_FILENO) real_stderr = (old_stderr_fileno == STDERR_FILENO) From 5c6e5970c9358cf70d3569aa36ba3bf2673d568f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:36:12 -0500 Subject: [PATCH 47/62] get this from pywin32 too --- src/allmydata/windows/fixups.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index e3e11a386..31d7931dd 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -21,6 +21,10 @@ from win32con import ( SEM_NOOPENFILEERRORBOX, ) +from win32file import ( + INVALID_HANDLE_VALUE, +) + # # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, # LPDWORD lpCharsWritten, LPVOID lpReserved); @@ -125,8 +129,6 @@ def initialize(): use_last_error=True )(("GetConsoleMode", windll.kernel32)) - INVALID_HANDLE_VALUE = DWORD(-1).value - def not_a_console(handle): if handle == INVALID_HANDLE_VALUE or handle is None: return True From 184b9735b5d5a938d3f80036f2fd50ebef19c70c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:41:48 -0500 Subject: [PATCH 48/62] another constant we can get from pywin32 --- src/allmydata/windows/fixups.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 31d7931dd..a3ce24a6d 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -23,8 +23,12 @@ from win32con import ( from win32file import ( INVALID_HANDLE_VALUE, + FILE_TYPE_CHAR, ) +# This one not exposed by pywin32 as far as I can tell. +FILE_TYPE_REMOTE = 0x8000 + # # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, # LPDWORD lpCharsWritten, LPVOID lpReserved); @@ -121,8 +125,6 @@ def initialize(): use_last_error=True )(("GetFileType", windll.kernel32)) - FILE_TYPE_CHAR = 0x0002 - FILE_TYPE_REMOTE = 0x8000 GetConsoleMode = WINFUNCTYPE( BOOL, HANDLE, POINTER(DWORD), From 52896432e1628a8a0f5864c102d023914a720742 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:41:54 -0500 Subject: [PATCH 49/62] it cannot return None --- src/allmydata/windows/fixups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index a3ce24a6d..5d5f25985 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -132,7 +132,7 @@ def initialize(): )(("GetConsoleMode", windll.kernel32)) def not_a_console(handle): - if handle == INVALID_HANDLE_VALUE or handle is None: + if handle == INVALID_HANDLE_VALUE: return True return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR or GetConsoleMode(handle, byref(DWORD())) == 0) From ad48e6c00502be231e241980c51a00ccf0d36d7a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:54:04 -0500 Subject: [PATCH 50/62] See if we can use pywin32 GetFileType --- src/allmydata/windows/fixups.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 5d5f25985..45b69cd87 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -24,6 +24,10 @@ from win32con import ( from win32file import ( INVALID_HANDLE_VALUE, FILE_TYPE_CHAR, + + # + # DWORD WINAPI GetFileType(DWORD hFile); + GetFileType, ) # This one not exposed by pywin32 as far as I can tell. @@ -113,19 +117,10 @@ def initialize(): # and TZOmegaTZIOY # . try: - # - # - # DWORD WINAPI GetFileType(DWORD hFile); # # # BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode); - GetFileType = WINFUNCTYPE( - DWORD, DWORD, - use_last_error=True - )(("GetFileType", windll.kernel32)) - - GetConsoleMode = WINFUNCTYPE( BOOL, HANDLE, POINTER(DWORD), use_last_error=True From 9d7b12292c35e24ba170a6684f2c2ff66085efdf Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 13:56:42 -0500 Subject: [PATCH 51/62] Get rid of FILE_TYPE_REMOTE --- src/allmydata/windows/fixups.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 45b69cd87..23b846360 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -30,9 +30,6 @@ from win32file import ( GetFileType, ) -# This one not exposed by pywin32 as far as I can tell. -FILE_TYPE_REMOTE = 0x8000 - # # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, # LPDWORD lpCharsWritten, LPVOID lpReserved); @@ -126,11 +123,15 @@ def initialize(): use_last_error=True )(("GetConsoleMode", windll.kernel32)) - def not_a_console(handle): + def a_console(handle): if handle == INVALID_HANDLE_VALUE: - return True - return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR - or GetConsoleMode(handle, byref(DWORD())) == 0) + return False + return ( + # It's a character file (eg a printer or a console) + GetFileType(handle) == FILE_TYPE_CHAR and + # Checking the console mode doesn't fail (thus it's a console) + GetConsoleMode(handle, byref(DWORD())) != 0 + ) old_stdout_fileno = None old_stderr_fileno = None @@ -144,12 +145,12 @@ def initialize(): if real_stdout: hStdout = GetStdHandle(STD_OUTPUT_HANDLE) - if not_a_console(hStdout): + if not a_console(hStdout): real_stdout = False if real_stderr: hStderr = GetStdHandle(STD_ERROR_HANDLE) - if not_a_console(hStderr): + if not a_console(hStderr): real_stderr = False if real_stdout: From e6ee13d11b63e30568be64f0621da54b49213b74 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 14:03:11 -0500 Subject: [PATCH 52/62] Shovel code around a bit more --- src/allmydata/windows/fixups.py | 68 +++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index 23b846360..c71b85681 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -1,6 +1,7 @@ from __future__ import print_function -done = False +import codecs, re +from functools import partial from ctypes import WINFUNCTYPE, windll, POINTER, c_int, WinError, byref, get_last_error from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID @@ -30,10 +31,22 @@ from win32file import ( GetFileType, ) +from allmydata.util import ( + log, +) + +# Keep track of whether `initialize` has run so we don't do any of the +# initialization more than once. +_done = False + +# +# pywin32 for Python 2.7 does not bind any of these *W variants so we do it +# ourselves. +# + # # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, # LPDWORD lpCharsWritten, LPVOID lpReserved); - WriteConsoleW = WINFUNCTYPE( BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), LPVOID, use_last_error=True @@ -51,6 +64,13 @@ CommandLineToArgvW = WINFUNCTYPE( use_last_error=True )(("CommandLineToArgvW", windll.shell32)) +# +# BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode); +GetConsoleMode = WINFUNCTYPE( + BOOL, HANDLE, POINTER(DWORD), + use_last_error=True +)(("GetConsoleMode", windll.kernel32)) + STDOUT_FILENO = 1 STDERR_FILENO = 2 @@ -80,16 +100,11 @@ def get_argv(): def initialize(): - global done + global _done import sys - if sys.platform != "win32" or done: + if sys.platform != "win32" or _done: return True - done = True - - import codecs, re - from functools import partial - - from allmydata.util import log + _done = True SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX) @@ -114,25 +129,6 @@ def initialize(): # and TZOmegaTZIOY # . try: - # - # - # BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode); - - GetConsoleMode = WINFUNCTYPE( - BOOL, HANDLE, POINTER(DWORD), - use_last_error=True - )(("GetConsoleMode", windll.kernel32)) - - def a_console(handle): - if handle == INVALID_HANDLE_VALUE: - return False - return ( - # It's a character file (eg a printer or a console) - GetFileType(handle) == FILE_TYPE_CHAR and - # Checking the console mode doesn't fail (thus it's a console) - GetConsoleMode(handle, byref(DWORD())) != 0 - ) - old_stdout_fileno = None old_stderr_fileno = None if hasattr(sys.stdout, 'fileno'): @@ -193,6 +189,20 @@ def initialize(): sys.argv[0] = sys.argv[0][:-9] +def a_console(handle): + """ + :return: ``True`` if ``handle`` refers to a console, ``False`` otherwise. + """ + if handle == INVALID_HANDLE_VALUE: + return False + return ( + # It's a character file (eg a printer or a console) + GetFileType(handle) == FILE_TYPE_CHAR and + # Checking the console mode doesn't fail (thus it's a console) + GetConsoleMode(handle, byref(DWORD())) != 0 + ) + + class UnicodeOutput(object): """ ``UnicodeOutput`` is a file-like object that encodes unicode to UTF-8 and From a29b061f917e7a7b80383b14e44a5fc64320da96 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 14:04:20 -0500 Subject: [PATCH 53/62] explain the nested import --- src/allmydata/test/test_windows.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 40ec4889e..eb9321bfc 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -125,6 +125,13 @@ class GetArgvTests(SyncTestCase): ``get_argv`` returns a list representing the result of tokenizing the "command line" argument string provided to Windows processes. """ + # Python 2.7 doesn't have good options for launching a process with + # non-ASCII in its command line. So use this alternative that does a + # better job. Bury the import here because it only works on Windows. + from ._win_subprocess import ( + Popen + ) + working_path = FilePath(self.mktemp()) working_path.makedirs() save_argv_path = working_path.child("script.py") @@ -144,11 +151,6 @@ class GetArgvTests(SyncTestCase): f.write(json.dumps(get_argv())) """.format(saved_argv_path.path)), ) - # Python 2.7 doesn't have good options for launching a process with - # non-ASCII in its command line. - from ._win_subprocess import ( - Popen - ) argv = [executable.decode("utf-8"), save_argv_path.path] + argv p = Popen(argv, stdin=PIPE, stdout=PIPE, stderr=PIPE) p.stdin.close() From a4c520ec2a62adfe58a813c6893533e4e4a6aa5f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 14:11:28 -0500 Subject: [PATCH 54/62] try to go faster without losing coverage --- src/allmydata/test/test_windows.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index eb9321bfc..2404542fa 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -72,6 +72,13 @@ from .common import ( slow_settings = settings( suppress_health_check=[HealthCheck.too_slow], deadline=None, + + # Reduce the number of examples required to consider the test a success. + # The default is 100. Launching a process is expensive so we'll try to do + # it as few times as we can get away with. To maintain good coverage, + # we'll try to pass as much data to each process as we can so we're still + # covering a good portion of the space. + max_examples=10, ) @skipUnless(platform.isWindows(), "get_argv is Windows-only") @@ -113,11 +120,11 @@ class GetArgvTests(SyncTestCase): # our generated arguments. blacklist_characters=('\x00',), ), - min_size=1, - max_size=4, + min_size=10, + max_size=20, ), - min_size=1, - max_size=4, + min_size=10, + max_size=20, ), ) def test_argv_values(self, argv): From 41d754852722d6405d57083bd3fa82556b6694a0 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 11 Jan 2021 14:16:02 -0500 Subject: [PATCH 55/62] typo --- src/allmydata/test/test_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_windows.py b/src/allmydata/test/test_windows.py index 2404542fa..f2c1318c5 100644 --- a/src/allmydata/test/test_windows.py +++ b/src/allmydata/test/test_windows.py @@ -206,7 +206,7 @@ class UnicodeOutputTests(SyncTestCase): initialize() # XXX A shortcoming of the monkey-patch approach is that you'd - # better not iport stdout or stderr before you call initialize. + # better not import stdout or stderr before you call initialize. from sys import argv, stdout, stderr stdout.write(chr(int(argv[1]))) From 11e4bcf47680f20eb9015df069ed30fd8c71ee0c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 19 Jan 2021 14:41:58 -0500 Subject: [PATCH 56/62] Add a direct unit test for FileHandle.get_encryption_key --- src/allmydata/test/test_upload.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/allmydata/test/test_upload.py b/src/allmydata/test/test_upload.py index 94d7575c3..664e4cc94 100644 --- a/src/allmydata/test/test_upload.py +++ b/src/allmydata/test/test_upload.py @@ -14,6 +14,9 @@ if PY2: import os, shutil from io import BytesIO +from base64 import ( + b64encode, +) from twisted.trial import unittest from twisted.python.failure import Failure @@ -877,6 +880,34 @@ def is_happy_enough(servertoshnums, h, k): return True +class FileHandleTests(unittest.TestCase): + """ + Tests for ``FileHandle``. + """ + def test_get_encryption_key_convergent(self): + """ + When ``FileHandle`` is initialized with a convergence secret, + ``FileHandle.get_encryption_key`` returns a deterministic result that + is a function of that secret. + """ + secret = b"\x42" * 16 + handle = upload.FileHandle(BytesIO(b"hello world"), secret) + handle.set_default_encoding_parameters({ + "k": 3, + "happy": 5, + "n": 10, + # Remember this is the *max* segment size. In reality, the data + # size is much smaller so the actual segment size incorporated + # into the encryption key is also smaller. + "max_segment_size": 128 * 1024, + }) + + self.assertEqual( + b64encode(self.successResultOf(handle.get_encryption_key())), + b"oBcuR/wKdCgCV2GKKXqiNg==", + ) + + class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin, ShouldFailMixin): From be5cf1a0bea687d21ded82a2bfdc2c6301afae73 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 19 Jan 2021 14:42:30 -0500 Subject: [PATCH 57/62] news fragment --- newsfragments/3593.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3593.minor diff --git a/newsfragments/3593.minor b/newsfragments/3593.minor new file mode 100644 index 000000000..e69de29bb From ddcb43561d119a87cb58d0ba65d9ae3c6d3d5ac5 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Mon, 8 Feb 2021 19:49:02 -0500 Subject: [PATCH 58/62] Try to convince Mypy it's okay --- src/allmydata/windows/fixups.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index abf4a8680..bb8e3fd97 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -1,5 +1,15 @@ from __future__ import print_function +# This code isn't loadable or sensible except on Windows. Importers all know +# this and are careful. Normally I would just let an import error from ctypes +# explain any mistakes but Mypy also needs some help here. This assert +# explains to it that this module is Windows-only. This prevents errors about +# ctypes.windll and such which only exist when running on Windows. +# +# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks +from sys import platform +assert platform == "win32" + import codecs, re from functools import partial From 541d7043d7674024e489757f23ee80f3a379316f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 9 Feb 2021 10:20:14 -0500 Subject: [PATCH 59/62] Some comments about unicode handling in this UnicodeOutput thing --- src/allmydata/windows/fixups.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index bb8e3fd97..b02c63b0b 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -275,10 +275,15 @@ class UnicodeOutput(object): def write(self, text): try: if self._hConsole is None: + # There is no Windows console available. That means we are + # responsible for encoding the unicode to a byte string to + # write it to a Python file object. if isinstance(text, unicode): text = text.encode('utf-8') self._stream.write(text) else: + # There is a Windows console available. That means Windows is + # responsible for dealing with the unicode itself. if not isinstance(text, unicode): text = str(text).decode('utf-8') remaining = len(text) From 27fcfe94dd371b6aeb6309cd2481ea4037fc2d39 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 9 Feb 2021 10:24:46 -0500 Subject: [PATCH 60/62] The code is 3-clause BSD licensed now. --- src/allmydata/test/_win_subprocess.py | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/allmydata/test/_win_subprocess.py b/src/allmydata/test/_win_subprocess.py index cc66f7552..1ca2de1f4 100644 --- a/src/allmydata/test/_win_subprocess.py +++ b/src/allmydata/test/_win_subprocess.py @@ -1,3 +1,37 @@ +# -*- coding: utf-8 -*- + +## Copyright (C) 2021 Valentin Lab +## +## Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions +## are met: +## +## 1. Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## +## 2. Redistributions in binary form must reproduce the above +## copyright notice, this list of conditions and the following +## disclaimer in the documentation and/or other materials provided +## with the distribution. +## +## 3. Neither the name of the copyright holder nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +## FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +## COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +## INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +## SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +## HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +## STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +## OF THE POSSIBILITY OF SUCH DAMAGE. +## + ## issue: https://bugs.python.org/issue19264 import os From b26652cad1cb5d5589b25a387c6ff10f9912c191 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 9 Feb 2021 14:36:19 -0500 Subject: [PATCH 61/62] Try to get Mypy to recognize it this way? --- src/allmydata/windows/fixups.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/allmydata/windows/fixups.py b/src/allmydata/windows/fixups.py index b02c63b0b..0d1ed2717 100644 --- a/src/allmydata/windows/fixups.py +++ b/src/allmydata/windows/fixups.py @@ -6,9 +6,12 @@ from __future__ import print_function # explains to it that this module is Windows-only. This prevents errors about # ctypes.windll and such which only exist when running on Windows. # +# Beware of the limitations of the Mypy AST analyzer. The check needs to take +# exactly this form or it may not be recognized. +# # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks -from sys import platform -assert platform == "win32" +import sys +assert sys.platform == "win32" import codecs, re from functools import partial From 28acc5ccb4cd911fd7a5d0272ccceef1b82ab30b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Tue, 9 Feb 2021 14:50:29 -0500 Subject: [PATCH 62/62] Duplicate the fix for the other Windows-only module --- src/allmydata/test/_win_subprocess.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/test/_win_subprocess.py b/src/allmydata/test/_win_subprocess.py index 1ca2de1f4..fe6960c73 100644 --- a/src/allmydata/test/_win_subprocess.py +++ b/src/allmydata/test/_win_subprocess.py @@ -34,6 +34,10 @@ ## issue: https://bugs.python.org/issue19264 +# See allmydata/windows/fixups.py +import sys +assert sys.platform == "win32" + import os import ctypes import subprocess