Reusable Django app for system monitoring. Built to make dev environment more like production environment. Combines celery with gunicorn and daphne.
initial commit
47 files changed, +513 -0
| @@ -0,0 +1,8 @@ | ||
| 1 | +Metadata-Version: 2.4 | |
| 2 | +Name: django-smartwatch | |
| 3 | +Version: 0.1.0 | |
| 4 | +Summary: Reusable Django app for system monitoring | |
| 5 | +Project-URL: Homepage, https://github.com/huncholane/django-smartwatch | |
| 6 | +Requires-Python: >=3.8 | |
| 7 | +Description-Content-Type: text/markdown | |
| 8 | +Requires-Dist: django |
| @@ -0,0 +1,24 @@ | ||
| 1 | +pyproject.toml | |
| 2 | +django_smartwatch.egg-info/PKG-INFO | |
| 3 | +django_smartwatch.egg-info/SOURCES.txt | |
| 4 | +django_smartwatch.egg-info/dependency_links.txt | |
| 5 | +django_smartwatch.egg-info/requires.txt | |
| 6 | +django_smartwatch.egg-info/top_level.txt | |
| 7 | +smartwatch/__init__.py | |
| 8 | +smartwatch/admin.py | |
| 9 | +smartwatch/apps.py | |
| 10 | +smartwatch/models.py | |
| 11 | +smartwatch/settings.py | |
| 12 | +smartwatch/tasks.py | |
| 13 | +smartwatch/tests.py | |
| 14 | +smartwatch/views.py | |
| 15 | +smartwatch/watch.py | |
| 16 | +smartwatch/management/commands/printenv.py | |
| 17 | +smartwatch/management/commands/smarttest.py | |
| 18 | +smartwatch/management/commands/smartwatch.py | |
| 19 | +smartwatch/management/commands/startcelery.py | |
| 20 | +smartwatch/migrations/0001_initial.py | |
| 21 | +smartwatch/migrations/0002_alter_filehash_file_path.py | |
| 22 | +smartwatch/migrations/0003_rename_filehash_smartfile.py | |
| 23 | +smartwatch/migrations/0004_delete_smartfile.py | |
| 24 | +smartwatch/migrations/__init__.py |
| @@ -0,0 +1 @@ | ||
| 1 | + |
| @@ -0,0 +1 @@ | ||
| 1 | +django |
| @@ -0,0 +1 @@ | ||
| 1 | +smartwatch |
| @@ -0,0 +1,14 @@ | ||
| 1 | +[project] | |
| 2 | +name = "django-smartwatch" | |
| 3 | +version = "0.1.0" | |
| 4 | +description = "Reusable Django app for system monitoring" | |
| 5 | +readme = "README.md" | |
| 6 | +requires-python = ">=3.8" | |
| 7 | +dependencies = ["django"] | |
| 8 | + | |
| 9 | +[project.urls] | |
| 10 | +Homepage = "https://github.com/huncholane/django-smartwatch" | |
| 11 | + | |
| 12 | +[build-system] | |
| 13 | +requires = ["setuptools"] | |
| 14 | +build-backend = "setuptools.build_meta" |
No content changes (mode or rename only).
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
| @@ -0,0 +1,6 @@ | ||
| 1 | +from django.apps import AppConfig | |
| 2 | + | |
| 3 | + | |
| 4 | +class SmartwatchConfig(AppConfig): | |
| 5 | + default_auto_field = 'django.db.models.BigAutoField' | |
| 6 | + name = 'smartwatch' |
Binary file changed.
Binary file changed.
Binary file changed.
| @@ -0,0 +1,11 @@ | ||
| 1 | +import os | |
| 2 | +from django.core.management.base import BaseCommand | |
| 3 | + | |
| 4 | + | |
| 5 | +class Command(BaseCommand): | |
| 6 | + help = "Print environment variables in alphabetical order" | |
| 7 | + | |
| 8 | + def handle(self, *args, **kwargs): | |
| 9 | + env_vars = sorted(os.environ.items()) | |
| 10 | + for key, value in env_vars: | |
| 11 | + self.stdout.write(f"\033[1;36m{key}\033[0m: {value}") |
| @@ -0,0 +1,78 @@ | ||
| 1 | +import os | |
| 2 | +import time | |
| 3 | +from multiprocessing import Process | |
| 4 | +from typing import Any, Iterable | |
| 5 | + | |
| 6 | +from django.core.management.commands.test import Command as TestCommand | |
| 7 | +from watchdog.events import FileSystemEventHandler | |
| 8 | +from watchdog.observers import Observer | |
| 9 | + | |
| 10 | + | |
| 11 | +class Command(TestCommand): | |
| 12 | + help = "Watch test files and run tests when they change. Runs the testing in another process." | |
| 13 | + args: Iterable[Any] | |
| 14 | + options: dict | |
| 15 | + | |
| 16 | + def handle(self, *args, **options): | |
| 17 | + self.args = args | |
| 18 | + self.options = options | |
| 19 | + | |
| 20 | + event_handler = TestFileChangeHandler(self) | |
| 21 | + observer = Observer() | |
| 22 | + observer.schedule(event_handler, os.getcwd(), recursive=True) | |
| 23 | + observer.start() | |
| 24 | + | |
| 25 | + try: | |
| 26 | + while True: | |
| 27 | + time.sleep(1) | |
| 28 | + except KeyboardInterrupt: | |
| 29 | + observer.stop() | |
| 30 | + observer.join() | |
| 31 | + | |
| 32 | + | |
| 33 | +class TestFileChangeHandler(FileSystemEventHandler): | |
| 34 | + process: Process | |
| 35 | + | |
| 36 | + def __init__(self, command: Command): | |
| 37 | + self.command = command | |
| 38 | + self.command.stdout.write( | |
| 39 | + self.command.style.SUCCESS("Running the tests for the first time...") | |
| 40 | + ) | |
| 41 | + # self.process = None | |
| 42 | + self.run_tests() | |
| 43 | + | |
| 44 | + def on_modified(self, event): | |
| 45 | + assert isinstance(event.src_path, str) | |
| 46 | + if event.src_path.endswith(".py"): | |
| 47 | + self.command.stdout.write( | |
| 48 | + self.command.style.SUCCESS("File change detected. Retesting...") | |
| 49 | + ) | |
| 50 | + if self.process: | |
| 51 | + self.process.kill() | |
| 52 | + self.run_tests() | |
| 53 | + self.command.stdout.write( | |
| 54 | + self.command.style.SUCCESS("Watching for changes to retest...") | |
| 55 | + ) | |
| 56 | + | |
| 57 | + def run_tests(self): | |
| 58 | + self.process = Process( | |
| 59 | + target=start_tests, args=self.command.args, kwargs=self.command.options | |
| 60 | + ) | |
| 61 | + self.process.start() | |
| 62 | + self.process.join() | |
| 63 | + | |
| 64 | + | |
| 65 | +def start_tests(*args, **_): | |
| 66 | + import dotenv | |
| 67 | + | |
| 68 | + dotenv.load_dotenv("smarttest.env") | |
| 69 | + try: | |
| 70 | + from django.core.management import execute_from_command_line | |
| 71 | + except ImportError as exc: | |
| 72 | + raise ImportError( | |
| 73 | + "Couldn't import Django. Are you sure it's installed and " | |
| 74 | + "available on your PYTHONPATH environment variable? Did you " | |
| 75 | + "forget to activate a virtual environment?" | |
| 76 | + ) from exc | |
| 77 | + args = ["manage.py", "test", "--keepdb", *args] | |
| 78 | + execute_from_command_line(args) |
| @@ -0,0 +1,10 @@ | ||
| 1 | +from django.core.management.base import BaseCommand | |
| 2 | + | |
| 3 | +from smartwatch.watch import watch_server | |
| 4 | + | |
| 5 | + | |
| 6 | +class Command(BaseCommand): | |
| 7 | + help = "Starts the file watcher" | |
| 8 | + | |
| 9 | + def handle(self, *args, **options): | |
| 10 | + watch_server() |
| @@ -0,0 +1,20 @@ | ||
| 1 | +import signal | |
| 2 | + | |
| 3 | +from django.core.management.base import BaseCommand | |
| 4 | +from psutil import Popen | |
| 5 | + | |
| 6 | +from smartwatch import settings | |
| 7 | + | |
| 8 | + | |
| 9 | +class Command(BaseCommand): | |
| 10 | + help = "Starts the celery worker" | |
| 11 | + celery_process: Popen | None = None | |
| 12 | + | |
| 13 | + def handle(self, *args, **kwargs): | |
| 14 | + signal.signal(signal.SIGINT, self.shutdown) | |
| 15 | + self.celery_process = Popen(["celery", "-A", settings.PROJECT_NAME, "worker"]) | |
| 16 | + self.celery_process.wait() | |
| 17 | + | |
| 18 | + def shutdown(self, *_): | |
| 19 | + if self.celery_process: | |
| 20 | + self.celery_process.send_signal(signal.SIGINT) |
| @@ -0,0 +1,29 @@ | ||
| 1 | +# Generated by Django 5.0.7 on 2024-10-31 11:38 | |
| 2 | + | |
| 3 | +from django.db import migrations, models | |
| 4 | + | |
| 5 | + | |
| 6 | +class Migration(migrations.Migration): | |
| 7 | + | |
| 8 | + initial = True | |
| 9 | + | |
| 10 | + dependencies = [] | |
| 11 | + | |
| 12 | + operations = [ | |
| 13 | + migrations.CreateModel( | |
| 14 | + name="FileHash", | |
| 15 | + fields=[ | |
| 16 | + ( | |
| 17 | + "id", | |
| 18 | + models.BigAutoField( | |
| 19 | + auto_created=True, | |
| 20 | + primary_key=True, | |
| 21 | + serialize=False, | |
| 22 | + verbose_name="ID", | |
| 23 | + ), | |
| 24 | + ), | |
| 25 | + ("file_path", models.CharField(max_length=1000)), | |
| 26 | + ("md5_hash", models.CharField(max_length=32)), | |
| 27 | + ], | |
| 28 | + ), | |
| 29 | + ] |
| @@ -0,0 +1,38 @@ | ||
| 1 | +# Generated by Django 5.0.7 on 2024-10-31 12:44 | |
| 2 | + | |
| 3 | +import logging | |
| 4 | +from django.db import migrations, models | |
| 5 | + | |
| 6 | + | |
| 7 | +def handle_duplicates(apps, _): | |
| 8 | + try: | |
| 9 | + FileHash = apps.get_model("smartwatch", "FileHash") | |
| 10 | + duplicates = ( | |
| 11 | + FileHash.objects.values("file_path") | |
| 12 | + .annotate(count=models.Count("id")) | |
| 13 | + .filter(count__gt=1) | |
| 14 | + ) | |
| 15 | + | |
| 16 | + for duplicate in duplicates: | |
| 17 | + file_path = duplicate["file_path"] | |
| 18 | + duplicate_entries = FileHash.objects.filter(file_path=file_path) | |
| 19 | + # Keep the first entry and delete the rest | |
| 20 | + first_entry = duplicate_entries.first() | |
| 21 | + duplicate_entries.exclude(id=first_entry.id).delete() | |
| 22 | + except Exception: | |
| 23 | + logging.error("Error while handling duplicates") | |
| 24 | + | |
| 25 | + | |
| 26 | +class Migration(migrations.Migration): | |
| 27 | + dependencies = [ | |
| 28 | + ("smartwatch", "0001_initial"), | |
| 29 | + ] | |
| 30 | + | |
| 31 | + operations = [ | |
| 32 | + migrations.RunPython(handle_duplicates), | |
| 33 | + migrations.AlterField( | |
| 34 | + model_name="filehash", | |
| 35 | + name="file_path", | |
| 36 | + field=models.CharField(max_length=1000, unique=True), | |
| 37 | + ), | |
| 38 | + ] |
| @@ -0,0 +1,17 @@ | ||
| 1 | +# Generated by Django 5.0.7 on 2024-11-02 22:47 | |
| 2 | + | |
| 3 | +from django.db import migrations | |
| 4 | + | |
| 5 | + | |
| 6 | +class Migration(migrations.Migration): | |
| 7 | + | |
| 8 | + dependencies = [ | |
| 9 | + ("smartwatch", "0002_alter_filehash_file_path"), | |
| 10 | + ] | |
| 11 | + | |
| 12 | + operations = [ | |
| 13 | + migrations.RenameModel( | |
| 14 | + old_name="FileHash", | |
| 15 | + new_name="SmartFile", | |
| 16 | + ), | |
| 17 | + ] |
| @@ -0,0 +1,16 @@ | ||
| 1 | +# Generated by Django 5.0.7 on 2024-12-04 21:55 | |
| 2 | + | |
| 3 | +from django.db import migrations | |
| 4 | + | |
| 5 | + | |
| 6 | +class Migration(migrations.Migration): | |
| 7 | + | |
| 8 | + dependencies = [ | |
| 9 | + ("smartwatch", "0003_rename_filehash_smartfile"), | |
| 10 | + ] | |
| 11 | + | |
| 12 | + operations = [ | |
| 13 | + migrations.DeleteModel( | |
| 14 | + name="SmartFile", | |
| 15 | + ), | |
| 16 | + ] |
No content changes (mode or rename only).
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
Binary file changed.
| @@ -0,0 +1 @@ | ||
| 1 | + |
| @@ -0,0 +1,64 @@ | ||
| 1 | +from django.conf import settings | |
| 2 | +from environs import Env | |
| 3 | + | |
| 4 | +env = Env() | |
| 5 | + | |
| 6 | +# Gunicorn settings | |
| 7 | +SMARTWATCH_GUNICORN_PORT = getattr( | |
| 8 | + settings, "SMARTWATCH_GUNICORN_PORT", env.int("SMARTWATCH_GUNICORN_PORT", 8000) | |
| 9 | +) | |
| 10 | +SMARTWATCH_GUNICORN_HOST = getattr( | |
| 11 | + settings, "SMARTWATCH_GUNICORN_HOST", env.str("SMARTWATCH_GUNICORN_HOST", "0.0.0.0") | |
| 12 | +) | |
| 13 | +SMARTWATCH_USE_GUNICORN = getattr( | |
| 14 | + settings, "SMARTWATCH_USE_GUNICORN", env.bool("SMARTWATCH_USE_GUNICORN", True) | |
| 15 | +) | |
| 16 | +SMARTWATCH_GUNICORN_WORKERS = getattr( | |
| 17 | + settings, "SMARTWATCH_GUNICORN_WORKERS", env.int("SMARTWATCH_GUNICORN_WORKERS", 4) | |
| 18 | +) | |
| 19 | +SMARTWATCH_GUNICORN_LOGLEVEL = getattr( | |
| 20 | + settings, | |
| 21 | + "SMARTWATCH_GUNICORN_LOGLEVEL", | |
| 22 | + env.str("SMARTWATCH_GUNICORN_LOGLEVEL", "warning"), | |
| 23 | +) | |
| 24 | + | |
| 25 | +# Daphne settings | |
| 26 | +SMARTWATCH_DAPHNE_PORT = getattr( | |
| 27 | + settings, "SMARTWATCH_DAPHNE_PORT", env.int("SMARTWATCH_DAPHNE_PORT", 8001) | |
| 28 | +) | |
| 29 | +SMARTWATCH_DAPHNE_HOST = getattr( | |
| 30 | + settings, "SMARTWATCH_DAPHNE_HOST", env.str("SMARTWATCH_DAPHNE_HOST", "0.0.0.0") | |
| 31 | +) | |
| 32 | +SMARTWATCH_USE_DAPHNE = getattr( | |
| 33 | + settings, "SMARTWATCH_USE_DAPHNE", env.bool("SMARTWATCH_USE_DAPHNE", True) | |
| 34 | +) | |
| 35 | + | |
| 36 | +# Celery settings | |
| 37 | +SMARTWATCH_START_CELERY = getattr( | |
| 38 | + settings, "SMARTWATCH_START_CELERY", env.bool("SMARTWATCH_START_CELERY", True) | |
| 39 | +) | |
| 40 | +SMARTWATCH_CELERY_LOGLEVEL = getattr( | |
| 41 | + settings, | |
| 42 | + "SMARTWATCH_CELERY_LOGLEVEL", | |
| 43 | + env.str("SMARTWATCH_CELERY_LOGLEVEL", "info"), | |
| 44 | +) | |
| 45 | + | |
| 46 | +# Other settings | |
| 47 | +SMARTWATCH_COLLECT_STATIC = getattr( | |
| 48 | + settings, "SMARTWATCH_COLLECT_STATIC", env.bool("SMARTWATCH_COLLECT_STATIC", True) | |
| 49 | +) | |
| 50 | +SMARTWATCH_MIGRATE = getattr( | |
| 51 | + settings, "SMARTWATCH_MIGRATE", env.bool("SMARTWATCH_MIGRATE", True) | |
| 52 | +) | |
| 53 | +SMARTWATCH_TEST_DB_HOST = getattr( | |
| 54 | + settings, "SMARTWATCH_TEST_DB_HOST", env.str("SMARTWATCH_TEST_DB_HOST", "localhost") | |
| 55 | +) | |
| 56 | +SMARTWATCH_TEST_DB_PORT = getattr( | |
| 57 | + settings, "SMARTWATCH_TEST_DB_PORT", env.int("SMARTWATCH_TEST_DB_PORT", 5432) | |
| 58 | +) | |
| 59 | +SMARTWATCH_DEBOUNCE_SECONDS = getattr( | |
| 60 | + settings, | |
| 61 | + "SMARTWATCH_DEBOUNCE_SECONDS", | |
| 62 | + env.float("SMARTWATCH_DEBOUNCE_SECONDS", 0.1), | |
| 63 | +) | |
| 64 | +PROJECT_NAME = getattr(settings, "PROJECT_NAME", env.str("PROJECT_NAME")) |
| @@ -0,0 +1,19 @@ | ||
| 1 | +from celery import shared_task | |
| 2 | +import os | |
| 3 | +from smartwatch import settings | |
| 4 | + | |
| 5 | + | |
| 6 | +@shared_task | |
| 7 | +def start_asgi(hostname=None, port=None): | |
| 8 | + hostname = hostname or settings.SMARTWATCH_DAPHNE_HOST | |
| 9 | + port = port or settings.SMARTWATCH_DAPHNE_PORT | |
| 10 | + os.system( | |
| 11 | + f"daphne -b {hostname} -p {port} {settings.PROJECT_NAME}.asgi:application" | |
| 12 | + ) | |
| 13 | + | |
| 14 | + | |
| 15 | +@shared_task | |
| 16 | +def start_wsgi(hostname=None, port=None): | |
| 17 | + hostname = hostname or settings.SMARTWATCH_GUNICORN_HOST | |
| 18 | + port = port or settings.SMARTWATCH_GUNICORN_PORT | |
| 19 | + os.system(f"gunicorn -b {hostname}:{port} {settings.PROJECT_NAME}.wsgi:application") |
| @@ -0,0 +1,155 @@ | ||
| 1 | +import logging | |
| 2 | +import signal | |
| 3 | +import time | |
| 4 | +from subprocess import DEVNULL | |
| 5 | +from threading import Thread | |
| 6 | + | |
| 7 | +from psutil import Popen | |
| 8 | +from watchdog.events import RegexMatchingEventHandler | |
| 9 | +from watchdog.observers import Observer | |
| 10 | + | |
| 11 | +from smartwatch import settings | |
| 12 | + | |
| 13 | + | |
| 14 | +def create_collectstatic(): | |
| 15 | + return Popen( | |
| 16 | + ["python", "manage.py", "collectstatic", "--noinput"], | |
| 17 | + ) | |
| 18 | + | |
| 19 | + | |
| 20 | +def create_migrate(): | |
| 21 | + return Popen(["python", "manage.py", "migrate"], stdout=DEVNULL, stderr=DEVNULL) | |
| 22 | + | |
| 23 | + | |
| 24 | +def create_gunicorn(): | |
| 25 | + return Popen( | |
| 26 | + [ | |
| 27 | + "gunicorn", | |
| 28 | + f"--bind={settings.SMARTWATCH_GUNICORN_HOST}:{settings.SMARTWATCH_GUNICORN_PORT}", | |
| 29 | + f"--workers={settings.SMARTWATCH_GUNICORN_WORKERS}", | |
| 30 | + f"{settings.PROJECT_NAME}.wsgi:application", | |
| 31 | + f"--log-level={settings.SMARTWATCH_GUNICORN_LOGLEVEL}", | |
| 32 | + ] | |
| 33 | + ) | |
| 34 | + | |
| 35 | + | |
| 36 | +def create_daphne(): | |
| 37 | + return Popen( | |
| 38 | + [ | |
| 39 | + "daphne", | |
| 40 | + "-b", | |
| 41 | + "0.0.0.0", | |
| 42 | + "-p", | |
| 43 | + str(settings.SMARTWATCH_DAPHNE_PORT), | |
| 44 | + f"{settings.PROJECT_NAME}.asgi:application", | |
| 45 | + ] | |
| 46 | + ) | |
| 47 | + | |
| 48 | + | |
| 49 | +def create_celery(): | |
| 50 | + return Popen( | |
| 51 | + [ | |
| 52 | + "celery", | |
| 53 | + "-A", | |
| 54 | + settings.PROJECT_NAME, | |
| 55 | + "worker", | |
| 56 | + f"--loglevel={settings.SMARTWATCH_CELERY_LOGLEVEL}", | |
| 57 | + ] | |
| 58 | + ) | |
| 59 | + | |
| 60 | + | |
| 61 | +def create_install(): | |
| 62 | + return Popen( | |
| 63 | + ["pip", "install", "-r", "requirements.txt"], | |
| 64 | + stdout=DEVNULL, | |
| 65 | + stderr=DEVNULL, | |
| 66 | + ) | |
| 67 | + | |
| 68 | + | |
| 69 | +def prep_server(): | |
| 70 | + create_install().wait() | |
| 71 | + logging.info("Requirements installed") | |
| 72 | + if settings.SMARTWATCH_MIGRATE: | |
| 73 | + create_migrate().wait() | |
| 74 | + logging.info("Migrated database") | |
| 75 | + if settings.SMARTWATCH_COLLECT_STATIC: | |
| 76 | + create_collectstatic().wait() | |
| 77 | + logging.info("Collected static files") | |
| 78 | + | |
| 79 | + | |
| 80 | +class ServerHandler(RegexMatchingEventHandler): | |
| 81 | + DEBOUNCE_SECONDS = settings.SMARTWATCH_DEBOUNCE_SECONDS | |
| 82 | + | |
| 83 | + def __init__(self, *args, **kwargs): | |
| 84 | + regexes = [".*.py", ".*.html", ".*/static/.*", ".*requirements.txt"] | |
| 85 | + super().__init__(*args, **kwargs, regexes=regexes, ignore_directories=True) | |
| 86 | + self.last_modified = time.time() | |
| 87 | + self.changes_queue = "" | |
| 88 | + self.is_restarting = False | |
| 89 | + self.is_running = True | |
| 90 | + """Adds all the changed files into a string to be processed at once""" | |
| 91 | + prep_server() | |
| 92 | + self.gunicorn = create_gunicorn() | |
| 93 | + self.daphne = create_daphne() | |
| 94 | + self.celery = create_celery() | |
| 95 | + | |
| 96 | + def on_any_event(self, event): | |
| 97 | + if event.event_type in ["modified", "created", "deleted", "moved"]: | |
| 98 | + self.changes_queue += str(event.src_path) | |
| 99 | + if not self.is_restarting: | |
| 100 | + Thread(target=self.restart).start() | |
| 101 | + | |
| 102 | + def restart(self): | |
| 103 | + self.is_restarting = True | |
| 104 | + prep_server() | |
| 105 | + if ".py" or ".html" in self.changes_queue: | |
| 106 | + self.gunicorn.send_signal(signal.SIGINT) | |
| 107 | + self.daphne.send_signal(signal.SIGINT) | |
| 108 | + self.celery.send_signal(signal.SIGINT) | |
| 109 | + self.gunicorn.wait() | |
| 110 | + self.gunicorn = create_gunicorn() | |
| 111 | + self.daphne.wait() | |
| 112 | + self.daphne = create_daphne() | |
| 113 | + self.celery.wait() | |
| 114 | + self.celery = create_celery() | |
| 115 | + self.changes_queue = "" | |
| 116 | + self.is_restarting = False | |
| 117 | + | |
| 118 | + def stop(self): | |
| 119 | + self.is_running = False | |
| 120 | + self.gunicorn.send_signal(signal.SIGINT) | |
| 121 | + self.daphne.send_signal(signal.SIGINT) | |
| 122 | + self.celery.send_signal(signal.SIGINT) | |
| 123 | + | |
| 124 | + def join(self): | |
| 125 | + self.gunicorn.wait() | |
| 126 | + self.daphne.wait() | |
| 127 | + self.celery.wait() | |
| 128 | + | |
| 129 | + def sleep(self, seconds): | |
| 130 | + start = time.time() | |
| 131 | + while self.is_running and time.time() - start < seconds: | |
| 132 | + continue | |
| 133 | + | |
| 134 | + | |
| 135 | +def watch_server(): | |
| 136 | + server_handler = ServerHandler() | |
| 137 | + observer = Observer() | |
| 138 | + observer.schedule(server_handler, ".", recursive=True) | |
| 139 | + observer.start() | |
| 140 | + | |
| 141 | + def shutdown(*_): | |
| 142 | + logging.warning("Stopping Smartwatch server from SIGKILL...") | |
| 143 | + server_handler.stop() | |
| 144 | + observer.stop() | |
| 145 | + observer.join() | |
| 146 | + | |
| 147 | + signal.signal(signal.SIGTERM, shutdown) | |
| 148 | + | |
| 149 | + try: | |
| 150 | + while observer.is_alive(): | |
| 151 | + observer.join(1) | |
| 152 | + except KeyboardInterrupt: | |
| 153 | + logging.warning("Stopping Smartwatch server...") | |
| 154 | + finally: | |
| 155 | + shutdown() |