from django.apps import AppConfig import os import sys from django.db.models.signals import post_migrate class PluginsConfig(AppConfig): name = "apps.plugins" verbose_name = "Plugins" def ready(self): """Wire up plugin discovery without hitting the DB during app init. - Skip during common management commands that don't need discovery. - Register post_migrate handler to sync plugin registry to DB after migrations. - Run in-memory discovery (no DB) in every non-Celery process so plugin modules are imported and monkey-patches apply. This includes uWSGI workers under lazy-apps=true, which each start cold and never inherit a warmed fork. Celery workers skip here and discover via the worker_ready signal instead. - One-shot startup tasks (schedule setup, repo refresh) are gated by should_skip_initialization() so they only fire in the master/main process. """ 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", ) from dispatcharr.app_initialization import should_skip_initialization # Skip discovery for Celery (worker_ready signal handles it) and # pure management commands that don't serve requests. Every other # process - including each uWSGI worker under lazy-apps=true - runs # discovery so plugin modules are imported and monkey-patches apply. _no_discovery_cmds = {'celery', 'beat', 'migrate', 'dbshell', 'loaddata'} if not any(cmd in sys.argv for cmd in _no_discovery_cmds): from .loader import PluginManager PluginManager.get().discover_plugins(sync_db=False) if should_skip_initialization(): return except Exception: # Avoid breaking startup due to plugin errors import logging logging.getLogger(__name__).exception("Plugin discovery wiring failed during app ready") # Register periodic task for refreshing plugin repo manifests self._setup_repo_refresh_schedule() # Refresh repo manifests once at startup so the UI always has current data self._enqueue_startup_refresh() def _enqueue_startup_refresh(self): from dispatcharr.app_initialization import should_skip_initialization if should_skip_initialization(): return try: from .tasks import refresh_plugin_repos refresh_plugin_repos.apply_async(countdown=10) except Exception: import logging logging.getLogger(__name__).debug( "Could not enqueue startup plugin repo refresh (Celery may not be ready yet)" ) def _setup_repo_refresh_schedule(self): from dispatcharr.app_initialization import should_skip_initialization if should_skip_initialization(): return try: from core.scheduling import create_or_update_periodic_task, delete_periodic_task from core.models import CoreSettings from .tasks import PLUGIN_REPO_REFRESH_TASK_NAME interval = 6 try: obj = CoreSettings.objects.get(key="plugin_repo_settings") interval = obj.value.get("refresh_interval_hours", 6) except CoreSettings.DoesNotExist: pass if interval == 0: delete_periodic_task(PLUGIN_REPO_REFRESH_TASK_NAME) else: create_or_update_periodic_task( task_name=PLUGIN_REPO_REFRESH_TASK_NAME, celery_task_path="apps.plugins.tasks.refresh_plugin_repos", interval_hours=interval, enabled=True, ) except Exception: import logging logging.getLogger(__name__).debug( "Could not set up plugin repo refresh schedule (migrations may not have run yet)" )