我一直在思考关于这个问题答案的批评,我相信我实现了一个更好的解决方案,可以这样使用:
with signal_fence(signal.SIGINT):
file = open(path, 'w')
dump(obj, file)
file.close()
< p >
signal_fence
上下文管理器如下所示,其改进了先前答案的方法。此函数的文档字符串记录了其接口和保证。 < /p >
import os
import signal
from contextlib import contextmanager
from types import FrameType
from typing import Callable, Iterator, Optional, Tuple
from typing_extensions import assert_never
@contextmanager
def signal_fence(
signum: signal.Signals,
*,
on_deferred_signal: Callable[[int, Optional[FrameType]], None] = None,
) -> Iterator[None]:
"""
A `signal_fence` creates an uninterruptible "fence" around a block of code. The
fence defers a specific signal received inside of the fence until the fence is
destroyed, at which point the original signal handler is called with the deferred
signal. Multiple deferred signals will result in a single call to the original
handler. An optional callback `on_deferred_signal` may be specified which will be
called each time a signal is handled while the fence is active, and can be used
to print a message or record the signal.
A `signal_fence` guarantees the following with regards to exception-safety:
1. If an exception occurs prior to creating the fence (installing a custom signal
handler), the exception will bubble up as normal. The code inside of the fence will
not run.
2. If an exception occurs after creating the fence, including in the fenced code,
the original signal handler will always be restored before the exception bubbles up.
3. If an exception occurs while the fence is calling the original signal handler on
destruction, the original handler may not be called, but the original handler will
be restored. The exception will bubble up and can be detected by calling code.
4. If an exception occurs while the fence is restoring the original signal handler
(exceedingly rare), the original signal handler will be restored regardless.
5. No guarantees about the fence's behavior are made if exceptions occur while
exceptions are being handled.
A `signal_fence` can only be used on the main thread, or else a `ValueError` will
raise when entering the fence.
"""
handled: Optional[Tuple[int, Optional[FrameType]]] = None
def handler(signum: int, frame: Optional[FrameType]) -> None:
nonlocal handled
if handled is None:
handled = (signum, frame)
if on_deferred_signal is not None:
try:
on_deferred_signal(signum, frame)
except:
pass
original_handler = signal.getsignal(signum)
if original_handler is None:
raise TypeError(
"signal_fence cannot be used with signal handlers that were not installed"
" from Python"
)
if isinstance(original_handler, int) and not isinstance(
original_handler, signal.Handlers
):
raise NotImplementedError(
"Your Python interpreter's signal module is using raw integers to"
" represent SIG_IGN and SIG_DFL, which shouldn't be possible!"
)
try:
try:
signal.signal(signum, handler)
yield
finally:
if handled is not None:
if isinstance(original_handler, signal.Handlers):
if original_handler is signal.Handlers.SIG_IGN:
pass
elif original_handler is signal.Handlers.SIG_DFL:
signal.signal(signum, signal.SIG_DFL)
os.kill(os.getpid(), signum)
else:
assert_never(original_handler)
elif callable(original_handler):
original_handler(*handled)
else:
assert_never(original_handler)
signal.signal(signum, original_handler)
except:
signal.signal(signum, original_handler)
raise
首先,为什么不使用线程(被接受的答案)?
在非守护线程中运行代码确保该线程会在解释器关闭时被加入,但是主线程上的任何异常(例如KeyboardInterrupt
)都不能阻止主线程继续执行。
考虑一下如果线程方法正在使用主线程在一个finally
块中变异的一些数据,而此时主线程发生了KeyboardInterrupt
会发生什么。
其次,针对@benrg对使用上下文管理器的最高投票答案的反馈:
- 如果在调用信号前抛出异常,但在
__enter__
返回之前,信号将永久性地被阻塞;
我的解决方案通过使用生成器上下文管理器及@contextmanager
装饰器避免了这个漏洞。有关详细信息,请参见上面的代码中的完整注释。
- 此代码可能在除主线程之外的其他线程中调用第三方异常处理程序,CPython从不这样做;
我不认为这个漏洞是真实存在的。必须从主线程中调用signal.signal
,否则会引发ValueError
。这些上下文管理器只能在主线程上运行,因此只会从主线程调用第三方异常处理程序。
- 如果信号返回一个非可调用值,则
__exit__
将崩溃
我的解决方案处理了信号处理程序的所有可能值,并适当地调用它们。此外,我使用assert_never
从静态分析器中受益。
请注意,signal_fence
设计用于处理主线程上的一个中断,例如KeyboardInterrupt
。如果用户在信号处理程序被恢复时不断按ctrl+c,那么没有什么可以拯救你。鉴于需要执行的操作码相对较少以恢复处理程序,这种情况不太可能发生。(为最大的健壮性,该解决方案需要以C语言重写)
SIGINT
之外也能响应SIGTERM
:https://gist.github.com/tcwalther/ae058c64d5d9078a9f333913718bba95 - Thomas Walthersignal
后但在__enter__
返回之前引发异常,则信号将永久地被阻塞;2.该代码可能会在非主线程中调用第三方异常处理程序,CPython从不这样做;3.如果signal
返回一个非可调用值,则__exit__
将崩溃。@ThomasWalther的版本部分修复了第三个漏洞,但增加了至少一个新漏洞。在Gist上有许多类似的类;所有这些都至少存在第1个漏洞。我建议不要尝试修复它们 - 这太难了,很难完全正确。 - benrg