From 39d01015e5b0c10dafb1615126bedab87dbf77df Mon Sep 17 00:00:00 2001 From: Donald Zou Date: Wed, 13 Aug 2025 21:41:28 +0800 Subject: [PATCH] Added plugins manager --- src/dashboard.py | 8 +- src/gunicorn.conf.py | 3 +- src/modules/DashboardPlugin.py | 117 ++++++++++++++++++++++++++++ src/plugins/somewhat_plugin/main.py | 2 + 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 src/modules/DashboardPlugin.py create mode 100644 src/plugins/somewhat_plugin/main.py diff --git a/src/dashboard.py b/src/dashboard.py index eb052a0a..5e28aeb7 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -37,6 +37,7 @@ from client import createClientBlueprint from logging.config import dictConfig from modules.DashboardClients import DashboardClients +from modules.DashboardPlugin import DashboardPlugin dictConfig({ 'version': 1, @@ -1442,12 +1443,11 @@ WireguardConfigurations: dict[str, WireguardConfiguration] = {} AllPeerShareLinks: PeerShareLinks = PeerShareLinks(DashboardConfig, WireguardConfigurations) AllPeerJobs: PeerJobs = PeerJobs(DashboardConfig, WireguardConfigurations) DashboardLogger: DashboardLogger = DashboardLogger() - +DashboardPlugin: DashboardPlugin = DashboardPlugin(app, WireguardConfigurations) InitWireguardConfigurationsList(startup=True) -import plugins.rrd_data.main as rrd_data with app.app_context(): DashboardClients: DashboardClients = DashboardClients(WireguardConfigurations) @@ -1458,12 +1458,10 @@ def startThreads(): bgThread.start() scheduleJobThread = threading.Thread(target=peerJobScheduleBackgroundThread, daemon=True) scheduleJobThread.start() - - t = threading.Thread(target=rrd_data.main, args=(WireguardConfigurations,), daemon=True) - t.start() if __name__ == "__main__": startThreads() + DashboardPlugin.startThreads() # logging.getLogger().addHandler(logging.StreamHandler()) app.logger.addHandler(logging.StreamHandler()) app.run(host=app_ip, debug=False, port=app_port) \ No newline at end of file diff --git a/src/gunicorn.conf.py b/src/gunicorn.conf.py index 5917c351..6dd3d06c 100644 --- a/src/gunicorn.conf.py +++ b/src/gunicorn.conf.py @@ -7,6 +7,7 @@ date = datetime.today().strftime('%Y_%m_%d_%H_%M_%S') def post_worker_init(worker): dashboard.startThreads() + dashboard.DashboardPlugin.startThreads() worker_class = 'gthread' workers = 1 @@ -23,4 +24,4 @@ pythonpath = "., ./modules" print(f"[Gunicorn] WGDashboard w/ Gunicorn will be running on {bind}", flush=True) print(f"[Gunicorn] Access log file is at {accesslog}", flush=True) -print(f"[Gunicorn] Error log file is at {errorlog}", flush=True) +print(f"[Gunicorn] Error log file is at {errorlog}", flush=True) \ No newline at end of file diff --git a/src/modules/DashboardPlugin.py b/src/modules/DashboardPlugin.py new file mode 100644 index 00000000..2cfba6e7 --- /dev/null +++ b/src/modules/DashboardPlugin.py @@ -0,0 +1,117 @@ +import os +import sys +import importlib.util +from pathlib import Path +from typing import Dict, Callable, List, Optional +import threading + + +class DashboardPlugin: + + def __init__(self, app, WireguardConfigurations, directory: str = 'plugins'): + self.directory = Path('plugins') + self.loadedPlugins: dict[str, Callable] = {} + self.errorPlugins: List[str] = [] + self.logger = app.logger + self.WireguardConfigurations = WireguardConfigurations + + def startThreads(self): + self.loadAllPlugins() + self.executeAllPlugins() + + def preparePlugins(self) -> list[Path]: + + readyPlugins = [] + + if not self.directory.exists(): + self.logger.error("Failed to load ./plugins directory") + return [] + + for plugin in self.directory.iterdir(): + if plugin.is_dir(): + codeFile = plugin / "main.py" + if codeFile.exists(): + self.logger.info(f"Prepared plugin: {plugin.name}") + readyPlugins.append(plugin) + + return readyPlugins + + def loadPlugin(self, path: Path) -> Optional[Callable]: + pluginName = path.name + codeFile = path / "main.py" + + try: + spec = importlib.util.spec_from_file_location( + f"WGDashboardPlugin_{pluginName}", + codeFile + ) + + if spec is None or spec.loader is None: + raise ImportError(f"Failed to create spec for {pluginName}") + + module = importlib.util.module_from_spec(spec) + + plugin_dir_str = str(path) + if plugin_dir_str not in sys.path: + sys.path.insert(0, plugin_dir_str) + + try: + spec.loader.exec_module(module) + finally: + if plugin_dir_str in sys.path: + sys.path.remove(plugin_dir_str) + + if hasattr(module, 'main'): + main_func = getattr(module, 'main') + if callable(main_func): + self.logger.info(f"Successfully loaded plugin [{pluginName}]") + return main_func + else: + raise AttributeError(f"'main' in {pluginName} is not callable") + else: + raise AttributeError(f"Plugin {pluginName} does not have a 'main' function") + + except Exception as e: + self.logger.error(f"Failed to load the plugin [{pluginName}]. Reason: {str(e)}") + self.errorPlugins.append(pluginName) + return None + + def loadAllPlugins(self): + self.loadedPlugins.clear() + self.errorPlugins.clear() + + preparedPlugins = self.preparePlugins() + + for plugin in preparedPlugins: + pluginName = plugin.name + mainFunction = self.loadPlugin(plugin) + + if mainFunction: + self.loadedPlugins[pluginName] = mainFunction + if self.errorPlugins: + self.logger.warning(f"Failed to load {len(self.errorPlugins)} plugin(s): {self.errorPlugins}") + + def executePlugin(self, pluginName: str): + if pluginName not in self.loadedPlugins.keys(): + self.logger.error(f"Failed to execute plugin [{pluginName}]. Reason: Not loaded") + return False + + plugin = self.loadedPlugins.get(pluginName) + + try: + t = threading.Thread(target=plugin, args=(self.WireguardConfigurations,), daemon=True) + t.name = f'WGDashboardPlugin_{pluginName}' + t.start() + + if t.is_alive(): + self.logger.info(f"Execute plugin [{pluginName}] success. PID: {t.native_id}") + + except Exception as e: + self.logger.error(f"Failed to execute plugin [{pluginName}]. Reason: {str(e)}") + return False + + return True + + def executeAllPlugins(self): + for plugin in self.loadedPlugins.keys(): + self.executePlugin(plugin) \ No newline at end of file diff --git a/src/plugins/somewhat_plugin/main.py b/src/plugins/somewhat_plugin/main.py new file mode 100644 index 00000000..5b83c36b --- /dev/null +++ b/src/plugins/somewhat_plugin/main.py @@ -0,0 +1,2 @@ +def main(WireguardConfigurations): + print("This is a plugin") \ No newline at end of file