Plain is headed towards 1.0! Subscribe for development updates →

  1import os
  2import subprocess
  3import time
  4import tomllib
  5from importlib.util import find_spec
  6from pathlib import Path
  7
  8import click
  9
 10from plain.runtime import APP_PATH, settings
 11
 12from .poncho.manager import Manager as PonchoManager
 13from .utils import has_pyproject_toml
 14
 15
 16class ServicesPid:
 17    def __init__(self):
 18        self.pidfile = settings.PLAIN_TEMP_PATH / "dev" / "services.pid"
 19
 20    def write(self):
 21        pid = os.getpid()
 22        self.pidfile.parent.mkdir(parents=True, exist_ok=True)
 23        with self.pidfile.open("w+") as f:
 24            f.write(str(pid))
 25
 26    def rm(self):
 27        self.pidfile.unlink()
 28
 29    def exists(self):
 30        return self.pidfile.exists()
 31
 32
 33class Services:
 34    @staticmethod
 35    def get_services(root):
 36        if not has_pyproject_toml(root):
 37            return {}
 38
 39        with open(Path(root, "pyproject.toml"), "rb") as f:
 40            pyproject = tomllib.load(f)
 41
 42        return (
 43            pyproject.get("tool", {})
 44            .get("plain", {})
 45            .get("dev", {})
 46            .get("services", {})
 47        )
 48
 49    def __init__(self):
 50        self.poncho = PonchoManager()
 51
 52    @staticmethod
 53    def are_running():
 54        pid = ServicesPid()
 55        return pid.exists()
 56
 57    def run(self):
 58        # Each user of Services will have to check if it is running by:
 59        # - using the context manager (with Services())
 60        # - calling are_running() directly
 61        pid = ServicesPid()
 62        pid.write()
 63
 64        try:
 65            services = self.get_services(APP_PATH.parent)
 66            for name, data in services.items():
 67                env = {
 68                    **os.environ,
 69                    "PYTHONUNBUFFERED": "true",
 70                    **data.get("env", {}),
 71                }
 72                self.poncho.add_process(name, data["cmd"], env=env)
 73
 74            self.poncho.loop()
 75        finally:
 76            pid.rm()
 77
 78    def __enter__(self):
 79        if not self.get_services(APP_PATH.parent):
 80            # No-op if no services are defined
 81            return
 82
 83        if self.are_running():
 84            click.secho("Services already running", fg="yellow")
 85            return
 86
 87        print("Starting `plain dev services`")
 88        self._subprocess = subprocess.Popen(
 89            ["plain", "dev", "services"], cwd=APP_PATH.parent
 90        )
 91
 92        if find_spec("plain.models"):
 93            time.sleep(0.5)  # Give it a chance to hit on the first try
 94            subprocess.check_call(["plain", "models", "db-wait"], env=os.environ)
 95        else:
 96            # A bit of a hack to wait for the services to start
 97            time.sleep(3)
 98
 99    def __exit__(self, *args):
100        if not hasattr(self, "_subprocess"):
101            return
102
103        self._subprocess.terminate()
104
105        # Flush the buffer so the output doesn't spill over
106        self._subprocess.communicate()