Skip to content

Commit 0ca0107

Browse files
gh-69573: Check the return value of msvcrt._putch() and _putwch() (GH-154364)
1 parent 22c7970 commit 0ca0107

4 files changed

Lines changed: 84 additions & 4 deletions

File tree

Doc/library/msvcrt.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,23 @@ Console I/O
135135

136136
.. function:: putch(char)
137137

138-
Print the byte string *char* to the console without buffering.
138+
Print the byte string *char* to the console without buffering. Raises
139+
:exc:`OSError` on failure, for example when the process has no console
140+
attached.
141+
142+
.. versionchanged:: next
143+
Failures are now reported by raising :exc:`OSError` instead of being
144+
silently ignored.
139145

140146

141147
.. function:: putwch(unicode_char)
142148

143149
Wide char variant of :func:`putch`, accepting a Unicode value.
144150

151+
.. versionchanged:: next
152+
Failures are now reported by raising :exc:`OSError` instead of being
153+
silently ignored.
154+
145155

146156
.. function:: ungetch(char)
147157

Lib/test/test_msvcrt.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ def test_get_osfhandle(self):
6161
c_encoded = b'\x57\x5b' # utf-16-le (which windows internally used) encoded char for this CJK char
6262

6363

64+
def has_console():
65+
# A process created without a console (for example by pythonw.exe, or with
66+
# the DETACHED_PROCESS creation flag) cannot write to the console.
67+
try:
68+
with open('CONOUT$', 'w'):
69+
return True
70+
except OSError:
71+
return False
72+
73+
74+
requires_console = unittest.skipUnless(has_console(), 'requires a console')
75+
76+
6477
class TestConsoleIO(unittest.TestCase):
6578
# CREATE_NEW_CONSOLE creates a "popup" window.
6679
@requires_resource('gui')
@@ -106,12 +119,36 @@ def test_getche(self):
106119
def test_getwche(self):
107120
self.check_getwch('getwche')
108121

122+
@requires_console
109123
def test_putch(self):
110124
msvcrt.putch(b'c')
111125

126+
@requires_console
112127
def test_putwch(self):
113128
msvcrt.putwch(c)
114129

130+
def test_putch_without_console(self):
131+
# gh-69573: putch() and putwch() must report the error instead of
132+
# silently ignoring it when the process has no console attached.
133+
code = dedent('''
134+
import msvcrt
135+
import sys
136+
137+
for name, arg in (('putch', b'c'), ('putwch', 'c')):
138+
func = getattr(msvcrt, name)
139+
try:
140+
func(arg)
141+
except OSError:
142+
pass
143+
else:
144+
sys.exit(f'msvcrt.{name}() did not raise OSError')
145+
''')
146+
# DETACHED_PROCESS: the child process is created without a console.
147+
proc = subprocess.run([sys.executable, '-c', code],
148+
creationflags=subprocess.DETACHED_PROCESS,
149+
capture_output=True, text=True)
150+
self.assertEqual(proc.returncode, 0, proc.stderr)
151+
115152

116153
class TestOther(unittest.TestCase):
117154
def test_heap_min(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`msvcrt.putch` and :func:`msvcrt.putwch` now check the return value of
2+
the underlying ``_putch()`` and ``_putwch()`` C functions and raise
3+
:exc:`OSError` on failure, for example when the process has no console
4+
attached, instead of silently ignoring the error.

PC/msvcrtmodule.c

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,22 @@ msvcrt_getwche_impl(PyObject *module)
317317

318318
#endif /* MS_WINDOWS_DESKTOP */
319319

320+
/* Raise an OSError for a failed _putch()/_putwch() call.
321+
322+
These functions fail, for example, when the process has no console
323+
attached, but the CRT reports the failure without setting errno (and
324+
without setting the Windows last error either), so fall back to a
325+
generic error message in that case. */
326+
static PyObject *
327+
set_console_write_error(void)
328+
{
329+
if (errno != 0) {
330+
return PyErr_SetFromErrno(PyExc_OSError);
331+
}
332+
PyErr_SetString(PyExc_OSError, "write to console failed");
333+
return NULL;
334+
}
335+
320336
/*[clinic input]
321337
msvcrt.putch
322338
@@ -330,9 +346,16 @@ static PyObject *
330346
msvcrt_putch_impl(PyObject *module, char char_value)
331347
/*[clinic end generated code: output=92ec9b81012d8f60 input=ec078dd10cb054d6]*/
332348
{
349+
int res;
350+
333351
_Py_BEGIN_SUPPRESS_IPH
334-
_putch(char_value);
352+
errno = 0;
353+
res = _putch(char_value);
335354
_Py_END_SUPPRESS_IPH
355+
356+
if (res == EOF) {
357+
return set_console_write_error();
358+
}
336359
Py_RETURN_NONE;
337360
}
338361

@@ -351,11 +374,17 @@ static PyObject *
351374
msvcrt_putwch_impl(PyObject *module, int unicode_char)
352375
/*[clinic end generated code: output=a3bd1a8951d28eee input=996ccd0bbcbac4c3]*/
353376
{
377+
wint_t res;
378+
354379
_Py_BEGIN_SUPPRESS_IPH
355-
_putwch(unicode_char);
380+
errno = 0;
381+
res = _putwch(unicode_char);
356382
_Py_END_SUPPRESS_IPH
357-
Py_RETURN_NONE;
358383

384+
if (res == WEOF) {
385+
return set_console_write_error();
386+
}
387+
Py_RETURN_NONE;
359388
}
360389

361390
#endif /* MS_WINDOWS_DESKTOP */

0 commit comments

Comments
 (0)