@@ -58,15 +58,20 @@ class PROCESS_INFORMATION(ctypes.Structure):
5858# Win32 API constants needed
5959ERROR_HANDLE_EOF = 38
6060ERROR_BROKEN_PIPE = 109
61+ ERROR_NO_DATA = 232
6162HANDLE_FLAG_INHERIT = 0x0001
6263STARTF_USESTDHANDLES = 0x0100
6364CREATE_SUSPENDED = 0x0004
6465CREATE_NEW_CONSOLE = 0x0010
66+ CREATE_NO_WINDOW = 0x08000000
6567STILL_ACTIVE = 259
6668WAIT_TIMEOUT = 0x0102
6769WAIT_FAILED = 0xFFFFFFFF
6870INFINITE = 0xFFFFFFFF
6971DUPLICATE_SAME_ACCESS = 0x00000002
72+ ENABLE_ECHO_INPUT = 0x0004
73+ ENABLE_LINE_INPUT = 0x0002
74+ ENABLE_PROCESSED_INPUT = 0x0001
7075
7176# Win32 API functions needed
7277GetLastError = ctypes .windll .kernel32 .GetLastError
@@ -108,6 +113,18 @@ class PROCESS_INFORMATION(ctypes.Structure):
108113WriteFile .argtypes = [HANDLE , LPVOID , DWORD , LPDWORD , LPVOID ]
109114WriteFile .restype = BOOL
110115
116+ GetConsoleMode = ctypes .windll .kernel32 .GetConsoleMode
117+ GetConsoleMode .argtypes = [HANDLE , LPDWORD ]
118+ GetConsoleMode .restype = BOOL
119+
120+ SetConsoleMode = ctypes .windll .kernel32 .SetConsoleMode
121+ SetConsoleMode .argtypes = [HANDLE , DWORD ]
122+ SetConsoleMode .restype = BOOL
123+
124+ FlushConsoleInputBuffer = ctypes .windll .kernel32 .FlushConsoleInputBuffer
125+ FlushConsoleInputBuffer .argtypes = [HANDLE ]
126+ FlushConsoleInputBuffer .restype = BOOL
127+
111128WaitForSingleObject = ctypes .windll .kernel32 .WaitForSingleObject
112129WaitForSingleObject .argtypes = [HANDLE , DWORD ]
113130WaitForSingleObject .restype = DWORD
@@ -262,7 +279,7 @@ def create_pipe(uninherit):
262279 siStartInfo .hStdOutput = c_hstdout
263280 siStartInfo .hStdError = c_hstderr
264281 siStartInfo .dwFlags = STARTF_USESTDHANDLES
265- dwCreationFlags = CREATE_SUSPENDED # | CREATE_NEW_CONSOLE
282+ dwCreationFlags = CREATE_SUSPENDED | CREATE_NO_WINDOW # | CREATE_NEW_CONSOLE
266283
267284 if not CreateProcess (None ,
268285 u"cmd.exe /c " + cmd ,
@@ -307,8 +324,6 @@ def create_pipe(uninherit):
307324 return self
308325
309326 def _stdin_thread (self , handle , hprocess , func , stdout_func ):
310- # TODO: Use WaitForInputIdle to avoid calling func() until
311- # an input is actually requested.
312327 exitCode = DWORD ()
313328 bytesWritten = DWORD (0 )
314329 while True :
@@ -351,6 +366,9 @@ def _stdin_thread(self, handle, hprocess, func, stdout_func):
351366 #print("Calling writefile")
352367 if not WriteFile (handle , data , len (data ),
353368 ctypes .byref (bytesWritten ), None ):
369+ # This occurs at exit
370+ if GetLastError () == ERROR_NO_DATA :
371+ return
354372 raise ctypes .WinError ()
355373 #print("Called writefile")
356374 data = data [bytesWritten .value :]
@@ -385,27 +403,30 @@ def run(self, stdout_func = None, stdin_func = None, stderr_func = None):
385403 if stdout_func == None and stdin_func == None and stderr_func == None :
386404 return self ._run_stdio ()
387405
388- if stderr_func != None and self .hstderr == None :
406+ if stderr_func != None and self .mergeout :
389407 raise RuntimeError ("Shell command was initiated with "
390408 "merged stdin/stdout, but a separate stderr_func "
391409 "was provided to the run() method" )
392410
393411 # Create a thread for each input/output handle
412+ stdin_thread = None
394413 threads = []
395414 if stdin_func :
396- threads . append ( threading .Thread (target = self ._stdin_thread ,
415+ stdin_thread = threading .Thread (target = self ._stdin_thread ,
397416 args = (self .hstdin , self .piProcInfo .hProcess ,
398- stdin_func , stdout_func )))
417+ stdin_func , stdout_func ))
399418 threads .append (threading .Thread (target = self ._stdout_thread ,
400419 args = (self .hstdout , stdout_func )))
401- if self .hstderr != None :
420+ if not self .mergeout :
402421 if stderr_func == None :
403422 stderr_func = stdout_func
404423 threads .append (threading .Thread (target = self ._stdout_thread ,
405424 args = (self .hstderr , stderr_func )))
406425 # Start the I/O threads and the process
407426 if ResumeThread (self .piProcInfo .hThread ) == 0xFFFFFFFF :
408427 raise ctypes .WinError ()
428+ if stdin_thread is not None :
429+ stdin_thread .start ()
409430 for thread in threads :
410431 thread .start ()
411432 # Wait for the process to complete
@@ -416,29 +437,67 @@ def run(self, stdout_func = None, stdin_func = None, stderr_func = None):
416437 for thread in threads :
417438 thread .join ()
418439
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 )
440+ # Wait for the stdin thread to complete
441+ if stdin_thread is not None :
442+ stdin_thread .join ()
443+
444+ def _stdin_raw_nonblock (self ):
445+ """Use the raw Win32 handle of sys.stdin to do non-blocking reads"""
446+ # WARNING: This is experimental, and produces inconsistent results.
447+ # It's possible for the handle not to be appropriate for use
448+ # with WaitForSingleObject, among other things.
449+ handle = msvcrt .get_osfhandle (sys .stdin .fileno ())
450+ result = WaitForSingleObject (handle , 100 )
451+ if result == WAIT_FAILED :
452+ raise ctypes .WinError ()
453+ elif result == WAIT_TIMEOUT :
454+ print ("." , end = '' )
433455 return None
456+ else :
457+ data = ctypes .create_string_buffer (256 )
458+ bytesRead = DWORD (0 )
459+ print ('?' , end = '' )
460+
461+ if not ReadFile (handle , data , 256 ,
462+ ctypes .byref (bytesRead ), None ):
463+ raise ctypes .WinError ()
464+ # This ensures the non-blocking works with an actual console
465+ # Not checking the error, so the processing will still work with
466+ # other handle types
467+ FlushConsoleInputBuffer (handle )
468+
469+ data = data .value
470+ data = data .replace ('\r \n ' , '\n ' )
471+ data = data .replace ('\r ' , '\n ' )
472+ print (repr (data ) + " " , end = '' )
473+ return data
474+
475+ def _stdin_raw_block (self ):
476+ """Use a blocking stdin read"""
477+ # The big problem with the blocking read is that it doesn't
478+ # exit when it's supposed to in all contexts. An extra
479+ # key-press may be required to trigger the exit.
480+ try :
481+ data = sys .stdin .read (1 )
482+ data = data .replace ('\r ' , '\n ' )
483+ return data
484+ except WindowsError as we :
485+ if we .winerror == ERROR_NO_DATA :
486+ # This error occurs when the pipe is closed
487+ return None
488+ else :
489+ # Otherwise let the error propagate
490+ raise we
434491
435492 def _stdout_raw (self , s ):
436493 """Writes the string to stdout"""
437494 print (s , end = '' , file = sys .stdout )
495+ sys .stdout .flush ()
438496
439497 def _stderr_raw (self , s ):
440498 """Writes the string to stdout"""
441499 print (s , end = '' , file = sys .stderr )
500+ sys .stderr .flush ()
442501
443502 def _run_stdio (self ):
444503 """Runs the process using the system standard I/O.
@@ -447,14 +506,27 @@ def _run_stdio(self):
447506 sys.stdin object is not used. Instead,
448507 msvcrt.kbhit/getwch are used asynchronously.
449508 """
450- if self .hstderr != None :
509+ # Disable Line and Echo mode
510+ #lpMode = DWORD()
511+ #handle = msvcrt.get_osfhandle(sys.stdin.fileno())
512+ #if GetConsoleMode(handle, ctypes.byref(lpMode)):
513+ # set_console_mode = True
514+ # if not SetConsoleMode(handle, lpMode.value &
515+ # ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT)):
516+ # raise ctypes.WinError()
517+
518+ if self .mergeout :
451519 return self .run (stdout_func = self ._stdout_raw ,
452- stdin_func = self ._stdin_raw ,
453- stderr_func = self ._stderr_raw )
520+ stdin_func = self ._stdin_raw_block )
454521 else :
455522 return self .run (stdout_func = self ._stdout_raw ,
456- stdin_func = self ._stdin_raw )
457-
523+ stdin_func = self ._stdin_raw_block ,
524+ stderr_func = self ._stderr_raw )
525+
526+ # Restore the previous console mode
527+ #if set_console_mode:
528+ # if not SetConsoleMode(handle, lpMode.value):
529+ # raise ctypes.WinError()
458530
459531 def __exit__ (self , exc_type , exc_value , traceback ):
460532 if self .hstdin :
0 commit comments