##// END OF EJS Templates
Create a shell application controller using direct Win32 API
Mark Wiebe -
Show More
This diff has been collapsed as it changes many lines, (502 lines changed) Show them Hide them
@@ -0,0 +1,502 b''
1 """Windows-specific implementation of process utilities with direct WinAPI.
2
3 This file is meant to be used by process.py
4 """
5
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team
8 #
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
12
13 from __future__ import print_function
14
15 # stdlib
16 import os, sys, time, threading
17 import ctypes, msvcrt
18
19 # Win32 API types needed for the API calls
20 from ctypes import POINTER
21 from ctypes.wintypes import HANDLE, HLOCAL, LPVOID, WORD, DWORD, BOOL, \
22 ULONG, LPCWSTR
23 LPDWORD = POINTER(DWORD)
24 LPHANDLE = POINTER(HANDLE)
25 ULONG_PTR = POINTER(ULONG)
26 class SECURITY_ATTRIBUTES(ctypes.Structure):
27 _fields_ = [("nLength", DWORD),
28 ("lpSecurityDescriptor", LPVOID),
29 ("bInheritHandle", BOOL)]
30 LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES)
31 class STARTUPINFO(ctypes.Structure):
32 _fields_ = [("cb", DWORD),
33 ("lpReserved", LPCWSTR),
34 ("lpDesktop", LPCWSTR),
35 ("lpTitle", LPCWSTR),
36 ("dwX", DWORD),
37 ("dwY", DWORD),
38 ("dwXSize", DWORD),
39 ("dwYSize", DWORD),
40 ("dwXCountChars", DWORD),
41 ("dwYCountChars", DWORD),
42 ("dwFillAttribute", DWORD),
43 ("dwFlags", DWORD),
44 ("wShowWindow", WORD),
45 ("cbReserved2", WORD),
46 ("lpReserved2", LPVOID),
47 ("hStdInput", HANDLE),
48 ("hStdOutput", HANDLE),
49 ("hStdError", HANDLE)]
50 LPSTARTUPINFO = POINTER(STARTUPINFO)
51 class PROCESS_INFORMATION(ctypes.Structure):
52 _fields_ = [("hProcess", HANDLE),
53 ("hThread", HANDLE),
54 ("dwProcessId", DWORD),
55 ("dwThreadId", DWORD)]
56 LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
57
58 # Win32 API constants needed
59 ERROR_HANDLE_EOF = 38
60 ERROR_BROKEN_PIPE = 109
61 HANDLE_FLAG_INHERIT = 0x0001
62 STARTF_USESTDHANDLES = 0x0100
63 CREATE_SUSPENDED = 0x0004
64 CREATE_NEW_CONSOLE = 0x0010
65 STILL_ACTIVE = 259
66 WAIT_TIMEOUT = 0x0102
67 WAIT_FAILED = 0xFFFFFFFF
68 INFINITE = 0xFFFFFFFF
69 DUPLICATE_SAME_ACCESS = 0x00000002
70
71 # Win32 API functions needed
72 GetLastError = ctypes.windll.kernel32.GetLastError
73 GetLastError.argtypes = []
74 GetLastError.restype = DWORD
75
76 CreateFile = ctypes.windll.kernel32.CreateFileW
77 CreateFile.argtypes = [LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE]
78 CreateFile.restype = HANDLE
79
80 CreatePipe = ctypes.windll.kernel32.CreatePipe
81 CreatePipe.argtypes = [POINTER(HANDLE), POINTER(HANDLE),
82 LPSECURITY_ATTRIBUTES, DWORD]
83 CreatePipe.restype = BOOL
84
85 CreateProcess = ctypes.windll.kernel32.CreateProcessW
86 CreateProcess.argtypes = [LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES,
87 LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFO,
88 LPPROCESS_INFORMATION]
89 CreateProcess.restype = BOOL
90
91 GetExitCodeProcess = ctypes.windll.kernel32.GetExitCodeProcess
92 GetExitCodeProcess.argtypes = [HANDLE, LPDWORD]
93 GetExitCodeProcess.restype = BOOL
94
95 GetCurrentProcess = ctypes.windll.kernel32.GetCurrentProcess
96 GetCurrentProcess.argtypes = []
97 GetCurrentProcess.restype = HANDLE
98
99 ResumeThread = ctypes.windll.kernel32.ResumeThread
100 ResumeThread.argtypes = [HANDLE]
101 ResumeThread.restype = DWORD
102
103 ReadFile = ctypes.windll.kernel32.ReadFile
104 ReadFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
105 ReadFile.restype = BOOL
106
107 WriteFile = ctypes.windll.kernel32.WriteFile
108 WriteFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
109 WriteFile.restype = BOOL
110
111 WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
112 WaitForSingleObject.argtypes = [HANDLE, DWORD]
113 WaitForSingleObject.restype = DWORD
114
115 DuplicateHandle = ctypes.windll.kernel32.DuplicateHandle
116 DuplicateHandle.argtypes = [HANDLE, HANDLE, HANDLE, LPHANDLE,
117 DWORD, BOOL, DWORD]
118 DuplicateHandle.restype = BOOL
119
120 SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation
121 SetHandleInformation.argtypes = [HANDLE, DWORD, DWORD]
122 SetHandleInformation.restype = BOOL
123
124 CloseHandle = ctypes.windll.kernel32.CloseHandle
125 CloseHandle.argtypes = [HANDLE]
126 CloseHandle.restype = BOOL
127
128 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
129 CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(ctypes.c_int)]
130 CommandLineToArgvW.restype = POINTER(LPCWSTR)
131
132 LocalFree = ctypes.windll.kernel32.LocalFree
133 LocalFree.argtypes = [HLOCAL]
134 LocalFree.restype = HLOCAL
135
136 class AvoidUNCPath(object):
137 """A context manager to protect command execution from UNC paths.
138
139 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
140 This context manager temporarily changes directory to the 'C:' drive on
141 entering, and restores the original working directory on exit.
142
143 The context manager returns the starting working directory *if* it made a
144 change and None otherwise, so that users can apply the necessary adjustment
145 to their system calls in the event of a change.
146
147 Example
148 -------
149 ::
150 cmd = 'dir'
151 with AvoidUNCPath() as path:
152 if path is not None:
153 cmd = '"pushd %s &&"%s' % (path, cmd)
154 os.system(cmd)
155 """
156 def __enter__(self):
157 self.path = os.getcwdu()
158 self.is_unc_path = self.path.startswith(r"\\")
159 if self.is_unc_path:
160 # change to c drive (as cmd.exe cannot handle UNC addresses)
161 os.chdir("C:")
162 return self.path
163 else:
164 # We return None to signal that there was no change in the working
165 # directory
166 return None
167
168 def __exit__(self, exc_type, exc_value, traceback):
169 if self.is_unc_path:
170 os.chdir(self.path)
171
172
173 class Win32ShellCommandController(object):
174 """Runs a shell command in a 'with' context.
175
176 This implementation is Win32-specific.
177
178 Example:
179 # Runs the command interactively with default console stdin/stdout
180 with ShellCommandController('python -i') as scc:
181 scc.run()
182
183 # Runs the command using the provided functions for stdin/stdout
184 def my_stdout_func(s):
185 # print or save the string 's'
186 write_to_stdout(s)
187 def my_stdin_func():
188 # If input is available, return it as a string.
189 if input_available():
190 return get_input()
191 # If no input available, return None after a short delay to
192 # keep from blocking.
193 else:
194 time.sleep(0.01)
195 return None
196
197 with ShellCommandController('python -i') as scc:
198 scc.run(my_stdout_func, my_stdin_func)
199 """
200
201 def __init__(self, cmd, mergeout = True):
202 """Initializes the shell command controller.
203
204 The cmd is the program to execute, and mergeout is
205 whether to blend stdout and stderr into one output
206 in stdout. Merging them together in this fashion more
207 reliably keeps stdout and stderr in the correct order
208 especially for interactive shell usage.
209 """
210 self.cmd = cmd
211 self.mergeout = mergeout
212
213 def __enter__(self):
214 cmd = self.cmd
215 mergeout = self.mergeout
216
217 self.hstdout, self.hstdin, self.hstderr = None, None, None
218 self.piProcInfo = None
219 try:
220 p_hstdout, c_hstdout, p_hstderr, \
221 c_hstderr, p_hstdin, c_hstdin = [None]*6
222
223 # SECURITY_ATTRIBUTES with inherit handle set to True
224 saAttr = SECURITY_ATTRIBUTES()
225 saAttr.nLength = ctypes.sizeof(saAttr)
226 saAttr.bInheritHandle = True
227 saAttr.lpSecurityDescriptor = None
228
229 def create_pipe(uninherit):
230 """Creates a Windows pipe, which consists of two handles.
231
232 The 'uninherit' parameter controls which handle is not
233 inherited by the child process.
234 """
235 handles = HANDLE(), HANDLE()
236 if not CreatePipe(ctypes.byref(handles[0]),
237 ctypes.byref(handles[1]), ctypes.byref(saAttr), 0):
238 raise ctypes.WinError()
239 if not SetHandleInformation(handles[uninherit],
240 HANDLE_FLAG_INHERIT, 0):
241 raise ctypes.WinError()
242 return handles[0].value, handles[1].value
243
244 p_hstdout, c_hstdout = create_pipe(uninherit=0)
245 # 'mergeout' signals that stdout and stderr should be merged.
246 # We do that by using one pipe for both of them.
247 if mergeout:
248 c_hstderr = HANDLE()
249 if not DuplicateHandle(GetCurrentProcess(), c_hstdout,
250 GetCurrentProcess(), ctypes.byref(c_hstderr),
251 0, True, DUPLICATE_SAME_ACCESS):
252 raise ctypes.WinError()
253 else:
254 p_hstderr, c_hstderr = create_pipe(uninherit=0)
255 c_hstdin, p_hstdin = create_pipe(uninherit=1)
256
257 # Create the process object
258 piProcInfo = PROCESS_INFORMATION()
259 siStartInfo = STARTUPINFO()
260 siStartInfo.cb = ctypes.sizeof(siStartInfo)
261 siStartInfo.hStdInput = c_hstdin
262 siStartInfo.hStdOutput = c_hstdout
263 siStartInfo.hStdError = c_hstderr
264 siStartInfo.dwFlags = STARTF_USESTDHANDLES
265 dwCreationFlags = CREATE_SUSPENDED # | CREATE_NEW_CONSOLE
266
267 if not CreateProcess(None,
268 u"cmd.exe /c " + cmd,
269 None, None, True, dwCreationFlags,
270 None, None, ctypes.byref(siStartInfo),
271 ctypes.byref(piProcInfo)):
272 raise ctypes.WinError()
273
274 # Close this process's versions of the child handles
275 CloseHandle(c_hstdin)
276 c_hstdin = None
277 CloseHandle(c_hstdout)
278 c_hstdout = None
279 if c_hstderr != None:
280 CloseHandle(c_hstderr)
281 c_hstderr = None
282
283 # Transfer ownership of the parent handles to the object
284 self.hstdin = p_hstdin
285 p_hstdin = None
286 self.hstdout = p_hstdout
287 p_hstdout = None
288 if not mergeout:
289 self.hstderr = p_hstderr
290 p_hstderr = None
291 self.piProcInfo = piProcInfo
292
293 finally:
294 if p_hstdin:
295 CloseHandle(p_hstdin)
296 if c_hstdin:
297 CloseHandle(c_hstdin)
298 if p_hstdout:
299 CloseHandle(p_hstdout)
300 if c_hstdout:
301 CloseHandle(c_hstdout)
302 if p_hstderr:
303 CloseHandle(p_hstderr)
304 if c_hstderr:
305 CloseHandle(c_hstderr)
306
307 return self
308
309 def _stdin_thread(self, handle, hprocess, func, stdout_func):
310 # TODO: Use WaitForInputIdle to avoid calling func() until
311 # an input is actually requested.
312 exitCode = DWORD()
313 bytesWritten = DWORD(0)
314 while True:
315 #print("stdin thread loop start")
316 # Get the input string (may be bytes or unicode)
317 data = func()
318
319 # None signals to poll whether the process has exited
320 if data is None:
321 #print("checking for process completion")
322 if not GetExitCodeProcess(hprocess, ctypes.byref(exitCode)):
323 raise ctypes.WinError()
324 if exitCode.value != STILL_ACTIVE:
325 return
326 # TESTING: Does zero-sized writefile help?
327 if not WriteFile(handle, "", 0,
328 ctypes.byref(bytesWritten), None):
329 raise ctypes.WinError()
330 continue
331 #print("\nGot str %s\n" % repr(data), file=sys.stderr)
332
333 # Encode the string to the console encoding
334 if isinstance(data, unicode): #FIXME: Python3
335 data = data.encode('utf_8')
336
337 # What we have now must be a string of bytes
338 if not isinstance(data, str): #FIXME: Python3
339 raise RuntimeError("internal stdin function string error")
340
341 # An empty string signals EOF
342 if len(data) == 0:
343 return
344
345 # In a windows console, sometimes the input is echoed,
346 # but sometimes not. How do we determine when to do this?
347 stdout_func(data)
348 # WriteFile may not accept all the data at once.
349 # Loop until everything is processed
350 while len(data) != 0:
351 #print("Calling writefile")
352 if not WriteFile(handle, data, len(data),
353 ctypes.byref(bytesWritten), None):
354 raise ctypes.WinError()
355 #print("Called writefile")
356 data = data[bytesWritten.value:]
357
358 def _stdout_thread(self, handle, func):
359 # Allocate the output buffer
360 data = ctypes.create_string_buffer(4096)
361 while True:
362 bytesRead = DWORD(0)
363 if not ReadFile(handle, data, 4096,
364 ctypes.byref(bytesRead), None):
365 le = GetLastError()
366 if le == ERROR_BROKEN_PIPE:
367 return
368 else:
369 raise ctypes.WinError()
370 # FIXME: Python3
371 s = data.value[0:bytesRead.value]
372 #print("\nv: %s" % repr(s), file=sys.stderr)
373 func(s.decode('utf_8', 'replace'))
374
375 def run(self, stdout_func = None, stdin_func = None, stderr_func = None):
376 """Runs the process, using the provided functions for I/O.
377
378 The function stdin_func should return strings whenever a
379 character or characters become available.
380 The functions stdout_func and stderr_func are called whenever
381 something is printed to stdout or stderr, respectively.
382 These functions are called from different threads (but not
383 concurrently, because of the GIL).
384 """
385 if stdout_func == None and stdin_func == None and stderr_func == None:
386 return self._run_stdio()
387
388 if stderr_func != None and self.hstderr == None:
389 raise RuntimeError("Shell command was initiated with "
390 "merged stdin/stdout, but a separate stderr_func "
391 "was provided to the run() method")
392
393 # Create a thread for each input/output handle
394 threads = []
395 if stdin_func:
396 threads.append(threading.Thread(target=self._stdin_thread,
397 args=(self.hstdin, self.piProcInfo.hProcess,
398 stdin_func, stdout_func)))
399 threads.append(threading.Thread(target=self._stdout_thread,
400 args=(self.hstdout, stdout_func)))
401 if self.hstderr != None:
402 if stderr_func == None:
403 stderr_func = stdout_func
404 threads.append(threading.Thread(target=self._stdout_thread,
405 args=(self.hstderr, stderr_func)))
406 # Start the I/O threads and the process
407 if ResumeThread(self.piProcInfo.hThread) == 0xFFFFFFFF:
408 raise ctypes.WinError()
409 for thread in threads:
410 thread.start()
411 # Wait for the process to complete
412 if WaitForSingleObject(self.piProcInfo.hProcess, INFINITE) == \
413 WAIT_FAILED:
414 raise ctypes.WinError()
415 # Wait for the I/O threads to complete
416 for thread in threads:
417 thread.join()
418
419 def _stdin_raw(self):
420 """Uses msvcrt.kbhit/getwch to do read stdin without blocking"""
421 if msvcrt.kbhit():
422 #s = msvcrt.getwch()
423 s = msvcrt.getwch()
424 # Key code for Enter is '\r', but need to give back '\n'
425 if s == u'\r':
426 s = u'\n'
427 return s
428 else:
429 # This should make it poll at about 100 Hz, which
430 # is hopefully good enough to be responsive but
431 # doesn't waste CPU.
432 time.sleep(0.01)
433 return None
434
435 def _stdout_raw(self, s):
436 """Writes the string to stdout"""
437 print(s, end='', file=sys.stdout)
438
439 def _stderr_raw(self, s):
440 """Writes the string to stdout"""
441 print(s, end='', file=sys.stderr)
442
443 def _run_stdio(self):
444 """Runs the process using the system standard I/O.
445
446 IMPORTANT: stdin needs to be asynchronous, so the Python
447 sys.stdin object is not used. Instead,
448 msvcrt.kbhit/getwch are used asynchronously.
449 """
450 if self.hstderr != None:
451 return self.run(stdout_func = self._stdout_raw,
452 stdin_func = self._stdin_raw,
453 stderr_func = self._stderr_raw)
454 else:
455 return self.run(stdout_func = self._stdout_raw,
456 stdin_func = self._stdin_raw)
457
458
459 def __exit__(self, exc_type, exc_value, traceback):
460 if self.hstdin:
461 CloseHandle(self.hstdin)
462 self.hstdin = None
463 if self.hstdout:
464 CloseHandle(self.hstdout)
465 self.hstdout = None
466 if self.hstderr:
467 CloseHandle(self.hstderr)
468 self.hstderr = None
469 if self.piProcInfo != None:
470 CloseHandle(self.piProcInfo.hProcess)
471 CloseHandle(self.piProcInfo.hThread)
472 self.piProcInfo = None
473
474
475 def system(cmd):
476 """Win32 version of os.system() that works with network shares.
477
478 Note that this implementation returns None, as meant for use in IPython.
479
480 Parameters
481 ----------
482 cmd : str
483 A command to be executed in the system shell.
484
485 Returns
486 -------
487 None : we explicitly do NOT return the subprocess status code, as this
488 utility is meant to be used extensively in IPython, where any return value
489 would trigger :func:`sys.displayhook` calls.
490 """
491 with AvoidUNCPath() as path:
492 if path is not None:
493 cmd = '"pushd %s &&"%s' % (path, cmd)
494 with Win32ShellCommandController(cmd) as scc:
495 scc.run()
496
497
498 if __name__ == "__main__":
499 print("Test starting!")
500 #system("cmd")
501 system("python -i")
502 print("Test finished!")
General Comments 0
You need to be logged in to leave comments. Login now