为什么我无法在 python 中处理键盘中断?

发布于 2024-10-10 20:10:04 字数 1421 浏览 6 评论 0原文

我正在 Windows 上编写 Python 2.6.6 代码,如下所示:

try:
    dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."

dostuff() 是一个永远循环的函数,从输入流中一次读取一行并对其进行操作。我希望能够在按下 ctrl-c 时停止它并进行清理。

相反,发生的情况是 except KeyboardInterrupt: 下的代码根本没有运行。唯一打印的内容是“清理...”,然后打印一个回溯,如下所示:

Traceback (most recent call last):
  File "filename.py", line 119, in <module>
    print 'cleaning up...'
KeyboardInterrupt

因此,异常处理代码未运行,并且回溯声称在finally子句期间发生了键盘中断 ,这是没有意义的,因为按下 ctrl-c 是导致该部分首先运行的原因!即使是通用的 except: 子句也没有运行。

编辑:根据评论,我用 sys.stdin.read() 替换了 try: 块的内容。问题仍然按照描述发生,finally: 块的第一行运行,然后打印相同的回溯。

编辑#2: 如果我在读取后添加几乎任何内容,处理程序就会起作用。所以,这失败了:

try:
    sys.stdin.read()
except KeyboardInterrupt:
    ...

但这是有效的:

try:
    sys.stdin.read()
    print "Done reading."
except KeyboardInterrupt:
    ...

这是打印的内容:

Done reading. Interrupted!
cleaning up...
done.

因此,出于某种原因,“完成阅读”。即使异常发生在上一行,也会打印该行。这并不是真正的问题 - 显然我必须能够处理“try”块内任何地方的异常。但是,打印无法正常工作 - 它不会像预期的那样打印换行符! “Interruped”打印在同一行...前面有一个空格,出于某种原因...?不管怎样,之后代码就完成了它应该做的事情。

在我看来,这是在阻塞系统调用期间处理中断的一个错误。

I'm writing python 2.6.6 code on windows that looks like this:

try:
    dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."

dostuff() is a function that loops forever, reading a line at a time from an input stream and acting on it. I want to be able to stop it and clean up when I hit ctrl-c.

What's happening instead is that the code under except KeyboardInterrupt: isn't running at all. The only thing that gets printed is "cleaning up...", and then a traceback is printed that looks like this:

Traceback (most recent call last):
  File "filename.py", line 119, in <module>
    print 'cleaning up...'
KeyboardInterrupt

So, exception handling code is NOT running, and the traceback claims that a KeyboardInterrupt occurred during the finally clause, which doesn't make sense because hitting ctrl-c is what caused that part to run in the first place! Even the generic except: clause isn't running.

EDIT: Based on the comments, I replaced the contents of the try: block with sys.stdin.read(). The problem still occurs exactly as described, with the first line of the finally: block running and then printing the same traceback.

EDIT #2:
If I add pretty much anything after the read, the handler works. So, this fails:

try:
    sys.stdin.read()
except KeyboardInterrupt:
    ...

But this works:

try:
    sys.stdin.read()
    print "Done reading."
except KeyboardInterrupt:
    ...

Here's what's printed:

Done reading. Interrupted!
cleaning up...
done.

So, for some reason, the "Done reading." line is printed, even though the exception occurred on the previous line. That's not really a problem - obviously I have to be able to handle an exception anywhere inside the "try" block. However, the print doesn't work normally - it doesn't print a newline afterwards like it's supposed to! The "Interruped" is printed on the same line... with a space before it, for some reason...? Anyway, after that the code does what it's supposed to.

It seems to me that this is a bug in handling an interrupt during a blocked system call.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

俯瞰星空 2024-10-17 20:10:04

遗憾的是,异步异常处理并不可靠(信号处理程序引发的异常、通过 C API 的外部上下文等)。如果代码中对哪一段代码负责捕获异步异常进行了一些协调(除了非常关键的函数之外,调用堆栈中的最高可能值似乎是合适的),那么您可以增加正确处理异步异常的机会。

