From e7f546b8b04934d097a9259d8496064c2e268c35 Mon Sep 17 00:00:00 2001
From: wolflu05 <76838159+wolflu05@users.noreply.github.com>
Date: Thu, 8 Jun 2023 21:06:14 +0000
Subject: [PATCH] Skip ready functions if not in main thread or plugins are not
 loaded yet

---
 InvenTree/InvenTree/apps.py  |  7 ++++++-
 InvenTree/InvenTree/ready.py | 33 +++++++++++++++++++++++++++++++++
 InvenTree/label/apps.py      |  7 ++++++-
 InvenTree/part/apps.py       |  7 ++++++-
 InvenTree/plugin/apps.py     |  6 +++++-
 InvenTree/report/apps.py     |  7 ++++++-
 InvenTree/users/apps.py      |  8 +++++++-
 7 files changed, 69 insertions(+), 6 deletions(-)

diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 1ba43edee7..066214ebc3 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -14,7 +14,8 @@ from django.db.utils import IntegrityError
 import InvenTree.conversion
 import InvenTree.tasks
 from InvenTree.config import get_setting
-from InvenTree.ready import canAppAccessDatabase, isInTestMode
+from InvenTree.ready import (canAppAccessDatabase, isInMainThread,
+                             isInTestMode, isPluginRegistryLoaded)
 
 logger = logging.getLogger("inventree")
 
@@ -34,6 +35,10 @@ class InvenTreeConfig(AppConfig):
         - Collecting notification methods
         - Adding users set in the current environment
         """
+        # skip loading if plugins are not loaded or we run in a background thread
+        if not isPluginRegistryLoaded() or not isInMainThread():
+            return
+
         if canAppAccessDatabase() or settings.TESTING_ENV:
             InvenTree.tasks.check_for_migrations(worker=False)
 
diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py
index e6a4ec9ae2..0b4bdb9727 100644
--- a/InvenTree/InvenTree/ready.py
+++ b/InvenTree/InvenTree/ready.py
@@ -1,5 +1,6 @@
 """Functions to check if certain parts of InvenTree are ready."""
 
+import os
 import sys
 
 
@@ -13,6 +14,20 @@ def isImportingData():
     return 'loaddata' in sys.argv
 
 
+def isInMainThread():
+    """Django starts two processes, one for the actual dev server and the other to reload the application.
+
+    - The RUN_MAIN env is set in that case. However if --noreload is applied, this variable
+    is not set because there are no different threads.
+    - If this app is run in gunicorn there are several threads having "equal rights" so there is no real
+    main thread so we skip this check
+    """
+    if '--noreload' in sys.argv or "gunicorn" in os.environ.get("SERVER_SOFTWARE", ""):
+        return True
+
+    return os.environ.get('RUN_MAIN', None) == 'true'
+
+
 def canAppAccessDatabase(allow_test: bool = False, allow_plugins: bool = False, allow_shell: bool = False):
     """Returns True if the apps.py file can access database records.
 
