Ctrl+C 取消进程后发送一次 EOFError

Ctrl+C sends EOFError once after cancelling process

我正在使用 python 制作一个非常基本的命令行实用程序,我正在尝试对其进行设置,以便用户能够在执行期间中断任何 运行ning 操作使用 Ctrl+C,然后使用 Ctrl+Z 发送 EOF 退出程序。然而,在最后一天我一直在与这个令人沮丧的问题作斗争,在用 KeyboardInterrupt 取消 运行ning 操作后,第二次按 Ctrl+C 发送 EOFError 而不是a KeyboardInterrupt,导致程序退出。随后按 Ctrl+C 会像往常一样发送 KeyboardInterrupt,除非我输入任何命令或空行,其中会发送额外的 KeyboardInterrupt 而不是我提供的输入。这样做之后,再次按 Ctrl+C 将再次发送 EOFError,然后从那里继续。这是演示我的问题的最小代码示例;

import time

def parse(inp):
    time.sleep(1)
    print(inp)
    if inp == 'e':
        return 'exit'

while True:
    try:
        user_in = input("> ").strip()
    except (KeyboardInterrupt, Exception) as e:
        print(e.__class__.__name__)
        continue

    if not user_in:
        continue

    try:
        if parse(user_in) == 'exit':
            break
    except KeyboardInterrupt:
        print("Cancelled")

这是我的代码的一些示例输出;

>
>
>
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
>
>
> ^Z
EOFError
> ^Z
EOFError
> ^Z
EOFError
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
>
> ^Z
EOFError
> KeyboardInterrupt
>
>

如您所见,当按下 Ctrl+C、Ctrl+Z 或空白行时,提示会按照您预期的方式正确响应每个错误。但是,如果我 运行 一个命令并尝试在执行过程中通过按 Ctrl+C 来取消它;

> test
Cancelled
>
>
>
> EOFError
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
KeyboardInterrupt
>
>
> EOFError
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
KeyboardInterrupt
>
>
>

我在上面的例子中只按了Ctrl+C和Enter;我先按回车键发送几个空行,然后按 Ctrl+C,它首先发送一个 EOFError。之后多次按 Ctrl+C,然后正确发送 KeyboardInterrupt。之后,发送一个空行,而不是发送一个KeyboardInterrupt,随后输入的空行被正常接收。从那里的程序执行开始重复此功能。

为什么会发生这种情况,我该如何解决?

所以。您找到了一个相当古老的 Python bug。这与键盘中断的异步性质有关,以及如果您在挂起时向 Python 发送 KeyboardInterrupt,并且它不响应中断,那么第二个中断将引发更强的 EOFError。然而,这两者似乎发生了冲突,如果你有一个异步 KeyboardInterrupt 被捕获,然后是第二个 KeyboardInterrupt 的输入,那么一些缓冲区中会留下一些东西,这会触发 EOFError .

我知道这不是很好的解释,也不是很清楚。但是,它允许进行非常简单的修复。让缓冲区赶上所有异步中断,然后开始等待输入:

import time

def parse(inp):
    time.sleep(1)
    print(inp)
    if inp == 'e':
        return 'exit'

while True:
    try:
        user_in = input("> ").strip()
    except (KeyboardInterrupt, Exception) as e:
        print(e.__class__.__name__)
        continue

    if not user_in:
        continue

    try:
        if parse(user_in) == 'exit':
            break
    except KeyboardInterrupt:
        print("Cancelled")
        time.sleep(0.1)    # This is the only line that's added

现在执行与您相同的操作会产生以下结果:

> test
Cancelled
>
>
>
>
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
> KeyboardInterrupt
>
> KeyboardInterrupt
>
>
> KeyboardInterrupt
> KeyboardInterrupt
>