From 1200d7d89456cbdbca24d5f8245b7b53515d2b8f Mon Sep 17 00:00:00 2001 From: Dispatcharr Date: Sun, 7 Sep 2025 17:38:34 -0500 Subject: [PATCH] Plugin discovery fix Fixed problem where plugins were trying to load before DB was ready. --- apps/plugins/apps.py | 47 ++++++++++++++++++++++++++++++++++++------ apps/plugins/loader.py | 23 ++++++++++++++++----- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/apps/plugins/apps.py b/apps/plugins/apps.py index 4cca8f6e..3ab44cb1 100644 --- a/apps/plugins/apps.py +++ b/apps/plugins/apps.py @@ -1,4 +1,7 @@ from django.apps import AppConfig +import os +import sys +from django.db.models.signals import post_migrate class PluginsConfig(AppConfig): @@ -6,14 +9,46 @@ class PluginsConfig(AppConfig): verbose_name = "Plugins" def ready(self): - # Perform plugin discovery on startup - try: - from .loader import PluginManager + """Wire up plugin discovery without hitting the DB during app init. - PluginManager.get().discover_plugins() + - Skip during common management commands that don't need discovery. + - Register post_migrate handler to sync plugin registry to DB after migrations. + - Do an in-memory discovery (no DB) so registry is available early. + """ + try: + # Allow explicit opt-out via env var + if os.environ.get("DISPATCHARR_SKIP_PLUGIN_AUTODISCOVERY", "").lower() in ("1", "true", "yes"): + return + + argv = sys.argv[1:] if len(sys.argv) > 1 else [] + mgmt_cmds_to_skip = { + # Skip immediate discovery for these commands + "makemigrations", "collectstatic", "check", "test", "shell", "showmigrations", + } + if argv and argv[0] in mgmt_cmds_to_skip: + return + + # Run discovery with DB sync after the plugins app has been migrated + def _post_migrate_discover(sender=None, app_config=None, **kwargs): + try: + if app_config and getattr(app_config, 'label', None) != 'plugins': + return + from .loader import PluginManager + PluginManager.get().discover_plugins(sync_db=True) + except Exception: + import logging + logging.getLogger(__name__).exception("Plugin discovery failed in post_migrate") + + post_migrate.connect( + _post_migrate_discover, + dispatch_uid="apps.plugins.post_migrate_discover", + ) + + # Perform non-DB discovery now to populate in-memory registry. + from .loader import PluginManager + PluginManager.get().discover_plugins(sync_db=False) except Exception: # Avoid breaking startup due to plugin errors import logging - logging.getLogger(__name__).exception("Plugin discovery failed during app ready") - + logging.getLogger(__name__).exception("Plugin discovery wiring failed during app ready") diff --git a/apps/plugins/loader.py b/apps/plugins/loader.py index 22562917..2999e869 100644 --- a/apps/plugins/loader.py +++ b/apps/plugins/loader.py @@ -45,8 +45,11 @@ class PluginManager: if self.plugins_dir not in sys.path: sys.path.append(self.plugins_dir) - def discover_plugins(self) -> Dict[str, LoadedPlugin]: - logger.info(f"Discovering plugins in {self.plugins_dir}") + def discover_plugins(self, *, sync_db: bool = True) -> Dict[str, LoadedPlugin]: + if sync_db: + logger.info(f"Discovering plugins in {self.plugins_dir}") + else: + logger.debug(f"Discovering plugins (no DB sync) in {self.plugins_dir}") self._registry.clear() try: @@ -66,8 +69,13 @@ class PluginManager: except FileNotFoundError: logger.warning(f"Plugins directory not found: {self.plugins_dir}") - # Sync DB records - self._sync_db_with_registry() + # Sync DB records (optional) + if sync_db: + try: + self._sync_db_with_registry() + except Exception: + # Defer sync if database is not ready (e.g., first startup before migrate) + logger.exception("Deferring plugin DB sync; database not ready yet") return self._registry def _load_plugin(self, key: str, path: str): @@ -156,7 +164,12 @@ class PluginManager: from .models import PluginConfig plugins: List[Dict[str, Any]] = [] - configs = {c.key: c for c in PluginConfig.objects.all()} + try: + configs = {c.key: c for c in PluginConfig.objects.all()} + except Exception as e: + # Database might not be migrated yet; fall back to registry only + logger.warning("PluginConfig table unavailable; listing registry only: %s", e) + configs = {} # First, include all discovered plugins for key, lp in self._registry.items():