被调用的函数 (dostuff) 或堆栈中更下方的函数本身可能捕获了您没有/无法解释的 KeyboardInterrupt 或 BaseException。

这个小案例在 python 2.6.6 (x64) Interactive + Windows 7 (64bit) 上运行得很好:

>>> import time
>>> def foo():
...     try:
...             time.sleep(100)
...     except KeyboardInterrupt:
...             print "INTERRUPTED!"
...
>>> foo()
INTERRUPTED!  #after pressing ctrl+c

编辑:

经过进一步调查,我尝试了我认为是其他人用来重现的示例问题。我很懒,所以我省略了“最后”,

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "BLAH"
...
>>> foo()

这在按下 CTRL+C 后立即返回。当我立即尝试再次调用 foo 时,有趣的事情发生了:

>>> foo()

Traceback (most recent call last):
  File "c:\Python26\lib\encodings\cp437.py", line 14, in decode
    def decode(self,input,errors='strict'):
KeyboardInterrupt

在我没有按下 CTRL+C 的情况下立即引发了异常。

这似乎是有道理的——看来我们正在处理 Python 中如何处理异步异常的细微差别。在实际弹出异步异常然后在当前执行上下文中引发之前,可​​能需要几个字节码指令。 (这是我过去使用它时看到的行为)

请参阅 C API: http://docs.python.org/c-api/init.html#PyThreadState_SetAsyncExc

因此,这在某种程度上解释了为什么在此示例中执行finally语句的上下文中引发了KeyboardInterrupt

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "interrupt"
...     finally:
...             print "FINALLY"
...
>>> foo()
FINALLY
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in foo
KeyboardInterrupt

:自定义信号处理程序与解释器的标准键盘中断/CTRL+C 处理程序的疯狂混合导致了这种行为。例如, read() 调用看到信号并进行保释,但它在取消注册其处理程序后重新引发信号。如果不检查解释器代码库,我就无法确定。

这就是为什么我通常回避使用异步异常......

编辑2

我认为错误报告是一个很好的例子。

再次更多理论...(仅基于阅读代码)请参阅文件对象源:http://svn.python.org/view/python/branches/release26-maint/Objects/fileobject.c?revision=81277&view=markup

file_read 调用 Py_UniversalNewlineFread()。 fread 可以返回错误,errno = EINTR(它执行自己的信号处理)。在这种情况下,Py_UniversalNewlineFread() 会终止,但不会使用 PyErr_CheckSignals() 执行任何信号检查,以便可以同步调用处理程序。 file_read 清除文件错误,但也不调用 PyErr_CheckSignals()。

