import logging import signal import time from subprocess import DEVNULL from threading import Thread from psutil import Popen from watchdog.events import RegexMatchingEventHandler from watchdog.observers import Observer from smartwatch import settings def create_collectstatic(): return Popen( ["python", "manage.py", "collectstatic", "--noinput"], ) def create_migrate(): return Popen(["python", "manage.py", "migrate"], stdout=DEVNULL, stderr=DEVNULL) def create_gunicorn(): return Popen( [ "gunicorn", f"--bind={settings.SMARTWATCH_GUNICORN_HOST}:{settings.SMARTWATCH_GUNICORN_PORT}", f"--workers={settings.SMARTWATCH_GUNICORN_WORKERS}", f"{settings.PROJECT_NAME}.wsgi:application", f"--log-level={settings.SMARTWATCH_GUNICORN_LOGLEVEL}", ] ) def create_daphne(): return Popen( [ "daphne", "-b", "0.0.0.0", "-p", str(settings.SMARTWATCH_DAPHNE_PORT), f"{settings.PROJECT_NAME}.asgi:application", ] ) def create_celery(): return Popen( [ "celery", "-A", settings.PROJECT_NAME, "worker", f"--loglevel={settings.SMARTWATCH_CELERY_LOGLEVEL}", ] ) def create_install(): return Popen( ["pip", "install", "-r", "requirements.txt"], stdout=DEVNULL, stderr=DEVNULL, ) def prep_server(): create_install().wait() logging.info("Requirements installed") if settings.SMARTWATCH_MIGRATE: create_migrate().wait() logging.info("Migrated database") if settings.SMARTWATCH_COLLECT_STATIC: create_collectstatic().wait() logging.info("Collected static files") class ServerHandler(RegexMatchingEventHandler): DEBOUNCE_SECONDS = settings.SMARTWATCH_DEBOUNCE_SECONDS def __init__(self, *args, **kwargs): regexes = [".*.py", ".*.html", ".*/static/.*", ".*requirements.txt"] super().__init__(*args, **kwargs, regexes=regexes, ignore_directories=True) self.last_modified = time.time() self.changes_queue = "" self.is_restarting = False self.is_running = True """Adds all the changed files into a string to be processed at once""" prep_server() self.gunicorn = create_gunicorn() self.daphne = create_daphne() self.celery = create_celery() def on_any_event(self, event): if event.event_type in ["modified", "created", "deleted", "moved"]: self.changes_queue += str(event.src_path) if not self.is_restarting: Thread(target=self.restart).start() def restart(self): self.is_restarting = True prep_server() if ".py" or ".html" in self.changes_queue: self.gunicorn.send_signal(signal.SIGINT) self.daphne.send_signal(signal.SIGINT) self.celery.send_signal(signal.SIGINT) self.gunicorn.wait() self.gunicorn = create_gunicorn() self.daphne.wait() self.daphne = create_daphne() self.celery.wait() self.celery = create_celery() self.changes_queue = "" self.is_restarting = False def stop(self): self.is_running = False self.gunicorn.send_signal(signal.SIGINT) self.daphne.send_signal(signal.SIGINT) self.celery.send_signal(signal.SIGINT) def join(self): self.gunicorn.wait() self.daphne.wait() self.celery.wait() def sleep(self, seconds): start = time.time() while self.is_running and time.time() - start < seconds: continue def watch_server(): server_handler = ServerHandler() observer = Observer() observer.schedule(server_handler, ".", recursive=True) observer.start() def shutdown(*_): logging.warning("Stopping Smartwatch server from SIGKILL...") server_handler.stop() observer.stop() observer.join() signal.signal(signal.SIGTERM, shutdown) try: while observer.is_alive(): observer.join(1) except KeyboardInterrupt: logging.warning("Stopping Smartwatch server...") finally: shutdown()