For Windows, we could combine the prompt and line-reading operations into a single _PyOS_WinStdioReadline()
call. If legacy mode is disabled, and sys_stdin
and sys_stdout
are console files, this call writes the prompt to stdout via WriteConsoleW()
and reads from stdin via ReadConsoleW()
. Otherwise it chains to _PyOS_StdioReadline()
, which writes the prompt to stderr
and reads via fgets()
. Define PyOS_StdioReadline
as a platform-dependent macro, so the implementation of PyOS_Readline()
can remain the same.
Other than moving all of the Windows-only code out of _PyOS_StdioReadline()
, the most significant change would be that stdout
will be used whenever stdin
and stdout
are console files. Thus if just stderr
is redirected to a file or pipe, UI prompts will still be displayed in the console. This agrees with POSIX systems, if the readline module is available and imported. The ReadConsoleW()
call is analogous to using the readline module. The consoleās line-input mode implements a basic line editor that supports command history and aliases.
Example implementation:
#ifdef MS_WINDOWS
extern char _get_console_type(HANDLE handle);
/* Readline implementation using ReadConsoleW */
char *
_PyOS_WinStdioReadline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt)
{
HANDLE hStdIn = _Py_get_osfhandle_noraise(fileno(sys_stdin));
HANDLE hStdOut = _Py_get_osfhandle_noraise(fileno(sys_stdout));
if (Py_LegacyWindowsStdioFlag ||
_get_console_type(hStdIn) != 'r' ||
_get_console_type(hStdOut) != 'w') {
return _PyOS_StdioReadline(sys_stdin, sys_stdout, prompt);
}
PyThreadState *tstate = _PyOS_ReadlineTState;
assert(tstate != NULL);
int err = 0;
char *buf = NULL;
static wchar_t wbuf_local[16 * 1024];
wchar_t *wbuf = wbuf_local;
DWORD wlen = sizeof(wbuf_local) / sizeof(wbuf_local[0]);
DWORD total_read = 0;
DWORD n;
fflush(sys_stdout);
if (prompt) {
DWORD pwlen = MultiByteToWideChar(CP_UTF8, 0, prompt, -1, wbuf, wlen);
if (pwlen == 0) {
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
err = GetLastError();
goto exit;
}
wlen = MultiByteToWideChar(CP_UTF8, 0, prompt, -1, NULL, 0);
wbuf = PyMem_RawMalloc(wlen * sizeof(wchar_t));
if (wbuf == NULL) {
err = ERROR_NOT_ENOUGH_MEMORY;
goto exit;
}
pwlen = MultiByteToWideChar(CP_UTF8, 0, prompt, -1, wbuf, wlen);
if (pwlen == 0) {
err = GetLastError();
goto exit;
}
}
/* pwlen includes the null terminator. */
if (!WriteConsoleW(hStdOut, wbuf, pwlen - 1, &n, NULL)) {
err = GetLastError();
goto exit;
}
}
while (1) {
if (PyOS_InputHook != NULL) {
(void)(PyOS_InputHook)();
}
n = (DWORD)-1;
BOOL res = ReadConsoleW(hStdIn, &wbuf[total_read],
wlen - total_read - 1, &n, NULL);
if (!res) {
err = GetLastError();
goto exit;
}
if (n == (DWORD)-1 && GetLastError() == ERROR_OPERATION_ABORTED) {
err = ERROR_OPERATION_ABORTED;
goto exit;
}
if (n == 0) {
if (GetLastError() != ERROR_OPERATION_ABORTED) {
err = GetLastError();
goto exit;
}
HANDLE hInterruptEvent = _PyOS_SigintEvent();
if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
== WAIT_OBJECT_0) {
int s;
ResetEvent(hInterruptEvent);
PyEval_RestoreThread(tstate);
s = PyErr_CheckSignals();
PyEval_SaveThread();
if (s < 0) {
goto exit;
}
}
continue;
}
total_read += n;
if (total_read == 0 || wbuf[total_read - 1] == L'\n') {
break;
}
wlen += 1024;
wchar_t *tmp = PyMem_RawRealloc(wbuf == wbuf_local ? NULL : wbuf,
wlen * sizeof(wchar_t));
if (tmp == NULL) {
err = ERROR_NOT_ENOUGH_MEMORY;
goto exit;
}
if (wbuf == wbuf_local) {
wbuf[total_read] = L'\0';
wcscpy_s(tmp, wlen, wbuf);
}
wbuf = tmp;
}
/* Input that begins with Ctrl+Z is handled as EOF. */
if (total_read > 0 && wbuf[0] == L'\x1a') {
total_read = 0;
}
DWORD u8len;
if (total_read == 0) {
u8len = 0;
} else {
u8len = WideCharToMultiByte(CP_UTF8, 0, wbuf, total_read,
NULL, 0, NULL, NULL);
if (u8len == 0) {
err = GetLastError();
goto exit;
}
}
buf = PyMem_RawMalloc(u8len + 1);
if (buf == NULL) {
err = ERROR_NOT_ENOUGH_MEMORY;
goto exit;
}
if (u8len > 0) {
u8len = WideCharToMultiByte(CP_UTF8, 0, wbuf, total_read,
buf, u8len, NULL, NULL);
}
buf[u8len] = '\0';
exit:
if (wbuf != wbuf_local) {
PyMem_RawFree(wbuf);
}
if (err) {
PyEval_RestoreThread(tstate);
if (err == ERROR_NOT_ENOUGH_MEMORY) {
PyErr_NoMemory();
} else {
PyErr_SetFromWindowsErr(err);
}
PyEval_SaveThread();
}
return buf;
}
#endif /* MS_WINDOWS */
#ifdef MS_WINDOWS
#define PyOS_StdioReadline _PyOS_WinStdioReadline
#else
#define PyOS_StdioReadline _PyOS_StdioReadline
#endif
This implementation also fixes the bug when SIGINT
or CTRL_C_EVENT
is ignored, but I hope to find a better solution. Typing Ctrl+C cancels a read in the console itself, so even if the CTRL_C_EVENT
is ignored by the process, whatever the user typed beforehand is lost. The display doesnāt change, so it misleadingly looks like the previous input will be read, but only subsequently typed text will be read.
The current implementation of PyOS_Readline()
just immediately returns an empty string after an ignored SIGINT
, instead of continuing the read loop. This signifies EOF, so the REPL exits and input()
raises EOFError
, which partially defeats the point of ignoring SIGINT
. The above implementation is at least better than that since it continues the read loop. Maybe the UI can be improved by using the pInputControl
parameter of ReadConsoleW()
.