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 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:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue