Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b0673ac

Browse files
committed
Tweak out the process controller class so it works interactively better
In the IPython shell launched from the console, it works pretty well now. From the qt-console, not so much.
1 parent 6a6c4e9 commit b0673ac

2 files changed

Lines changed: 103 additions & 30 deletions

File tree

IPython/utils/_process_win32.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import os
2020
import sys
2121
import ctypes
22+
import msvcrt
2223

2324
from ctypes import c_int, POINTER
2425
from ctypes.wintypes import LPCWSTR, HLOCAL
@@ -121,10 +122,10 @@ def system(cmd):
121122
utility is meant to be used extensively in IPython, where any return value
122123
would trigger :func:`sys.displayhook` calls.
123124
"""
124-
with AvoidUNCPath() as path:
125-
if path is not None:
126-
cmd = '"pushd %s &&"%s' % (path, cmd)
127-
return process_handler(cmd, _system_body)
125+
# The controller provides interactivity with both
126+
# stdin and stdout
127+
import _process_win32_controller
128+
_process_win32_controller.system(cmd)
128129

129130

130131
def getoutput(cmd):

IPython/utils/_process_win32_controller.py

Lines changed: 98 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,20 @@ class PROCESS_INFORMATION(ctypes.Structure):
5858
# Win32 API constants needed
5959
ERROR_HANDLE_EOF = 38
6060
ERROR_BROKEN_PIPE = 109
61+
ERROR_NO_DATA = 232
6162
HANDLE_FLAG_INHERIT = 0x0001
6263
STARTF_USESTDHANDLES = 0x0100
6364
CREATE_SUSPENDED = 0x0004
6465
CREATE_NEW_CONSOLE = 0x0010
66+
CREATE_NO_WINDOW = 0x08000000
6567
STILL_ACTIVE = 259
6668
WAIT_TIMEOUT = 0x0102
6769
WAIT_FAILED = 0xFFFFFFFF
6870
INFINITE = 0xFFFFFFFF
6971
DUPLICATE_SAME_ACCESS = 0x00000002
72+
ENABLE_ECHO_INPUT = 0x0004
73+
ENABLE_LINE_INPUT = 0x0002
74+
ENABLE_PROCESSED_INPUT = 0x0001
7075

7176
# Win32 API functions needed
7277
GetLastError = ctypes.windll.kernel32.GetLastError
@@ -108,6 +113,18 @@ class PROCESS_INFORMATION(ctypes.Structure):
108113
WriteFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPVOID]
109114
WriteFile.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+
111128
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
112129
WaitForSingleObject.argtypes = [HANDLE, DWORD]
113130
WaitForSingleObject.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

Comments
 (0)