diff options
author | Frank Benkstein <frank.benkstein@sap.com> | 2016-01-25 14:36:37 +0100 |
---|---|---|
committer | Frank Benkstein <frank.benkstein@sap.com> | 2016-01-27 14:18:59 +0100 |
commit | cbccd44acbedb5e839f54b00243191d553ab8d36 (patch) | |
tree | 3d6b8d3455f97ec103a5060a11e3050e37ebaa8d | |
parent | 4ceb5ecbf3f086ebe629ca567bafaff7e18ce4ec (diff) | |
download | psutil-cbccd44acbedb5e839f54b00243191d553ab8d36.tar.gz |
address 32bit/64bit confusion in psutil_get_parameters
Replace the hard-coded offsets into compiler generated
ones by providing struct definitions for the data to be
fetched from the target process. This allows a 64 bit
process to query both other 64 bit and 32 bit processes.
A 32 bit process currently can only query other 32 bit
processes.
-rw-r--r-- | HISTORY.rst | 3 | ||||
-rw-r--r-- | psutil/_psutil_windows.c | 15 | ||||
-rw-r--r-- | psutil/arch/windows/process_info.c | 502 | ||||
-rw-r--r-- | test/_windows.py | 74 | ||||
-rw-r--r-- | test/test_psutil.py | 2 |
5 files changed, 471 insertions, 125 deletions
diff --git a/HISTORY.rst b/HISTORY.rst index 29c6b80c..05bfad59 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -14,6 +14,9 @@ Bug tracker at https://github.com/giampaolo/psutil/issues UnicodeDecodeError exceptions. 'surrogateescape' error handler is now used as a workaround for replacing the corrupted data. - #741: [OpenBSD] fix compilation on mips64. +- #737: [Windows] when the bitness of psutil and the target process was + different cmdline() and cwd() could return a wrong result or incorrectly + report an AccessDenied error. 3.4.2 - 2016-01-20 diff --git a/psutil/_psutil_windows.c b/psutil/_psutil_windows.c index 186587ac..c351a2ba 100644 --- a/psutil/_psutil_windows.c +++ b/psutil/_psutil_windows.c @@ -575,7 +575,6 @@ static PyObject * psutil_proc_cmdline(PyObject *self, PyObject *args) { long pid; int pid_return; - PyObject *py_retlist; if (! PyArg_ParseTuple(args, "l", &pid)) return NULL; @@ -588,19 +587,7 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { if (pid_return == -1) return NULL; - // XXX the assumptio below probably needs to go away - - // May fail any of several ReadProcessMemory calls etc. and - // not indicate a real problem so we ignore any errors and - // just live without commandline. - py_retlist = psutil_get_cmdline(pid); - if ( NULL == py_retlist ) { - // carry on anyway, clear any exceptions too - PyErr_Clear(); - return Py_BuildValue("[]"); - } - - return py_retlist; + return psutil_get_cmdline(pid); } diff --git a/psutil/arch/windows/process_info.c b/psutil/arch/windows/process_info.c index f6ee7df5..e66166c0 100644 --- a/psutil/arch/windows/process_info.c +++ b/psutil/arch/windows/process_info.c @@ -67,19 +67,6 @@ psutil_handle_from_pid(DWORD pid) { } -// fetch the PEB base address from NtQueryInformationProcess() -PVOID -psutil_get_peb_address(HANDLE ProcessHandle) { - _NtQueryInformationProcess NtQueryInformationProcess = - (_NtQueryInformationProcess)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); - PROCESS_BASIC_INFORMATION pbi; - - NtQueryInformationProcess(ProcessHandle, 0, &pbi, sizeof(pbi), NULL); - return pbi.PebBaseAddress; -} - - DWORD * psutil_get_pids(DWORD *numberOfReturnedPIDs) { // Win32 SDK says the only way to know if our process array @@ -203,6 +190,124 @@ handlep_is_running(HANDLE hProcess) { return 0; } +// Helper structures to access the memory correctly. Some of these might also +// be defined in the winternl.h header file but unfortunately not in a usable +// way. + +// see http://msdn2.microsoft.com/en-us/library/aa489609.aspx +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) +#endif + +// http://msdn.microsoft.com/en-us/library/aa813741(VS.85).aspx +typedef struct { + BYTE Reserved1[16]; + PVOID Reserved2[5]; + UNICODE_STRING CurrentDirectoryPath; + PVOID CurrentDirectoryHandle; + UNICODE_STRING DllPath; + UNICODE_STRING ImagePathName; + UNICODE_STRING CommandLine; + LPCWSTR env; +} RTL_USER_PROCESS_PARAMETERS_, *PRTL_USER_PROCESS_PARAMETERS_; + +// https://msdn.microsoft.com/en-us/library/aa813706(v=vs.85).aspx +#ifdef _WIN64 +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[21]; + PVOID LoaderData; + PRTL_USER_PROCESS_PARAMETERS_ ProcessParameters; + /* More fields ... */ +} PEB_; +#else +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[1]; + PVOID Reserved3[2]; + PVOID Ldr; + PRTL_USER_PROCESS_PARAMETERS_ ProcessParameters; + /* More fields ... */ +} PEB_; +#endif + +#ifdef _WIN64 +/* When we are a 64 bit process accessing a 32 bit (WoW64) process we need to + use the 32 bit structure layout. */ +typedef struct { + USHORT Length; + USHORT MaxLength; + DWORD Buffer; +} UNICODE_STRING32; + +typedef struct { + BYTE Reserved1[16]; + DWORD Reserved2[5]; + UNICODE_STRING32 CurrentDirectoryPath; + DWORD CurrentDirectoryHandle; + UNICODE_STRING32 DllPath; + UNICODE_STRING32 ImagePathName; + UNICODE_STRING32 CommandLine; + DWORD env; +} RTL_USER_PROCESS_PARAMETERS32; + +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[1]; + DWORD Reserved3[2]; + DWORD Ldr; + DWORD ProcessParameters; + /* More fields ... */ +} PEB32; +#else +/* When we are a 32 bit (WoW64) process accessing a 64 bit process we need to + use the 64 bit structure layout and a special function to read its memory. + */ +typedef NTSTATUS (NTAPI *_NtWow64ReadVirtualMemory64)( + IN HANDLE ProcessHandle, + IN PVOID64 BaseAddress, + OUT PVOID Buffer, + IN ULONG64 Size, + OUT PULONG64 NumberOfBytesRead); + +typedef struct { + PVOID Reserved1[2]; + PVOID64 PebBaseAddress; + PVOID Reserved2[4]; + PVOID UniqueProcessId[2]; + PVOID Reserved3[2]; +} PROCESS_BASIC_INFORMATION64; + +typedef struct { + USHORT Length; + USHORT MaxLength; + PVOID64 Buffer; +} UNICODE_STRING64; + +typedef struct { + BYTE Reserved1[16]; + PVOID64 Reserved2[5]; + UNICODE_STRING64 CurrentDirectoryPath; + PVOID64 CurrentDirectoryHandle; + UNICODE_STRING64 DllPath; + UNICODE_STRING64 ImagePathName; + UNICODE_STRING64 CommandLine; + PVOID64 env; +} RTL_USER_PROCESS_PARAMETERS64; + +typedef struct { + BYTE Reserved1[2]; + BYTE BeingDebugged; + BYTE Reserved2[21]; + PVOID64 LoaderData; + PVOID64 ProcessParameters; + /* More fields ... */ +} PEB64; +#endif + /* Get one or more parameters of the process with the given pid: pcmdline: the command line as a Python list @@ -210,85 +315,311 @@ handlep_is_running(HANDLE hProcess) { On success 0 is returned. On error the given output parameters are not touched, -1 is returned, and an appropriate Python exception is set. */ -static int psutil_get_parameters(long pid, PyObject **pcmdline, PyObject **pcwd) { +static int psutil_get_parameters(long pid, PyObject **pcmdline, + PyObject **pcwd) { + /* This function is quite complex because there are several cases to be + considered: + + Two cases are really simple: we (i.e. the python interpreter) and the + target process are both 32 bit or both 64 bit. In that case the memory + layout of the structures matches up and all is well. + + When we are 64 bit and the target process is 32 bit we need to use + custom 32 bit versions of the structures. + + When we are 32 bit and the target process is 64 bit we need to use + custom 64 bit version of the structures. Also we need to use separate + Wow64 functions to get the information. + + A few helper structs are defined above so that the compiler can handle + calculating the correct offsets. + + Additional help also came from the following sources: + + https://github.com/kohsuke/winp and + http://wj32.org/wp/2009/01/24/howto-get-the-command-line-of-processes/ + http://stackoverflow.com/a/14012919 + http://www.drdobbs.com/embracing-64-bit-windows/184401966 + */ + static _NtQueryInformationProcess NtQueryInformationProcess = NULL; +#ifndef _WIN64 + static _NtQueryInformationProcess NtWow64QueryInformationProcess64 = NULL; + static _NtWow64ReadVirtualMemory64 NtWow64ReadVirtualMemory64 = NULL; +#endif int nArgs, i; LPWSTR *szArglist = NULL; HANDLE hProcess = NULL; - PVOID pebAddress; - PVOID rtlUserProcParamsAddress; - UNICODE_STRING commandLine; WCHAR *commandLineContents = NULL; - UNICODE_STRING currentDirectory; WCHAR *currentDirectoryContent = NULL; +#ifdef _WIN64 + LPVOID ppeb32 = NULL; +#else + BOOL weAreWow64; + BOOL theyAreWow64; +#endif PyObject *py_unicode = NULL; PyObject *py_retlist = NULL; hProcess = psutil_handle_from_pid(pid); if (hProcess == NULL) return -1; - pebAddress = psutil_get_peb_address(hProcess); - // get the address of ProcessParameters + if (NtQueryInformationProcess == NULL) { + NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress( + GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess"); + } + #ifdef _WIN64 - if (!ReadProcessMemory(hProcess, (PCHAR)pebAddress + 32, - &rtlUserProcParamsAddress, sizeof(PVOID), NULL)) + /* 64 bit case. Check if the target is a 32 bit process running in WoW64 + * mode. */ + if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, + ProcessWow64Information, + &ppeb32, + sizeof(LPVOID), + NULL))) { + PyErr_SetFromWindowsErr(0); + goto error; + } #else - if (!ReadProcessMemory(hProcess, (PCHAR)pebAddress + 0x10, - &rtlUserProcParamsAddress, sizeof(PVOID), NULL)) + /* 32 bit case. Check if the target is also 32 bit. */ + if (!IsWow64Process(GetCurrentProcess(), &weAreWow64) || + !IsWow64Process(hProcess, &theyAreWow64)) { + PyErr_SetFromWindowsErr(0); + goto error; + } #endif - { - if (GetLastError() == ERROR_PARTIAL_COPY) { - // this occurs quite often with system processes - AccessDenied(); + +#ifdef _WIN64 + if (ppeb32 != NULL) { + /* We are 64 bit. Target process is 32 bit running in WoW64 mode. */ + PEB32 peb32; + RTL_USER_PROCESS_PARAMETERS32 procParameters32; + + // read PEB + if(!ReadProcessMemory(hProcess, ppeb32, &peb32, sizeof(peb32), NULL)) { + PyErr_SetFromWindowsErr(0); + goto error; } - else { + + // read process parameters + if(!ReadProcessMemory(hProcess, + UlongToPtr(peb32.ProcessParameters), + &procParameters32, + sizeof(procParameters32), + NULL)) { PyErr_SetFromWindowsErr(0); + goto error; } - goto error; - } - if (pcmdline != NULL) { - // read the CommandLine UNICODE_STRING structure -#ifdef _WIN64 - if (!ReadProcessMemory(hProcess, (PCHAR)rtlUserProcParamsAddress + 112, - &commandLine, sizeof(commandLine), NULL)) + if (pcmdline != NULL) { + // read command line aguments + commandLineContents = + calloc(procParameters32.CommandLine.Length + 2, 1); + if (commandLineContents == NULL) { + PyErr_NoMemory(); + goto error; + } + + if(!ReadProcessMemory( + hProcess, + UlongToPtr(procParameters32.CommandLine.Buffer), + commandLineContents, + procParameters32.CommandLine.Length, + NULL)) { + PyErr_SetFromWindowsErr(0); + goto error; + } + } + + if (pcwd != NULL) { + // read cwd + currentDirectoryContent = + calloc(procParameters32.CurrentDirectoryPath.Length + 2, 1); + if (currentDirectoryContent == NULL) { + PyErr_NoMemory(); + goto error; + } + + if (!ReadProcessMemory( + hProcess, + UlongToPtr(procParameters32.CurrentDirectoryPath.Buffer), + currentDirectoryContent, + procParameters32.CurrentDirectoryPath.Length, + NULL)) { + PyErr_SetFromWindowsErr(0); + goto error; + } + } + } else #else - if (!ReadProcessMemory(hProcess, (PCHAR)rtlUserProcParamsAddress + 0x40, - &commandLine, sizeof(commandLine), NULL)) -#endif - { - if (GetLastError() == ERROR_PARTIAL_COPY) { - // this occurs quite often with system processes - AccessDenied(); + if (weAreWow64 && !theyAreWow64) { + /* We are 32 bit running in WoW64 mode. Target process is 64 bit. */ + PROCESS_BASIC_INFORMATION64 pbi64; + PEB64 peb64; + RTL_USER_PROCESS_PARAMETERS64 procParameters64; + + if (NtWow64QueryInformationProcess64 == NULL) { + NtWow64QueryInformationProcess64 = + (_NtQueryInformationProcess)GetProcAddress( + GetModuleHandleA("ntdll.dll"), + "NtWow64QueryInformationProcess64"); + } + + if (!NT_SUCCESS(NtWow64QueryInformationProcess64( + hProcess, + ProcessBasicInformation, + &pbi64, + sizeof(pbi64), + NULL))) { + PyErr_SetFromWindowsErr(0); + goto error; + } + + // read peb + if (NtWow64ReadVirtualMemory64 == NULL) { + NtWow64ReadVirtualMemory64 = + (_NtWow64ReadVirtualMemory64)GetProcAddress( + GetModuleHandleA("ntdll.dll"), + "NtWow64ReadVirtualMemory64"); + } + + if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, + pbi64.PebBaseAddress, + &peb64, + sizeof(peb64), + NULL))) { + PyErr_SetFromWindowsErr(0); + goto error; + } + + // read process parameters + if (!NT_SUCCESS(NtWow64ReadVirtualMemory64(hProcess, + peb64.ProcessParameters, + &procParameters64, + sizeof(procParameters64), + NULL))) { + PyErr_SetFromWindowsErr(0); + goto error; + } + + if (pcmdline != NULL) { + // read command line aguments + commandLineContents = + calloc(procParameters64.CommandLine.Length + 2, 1); + if(!commandLineContents) { + PyErr_NoMemory(); + goto error; + } + + if (!NT_SUCCESS(NtWow64ReadVirtualMemory64( + hProcess, + procParameters64.CommandLine.Buffer, + commandLineContents, + procParameters64.CommandLine.Length, + NULL))) { + PyErr_SetFromWindowsErr(0); + goto error; + } + } + + if (pcwd != NULL) { + // read cwd + currentDirectoryContent = + calloc(procParameters64.CurrentDirectoryPath.Length + 2, 1); + if (!currentDirectoryContent) { + PyErr_NoMemory(); + goto error; } - else { + + if (!NT_SUCCESS(NtWow64ReadVirtualMemory64( + hProcess, + procParameters64.CurrentDirectoryPath.Buffer, + currentDirectoryContent, + procParameters64.CurrentDirectoryPath.Length, + NULL))) { PyErr_SetFromWindowsErr(0); + goto error; } + } + } else +#endif + + /* Target process is of the same bitness as us. */ + { + PROCESS_BASIC_INFORMATION pbi; + PEB_ peb; + RTL_USER_PROCESS_PARAMETERS_ procParameters; + + if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, + ProcessBasicInformation, + &pbi, + sizeof(pbi), + NULL))) { + PyErr_SetFromWindowsErr(0); goto error; } - // allocate memory to hold the command line - commandLineContents = (WCHAR *)malloc(commandLine.Length + 1); - if (commandLineContents == NULL) { - PyErr_NoMemory(); + // read peb + if(!ReadProcessMemory(hProcess, + pbi.PebBaseAddress, + &peb, + sizeof(peb), + NULL)) { + PyErr_SetFromWindowsErr(0); goto error; } - // read the command line - if (!ReadProcessMemory(hProcess, commandLine.Buffer, - commandLineContents, commandLine.Length, NULL)) - { + // read process parameters + if(!ReadProcessMemory(hProcess, + peb.ProcessParameters, + &procParameters, + sizeof(procParameters), + NULL)) { PyErr_SetFromWindowsErr(0); goto error; } - // Null-terminate the string to prevent wcslen from returning - // incorrect length the length specifier is in characters, but - // commandLine.Length is in bytes. - commandLineContents[(commandLine.Length / sizeof(WCHAR))] = '\0'; + if (pcmdline != NULL) { + // read command line aguments + commandLineContents = + calloc(procParameters.CommandLine.Length + 2, 1); + if(!commandLineContents) { + PyErr_NoMemory(); + goto error; + } + + if(!ReadProcessMemory(hProcess, + procParameters.CommandLine.Buffer, + commandLineContents, + procParameters.CommandLine.Length, + NULL)) { + PyErr_SetFromWindowsErr(0); + goto error; + } + } + + if (pcwd != NULL) { + // read cwd + currentDirectoryContent = + calloc(procParameters.CurrentDirectoryPath.Length + 2, 1); + if (!currentDirectoryContent) { + PyErr_NoMemory(); + goto error; + } - // attempt to parse the command line using Win32 API, fall back - // on string cmdline version otherwise + if (!ReadProcessMemory(hProcess, + procParameters.CurrentDirectoryPath.Buffer, + currentDirectoryContent, + procParameters.CurrentDirectoryPath.Length, + NULL)) { + PyErr_SetFromWindowsErr(0); + goto error; + } + } + } + + if (pcmdline != NULL) { + // attempt to parse the command line using Win32 API szArglist = CommandLineToArgvW(commandLineContents, &nArgs); if (szArglist == NULL) { PyErr_SetFromWindowsErr(0); @@ -320,60 +651,9 @@ static int psutil_get_parameters(long pid, PyObject **pcmdline, PyObject **pcwd) } if (pcwd != NULL) { - // Read the currentDirectory UNICODE_STRING structure. - // 0x24 refers to "CurrentDirectoryPath" of RTL_USER_PROCESS_PARAMETERS - // structure, see: - // http://wj32.wordpress.com/2009/01/24/ - // howto-get-the-command-line-of-processes/ -#ifdef _WIN64 - if (!ReadProcessMemory(hProcess, (PCHAR)rtlUserProcParamsAddress + 56, - ¤tDirectory, sizeof(currentDirectory), NULL)) -#else - if (!ReadProcessMemory(hProcess, - (PCHAR)rtlUserProcParamsAddress + 0x24, - ¤tDirectory, sizeof(currentDirectory), NULL)) -#endif - { - if (GetLastError() == ERROR_PARTIAL_COPY) { - // this occurs quite often with system processes - AccessDenied(); - } - else { - PyErr_SetFromWindowsErr(0); - } - goto error; - } - - // allocate memory to hold cwd - currentDirectoryContent = (WCHAR *)malloc(currentDirectory.Length + 1); - if (currentDirectoryContent == NULL) { - PyErr_NoMemory(); - goto error; - } - - // read cwd - if (!ReadProcessMemory(hProcess, currentDirectory.Buffer, - currentDirectoryContent, currentDirectory.Length, - NULL)) - { - if (GetLastError() == ERROR_PARTIAL_COPY) { - // this occurs quite often with system processes - AccessDenied(); - } - else { - PyErr_SetFromWindowsErr(0); - } - goto error; - } - - // null-terminate the string to prevent wcslen from returning - // incorrect length the length specifier is in characters, but - // currentDirectory.Length is in bytes - currentDirectoryContent[(currentDirectory.Length / sizeof(WCHAR))] = '\0'; - // convert wchar array to a Python unicode string - py_unicode = PyUnicode_FromWideChar( - currentDirectoryContent, wcslen(currentDirectoryContent)); + py_unicode = PyUnicode_FromWideChar(currentDirectoryContent, + wcslen(currentDirectoryContent)); if (py_unicode == NULL) goto error; CloseHandle(hProcess); diff --git a/test/_windows.py b/test/_windows.py index 2dbc2a3b..90b3bd97 100644 --- a/test/_windows.py +++ b/test/_windows.py @@ -8,6 +8,7 @@ """Windows specific tests. These are implicitly run by test_psutil.py.""" import errno +import glob import os import platform import signal @@ -42,6 +43,9 @@ from test_psutil import WINDOWS cext = psutil._psplatform.cext +# are we a 64 bit process +IS_64_BIT = sys.maxsize > 2**32 + def wrap_exceptions(fun): def wrapper(self, *args, **kwargs): @@ -484,10 +488,80 @@ class TestDualProcessImplementation(unittest.TestCase): self.assertRaises(psutil.NoSuchProcess, meth, ZOMBIE_PID) +class RemoteProcessTestCase(unittest.TestCase): + """Certain functions require calling ReadProcessMemory. This trivially + works when called on the current process. Check that this works on other + processes, especially when they have a different bitness.""" + + @staticmethod + def find_other_interpreter(): + # find a python interpreter that is of the opposite bitness from us + code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))" + + # XXX: a different and probably more stable approach might be to access + # the registry but accessing 64 bit paths from a 32 bit process + for filename in glob.glob(r"C:\Python*\python.exe"): + proc = subprocess.Popen(args=[filename, "-c", code], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + output, _ = proc.communicate() + if output == str(not IS_64_BIT): + return filename + + @classmethod + def setUpClass(cls): + other_python = cls.find_other_interpreter() + + if other_python is None: + raise unittest.SkipTest( + "could not find interpreter with opposite bitness") + + if IS_64_BIT: + cls.python64 = sys.executable + cls.python32 = other_python + else: + cls.python64 = other_python + cls.python32 = sys.executable + + test_args = ["-c", "import sys; sys.stdin.read()"] + + def setUp(self): + self.proc32 = get_test_subprocess([self.python32] + self.test_args) + self.proc64 = get_test_subprocess([self.python64] + self.test_args) + + def tearDown(self): + self.proc32.communicate() + self.proc64.communicate() + reap_children() + + @classmethod + def tearDownClass(cls): + reap_children() + + def test_cmdline_32(self): + p = psutil.Process(self.proc32.pid) + self.assertEqual(len(p.cmdline()), 3) + self.assertEqual(p.cmdline()[1:], self.test_args) + + def test_cmdline_64(self): + p = psutil.Process(self.proc64.pid) + self.assertEqual(len(p.cmdline()), 3) + self.assertEqual(p.cmdline()[1:], self.test_args) + + def test_cwd_32(self): + p = psutil.Process(self.proc32.pid) + self.assertEqual(p.cwd(), os.getcwd()) + + def test_cwd_64(self): + p = psutil.Process(self.proc64.pid) + self.assertEqual(p.cwd(), os.getcwd()) + + def main(): test_suite = unittest.TestSuite() test_suite.addTest(unittest.makeSuite(WindowsSpecificTestCase)) test_suite.addTest(unittest.makeSuite(TestDualProcessImplementation)) + test_suite.addTest(unittest.makeSuite(RemoteProcessTestCase)) result = unittest.TextTestRunner(verbosity=2).run(test_suite) return result.wasSuccessful() diff --git a/test/test_psutil.py b/test/test_psutil.py index d5f52e56..a1dcd082 100644 --- a/test/test_psutil.py +++ b/test/test_psutil.py @@ -3454,7 +3454,9 @@ def main(): elif WINDOWS: from _windows import WindowsSpecificTestCase as stc from _windows import TestDualProcessImplementation + from _windows import RemoteProcessTestCase tests.append(TestDualProcessImplementation) + tests.append(RemoteProcessTestCase) elif OSX: from _osx import OSXSpecificTestCase as stc elif SUNOS: |