run bot in a fresh interpreter
This commit is contained in:
parent
fef8cfdbb8
commit
9c7f18d532
1 changed files with 75 additions and 44 deletions
103
main.py
103
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,66 +55,61 @@ 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 process in processes:
|
||||
for name, target in definitions:
|
||||
process = context.Process(target=target, name=name)
|
||||
process.start()
|
||||
processes.append(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.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
|
||||
except KeyboardInterrupt:
|
||||
models.logger.error("Main", "Stopping...")
|
||||
return 0
|
||||
except Exception as error:
|
||||
models.logger.error("Main", f"{error}")
|
||||
|
|
@ -122,6 +118,41 @@ def run() -> int:
|
|||
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 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__":
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue