const fs = require("fs"); const { log } = require("../src/util"); const path = require("path"); const axios = require("axios"); const { Git } = require("./git"); const childProcess = require("child_process"); class PluginsManager { static disable = false; /** * Plugin List * @type {PluginWrapper[]} */ pluginList = []; /** * Plugins Dir */ pluginsDir; server; /** * * @param {UptimeKumaServer} server */ constructor(server) { this.server = server; if (!PluginsManager.disable) { this.pluginsDir = "./data/plugins/"; if (! fs.existsSync(this.pluginsDir)) { fs.mkdirSync(this.pluginsDir, { recursive: true }); } log.debug("plugin", "Scanning plugin directory"); let list = fs.readdirSync(this.pluginsDir); this.pluginList = []; for (let item of list) { this.loadPlugin(item); } } else { log.warn("PLUGIN", "Skip scanning plugin directory"); } } /** * Install a Plugin */ async loadPlugin(name) { log.info("plugin", "Load " + name); let plugin = new PluginWrapper(this.server, this.pluginsDir + name); try { await plugin.load(); this.pluginList.push(plugin); } catch (e) { log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name); log.error("plugin", "Reason: " + e.message); } } /** * Download a Plugin * @param {string} repoURL Git repo url * @param {string} name Directory name, also known as plugin unique name */ downloadPlugin(repoURL, name) { if (fs.existsSync(this.pluginsDir + name)) { log.info("plugin", "Plugin folder already exists? Removing..."); fs.rmSync(this.pluginsDir + name, { recursive: true }); } log.info("plugin", "Installing plugin: " + name + " " + repoURL); let result = Git.clone(repoURL, this.pluginsDir, name); log.info("plugin", "Install result: " + result); } /** * Remove a plugin * @param {string} name */ async removePlugin(name) { log.info("plugin", "Removing plugin: " + name); for (let plugin of this.pluginList) { if (plugin.info.name === name) { await plugin.unload(); // Delete the plugin directory fs.rmSync(this.pluginsDir + name, { recursive: true }); this.pluginList.splice(this.pluginList.indexOf(plugin), 1); return; } } log.warn("plugin", "Plugin not found: " + name); throw new Error("Plugin not found: " + name); } /** * TODO: Update a plugin * Only available for plugins which were downloaded from the official list * @param pluginID */ updatePlugin(pluginID) { } /** * Get the plugin list from server + local installed plugin list * Item will be merged if the `name` is the same. * @returns {Promise<[]>} */ async fetchPluginList() { let remotePluginList; try { const res = await axios.get("https://uptime.kuma.pet/c/plugins.json"); remotePluginList = res.data.pluginList; } catch (e) { log.error("plugin", "Failed to fetch plugin list: " + e.message); remotePluginList = []; } for (let plugin of this.pluginList) { let find = false; // Try to merge for (let remotePlugin of remotePluginList) { if (remotePlugin.name === plugin.info.name) { find = true; remotePlugin.installed = true; remotePlugin.name = plugin.info.name; remotePlugin.fullName = plugin.info.fullName; remotePlugin.description = plugin.info.description; remotePlugin.version = plugin.info.version; break; } } // Local plugin if (!find) { plugin.info.local = true; remotePluginList.push(plugin.info); } } // Sort Installed first, then sort by name return remotePluginList.sort((a, b) => { if (a.installed === b.installed) { if (a.fullName < b.fullName) { return -1; } if (a.fullName > b.fullName) { return 1; } return 0; } else if (a.installed) { return -1; } else { return 1; } }); } } class PluginWrapper { server = undefined; pluginDir = undefined; /** * Must be an `new-able` class. * @type {function} */ pluginClass = undefined; /** * * @type {Plugin} */ object = undefined; info = {}; /** * * @param {UptimeKumaServer} server * @param {string} pluginDir */ constructor(server, pluginDir) { this.server = server; this.pluginDir = pluginDir; } async load() { let indexFile = this.pluginDir + "/index.js"; let packageJSON = this.pluginDir + "/package.json"; log.info("plugin", "Installing dependencies"); if (fs.existsSync(indexFile)) { // Install dependencies let result = childProcess.spawnSync("npm", [ "install" ], { cwd: this.pluginDir, env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor } }); if (result.stdout) { log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8")); } else { log.warn("plugin", "Install dependencies result: no output"); } this.pluginClass = require(path.join(process.cwd(), indexFile)); let pluginClassType = typeof this.pluginClass; if (pluginClassType === "function") { this.object = new this.pluginClass(this.server); await this.object.load(); } else { throw new Error("Invalid plugin, it does not export a class"); } if (fs.existsSync(packageJSON)) { this.info = require(path.join(process.cwd(), packageJSON)); } else { this.info.fullName = this.pluginDir; this.info.name = "[unknown]"; this.info.version = "[unknown-version]"; } this.info.installed = true; log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`); } } async unload() { await this.object.unload(); } } module.exports = { PluginsManager, PluginWrapper };