mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-30 16:41:35 +00:00
Plugin sequence fix (#10057)
* Fix error message * Add registry ready flag * Add is_ready func * Cleaner init code * Protect against plugin access until ready * Add decorator for registry entrypoint funcs * Tweak return value * Only add plugin URLs if registry is ready * Fix load logic * Refactor @registry_entrypoint decorator * Tweak * Typing * Reimplement is_ready property
This commit is contained in:
@@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError, OperationalError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from allauth.socialaccount.signals import social_account_updated
|
from allauth.socialaccount.signals import social_account_updated
|
||||||
@@ -65,7 +65,8 @@ class InvenTreeConfig(AppConfig):
|
|||||||
self.start_background_tasks()
|
self.start_background_tasks()
|
||||||
|
|
||||||
if not InvenTree.ready.isInTestMode(): # pragma: no cover
|
if not InvenTree.ready.isInTestMode(): # pragma: no cover
|
||||||
self.update_exchange_rates()
|
# Update exchange rates
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.update_exchange_rates)
|
||||||
# Let the background worker check for migrations
|
# Let the background worker check for migrations
|
||||||
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
|
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
|
||||||
|
|
||||||
@@ -179,6 +180,8 @@ class InvenTreeConfig(AppConfig):
|
|||||||
InvenTree.tasks.offload_task(
|
InvenTree.tasks.offload_task(
|
||||||
InvenTree.tasks.heartbeat, force_async=True, group='heartbeat'
|
InvenTree.tasks.heartbeat, force_async=True, group='heartbeat'
|
||||||
)
|
)
|
||||||
|
except AppRegistryNotReady: # pragma: no cover
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -194,63 +197,6 @@ class InvenTreeConfig(AppConfig):
|
|||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
logger.exception('Error loading tasks for %s: %s', app_name, e)
|
logger.exception('Error loading tasks for %s: %s', app_name, e)
|
||||||
|
|
||||||
def update_exchange_rates(self): # pragma: no cover
|
|
||||||
"""Update exchange rates each time the server is started.
|
|
||||||
|
|
||||||
Only runs *if*:
|
|
||||||
a) Have not been updated recently (one day or less)
|
|
||||||
b) The base exchange rate has been altered
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from djmoney.contrib.exchange.models import ExchangeBackend
|
|
||||||
|
|
||||||
from common.currency import currency_code_default
|
|
||||||
from InvenTree.tasks import update_exchange_rates
|
|
||||||
except AppRegistryNotReady: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
base_currency = currency_code_default()
|
|
||||||
|
|
||||||
update = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')
|
|
||||||
|
|
||||||
if backend.exists():
|
|
||||||
backend = backend.first()
|
|
||||||
|
|
||||||
last_update = backend.last_update
|
|
||||||
|
|
||||||
if last_update is None:
|
|
||||||
# Never been updated
|
|
||||||
logger.info('Exchange backend has never been updated')
|
|
||||||
update = True
|
|
||||||
|
|
||||||
# Backend currency has changed?
|
|
||||||
if base_currency != backend.base_currency:
|
|
||||||
logger.info(
|
|
||||||
'Base currency changed from %s to %s',
|
|
||||||
backend.base_currency,
|
|
||||||
base_currency,
|
|
||||||
)
|
|
||||||
update = True
|
|
||||||
|
|
||||||
except ExchangeBackend.DoesNotExist:
|
|
||||||
logger.info('Exchange backend not found - updating')
|
|
||||||
update = True
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# Some other error - potentially the tables are not ready yet
|
|
||||||
return
|
|
||||||
|
|
||||||
if update:
|
|
||||||
try:
|
|
||||||
update_exchange_rates()
|
|
||||||
except OperationalError:
|
|
||||||
logger.warning('Could not update exchange rates - database not ready')
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception('Error updating exchange rates: %s (%s)', e, type(e))
|
|
||||||
|
|
||||||
def update_site_url(self):
|
def update_site_url(self):
|
||||||
"""Update the site URL setting.
|
"""Update the site URL setting.
|
||||||
|
|
||||||
|
@@ -15,12 +15,14 @@ import InvenTree.helpers
|
|||||||
logger = structlog.get_logger('inventree')
|
logger = structlog.get_logger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def currency_code_default():
|
def currency_code_default(create: bool = True):
|
||||||
"""Returns the default currency code (or USD if not specified)."""
|
"""Returns the default currency code (or USD if not specified)."""
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
|
|
||||||
try:
|
try:
|
||||||
code = get_global_setting('INVENTREE_DEFAULT_CURRENCY', create=True, cache=True)
|
code = get_global_setting(
|
||||||
|
'INVENTREE_DEFAULT_CURRENCY', create=create, cache=True
|
||||||
|
)
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# Database may not yet be ready, no need to throw an error here
|
# Database may not yet be ready, no need to throw an error here
|
||||||
code = ''
|
code = ''
|
||||||
|
@@ -32,23 +32,11 @@ class PluginAppConfig(AppConfig):
|
|||||||
else:
|
else:
|
||||||
logger.info('Loading InvenTree plugins')
|
logger.info('Loading InvenTree plugins')
|
||||||
|
|
||||||
if not registry.is_loading:
|
if not registry.is_ready:
|
||||||
# this is the first startup
|
# Mark the registry as ready
|
||||||
try:
|
# This ensures that other apps cannot access the registry before it is fully initialized
|
||||||
from common.models import InvenTreeSetting
|
logger.info('Plugin registry is ready - performing initial load')
|
||||||
|
registry.set_ready()
|
||||||
if InvenTreeSetting.get_setting(
|
|
||||||
'PLUGIN_ON_STARTUP', create=False, cache=False
|
|
||||||
):
|
|
||||||
# make sure all plugins are installed
|
|
||||||
registry.install_plugin_file()
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Perform a full reload of the plugin registry
|
|
||||||
registry.reload_plugins(
|
|
||||||
full_reload=True, force_reload=True, collect=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# drop out of maintenance
|
# drop out of maintenance
|
||||||
# makes sure we did not have an error in reloading and maintenance is still active
|
# makes sure we did not have an error in reloading and maintenance is still active
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
- Manages setup and teardown of plugin class instances
|
- Manages setup and teardown of plugin class instances
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
@@ -43,6 +44,37 @@ from .plugin import InvenTreePlugin
|
|||||||
logger = structlog.get_logger('inventree')
|
logger = structlog.get_logger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
def registry_entrypoint(check_reload: bool = True, default_value: Any = None) -> Any:
|
||||||
|
"""Function decorator for registry entrypoints methods.
|
||||||
|
|
||||||
|
- Ensure that the registry is ready before calling the method.
|
||||||
|
- Check if the registry needs to be reloaded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(method):
|
||||||
|
"""Decorator to ensure registry is ready before calling the method."""
|
||||||
|
|
||||||
|
@functools.wraps(method)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
"""Wrapper function to ensure registry is ready."""
|
||||||
|
if not self.ready:
|
||||||
|
logger.warning(
|
||||||
|
"Plugin registry is not ready - cannot call method '%s'",
|
||||||
|
method.__name__,
|
||||||
|
)
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
# Check if the registry needs to be reloaded
|
||||||
|
if check_reload:
|
||||||
|
self.check_reload()
|
||||||
|
|
||||||
|
return method(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class PluginsRegistry:
|
class PluginsRegistry:
|
||||||
"""The PluginsRegistry class."""
|
"""The PluginsRegistry class."""
|
||||||
|
|
||||||
@@ -68,6 +100,8 @@ class PluginsRegistry:
|
|||||||
'parameter-exporter',
|
'parameter-exporter',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ready: bool
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize registry.
|
"""Initialize registry.
|
||||||
|
|
||||||
@@ -82,6 +116,8 @@ class PluginsRegistry:
|
|||||||
str, InvenTreePlugin
|
str, InvenTreePlugin
|
||||||
] = {} # List of all plugin instances
|
] = {} # List of all plugin instances
|
||||||
|
|
||||||
|
self.ready = False # Marks if the registry is ready to be used
|
||||||
|
|
||||||
# Keep an internal hash of the plugin registry state
|
# Keep an internal hash of the plugin registry state
|
||||||
self.registry_hash = None
|
self.registry_hash = None
|
||||||
|
|
||||||
@@ -100,11 +136,35 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
self.installed_apps = [] # Holds all added plugin_paths
|
self.installed_apps = [] # Holds all added plugin_paths
|
||||||
|
|
||||||
|
def set_ready(self):
|
||||||
|
"""Set the registry as ready to be used.
|
||||||
|
|
||||||
|
This method should only be called once per application start,
|
||||||
|
after all apps have been loaded and the registry is fully initialized.
|
||||||
|
"""
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
self.ready = True
|
||||||
|
|
||||||
|
# Install plugins from file (if required)
|
||||||
|
if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False, cache=False):
|
||||||
|
# make sure all plugins are installed
|
||||||
|
registry.install_plugin_file()
|
||||||
|
|
||||||
|
# Perform initial plugin discovery
|
||||||
|
self.reload_plugins(full_reload=True, force_reload=True, collect=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ready(self) -> bool:
|
||||||
|
"""Return True if the plugin registry is ready to be used."""
|
||||||
|
return self.ready
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_loading(self) -> bool:
|
def is_loading(self) -> bool:
|
||||||
"""Return True if the plugin registry is currently loading."""
|
"""Return True if the plugin registry is currently loading."""
|
||||||
return self.loading_lock.locked()
|
return self.loading_lock.locked()
|
||||||
|
|
||||||
|
@registry_entrypoint()
|
||||||
def get_plugin(
|
def get_plugin(
|
||||||
self, slug: str, active: bool = True, with_mixin: Optional[str] = None
|
self, slug: str, active: bool = True, with_mixin: Optional[str] = None
|
||||||
) -> InvenTreePlugin:
|
) -> InvenTreePlugin:
|
||||||
@@ -118,9 +178,6 @@ class PluginsRegistry:
|
|||||||
Returns:
|
Returns:
|
||||||
InvenTreePlugin or None: The plugin instance if found, otherwise None.
|
InvenTreePlugin or None: The plugin instance if found, otherwise None.
|
||||||
"""
|
"""
|
||||||
# Check if the registry needs to be reloaded
|
|
||||||
self.check_reload()
|
|
||||||
|
|
||||||
if slug not in self.plugins:
|
if slug not in self.plugins:
|
||||||
logger.warning("Plugin registry has no record of plugin '%s'", slug)
|
logger.warning("Plugin registry has no record of plugin '%s'", slug)
|
||||||
return None
|
return None
|
||||||
@@ -169,6 +226,7 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
@registry_entrypoint()
|
||||||
def set_plugin_state(self, slug: str, state: bool):
|
def set_plugin_state(self, slug: str, state: bool):
|
||||||
"""Set the state(active/inactive) of a plugin.
|
"""Set the state(active/inactive) of a plugin.
|
||||||
|
|
||||||
@@ -176,9 +234,6 @@ class PluginsRegistry:
|
|||||||
slug (str): Plugin slug
|
slug (str): Plugin slug
|
||||||
state (bool): Plugin state - true = active, false = inactive
|
state (bool): Plugin state - true = active, false = inactive
|
||||||
"""
|
"""
|
||||||
# Check if the registry needs to be reloaded
|
|
||||||
self.check_reload()
|
|
||||||
|
|
||||||
if slug not in self.plugins_full:
|
if slug not in self.plugins_full:
|
||||||
logger.warning("Plugin registry has no record of plugin '%s'", slug)
|
logger.warning("Plugin registry has no record of plugin '%s'", slug)
|
||||||
return
|
return
|
||||||
@@ -190,6 +245,7 @@ class PluginsRegistry:
|
|||||||
# Update the registry hash value
|
# Update the registry hash value
|
||||||
self.update_plugin_hash()
|
self.update_plugin_hash()
|
||||||
|
|
||||||
|
@registry_entrypoint()
|
||||||
def call_plugin_function(self, slug: str, func: str, *args, **kwargs):
|
def call_plugin_function(self, slug: str, func: str, *args, **kwargs):
|
||||||
"""Call a member function (named by 'func') of the plugin named by 'slug'.
|
"""Call a member function (named by 'func') of the plugin named by 'slug'.
|
||||||
|
|
||||||
@@ -198,9 +254,6 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
Instead, any error messages are returned to the worker.
|
Instead, any error messages are returned to the worker.
|
||||||
"""
|
"""
|
||||||
# Check if the registry needs to be reloaded
|
|
||||||
self.check_reload()
|
|
||||||
|
|
||||||
raise_error = kwargs.pop('raise_error', True)
|
raise_error = kwargs.pop('raise_error', True)
|
||||||
|
|
||||||
plugin = self.get_plugin(slug)
|
plugin = self.get_plugin(slug)
|
||||||
@@ -220,6 +273,8 @@ class PluginsRegistry:
|
|||||||
return plugin_func(*args, **kwargs)
|
return plugin_func(*args, **kwargs)
|
||||||
|
|
||||||
# region registry functions
|
# region registry functions
|
||||||
|
|
||||||
|
@registry_entrypoint(default_value=[])
|
||||||
def with_mixin(
|
def with_mixin(
|
||||||
self, mixin: str, active: bool = True, builtin: Optional[bool] = None
|
self, mixin: str, active: bool = True, builtin: Optional[bool] = None
|
||||||
) -> list[InvenTreePlugin]:
|
) -> list[InvenTreePlugin]:
|
||||||
@@ -230,9 +285,6 @@ class PluginsRegistry:
|
|||||||
active (bool, optional): Filter by 'active' status of plugin. Defaults to True.
|
active (bool, optional): Filter by 'active' status of plugin. Defaults to True.
|
||||||
builtin (bool, optional): Filter by 'builtin' status of plugin. Defaults to None.
|
builtin (bool, optional): Filter by 'builtin' status of plugin. Defaults to None.
|
||||||
"""
|
"""
|
||||||
# Check if the registry needs to be loaded
|
|
||||||
self.check_reload()
|
|
||||||
|
|
||||||
mixin = str(mixin).lower().strip()
|
mixin = str(mixin).lower().strip()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
@@ -304,6 +356,7 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
logger.info('Finished unloading plugins')
|
logger.info('Finished unloading plugins')
|
||||||
|
|
||||||
|
@registry_entrypoint(check_reload=False)
|
||||||
def reload_plugins(
|
def reload_plugins(
|
||||||
self,
|
self,
|
||||||
full_reload: bool = False,
|
full_reload: bool = False,
|
||||||
@@ -369,7 +422,7 @@ class PluginsRegistry:
|
|||||||
logger.info('Plugin Registry: Loaded %s plugins', len(self.plugins))
|
logger.info('Plugin Registry: Loaded %s plugins', len(self.plugins))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception('Expected error during plugin reload: %s', e)
|
logger.exception('Unexpected error during plugin reload: %s', e)
|
||||||
log_error('reload_plugins', scope='plugins')
|
log_error('reload_plugins', scope='plugins')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
@@ -900,6 +953,7 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
return str(data.hexdigest())
|
return str(data.hexdigest())
|
||||||
|
|
||||||
|
@registry_entrypoint(default_value=False, check_reload=False)
|
||||||
def check_reload(self):
|
def check_reload(self):
|
||||||
"""Determine if the registry needs to be reloaded.
|
"""Determine if the registry needs to be reloaded.
|
||||||
|
|
||||||
|
@@ -18,28 +18,32 @@ def get_plugin_urls():
|
|||||||
|
|
||||||
urls = []
|
urls = []
|
||||||
|
|
||||||
if get_global_setting('ENABLE_PLUGINS_URL', False) or settings.PLUGIN_TESTING_SETUP:
|
if registry.is_ready:
|
||||||
for plugin in registry.with_mixin(PluginMixinEnum.URLS):
|
if (
|
||||||
try:
|
get_global_setting('ENABLE_PLUGINS_URL', False)
|
||||||
if plugin_urls := plugin.urlpatterns:
|
or settings.PLUGIN_TESTING_SETUP
|
||||||
# Check if the plugin has a custom URL pattern
|
):
|
||||||
for url in plugin_urls:
|
for plugin in registry.with_mixin(PluginMixinEnum.URLS):
|
||||||
# Attempt to resolve against the URL pattern as a validation check
|
try:
|
||||||
try:
|
if plugin_urls := plugin.urlpatterns:
|
||||||
url.resolve('')
|
# Check if the plugin has a custom URL pattern
|
||||||
except Resolver404:
|
for url in plugin_urls:
|
||||||
pass
|
# Attempt to resolve against the URL pattern as a validation check
|
||||||
|
try:
|
||||||
|
url.resolve('')
|
||||||
|
except Resolver404:
|
||||||
|
pass
|
||||||
|
|
||||||
urls.append(
|
urls.append(
|
||||||
re_path(
|
re_path(
|
||||||
f'^{plugin.slug}/',
|
f'^{plugin.slug}/',
|
||||||
include((plugin_urls, plugin.slug)),
|
include((plugin_urls, plugin.slug)),
|
||||||
name=plugin.slug,
|
name=plugin.slug,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
except Exception:
|
||||||
except Exception:
|
log_error('get_plugin_urls', plugin=plugin.slug)
|
||||||
log_error('get_plugin_urls', plugin=plugin.slug)
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
# Redirect anything else to the root index
|
# Redirect anything else to the root index
|
||||||
urls.append(
|
urls.append(
|
||||||
|
Reference in New Issue
Block a user