| """Windows-specific implementation of process utilities with direct WinAPI. |
| |
| This file is meant to be used by process.py |
| """ |
|
|
| |
| |
| |
| |
| |
| |
|
|
|
|
| |
| import os, sys, threading |
| import ctypes, msvcrt |
|
|
| |
| from ctypes import POINTER |
| from ctypes.wintypes import HANDLE, HLOCAL, LPVOID, WORD, DWORD, BOOL, \ |
| ULONG, LPCWSTR |
| LPDWORD = POINTER(DWORD) |
| LPHANDLE = POINTER(HANDLE) |
| ULONG_PTR = POINTER(ULONG) |
| class SECURITY_ATTRIBUTES(ctypes.Structure): |
| _fields_ = [("nLength", DWORD), |
| ("lpSecurityDescriptor", LPVOID), |
| ("bInheritHandle", BOOL)] |
| LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES) |
| class STARTUPINFO(ctypes.Structure): |
| _fields_ = [("cb", DWORD), |
| ("lpReserved", LPCWSTR), |
| ("lpDesktop", LPCWSTR), |
| ("lpTitle", LPCWSTR), |
| ("dwX", DWORD), |
| ("dwY", DWORD), |
| ("dwXSize", DWORD), |
| ("dwYSize", DWORD), |
| ("dwXCountChars", DWORD), |
| ("dwYCountChars", DWORD), |
| ("dwFillAttribute", DWORD), |
| ("dwFlags", DWORD), |
| ("wShowWindow", WORD), |
| ("cbReserved2", WORD), |
| ("lpReserved2", LPVOID), |
| ("hStdInput", HANDLE), |
| ("hStdOutput", HANDLE), |
| ("hStdError", HANDLE)] |
| LPSTARTUPINFO = POINTER(STARTUPINFO) |
| class PROCESS_INFORMATION(ctypes.Structure): |
| _fields_ = [("hProcess", HANDLE), |
| ("hThread", HANDLE), |
| ("dwProcessId", DWORD), |
| ("dwThreadId", DWORD)] |
| LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) |
|
|
| |
| ERROR_HANDLE_EOF = 38 |
| ERROR_BROKEN_PIPE = 109 |
| ERROR_NO_DATA = 232 |
| HANDLE_FLAG_INHERIT = 0x0001 |
| STARTF_USESTDHANDLES = 0x0100 |
| CREATE_SUSPENDED = 0x0004 |
| CREATE_NEW_CONSOLE = 0x0010 |
| CREATE_NO_WINDOW = 0x08000000 |
| STILL_ACTIVE = 259 |
| WAIT_TIMEOUT = 0x0102 |
| WAIT_FAILED = 0xFFFFFFFF |
| INFINITE = 0xFFFFFFFF |
| DUPLICATE_SAME_ACCESS = 0x00000002 |
| ENABLE_ECHO_INPUT = 0x0004 |
| ENABLE_LINE_INPUT = 0x0002 |
| ENABLE_PROCESSED_INPUT = 0x0001 |
|
|
| |
| GetLastError = ctypes.windll.kernel32.GetLastError |
| GetLastError.argtypes = [] |
| GetLastError.restype = DWORD |
|
|
| CreateFile = ctypes.windll.kernel32.CreateFileW |
| CreateFile.argtypes = [LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE] |
| CreateFile.restype = HANDLE |
|
|
| CreatePipe = ctypes.windll.kernel32.CreatePipe |
| CreatePipe.argtypes = [POINTER(HANDLE), POINTER(HANDLE), |
| LPSECURITY_ATTRIBUTES, DWORD] |
| CreatePipe.restype = BOOL |
|
|
| CreateProcess = ctypes.windll.kernel32.CreateProcessW |
| CreateProcess.argtypes = [LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES, |
| LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFO, |
| LPPROCESS_INFORMATION] |
| CreateProcess.restype = BOOL |
|
|
| GetExitCodeProcess = ctypes.windll.kernel32.GetExitCodeProcess |
| GetExitCodeProcess.argtypes = [HANDLE, LPDWORD] |
| GetExitCodeProcess.restype = BOOL |
|
|
| GetCurrentProcess = ctypes.windll.kernel32.GetCurrentProcess |
| GetCurrentProcess.argtypes = [] |
| GetCurrentProcess.restype = HANDLE |
|
|
| ResumeThread = ctypes.windll.kernel32.ResumeThread |
| ResumeThread.argtypes = [HANDLE] |
| ResumeThread.restype = DWORD |
|
|
| ReadFile = ctypes.windll.kernel32.ReadFile |
| ReadFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID] |
| ReadFile.restype = BOOL |
|
|
| WriteFile = ctypes.windll.kernel32.WriteFile |
| WriteFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID] |
| WriteFile.restype = BOOL |
|
|
| GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode |
| GetConsoleMode.argtypes = [HANDLE, LPDWORD] |
| GetConsoleMode.restype = BOOL |
|
|
| SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode |
| SetConsoleMode.argtypes = [HANDLE, DWORD] |
| SetConsoleMode.restype = BOOL |
|
|
| FlushConsoleInputBuffer = ctypes.windll.kernel32.FlushConsoleInputBuffer |
| FlushConsoleInputBuffer.argtypes = [HANDLE] |
| FlushConsoleInputBuffer.restype = BOOL |
|
|
| WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject |
| WaitForSingleObject.argtypes = [HANDLE, DWORD] |
| WaitForSingleObject.restype = DWORD |
|
|
| DuplicateHandle = ctypes.windll.kernel32.DuplicateHandle |
| DuplicateHandle.argtypes = [HANDLE, HANDLE, HANDLE, LPHANDLE, |
| DWORD, BOOL, DWORD] |
| DuplicateHandle.restype = BOOL |
|
|
| SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation |
| SetHandleInformation.argtypes = [HANDLE, DWORD, DWORD] |
| SetHandleInformation.restype = BOOL |
|
|
| CloseHandle = ctypes.windll.kernel32.CloseHandle |
| CloseHandle.argtypes = [HANDLE] |
| CloseHandle.restype = BOOL |
|
|
| CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW |
| CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(ctypes.c_int)] |
| CommandLineToArgvW.restype = POINTER(LPCWSTR) |
|
|
| LocalFree = ctypes.windll.kernel32.LocalFree |
| LocalFree.argtypes = [HLOCAL] |
| LocalFree.restype = HLOCAL |
|
|
| class AvoidUNCPath: |
| """A context manager to protect command execution from UNC paths. |
| |
| In the Win32 API, commands can't be invoked with the cwd being a UNC path. |
| This context manager temporarily changes directory to the 'C:' drive on |
| entering, and restores the original working directory on exit. |
| |
| The context manager returns the starting working directory *if* it made a |
| change and None otherwise, so that users can apply the necessary adjustment |
| to their system calls in the event of a change. |
| |
| Examples |
| -------- |
| :: |
| cmd = 'dir' |
| with AvoidUNCPath() as path: |
| if path is not None: |
| cmd = '"pushd %s &&"%s' % (path, cmd) |
| os.system(cmd) |
| """ |
|
|
| def __enter__(self) -> None: |
| self.path = os.getcwd() |
| self.is_unc_path = self.path.startswith(r"\\") |
| if self.is_unc_path: |
| |
| os.chdir("C:") |
| return self.path |
| else: |
| |
| |
| return None |
|
|
| def __exit__(self, exc_type, exc_value, traceback) -> None: |
| if self.is_unc_path: |
| os.chdir(self.path) |
|
|
|
|
| class Win32ShellCommandController: |
| """Runs a shell command in a 'with' context. |
| |
| This implementation is Win32-specific. |
| |
| Example: |
| # Runs the command interactively with default console stdin/stdout |
| with ShellCommandController('python -i') as scc: |
| scc.run() |
| |
| # Runs the command using the provided functions for stdin/stdout |
| def my_stdout_func(s): |
| # print or save the string 's' |
| write_to_stdout(s) |
| def my_stdin_func(): |
| # If input is available, return it as a string. |
| if input_available(): |
| return get_input() |
| # If no input available, return None after a short delay to |
| # keep from blocking. |
| else: |
| time.sleep(0.01) |
| return None |
| |
| with ShellCommandController('python -i') as scc: |
| scc.run(my_stdout_func, my_stdin_func) |
| """ |
|
|
| def __init__(self, cmd, mergeout = True): |
| """Initializes the shell command controller. |
| |
| The cmd is the program to execute, and mergeout is |
| whether to blend stdout and stderr into one output |
| in stdout. Merging them together in this fashion more |
| reliably keeps stdout and stderr in the correct order |
| especially for interactive shell usage. |
| """ |
| self.cmd = cmd |
| self.mergeout = mergeout |
|
|
| def __enter__(self): |
| cmd = self.cmd |
| mergeout = self.mergeout |
|
|
| self.hstdout, self.hstdin, self.hstderr = None, None, None |
| self.piProcInfo = None |
| try: |
| p_hstdout, c_hstdout, p_hstderr, \ |
| c_hstderr, p_hstdin, c_hstdin = [None]*6 |
|
|
| |
| saAttr = SECURITY_ATTRIBUTES() |
| saAttr.nLength = ctypes.sizeof(saAttr) |
| saAttr.bInheritHandle = True |
| saAttr.lpSecurityDescriptor = None |
|
|
| def create_pipe(uninherit): |
| """Creates a Windows pipe, which consists of two handles. |
| |
| The 'uninherit' parameter controls which handle is not |
| inherited by the child process. |
| """ |
| handles = HANDLE(), HANDLE() |
| if not CreatePipe(ctypes.byref(handles[0]), |
| ctypes.byref(handles[1]), ctypes.byref(saAttr), 0): |
| raise ctypes.WinError() |
| if not SetHandleInformation(handles[uninherit], |
| HANDLE_FLAG_INHERIT, 0): |
| raise ctypes.WinError() |
| return handles[0].value, handles[1].value |
|
|
| p_hstdout, c_hstdout = create_pipe(uninherit=0) |
| |
| |
| if mergeout: |
| c_hstderr = HANDLE() |
| if not DuplicateHandle(GetCurrentProcess(), c_hstdout, |
| GetCurrentProcess(), ctypes.byref(c_hstderr), |
| 0, True, DUPLICATE_SAME_ACCESS): |
| raise ctypes.WinError() |
| else: |
| p_hstderr, c_hstderr = create_pipe(uninherit=0) |
| c_hstdin, p_hstdin = create_pipe(uninherit=1) |
|
|
| |
| piProcInfo = PROCESS_INFORMATION() |
| siStartInfo = STARTUPINFO() |
| siStartInfo.cb = ctypes.sizeof(siStartInfo) |
| siStartInfo.hStdInput = c_hstdin |
| siStartInfo.hStdOutput = c_hstdout |
| siStartInfo.hStdError = c_hstderr |
| siStartInfo.dwFlags = STARTF_USESTDHANDLES |
| dwCreationFlags = CREATE_SUSPENDED | CREATE_NO_WINDOW |
|
|
| if not CreateProcess(None, |
| u"cmd.exe /c " + cmd, |
| None, None, True, dwCreationFlags, |
| None, None, ctypes.byref(siStartInfo), |
| ctypes.byref(piProcInfo)): |
| raise ctypes.WinError() |
|
|
| |
| CloseHandle(c_hstdin) |
| c_hstdin = None |
| CloseHandle(c_hstdout) |
| c_hstdout = None |
| if c_hstderr is not None: |
| CloseHandle(c_hstderr) |
| c_hstderr = None |
|
|
| |
| self.hstdin = p_hstdin |
| p_hstdin = None |
| self.hstdout = p_hstdout |
| p_hstdout = None |
| if not mergeout: |
| self.hstderr = p_hstderr |
| p_hstderr = None |
| self.piProcInfo = piProcInfo |
|
|
| finally: |
| if p_hstdin: |
| CloseHandle(p_hstdin) |
| if c_hstdin: |
| CloseHandle(c_hstdin) |
| if p_hstdout: |
| CloseHandle(p_hstdout) |
| if c_hstdout: |
| CloseHandle(c_hstdout) |
| if p_hstderr: |
| CloseHandle(p_hstderr) |
| if c_hstderr: |
| CloseHandle(c_hstderr) |
|
|
| return self |
|
|
| def _stdin_thread(self, handle, hprocess, func, stdout_func): |
| exitCode = DWORD() |
| bytesWritten = DWORD(0) |
| while True: |
| |
| |
| data = func() |
|
|
| |
| if data is None: |
| |
| if not GetExitCodeProcess(hprocess, ctypes.byref(exitCode)): |
| raise ctypes.WinError() |
| if exitCode.value != STILL_ACTIVE: |
| return |
| |
| if not WriteFile(handle, "", 0, |
| ctypes.byref(bytesWritten), None): |
| raise ctypes.WinError() |
| continue |
| |
|
|
| |
| if isinstance(data, unicode): |
| data = data.encode('utf_8') |
|
|
| |
| if not isinstance(data, str): |
| raise RuntimeError("internal stdin function string error") |
|
|
| |
| if len(data) == 0: |
| return |
|
|
| |
| |
| stdout_func(data) |
| |
| |
| while len(data) != 0: |
| |
| if not WriteFile(handle, data, len(data), |
| ctypes.byref(bytesWritten), None): |
| |
| if GetLastError() == ERROR_NO_DATA: |
| return |
| raise ctypes.WinError() |
| |
| data = data[bytesWritten.value:] |
|
|
| def _stdout_thread(self, handle, func): |
| |
| data = ctypes.create_string_buffer(4096) |
| while True: |
| bytesRead = DWORD(0) |
| if not ReadFile(handle, data, 4096, |
| ctypes.byref(bytesRead), None): |
| le = GetLastError() |
| if le == ERROR_BROKEN_PIPE: |
| return |
| else: |
| raise ctypes.WinError() |
| |
| s = data.value[0:bytesRead.value] |
| |
| func(s.decode('utf_8', 'replace')) |
|
|
| def run(self, stdout_func = None, stdin_func = None, stderr_func = None): |
| """Runs the process, using the provided functions for I/O. |
| |
| The function stdin_func should return strings whenever a |
| character or characters become available. |
| The functions stdout_func and stderr_func are called whenever |
| something is printed to stdout or stderr, respectively. |
| These functions are called from different threads (but not |
| concurrently, because of the GIL). |
| """ |
| if stdout_func is None and stdin_func is None and stderr_func is None: |
| return self._run_stdio() |
|
|
| if stderr_func is not None and self.mergeout: |
| raise RuntimeError("Shell command was initiated with " |
| "merged stdin/stdout, but a separate stderr_func " |
| "was provided to the run() method") |
|
|
| |
| stdin_thread = None |
| threads = [] |
| if stdin_func: |
| stdin_thread = threading.Thread(target=self._stdin_thread, |
| args=(self.hstdin, self.piProcInfo.hProcess, |
| stdin_func, stdout_func)) |
| threads.append(threading.Thread(target=self._stdout_thread, |
| args=(self.hstdout, stdout_func))) |
| if not self.mergeout: |
| if stderr_func is None: |
| stderr_func = stdout_func |
| threads.append(threading.Thread(target=self._stdout_thread, |
| args=(self.hstderr, stderr_func))) |
| |
| if ResumeThread(self.piProcInfo.hThread) == 0xFFFFFFFF: |
| raise ctypes.WinError() |
| if stdin_thread is not None: |
| stdin_thread.start() |
| for thread in threads: |
| thread.start() |
| |
| if WaitForSingleObject(self.piProcInfo.hProcess, INFINITE) == \ |
| WAIT_FAILED: |
| raise ctypes.WinError() |
| |
| for thread in threads: |
| thread.join() |
|
|
| |
| if stdin_thread is not None: |
| stdin_thread.join() |
|
|
| def _stdin_raw_nonblock(self): |
| """Use the raw Win32 handle of sys.stdin to do non-blocking reads""" |
| |
| |
| |
| handle = msvcrt.get_osfhandle(sys.stdin.fileno()) |
| result = WaitForSingleObject(handle, 100) |
| if result == WAIT_FAILED: |
| raise ctypes.WinError() |
| elif result == WAIT_TIMEOUT: |
| print(".", end='') |
| return None |
| else: |
| data = ctypes.create_string_buffer(256) |
| bytesRead = DWORD(0) |
| print('?', end='') |
|
|
| if not ReadFile(handle, data, 256, |
| ctypes.byref(bytesRead), None): |
| raise ctypes.WinError() |
| |
| |
| |
| FlushConsoleInputBuffer(handle) |
|
|
| data = data.value |
| data = data.replace('\r\n', '\n') |
| data = data.replace('\r', '\n') |
| print(repr(data) + " ", end='') |
| return data |
|
|
| def _stdin_raw_block(self): |
| """Use a blocking stdin read""" |
| |
| |
| |
| try: |
| data = sys.stdin.read(1) |
| data = data.replace('\r', '\n') |
| return data |
| except WindowsError as we: |
| if we.winerror == ERROR_NO_DATA: |
| |
| return None |
| else: |
| |
| raise we |
|
|
| def _stdout_raw(self, s): |
| """Writes the string to stdout""" |
| print(s, end='', file=sys.stdout) |
| sys.stdout.flush() |
|
|
| def _stderr_raw(self, s): |
| """Writes the string to stdout""" |
| print(s, end='', file=sys.stderr) |
| sys.stderr.flush() |
|
|
| def _run_stdio(self): |
| """Runs the process using the system standard I/O. |
| |
| IMPORTANT: stdin needs to be asynchronous, so the Python |
| sys.stdin object is not used. Instead, |
| msvcrt.kbhit/getwch are used asynchronously. |
| """ |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| if self.mergeout: |
| return self.run(stdout_func = self._stdout_raw, |
| stdin_func = self._stdin_raw_block) |
| else: |
| return self.run(stdout_func = self._stdout_raw, |
| stdin_func = self._stdin_raw_block, |
| stderr_func = self._stderr_raw) |
|
|
| |
| |
| |
| |
|
|
| def __exit__(self, exc_type, exc_value, traceback): |
| if self.hstdin: |
| CloseHandle(self.hstdin) |
| self.hstdin = None |
| if self.hstdout: |
| CloseHandle(self.hstdout) |
| self.hstdout = None |
| if self.hstderr: |
| CloseHandle(self.hstderr) |
| self.hstderr = None |
| if self.piProcInfo is not None: |
| CloseHandle(self.piProcInfo.hProcess) |
| CloseHandle(self.piProcInfo.hThread) |
| self.piProcInfo = None |
|
|
|
|
| def system(cmd: str) -> None: |
| """Win32 version of os.system() that works with network shares. |
| |
| Note that this implementation returns None, as meant for use in IPython. |
| |
| Parameters |
| ---------- |
| cmd : str |
| A command to be executed in the system shell. |
| |
| Returns |
| ------- |
| None : we explicitly do NOT return the subprocess status code, as this |
| utility is meant to be used extensively in IPython, where any return value |
| would trigger : func:`sys.displayhook` calls. |
| """ |
| with AvoidUNCPath() as path: |
| if path is not None: |
| cmd = '"pushd %s &&"%s' % (path, cmd) |
| with Win32ShellCommandController(cmd) as scc: |
| scc.run() |
|
|
|
|
| if __name__ == "__main__": |
| print("Test starting!") |
| |
| system("python -i") |
| print("Test finished!") |
|
|