run bot in a fresh interpreter

This commit is contained in:
синечка ♡ 2026-09-16 07:07:47 +00:00
parent fef8cfdbb8
commit 9c7f18d532

103
main.py
View file

@ -2,10 +2,11 @@ from __future__ import annotations
import os import os
import signal import signal
import subprocess
import sys import sys
import time
from multiprocessing import Process, get_context from multiprocessing import Process, get_context
from pathlib import Path from pathlib import Path
from collections.abc import Callable
from typing import BinaryIO from typing import BinaryIO
@ -54,66 +55,61 @@ def stop_processes(processes: list[Process]) -> None:
process.join() process.join()
def run_worker(target: Callable[[], None], lock_fd: int) -> None: def stop_subprocesses(processes: list[subprocess.Popen]) -> None:
# Keep an explicit duplicate alive in the worker. A Linux flock belongs to for process in processes:
# the shared open-file description, so it remains held if the parent dies. if process.poll() is None:
with os.fdopen(os.dup(lock_fd), "rb"): process.terminate()
target() for process in processes:
try:
process.wait(SHUTDOWN_TIMEOUT)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
def run() -> int: def run() -> int:
# Import only after taking the lock so a rejected duplicate has no bot/event # 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 # side effects. Linux workers are fresh interpreters which explicitly inherit
# supervisor dies unexpectedly, an orphan cannot poll alongside a replacement. # the lock descriptor. If the supervisor dies, an orphan keeps the lock and a
# replacement cannot poll alongside it.
instance_lock = acquire_instance_lock() instance_lock = acquire_instance_lock()
import bot
import event
import models.logger import models.logger
context = get_context("spawn" if sys.platform == "win32" else "fork") shutdown_requested = False
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",
),
]
def request_shutdown(_signum, _frame) -> None: def request_shutdown(_signum, _frame) -> None:
raise KeyboardInterrupt nonlocal shutdown_requested
shutdown_requested = True
signal.signal(signal.SIGINT, request_shutdown) signal.signal(signal.SIGINT, request_shutdown)
signal.signal(signal.SIGTERM, request_shutdown) signal.signal(signal.SIGTERM, request_shutdown)
os.system("cls" if sys.platform == "win32" else "clear") os.system("cls" if sys.platform == "win32" else "clear")
models.logger.log("Main", "Startup...") 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: try:
for process in processes: for name, target in definitions:
process = context.Process(target=target, name=name)
process.start() process.start()
processes.append(process)
while all(process.is_alive() for process in processes): while not shutdown_requested and all(process.is_alive() for process in processes):
for process in processes: time.sleep(0.5)
process.join(timeout=0.5)
if not shutdown_requested:
stopped = next(process for process in processes if not process.is_alive()) stopped = next(process for process in processes if not process.is_alive())
models.logger.error( models.logger.error(
"Main", "Main",
f"{stopped.name} process exited unexpectedly with code {stopped.exitcode}", f"{stopped.name} process exited unexpectedly with code {stopped.exitcode}",
) )
return 1 return 1
except KeyboardInterrupt:
models.logger.error("Main", "Stopping...")
return 0 return 0
except Exception as error: except Exception as error:
models.logger.error("Main", f"{error}") models.logger.error("Main", f"{error}")
@ -122,6 +118,41 @@ def run() -> int:
stop_processes(processes) stop_processes(processes)
instance_lock.close() 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 name, command in definitions:
process = subprocess.Popen(
[sys.executable, "-c", command],
cwd=project_dir,
pass_fds=(instance_lock.fileno(),),
)
processes.append((name, process))
while not shutdown_requested and all(process.poll() is None for _, process in processes):
time.sleep(0.5)
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_subprocesses([process for _, process in processes])
instance_lock.close()
if __name__ == "__main__": if __name__ == "__main__":
try: try: