From e1572fd3e3ec24b1aed1ef43b325352dc75b247f Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 18 Apr 2026 12:51:27 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20trimite=20alert=20Telegram=20la=20=C3=AE?= =?UTF-8?q?nchidere=20fereastr=C4=83=20CMD=20pe=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CTRL_CLOSE_EVENT omora procesul instant, înainte ca finally să ruleze. _run_live_win32 instalează SetConsoleCtrlHandler care cancelează task-ul asyncio → finally trimite "ATM oprit" și flushează notifier-ul (~5s fereastra). Co-Authored-By: Claude Sonnet 4.6 --- src/atm/main.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/atm/main.py b/src/atm/main.py index cba4489..43001d0 100644 --- a/src/atm/main.py +++ b/src/atm/main.py @@ -1124,7 +1124,69 @@ async def _drain_cmd_queue(ctx: RunContext) -> None: def run_live(cfg, duration_s=None, capture_stub: bool = False) -> None: """Sync entry point — delegates to asyncio event loop.""" - asyncio.run(run_live_async(cfg, duration_s=duration_s, capture_stub=capture_stub)) + if sys.platform == "win32": + _run_live_win32(cfg, duration_s=duration_s, capture_stub=capture_stub) + else: + asyncio.run(run_live_async(cfg, duration_s=duration_s, capture_stub=capture_stub)) + + +def _run_live_win32(cfg, duration_s=None, capture_stub: bool = False) -> None: + """Windows variant: catches CTRL_CLOSE_EVENT so the finally block (Telegram alert) runs.""" + try: + import win32api # type: ignore[import-untyped] + except ImportError: + asyncio.run(run_live_async(cfg, duration_s=duration_s, capture_stub=capture_stub)) + return + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + _task: list[asyncio.Task] = [] + + def _close_handler(event: int) -> bool: + # CTRL_CLOSE_EVENT=2, CTRL_LOGOFF_EVENT=5, CTRL_SHUTDOWN_EVENT=6 + if event in (2, 5, 6) and _task: + loop.call_soon_threadsafe(_task[0].cancel) + return True # tell Windows we handled it; gives ~5 s before force-kill + return False # CTRL+C / CTRL+BREAK: let Python's default handler fire + + win32api.SetConsoleCtrlHandler(_close_handler, True) + + async def _run() -> None: + t = asyncio.create_task( + run_live_async(cfg, duration_s=duration_s, capture_stub=capture_stub), + name="atm_live", + ) + _task.append(t) + try: + await t + except asyncio.CancelledError: + pass # finally in run_live_async already sent the Telegram alert + + try: + loop.run_until_complete(_run()) + except KeyboardInterrupt: + # CTRL+C: cancel the live task and let its finally block run + if _task and not _task[0].done(): + _task[0].cancel() + try: + loop.run_until_complete(_task[0]) + except (asyncio.CancelledError, Exception): + pass + finally: + pending = asyncio.all_tasks(loop) + for t in pending: + t.cancel() + if pending: + try: + loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + except Exception: + pass + try: + loop.run_until_complete(loop.shutdown_asyncgens()) + except Exception: + pass + asyncio.set_event_loop(None) + loop.close() async def run_live_async(cfg, duration_s=None, capture_stub: bool = False) -> None: