python / python/cpython

Ctrl-C can cause Windows Console IO reads to incorrectly return empty string

Đang mở
#130,323 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

OS-windows type-bug
Ngôn ngữ chính
Python
Star
77.2k
Fork
35.9k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

Bug report

Bug description:

Many file-like read functions are expected to return non-empty strings/bytes until EOF, eg the docs for io.RawIOBase here state:

If 0 bytes are returned, and size was not 0, this indicates end of file. If the object is in non-blocking mode and no bytes are available, None is returned.

However, on Windows, when reading from a console, Python will return 0 bytes on Ctrl-C if the default SIGINT handler is replaced with something that doesn't raise an exception, or presumably if you are reading from a thread other than the main thread.

To reproduce:

import signal, sys

signal.signal(signal.SIGINT, lambda *args: print('<SIGINT>', flush=True))

for i in range(10):
    print(f"INPUT{i} = {sys.stdin.readline()!r}", flush=True)

Then run it in a console with various inputs:

C:\>py script1.py
line0
INPUT0 = 'line0\n'
line1
INPUT1 = 'line1\n'
<SIGINT>
INPUT2 = ''
line3
INPUT3 = 'line3\n'
line4<SIGINT>
INPUT4 = ''
line5
INPUT5 = 'line5\n'
^Z
INPUT6 = ''
^Zline7
INPUT7 = ''
line8
INPUT8 = 'line8\n'
line9
INPUT9 = 'line9\n'

As you can see, when CTRL-C is pressed, the custom SIGINT handler runs, doesn't raise KeyboardInterrupt, but readline() then returns an empty string. There is also similar behaviour for lines beginning with CTRL-Z, which appears to be intentional, and somewhat consistent with how cmd behaves, but since it doesn't actually close the underlying file handle (you can still read more data after encountering CTRL-Z) it might be surprising, and potentially worth documenting (though I'm not sure where, and it might be already somewhere that I didn't look 😛).

Just for reference, this doesn't affect reading from a pipe, for example, or presmably other file-like objects. Using the following script:

import signal, sys, time

signal.signal(signal.SIGINT, lambda *args: print('<CTRL-C>', file=sys.stderr, flush=True))

for i in range(5):
    time.sleep(1)
    print(f"test", flush=True)
C:\>py script2.py | py script1.py
INPUT0 = 'test0\n'
<CTRL-C>
<SIGINT>
INPUT1 = 'test1\n'
INPUT2 = 'test2\n'
<CTRL-C>
<CTRL-C>
<CTRL-C>
<SIGINT>
INPUT3 = 'test3\n'
<CTRL-C>
<SIGINT>
INPUT4 = 'test4\n'
INPUT5 = ''
INPUT6 = ''
INPUT7 = ''
INPUT8 = ''
INPUT9 = ''

Also interestingly in this case, it seems like the ctrl-c isn't delivered to the reading process till the next read completes (the writing process can receive and handle multiple interrupts between write operations). I'm not sure this behaviour is correct either, as it means there's no way to interrupt a read on a pipe that isn't receiving any data.

From what I can tell, the bug is in the read_console_w() function in _io/winconsoleio.c:

if (n == 0) {
    err = GetLastError();
    if (err != ERROR_OPERATION_ABORTED)
        break;
    err = 0;
    HANDLE hInterruptEvent = _PyOS_SigintEvent();
    if (WaitForSingleObjectEx(hInterruptEvent, 100, FALSE)
            == WAIT_OBJECT_0) {
        ResetEvent(hInterruptEvent);
        Py_BLOCK_THREADS
        sig = PyErr_CheckSignals();
        Py_UNBLOCK_THREADS
        if (sig < 0)
            break;
    }
}
*readlen += n;

/* If we didn't read a full buffer that time, don't try
    again or we will block a second time. */
if (n < len)
    break;

The code seems to check for Ctrl-C (signified by n == 0 && err == ERROR_OPERATION_ABORTED), then checks for a pending exception in a signal handler, but if there isn't one (sig == 0) it just falls through and then breaks out because n < len - with the comment about not blocking a second time. I think the solution would be to simply continue at the end of the n == 0 block, but I don't entirely know how the _PyOS_SigintEvent() thing works, and if it matters which side of that if-block you are.

CPython versions tested on:

3.12

Operating systems tested on:

Windows

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu trong Modules/_io/winconsoleio.c tại read_console_w(), đặc biệt là phần xử lý ERROR_OPERATION_ABORTED và logic đọc ngắn ngay sau đó. Chạy bản tái hiện console Windows được cung cấp với một trình xử lý SIGINT tùy chỉnh và so sánh hành vi trước và sau thay đổi. Được xem là hoàn tất khi Ctrl-C không còn khiến readline() trả về chuỗi rỗng nếu trình xử lý không phát sinh ngoại lệ, trong khi hành vi EOF thông thường vẫn được giữ nguyên.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
c, python
Lĩnh vực
operating-systems
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
45/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.