1010import os
1111import time
1212from pathlib import Path
13+ from threading import Lock
1314from typing import Any , Dict , List , OrderedDict
1415
1516from django .apps import apps
@@ -58,15 +59,25 @@ def __init__(self) -> None:
5859
5960 self .errors = {} # Holds discovering errors
6061
62+ self .loading_lock = Lock () # Lock to prevent multiple loading at the same time
63+
6164 # flags
62- self .is_loading = False # Are plugins being loaded right now
6365 self .plugins_loaded = False # Marks if the registry fully loaded and all django apps are reloaded
6466 self .apps_loading = True # Marks if apps were reloaded yet
6567
6668 self .installed_apps = [] # Holds all added plugin_paths
6769
70+ @property
71+ def is_loading (self ):
72+ """Return True if the plugin registry is currently loading"""
73+ return self .loading_lock .locked ()
74+
6875 def get_plugin (self , slug ):
6976 """Lookup plugin by slug (unique key)."""
77+
78+ # Check if the registry needs to be reloaded
79+ self .check_reload ()
80+
7081 if slug not in self .plugins :
7182 logger .warning ("Plugin registry has no record of plugin '%s'" , slug )
7283 return None
@@ -80,6 +91,10 @@ def set_plugin_state(self, slug, state):
8091 slug (str): Plugin slug
8192 state (bool): Plugin state - true = active, false = inactive
8293 """
94+
95+ # Check if the registry needs to be reloaded
96+ self .check_reload ()
97+
8398 if slug not in self .plugins_full :
8499 logger .warning ("Plugin registry has no record of plugin '%s'" , slug )
85100 return
@@ -96,6 +111,10 @@ def call_plugin_function(self, slug, func, *args, **kwargs):
96111
97112 Instead, any error messages are returned to the worker.
98113 """
114+
115+ # Check if the registry needs to be reloaded
116+ self .check_reload ()
117+
99118 plugin = self .get_plugin (slug )
100119
101120 if not plugin :
@@ -105,9 +124,35 @@ def call_plugin_function(self, slug, func, *args, **kwargs):
105124
106125 return plugin_func (* args , ** kwargs )
107126
108- # region public functions
127+ # region registry functions
128+ def with_mixin (self , mixin : str , active = None , builtin = None ):
129+ """Returns reference to all plugins that have a specified mixin enabled."""
130+
131+ # Check if the registry needs to be loaded
132+ self .check_reload ()
133+
134+ result = []
135+
136+ for plugin in self .plugins .values ():
137+ if plugin .mixin_enabled (mixin ):
138+
139+ if active is not None :
140+ # Filter by 'active' status of plugin
141+ if active != plugin .is_active ():
142+ continue
143+
144+ if builtin is not None :
145+ # Filter by 'builtin' status of plugin
146+ if builtin != plugin .is_builtin :
147+ continue
148+
149+ result .append (plugin )
150+
151+ return result
152+ # endregion
153+
109154 # region loading / unloading
110- def load_plugins (self , full_reload : bool = False ):
155+ def _load_plugins (self , full_reload : bool = False ):
111156 """Load and activate all IntegrationPlugins.
112157
113158 Args:
@@ -175,7 +220,7 @@ def load_plugins(self, full_reload: bool = False):
175220 from plugin .events import trigger_event
176221 trigger_event ('plugins_loaded' )
177222
178- def unload_plugins (self , force_reload : bool = False ):
223+ def _unload_plugins (self , force_reload : bool = False ):
179224 """Unload and deactivate all IntegrationPlugins.
180225
181226 Args:
@@ -202,7 +247,9 @@ def unload_plugins(self, force_reload: bool = False):
202247 logger .info ('Finished unloading plugins' )
203248
204249 def reload_plugins (self , full_reload : bool = False , force_reload : bool = False , collect : bool = False ):
205- """Safely reload.
250+ """Reload the plugin registry.
251+
252+ This should be considered the single point of entry for loading plugins!
206253
207254 Args:
208255 full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
@@ -211,21 +258,25 @@ def reload_plugins(self, full_reload: bool = False, force_reload: bool = False,
211258 """
212259 # Do not reload when currently loading
213260 if self .is_loading :
214- return # pragma: no cover
261+ logger .debug ("Skipping reload - plugin registry is currently loading" )
262+ return
263+
264+ if self .loading_lock .acquire (blocking = False ):
215265
216- logger .info ('Start reloading plugins' )
266+ logger .info ('Plugin Registry: Reloading plugins' )
217267
218- with maintenance_mode_on ():
219- if collect :
220- logger .info ('Collecting plugins' )
221- self .plugin_modules = self .collect_plugins ()
268+ with maintenance_mode_on ():
269+ if collect :
270+ logger .info ('Collecting plugins' )
271+ self .plugin_modules = self .collect_plugins ()
222272
223- self .plugins_loaded = False
224- self .unload_plugins (force_reload = force_reload )
225- self .plugins_loaded = True
226- self .load_plugins (full_reload = full_reload )
273+ self .plugins_loaded = False
274+ self ._unload_plugins (force_reload = force_reload )
275+ self .plugins_loaded = True
276+ self ._load_plugins (full_reload = full_reload )
227277
228- logger .info ('Finished reloading plugins' )
278+ self .loading_lock .release ()
279+ logger .info ('Plugin Registry: Loaded %s plugins' , len (self .plugins ))
229280
230281 def plugin_dirs (self ):
231282 """Construct a list of directories from where plugins can be loaded"""
@@ -360,30 +411,6 @@ def install_plugin_file(self):
360411
361412 # endregion
362413
363- # region registry functions
364- def with_mixin (self , mixin : str , active = None , builtin = None ):
365- """Returns reference to all plugins that have a specified mixin enabled."""
366- result = []
367-
368- for plugin in self .plugins .values ():
369- if plugin .mixin_enabled (mixin ):
370-
371- if active is not None :
372- # Filter by 'active' status of plugin
373- if active != plugin .is_active ():
374- continue
375-
376- if builtin is not None :
377- # Filter by 'builtin' status of plugin
378- if builtin != plugin .is_builtin :
379- continue
380-
381- result .append (plugin )
382-
383- return result
384- # endregion
385- # endregion
386-
387414 # region general internal loading /activating / deactivating / deloading
388415 def _init_plugins (self , disabled : str = None ):
389416 """Initialise all found plugins.
@@ -540,7 +567,7 @@ def _try_reload(self, cmd, *args, **kwargs):
540567 cmd (* args , ** kwargs )
541568 return True , []
542569 except Exception as error : # pragma: no cover
543- handle_error (error )
570+ handle_error (error , do_raise = False )
544571
545572 def _reload_apps (self , force_reload : bool = False , full_reload : bool = False ):
546573 """Internal: reload apps using django internal functions.
@@ -549,9 +576,7 @@ def _reload_apps(self, force_reload: bool = False, full_reload: bool = False):
549576 force_reload (bool, optional): Also reload base apps. Defaults to False.
550577 full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
551578 """
552- # If full_reloading is set to true we do not want to set the flag
553- if not full_reload :
554- self .is_loading = True # set flag to disable loop reloading
579+
555580 if force_reload :
556581 # we can not use the built in functions as we need to brute force the registry
557582 apps .app_configs = OrderedDict ()
@@ -560,7 +585,6 @@ def _reload_apps(self, force_reload: bool = False, full_reload: bool = False):
560585 self ._try_reload (apps .populate , settings .INSTALLED_APPS )
561586 else :
562587 self ._try_reload (apps .set_installed_apps , settings .INSTALLED_APPS )
563- self .is_loading = False
564588
565589 def _clean_installed_apps (self ):
566590 for plugin in self .installed_apps :
@@ -601,6 +625,68 @@ def _update_urls(self):
601625 clear_url_caches ()
602626 # endregion
603627
628+ # region plugin registry hash calculations
629+ def update_plugin_hash (self ):
630+ """When the state of the plugin registry changes, update the hash"""
631+
632+ from common .models import InvenTreeSetting
633+
634+ plg_hash = self .calculate_plugin_hash ()
635+
636+ try :
637+ old_hash = InvenTreeSetting .get_setting ("_PLUGIN_REGISTRY_HASH" , "" , create = False , cache = False )
638+ except Exception :
639+ old_hash = ""
640+
641+ if old_hash != plg_hash :
642+ try :
643+ logger .debug ("Updating plugin registry hash: %s" , str (plg_hash ))
644+ InvenTreeSetting .set_setting ("_PLUGIN_REGISTRY_HASH" , plg_hash , change_user = None )
645+ except Exception as exc :
646+ logger .exception ("Failed to update plugin registry hash: %s" , str (exc ))
647+
648+ def calculate_plugin_hash (self ):
649+ """Calculate a 'hash' value for the current registry
650+
651+ This is used to detect changes in the plugin registry,
652+ and to inform other processes that the plugin registry has changed
653+ """
654+
655+ from hashlib import md5
656+
657+ data = md5 ()
658+
659+ # Hash for all loaded plugins
660+ for slug , plug in self .plugins .items ():
661+ data .update (str (slug ).encode ())
662+ data .update (str (plug .version ).encode ())
663+ data .update (str (plug .is_active ).encode ())
664+
665+ return str (data .hexdigest ())
666+
667+ def check_reload (self ):
668+ """Determine if the registry needs to be reloaded"""
669+
670+ from common .models import InvenTreeSetting
671+
672+ if settings .TESTING :
673+ # Skip if running during unit testing
674+ return
675+
676+ logger .debug ("Checking plugin registry hash" )
677+
678+ try :
679+ reg_hash = InvenTreeSetting .get_setting ("_PLUGIN_REGISTRY_HASH" , "" , create = False , cache = False )
680+ except Exception as exc :
681+ logger .exception ("Failed to retrieve plugin registry hash: %s" , str (exc ))
682+ return
683+
684+ if reg_hash and reg_hash != self .calculate_plugin_hash ():
685+ logger .info ("Plugin registry hash has changed - reloading" )
686+ self .reload_plugins (full_reload = True , force_reload = True , collect = True )
687+
688+ # endregion
689+
604690
605691registry : PluginsRegistry = PluginsRegistry ()
606692
0 commit comments