|
|
"""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(object): |
|
|
"""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): |
|
|
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): |
|
|
if self.is_unc_path: |
|
|
os.chdir(self.path) |
|
|
|
|
|
|
|
|
class Win32ShellCommandController(object): |
|
|
"""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): |
|
|
"""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!") |
|
|
|