Launch multiple honcho files depending on the context

This commit is contained in:
Antoine Bartuccio 2025-03-04 14:48:44 +01:00
parent 87f790a044
commit 6b27a97e7b
11 changed files with 66 additions and 48 deletions

View File

@ -13,5 +13,5 @@ CACHE_URL=redis://127.0.0.1:${REDIS_PORT}/0
# Used to select which other services to run alongside
# manage.py, pytest and runserver
PROCFILE_FULL=Procfile.full
PROCFILE_MINIMAL=Procfile.minimal
PROCFILE_STATIC=Procfile.static
PROCFILE_SERVICE=Procfile.service

View File

@ -1,2 +0,0 @@
bundler: npm run serve
redis: redis-server --port $REDIS_PORT

1
Procfile.static Normal file
View File

@ -0,0 +1 @@
bundler: npm run serve

View File

@ -88,15 +88,15 @@ tous ces services sont déjà configurés.
Pour désactiver Honcho il suffit de ne sélectionner aucun `PROCFILE_` dans la config.
```dotenv
PROCFILE_FULL=
PROCFILE_MINIMAL=
PROCFILE_STATIC=
PROCFILE_SERVICE=
```
!!! note
Si honcho ne tourne plus, la recompilation automatique
Si `PROCFILE_STATIC` est désactivé, la recompilation automatique
des fichiers statiques ne se fait plus.
Si vous en avez besoin et que vous travaillez sans honcho,
Si vous en avez besoin et que vous travaillez sans `PROCFILE_STATIC`,
vous devez ouvrir une autre fenêtre de votre terminal
et lancer la commande `npm run serve`

View File