@@ -60,3 +75,21 @@ def canAppAccessDatabase(allow_test: bool = False, allow_plugins: bool = False,
             return False
 
     return True
+
+
+def isPluginRegistryLoaded():
+    """The plugin registry reloads all apps onetime after starting so that the discovered AppConfigs are added to Django.
+
+    This triggers the ready function of AppConfig to execute twice. Add this check to prevent from running two times.
+
+    Returns: 'False' if the apps have not been reloaded already to prevent running the ready function twice
+    """
+    from django.conf import settings
+
+    # If plugins are not enabled, there won't be a second load
+    if not settings.PLUGINS_ENABLED:
+        return True
+
+    from plugin import registry
+
+    return not registry.is_loading
diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py
index 51243e238b..a53c62c253 100644
--- a/InvenTree/label/apps.py
+++ b/InvenTree/label/apps.py
@@ -12,7 +12,8 @@ from django.conf import settings
 from django.core.exceptions import AppRegistryNotReady
 from django.db.utils import OperationalError
 
-from InvenTree.ready import canAppAccessDatabase
+from InvenTree.ready import (canAppAccessDatabase, isInMainThread,
+                             isPluginRegistryLoaded)
 
 logger = logging.getLogger("inventree")
 
@@ -35,6 +36,10 @@ class LabelConfig(AppConfig):
 
     def ready(self):
         """This function is called whenever the label app is loaded."""
+        # skip loading if plugins are not loaded or we run in a background thread
+        if not isPluginRegistryLoaded() or not isInMainThread():
+            return
+
         if canAppAccessDatabase():
 
             try:
diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py
index 298ff486cd..1e0405a379 100644
--- a/InvenTree/part/apps.py
+++ b/InvenTree/part/apps.py
@@ -5,7 +5,8 @@ import logging
 from django.apps import AppConfig
 from django.db.utils import OperationalError, ProgrammingError
 
-from InvenTree.ready import canAppAccessDatabase, isImportingData
+from InvenTree.ready import (canAppAccessDatabase, isImportingData,
+                             isInMainThread, isPluginRegistryLoaded)
 
 logger = logging.getLogger("inventree")
 
@@ -16,6 +17,10 @@ class PartConfig(AppConfig):
 
     def ready(self):
         """This function is called whenever the Part app is loaded."""
+        # skip loading if plugins are not loaded or we run in a background thread
+        if not isPluginRegistryLoaded() or not isInMainThread():
+            return
+
         if canAppAccessDatabase():
             self.update_trackable_status()
             self.reset_part_pricing_flags()
diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py
index b3b1a4cb6a..ca26228653 100644
--- a/InvenTree/plugin/apps.py
+++ b/InvenTree/plugin/apps.py
@@ -10,7 +10,7 @@ from django.apps import AppConfig
 
 from maintenance_mode.core import set_maintenance_mode
 
-from InvenTree.ready import canAppAccessDatabase
+from InvenTree.ready import canAppAccessDatabase, isInMainThread
 from plugin import registry
 
 logger = logging.getLogger('inventree')
@@ -23,6 +23,10 @@ class PluginAppConfig(AppConfig):
 
     def ready(self):
         """The ready method is extended to initialize plugins."""
+        # skip loading if we run in a background thread
+        if not isInMainThread():
+            return
+
         if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
             logger.info("Skipping plugin loading sequence")  # pragma: no cover
         else:
diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py
index 5118333922..95575d3bdf 100644
--- a/InvenTree/report/apps.py
+++ b/InvenTree/report/apps.py
@@ -18,7 +18,12 @@ class ReportConfig(AppConfig):
     def ready(self):
         """This function is called whenever the report app is loaded."""
 
-        from InvenTree.ready import canAppAccessDatabase
+        from InvenTree.ready import (canAppAccessDatabase, isInMainThread,
+                                     isPluginRegistryLoaded)
+
+        # skip loading if plugins are not loaded or we run in a background thread
+        if not isPluginRegistryLoaded() or not isInMainThread():
+            return
 
         # Configure logging for PDF generation (disable "info" messages)
         logging.getLogger('fontTools').setLevel(logging.WARNING)
diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py
index 71c2d079ce..42819ee314 100644
--- a/InvenTree/users/apps.py
+++ b/InvenTree/users/apps.py
@@ -5,7 +5,8 @@ import logging
 from django.apps import AppConfig
 from django.db.utils import OperationalError, ProgrammingError
 
-from InvenTree.ready import canAppAccessDatabase
+from InvenTree.ready import (canAppAccessDatabase, isInMainThread,
+                             isPluginRegistryLoaded)
 
 logger = logging.getLogger('inventree')
 
@@ -17,6 +18,11 @@ class UsersConfig(AppConfig):
 
     def ready(self):
         """Called when the 'users' app is loaded at runtime"""
+
+        # skip loading if plugins are not loaded or we run in a background thread
+        if not isPluginRegistryLoaded() or not isInMainThread():
+            return
+
         if canAppAccessDatabase(allow_test=True):
 
             try: