Source code for trader.daemon.signals

"""Shutdown signalling for the daemon.

A flag rather than an exception raised from the handler: the loop must be
allowed to *finish the cycle it is in*. A `KeyboardInterrupt` landing between
"cancel the protective stop" and "submit the sell" is exactly the interruption
this app's ordering rules exist to prevent.
"""

import signal
from collections.abc import Callable
from types import FrameType

__all__ = ["ShutdownFlag", "install_handlers"]


[docs] class ShutdownFlag: """Latching "stop after this cycle" flag, set from a signal handler. Latching on purpose: a second SIGTERM must not clear it. An operator pressing Ctrl-C twice means "stop harder", never "carry on". """ def __init__(self) -> None: self._set = False self._reason = ""
[docs] def is_set(self) -> bool: """Whether shutdown has been requested.""" return self._set
@property def reason(self) -> str: """What asked the daemon to stop, for the final log line.""" return self._reason
[docs] def request(self, signum: int | None = None, frame: FrameType | None = None) -> None: """Request shutdown. Signature matches `signal.signal`'s handler.""" if self._set: return self._set = True self._reason = signal.Signals(signum).name if signum else "requested"
[docs] def install_handlers(flag: ShutdownFlag) -> Callable[[], None]: """Route SIGINT and SIGTERM to `flag`, returning a restore function. Returning the restore rather than installing permanently keeps a test process from inheriting the daemon's handlers, and makes the installation itself assertable. """ previous = {sig: signal.getsignal(sig) for sig in (signal.SIGINT, signal.SIGTERM)} for sig in previous: signal.signal(sig, flag.request) def restore() -> None: for sig, handler in previous.items(): signal.signal(sig, handler) return restore