From 9c7f18d53282a56477dcfd8c8a2a3f055afdd8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=81=D0=B8=D0=BD=D0=B5=D1=87=D0=BA=D0=B0=20=E2=99=A1?= <63897347+ktnk-dev@users.noreply.github.com> Date: Wed, 16 Sep 2026 07:07:47 +0000 Subject: [PATCH] run bot in a fresh interpreter --- main.py | 119 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/main.py b/main.py index cb4ab79..25c736d 100644 --- a/main.py +++ b/main.py @@ -2,10 +2,11 @@ from __future__ import annotations import os import signal +import subprocess import sys +import time from multiprocessing import Process, get_context from pathlib import Path -from collections.abc import Callable from typing import BinaryIO @@ -54,72 +55,102 @@ def stop_processes(processes: list[Process]) -> None: process.join() -def run_worker(target: Callable[[], None], lock_fd: int) -> None: - # Keep an explicit duplicate alive in the worker. A Linux flock belongs to - # the shared open-file description, so it remains held if the parent dies. - with os.fdopen(os.dup(lock_fd), "rb"): - target() +def stop_subprocesses(processes: list[subprocess.Popen]) -> None: + for process in processes: + if process.poll() is None: + process.terminate() + for process in processes: + try: + process.wait(SHUTDOWN_TIMEOUT) + except subprocess.TimeoutExpired: + process.kill() + process.wait() def run() -> int: # Import only after taking the lock so a rejected duplicate has no bot/event - # side effects. Linux workers explicitly retain the lock descriptor: if the - # supervisor dies unexpectedly, an orphan cannot poll alongside a replacement. + # side effects. Linux workers are fresh interpreters which explicitly inherit + # the lock descriptor. If the supervisor dies, an orphan keeps the lock and a + # replacement cannot poll alongside it. instance_lock = acquire_instance_lock() - import bot - import event import models.logger - context = get_context("spawn" if sys.platform == "win32" else "fork") - if sys.platform == "win32": - processes = [ - context.Process(target=bot.run, name="bot"), - context.Process(target=event.run, name="events"), - ] - else: - processes = [ - context.Process( - target=run_worker, - args=(bot.run, instance_lock.fileno()), - name="bot", - ), - context.Process( - target=run_worker, - args=(event.run, instance_lock.fileno()), - name="events", - ), - ] + shutdown_requested = False def request_shutdown(_signum, _frame) -> None: - raise KeyboardInterrupt + nonlocal shutdown_requested + shutdown_requested = True signal.signal(signal.SIGINT, request_shutdown) signal.signal(signal.SIGTERM, request_shutdown) os.system("cls" if sys.platform == "win32" else "clear") models.logger.log("Main", "Startup...") + + if sys.platform == "win32": + import bot + import event + + context = get_context("spawn") + processes: list[Process] = [] + definitions = [("bot", bot.run), ("events", event.run)] + try: + for name, target in definitions: + process = context.Process(target=target, name=name) + process.start() + processes.append(process) + + while not shutdown_requested and all(process.is_alive() for process in processes): + time.sleep(0.5) + + if not shutdown_requested: + stopped = next(process for process in processes if not process.is_alive()) + models.logger.error( + "Main", + f"{stopped.name} process exited unexpectedly with code {stopped.exitcode}", + ) + return 1 + return 0 + except Exception as error: + models.logger.error("Main", f"{error}") + return 1 + finally: + stop_processes(processes) + instance_lock.close() + + project_dir = Path(__file__).resolve().parent + definitions = [ + ("bot", "import bot; bot.run()"), + ("events", "import event; event.run()"), + ] + processes: list[tuple[str, subprocess.Popen]] = [] try: - for process in processes: - process.start() + for name, command in definitions: + process = subprocess.Popen( + [sys.executable, "-c", command], + cwd=project_dir, + pass_fds=(instance_lock.fileno(),), + ) + processes.append((name, process)) - while all(process.is_alive() for process in processes): - for process in processes: - process.join(timeout=0.5) + while not shutdown_requested and all(process.poll() is None for _, process in processes): + time.sleep(0.5) - stopped = next(process for process in processes if not process.is_alive()) - models.logger.error( - "Main", - f"{stopped.name} process exited unexpectedly with code {stopped.exitcode}", - ) - return 1 - except KeyboardInterrupt: - models.logger.error("Main", "Stopping...") + if not shutdown_requested: + name, stopped = next( + (name, process) for name, process in processes if process.poll() is not None + ) + models.logger.error( + "Main", + f"{name} process exited unexpectedly with code {stopped.returncode}", + ) + return 1 return 0 except Exception as error: models.logger.error("Main", f"{error}") return 1 finally: - stop_processes(processes) + stop_subprocesses([process for _, process in processes]) instance_lock.close()