@ -82,8 +82,8 @@ sith/
├── pyproject.toml (31)
├── .venv/ (32)
├── .python-version (33)
├── Procfile.full (34)
├── Procfile.minimal (35)
├── Procfile.static (34)
├── Procfile.service (35)
└── README.md
```
</div>
@ -142,10 +142,10 @@ sith/
de certaines d'entre elles.
32. Dossier d'environnement virtuel généré par uv
33. Fichier qui contrôle quelle version de python utiliser pour le projet
34. Fichier qui contrôle les services additionnels à lancer
avec le serveur de développement. Serveurs + gestion des statics.
35. Fichier qui contrôle les services additionnels à lancer
avec les autres commandes (pytest, shell…). Serveurs uniquement.
34. Fichier qui contrôle les commandes à lancer pour gérer la compilation
automatique des static et autres services nécessaires à la command runserver.
35. Fichier qui contrôle les services tiers nécessaires au fonctionnement
du Sith tel que redis.
## L'application principale

View File

@ -21,7 +21,7 @@ import sys
from django.utils.autoreload import DJANGO_AUTORELOAD_ENV
from sith.composer import start_composer, stop_composer
from sith.settings import PROCFILE_FULL, PROCFILE_MINIMAL
from sith.settings import PROCFILE_SERVICE
if __name__ == "__main__":
logging.basicConfig(encoding="utf-8", level=logging.INFO)
@ -30,9 +30,8 @@ if __name__ == "__main__":
from django.core.management import execute_from_command_line
procfile = PROCFILE_FULL if sys.argv[1] == "runserver" else PROCFILE_MINIMAL
if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and procfile is not None:
start_composer(procfile)
_ = atexit.register(stop_composer)
if os.environ.get(DJANGO_AUTORELOAD_ENV) is None and PROCFILE_SERVICE is not None:
start_composer(PROCFILE_SERVICE)
_ = atexit.register(stop_composer, procfile=PROCFILE_SERVICE)
execute_from_command_line(sys.argv)

View File

@ -9,30 +9,37 @@ import psutil
from sith import settings
def get_pid() -> int | None:
def get_pid_file(procfile: Path) -> Path:
"""Get the PID file associated with a procfile"""
return settings.BASE_DIR / procfile.with_suffix(f"{procfile.suffix}.pid")
def get_pid(procfile: Path) -> int | None:
"""Read the PID file to get the currently running composer if it exists"""
if not settings.COMPOSER_PID_PATH.exists():
file = get_pid_file(procfile)
if not file.exists():
return None
with open(settings.COMPOSER_PID_PATH, "r", encoding="utf8") as f:
with open(file, "r", encoding="utf8") as f:
return int(f.read())
def write_pid(pid: int):
def write_pid(procfile: Path, 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:
file = get_pid_file(procfile)
if not file.exists():
file.parent.mkdir(parents=True, exist_ok=True)
with open(file, "w", encoding="utf8") as f:
_ = f.write(str(pid))
def delete_pid():
def delete_pid(procfile: Path):
"""Delete PID file for cleanup"""
settings.COMPOSER_PID_PATH.unlink(missing_ok=True)
get_pid_file(procfile).unlink(missing_ok=True)
def is_composer_running() -> bool:
def is_composer_running(procfile: Path) -> bool:
"""Check if the process in the PID file is running"""
pid = get_pid()
pid = get_pid(procfile)
if pid is None:
return False
try:
@ -45,25 +52,29 @@ def start_composer(procfile: Path):
"""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()}")
if is_composer_running(procfile):
logging.info(
f"If this is a mistake, please delete {settings.COMPOSER_PID_PATH} and restart the process"
f"Composer for {procfile} is already running with pid {get_pid(procfile)}"
)
logging.info(
f"If this is a mistake, please delete {get_pid_file(procfile)} and restart the process"
)
return
process = subprocess.Popen(
[sys.executable, "-m", "honcho", "-f", str(procfile), "start"],
)
write_pid(process.pid)
write_pid(procfile, process.pid)
def stop_composer():
def stop_composer(procfile: Path):
"""Stops the composer if it was started before"""
if is_composer_running():
process = psutil.Process(get_pid())
if is_composer_running(procfile):
process = psutil.Process(get_pid(procfile))
if process.parent() != psutil.Process():
logging.info("Currently running composer is controlled by another process")
logging.info(
f"Currently running composer for {procfile} is controlled by another process"
)
return
process.send_signal(signal.SIGTERM)
_ = process.wait()
delete_pid()
delete_pid(procfile)

View File

@ -3,7 +3,7 @@ import atexit
import pytest
from .composer import start_composer, stop_composer
from .settings import PROCFILE_MINIMAL
from .settings import PROCFILE_SERVICE
# it's the first hook loaded by pytest and can only
# be defined in a proper pytest plugin
@ -15,6 +15,6 @@ from .settings import PROCFILE_MINIMAL
@pytest.hookimpl(tryfirst=True)
def pytest_load_initial_conftests(early_config, parser, args):
"""Hook that loads the composer before the pytest-django plugin"""
if PROCFILE_MINIMAL is not None:
start_composer(PROCFILE_MINIMAL)
_ = atexit.register(stop_composer)
if PROCFILE_SERVICE is not None:
start_composer(PROCFILE_SERVICE)
_ = atexit.register(stop_composer, procfile=PROCFILE_SERVICE)

View File

@ -64,11 +64,8 @@ def optional_file_parser(value: str) -> Path | None:
BASE_DIR = Path(__file__).parent.parent.resolve()
# Composer settings
PROCFILE_FULL = env.optional_file("PROCFILE_FULL", None)
PROCFILE_MINIMAL = env.optional_file("PROCFILE_MINIMAL", 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")
PROCFILE_STATIC = env.optional_file("PROCFILE_STATIC", None)
PROCFILE_SERVICE = env.optional_file("PROCFILE_SERVICE", None)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/

View File

@ -1,7 +1,13 @@
import atexit
import os
from django.conf import settings
from django.contrib.staticfiles.management.commands.runserver import (
Command as Runserver,
)
from django.utils.autoreload import DJANGO_AUTORELOAD_ENV
from sith.composer import start_composer, stop_composer
from staticfiles.processors import OpenApi
@ -10,4 +16,10 @@ class Command(Runserver):
def run(self, **options):
OpenApi.compile()
if (
os.environ.get(DJANGO_AUTORELOAD_ENV) is None
and settings.PROCFILE_STATIC is not None
):
start_composer(settings.PROCFILE_STATIC)
_ = atexit.register(stop_composer, procfile=settings.PROCFILE_STATIC)
super().run(**options)