From e80bd6894ff823d17d22adb77cc08e05df036913 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 10 Jan 2021 10:39:17 -0500 Subject: [PATCH] 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.