diff --git a/manage.py b/manage.py index 444f6799..05877790 100755 --- a/manage.py +++ b/manage.py @@ -13,6 +13,8 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +import atexit +import logging import os import sys @@ -22,13 +24,14 @@ from sith.composer import start_composer, stop_composer from sith.settings import PROCFILE_RUNSERVER if __name__ == "__main__": + logging.basicConfig(encoding="utf-8", level=logging.INFO) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sith.settings") from django.core.management import execute_from_command_line if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_RUNSERVER is not None: start_composer(PROCFILE_RUNSERVER) + _ = atexit.register(stop_composer) execute_from_command_line(sys.argv) - - stop_composer() diff --git a/sith/composer.py b/sith/composer.py index 8355e205..e0a97bbd 100644 --- a/sith/composer.py +++ b/sith/composer.py @@ -1,26 +1,68 @@ -import os +import logging import signal import subprocess import sys 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): """Starts the composer and stores the PID as an environment variable 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( [sys.executable, "-m", "honcho", "-f", procfile, "start"], ) - os.environ[COMPOSER_PID] = str(process.pid) + write_pid(process.pid) def stop_composer(): """Stops the composer if it was started before""" - if (pid := os.environ.get(COMPOSER_PID, None)) is not None: - process = psutil.Process(int(pid)) + if is_composer_running(): + 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.wait() + _ = process.wait() + delete_pid() diff --git a/sith/pytest.py b/sith/pytest.py index 29250e16..731cd584 100644 --- a/sith/pytest.py +++ b/sith/pytest.py @@ -1,3 +1,5 @@ +import atexit + import pytest 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""" if PROCFILE_PYTEST is not None: start_composer(PROCFILE_PYTEST) - - -def pytest_unconfigure(config): - stop_composer() + _ = atexit.register(stop_composer) diff --git a/sith/settings.py b/sith/settings.py index cc434703..b9acd61d 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -56,6 +56,9 @@ BASE_DIR = Path(__file__).parent.parent.resolve() PROCFILE_RUNSERVER = env.str("PROCFILE_RUNSERVER", 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 # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/