有关如何使用它的示例,请参阅 getline() 和 getline_via_fgets()。该模式记录在类似问题的错误报告中:( http://bugs.python.org/issue1195)。因此,信号似乎是由解释器在不确定的时间处理的。

我想深入研究没有什么价值,因为仍然不清楚 sys.stdin.read() 示例是否是“dostuff()”函数的正确模拟。 (可能存在多个错误)

Asynchronous exception handling is unfortunately not reliable (exceptions raised by signal handlers, outside contexts via C API, etc). You can increase your chances of handling the async exception properly if there is some coordination in the code about what piece of code is responsible for catching them (highest possible in the call stack seems appropriate except for very critical functions).

The called function (dostuff) or functions further down the stack may itself have a catch for KeyboardInterrupt or BaseException that you didn't/couldn't account for.

This trivial case worked just fine with python 2.6.6 (x64) interactive + Windows 7 (64bit):

>>> import time
>>> def foo():
...     try:
...             time.sleep(100)
...     except KeyboardInterrupt:
...             print "INTERRUPTED!"
...
>>> foo()
INTERRUPTED!  #after pressing ctrl+c

EDIT:

Upon further investigation, I tried what I believe is the example that others have used to reproduce the issue. I was lazy so I left out the "finally"

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "BLAH"
...
>>> foo()

This returns immediately after hitting CTRL+C. The interesting thing happened when I immediately tried to call foo again:

>>> foo()

Traceback (most recent call last):
  File "c:\Python26\lib\encodings\cp437.py", line 14, in decode
    def decode(self,input,errors='strict'):
KeyboardInterrupt

The exception was raised immediately without me hitting CTRL+C.

This would seem to make sense - it appears that we are dealing with nuances in how asynchronous exceptions are handled in Python. It can take several bytecode instructions before the async exception is actually popped and then raised within the current execution context. (That's the behavior that I've seen when playing with it in the past)

See the C API: http://docs.python.org/c-api/init.html#PyThreadState_SetAsyncExc

So this somewhat explains why KeyboardInterrupt gets raised in the context of the execution of the finally statement in this example:

>>> def foo():
...     try:
...             sys.stdin.read()
...     except KeyboardInterrupt:
...             print "interrupt"
...     finally:
...             print "FINALLY"
...
>>> foo()
FINALLY
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in foo
KeyboardInterrupt

There could be some crazy mixing of custom signal handlers mixed with the interpreter's standard KeyboardInterrupt/CTRL+C handler that's resulting in this sort of behavior. For example, the read() call sees the signal and bails, but it re-raises the signal after deregistering it's handler. I wouldn't know for sure without inspecting the interpreter codebase.

This is why I generally shy away from making use of async exceptions....

EDIT 2

I think there's a good case for a bug report.

Again more theories...(just based on reading code) See the file object source: http://svn.python.org/view/python/branches/release26-maint/Objects/fileobject.c?revision=81277&view=markup

file_read calls Py_UniversalNewlineFread(). fread can return with an error with errno = EINTR (it performs its own signal handling). In this case Py_UniversalNewlineFread() bails but does not perform any signal checking with PyErr_CheckSignals() so that the handlers can get called synchronously. file_read clears the file error but also does not call PyErr_CheckSignals().

See getline() and getline_via_fgets() for examples of how it's used. The pattern is documented in this bug report for a similar issue: ( http://bugs.python.org/issue1195 ). So it seems that the signal is handled at an indeterminate time by the interpreter.

I guess there's little value in diving any deeper since it's still not clear whether the sys.stdin.read() example is a proper analog of your "dostuff()" function. (there could be multiple bugs at play)

明天过后 2024-10-17 20:10:04

sys.stdin.read() 是一个系统调用,因此每个系统的行为都会有所不同。对于 Windows 7,我认为发生的情况是输入正在被缓冲,因此您将得到 sys.stdin.read() 将所有内容返回到 Ctrl-C 并且一旦您再次访问 sys.stdin 它将发送“Ctrl-C”。

尝试以下操作,

def foo():
    try:
        print sys.stdin.read()
        print sys.stdin.closed
    except KeyboardInterrupt:
        print "Interrupted!"

这表明正在进行 stdin 缓冲,导致再次调用 stdin 来识别键盘输入,

def foo():
    try:
        x=0
        while 1:
            x += 1
        print x
    except KeyboardInterrupt:
        print "Interrupted!"

这似乎没有问题。

dostuff() 是否从标准输入读取?

sys.stdin.read() is a system call and so the behavior is going to be different for each system. For windows 7 I think what is happening is that the input is being buffered and so you're getting where sys.stdin.read() is returning everything up to the Ctrl-C and as soon as you access sys.stdin again it'll send the "Ctrl-C".

try the following,

def foo():
    try:
        print sys.stdin.read()
        print sys.stdin.closed
    except KeyboardInterrupt:
        print "Interrupted!"

This suggests that there is a buffering of stdin going on that is causing another call to stdin to recognize the keyboard input

def foo():
    try:
        x=0
        while 1:
            x += 1
        print x
    except KeyboardInterrupt:
        print "Interrupted!"

there doesn't appear to be a problem.

Is dostuff() reading from stdin?

落花浅忆 2024-10-17 20:10:04

遇到类似的问题,这是我的解决方法:

try:
    some_blocking_io_here() # CTRL-C to interrupt
except:
    try:
        print() # any i/o will get the second KeyboardInterrupt here?
    except:
        real_handler_here()

Having similar problem and this is my workaround:

try:
    some_blocking_io_here() # CTRL-C to interrupt
except:
    try:
        print() # any i/o will get the second KeyboardInterrupt here?
    except:
        real_handler_here()
碍人泪离人颜 2024-10-17 20:10:04

您可以使用装饰器包装函数,以便在 Windows 上处理 KeyboardInterrupt

def safe_system_call(func):
    """
    Decorator to be used to handle Windows errors when program
    is halted by user termination (CTRL+C).

    Meant to be wrap around functions like {time.sleep}, print and input.
    """

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except KeyboardInterrupt:
            sys.exit(-1) # Handle Keyboard Interrupt for system call
    return wrapper

使用

import sys
import time

time.sleep = safe_system_call(time.sleep)
sys.stdin.read = safe_system_call(sys.stdin.read)

KeyboardInterrupt 应该开箱即用,因为它在 Linux 中是这样的。这是处理此类错误的解决方法。但它仍然有陷阱。可能很难涵盖代码中的所有系统调用。

You can wrap functions with a decorator in order to handle KeyboardInterrupt on Windows too:

def safe_system_call(func):
    """
    Decorator to be used to handle Windows errors when program
    is halted by user termination (CTRL+C).

    Meant to be wrap around functions like {time.sleep}, print and input.
    """

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except KeyboardInterrupt:
            sys.exit(-1) # Handle Keyboard Interrupt for system call
    return wrapper

Use

import sys
import time

time.sleep = safe_system_call(time.sleep)
sys.stdin.read = safe_system_call(sys.stdin.read)

KeyboardInterrupt should work out of the box as it does in Linux. This is a workaround to handle such errors. Still it has pitfalls. It might be hard to cover all system calls in your code.

儭儭莪哋寶赑 2024-10-17 20:10:04

下面是对发生的情况的猜测:

  • 按 Ctrl-C 会中断“print”语句(无论出于何种原因...bug?Win32 限制?)
  • 按 Ctrl-C 还会引发第一个 KeyboardInterrupt,在 dostuff() 中
  • 异常处理程序运行并尝试打印“Interrupted”,但“print”语句被破坏并引发另一个KeyboardInterrupt。
  • finally 子句运行并尝试打印“cleaning up....”,但“print”语句被破坏并引发另一个键盘中断。

Here's a guess about what's happening:

  • pressing Ctrl-C breaks the "print" statement (for whatever reason... bug? Win32 limitation?)
  • pressing Ctrl-C also throws the first KeyboardInterrupt, in dostuff()
  • The exception handler runs and tries to print "Interrupted", but the "print" statement is broken and throws another KeyboardInterrupt.
  • The finally clause runs and tries to print "cleaning up....", but the "print" statement is broken and throws yet another KeyboardInterrupt.
佼人 2024-10-17 20:10:04

这对我有用:

import sys

if __name__ == "__main__":
    try:
        sys.stdin.read()
        print "Here"
    except KeyboardInterrupt:
        print "Worked"
    except:
        print "Something else"
    finally:
        print "Finally"

尝试在 dostuff() 函数之外放一行,或者将循环条件移到函数之外。例如:

try:
    while True:
        dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."

This works for me:

import sys

if __name__ == "__main__":
    try:
        sys.stdin.read()
        print "Here"
    except KeyboardInterrupt:
        print "Worked"
    except:
        print "Something else"
    finally:
        print "Finally"

Try putting a line outside of your dostuff() function or move the loop condition outside of the function. For example:

try:
    while True:
        dostuff()
except KeyboardInterrupt:
    print "Interrupted!"
except:
    print "Some other exception?"
finally:
    print "cleaning up...."
    print "done."
╰ゝ天使的微笑 2024-10-17 20:10:04
def foo():
    try:
        x=0
        while 1:
            x+=1
            print (x)
    except KeyboardInterrupt:
       print ("interrupted!!")
foo()

效果很好。

def foo():
    try:
        x=0
        while 1:
            x+=1
            print (x)
    except KeyboardInterrupt:
       print ("interrupted!!")
foo()

That works fine.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文