Add a pid file to avoid running honcho multiple times

This commit is contained in:
Antoine Bartuccio 2025-02-26 10:42:11 +01:00
parent aa66fc61ab
commit e542fe11b9
4 changed files with 59 additions and 12 deletions

View File

@ -13,6 +13,8 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
import atexit
import logging
import os import os
import sys import sys
@ -22,13 +24,14 @@ from sith.composer import start_composer, stop_composer
from sith.settings import PROCFILE_RUNSERVER from sith.settings import PROCFILE_RUNSERVER
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(encoding="utf-8", level=logging.INFO)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sith.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sith.settings")
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_RUNSERVER is not None: if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_RUNSERVER is not None:
start_composer(PROCFILE_RUNSERVER) start_composer(PROCFILE_RUNSERVER)
_ = atexit.register(stop_composer)
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
stop_composer()

View File

@ -1,26 +1,68 @@
import os import logging
import signal import signal
import subprocess import subprocess
import sys import sys
import psutil import psutil
COMPOSER_PID = "COMPOSER_PID" from sith import settings
def get_pid() -> int | None:
"""Read the PID file to get the currently running composer if it exists"""
if not settings.COMPOSER_PID_PATH.exists():
return None
with open(settings.COMPOSER_PID_PATH, "r", encoding="utf8") as f:
return int(f.read())
def write_pid(pid: int):
"""Write currently running composer pid in PID file"""
if not settings.COMPOSER_PID_PATH.exists():
settings.COMPOSER_PID_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(settings.COMPOSER_PID_PATH, "w", encoding="utf8") as f:
_ = f.write(str(pid))
def delete_pid():
"""Delete PID file for cleanup"""
settings.COMPOSER_PID_PATH.unlink(missing_ok=True)
def is_composer_running() -> bool:
"""Check if the process in the PID file is running"""
pid = get_pid()
if pid is None:
return False
try:
return psutil.Process(pid).is_running()
except psutil.NoSuchProcess:
return False
def start_composer(procfile: str): def start_composer(procfile: str):
"""Starts the composer and stores the PID as an environment variable """Starts the composer and stores the PID as an environment variable
This allows for running smoothly with the django reloader This allows for running smoothly with the django reloader
""" """
if is_composer_running():
logging.info(f"Composer is already running with pid {get_pid()}")
logging.info(
f"If this is a mistake, please delete {settings.COMPOSER_PID_PATH} and restart the process"
)
return
process = subprocess.Popen( process = subprocess.Popen(
[sys.executable, "-m", "honcho", "-f", procfile, "start"], [sys.executable, "-m", "honcho", "-f", procfile, "start"],
) )
os.environ[COMPOSER_PID] = str(process.pid) write_pid(process.pid)
def stop_composer(): def stop_composer():
"""Stops the composer if it was started before""" """Stops the composer if it was started before"""
if (pid := os.environ.get(COMPOSER_PID, None)) is not None: if is_composer_running():
process = psutil.Process(int(pid)) process = psutil.Process(get_pid())
if process.parent() != psutil.Process():
logging.info("Currently running composer is controlled by another process")
return
process.send_signal(signal.SIGTERM) process.send_signal(signal.SIGTERM)
process.wait() _ = process.wait()
delete_pid()

View File

@ -1,3 +1,5 @@
import atexit
import pytest import pytest
from .composer import start_composer, stop_composer from .composer import start_composer, stop_composer
@ -15,7 +17,4 @@ def pytest_load_initial_conftests(early_config, parser, args):
"""Hook that loads the composer before the pytest-django plugin""" """Hook that loads the composer before the pytest-django plugin"""
if PROCFILE_PYTEST is not None: if PROCFILE_PYTEST is not None:
start_composer(PROCFILE_PYTEST) start_composer(PROCFILE_PYTEST)
_ = atexit.register(stop_composer)
def pytest_unconfigure(config):
stop_composer()

View File

@ -56,6 +56,9 @@ BASE_DIR = Path(__file__).parent.parent.resolve()
PROCFILE_RUNSERVER = env.str("PROCFILE_RUNSERVER", None) PROCFILE_RUNSERVER = env.str("PROCFILE_RUNSERVER", None)
PROCFILE_PYTEST = env.str("PROCFILE_PYTEST", None) PROCFILE_PYTEST = env.str("PROCFILE_PYTEST", None)
## File path used to avoid running the composer multiple times at the same time
COMPOSER_PID_PATH = env.path("COMPOSER_PID_PATH", BASE_DIR / "composer.pid")
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/