UNPKG

homebridge

Version:
380 lines 18.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginManager = void 0; const child_process_1 = require("child_process"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const logger_1 = require("./logger"); const plugin_1 = require("./plugin"); const log = logger_1.Logger.internal; /** * Utility which exposes methods to search for installed Homebridge plugins */ class PluginManager { // name must be prefixed with 'homebridge-' or '@scope/homebridge-' static PLUGIN_IDENTIFIER_PATTERN = /^((@[\w-]+(\.[\w-]+)*)\/)?(homebridge-[\w-]+)$/; api; searchPaths = new Set(); // unique set of search paths we will use to discover installed plugins strictPluginResolution = false; activePlugins; disabledPlugins; plugins = new Map(); // we have some plugins which simply pass a wrong or misspelled plugin name to the api calls, this translation tries to mitigate this pluginIdentifierTranslation = new Map(); accessoryToPluginMap = new Map(); platformToPluginMap = new Map(); currentInitializingPlugin; // used to match registering plugins, see handleRegisterAccessory and handleRegisterPlatform constructor(api, options) { this.api = api; if (options) { if (options.customPluginPath) { this.searchPaths.add(path_1.default.resolve(process.cwd(), options.customPluginPath)); } this.strictPluginResolution = options.strictPluginResolution || false; this.activePlugins = options.activePlugins; this.disabledPlugins = Array.isArray(options.disabledPlugins) ? options.disabledPlugins : undefined; } this.api.on("registerAccessory" /* InternalAPIEvent.REGISTER_ACCESSORY */, this.handleRegisterAccessory.bind(this)); this.api.on("registerPlatform" /* InternalAPIEvent.REGISTER_PLATFORM */, this.handleRegisterPlatform.bind(this)); } static isQualifiedPluginIdentifier(identifier) { return PluginManager.PLUGIN_IDENTIFIER_PATTERN.test(identifier); } static extractPluginName(name) { return name.match(PluginManager.PLUGIN_IDENTIFIER_PATTERN)[4]; } static extractPluginScope(name) { return name.match(PluginManager.PLUGIN_IDENTIFIER_PATTERN)[2]; } static getAccessoryName(identifier) { if (identifier.indexOf(".") === -1) { return identifier; } return identifier.split(".")[1]; } static getPlatformName(identifier) { if (identifier.indexOf(".") === -1) { return identifier; } return identifier.split(".")[1]; } static getPluginIdentifier(identifier) { return identifier.split(".")[0]; } async initializeInstalledPlugins() { log.info("---"); this.loadInstalledPlugins(); for (const [identifier, plugin] of this.plugins) { try { await plugin.load(); } catch (error) { log.error("===================="); log.error(`ERROR LOADING PLUGIN ${identifier}:`); log.error(error.stack); log.error("===================="); this.plugins.delete(identifier); continue; } if (this.disabledPlugins && this.disabledPlugins.includes(plugin.getPluginIdentifier())) { plugin.disabled = true; } if (plugin.disabled) { log.warn(`Disabled plugin: ${identifier}@${plugin.version}`); } else { log.info(`Loaded plugin: ${identifier}@${plugin.version}`); } await this.initializePlugin(plugin, identifier); log.info("---"); } this.currentInitializingPlugin = undefined; } async initializePlugin(plugin, identifier) { try { this.currentInitializingPlugin = plugin; await plugin.initialize(this.api); // call the plugin's initializer and pass it our API instance } catch (error) { log.error("===================="); log.error(`ERROR INITIALIZING PLUGIN ${identifier}:`); log.error(error.stack); log.error("===================="); this.plugins.delete(identifier); return; } } handleRegisterAccessory(name, constructor, pluginIdentifier) { if (!this.currentInitializingPlugin) { throw new Error(`Unexpected accessory registration. Plugin ${pluginIdentifier ? `'${pluginIdentifier}' ` : ""}tried to register outside the initializer function!`); } if (pluginIdentifier && pluginIdentifier !== this.currentInitializingPlugin.getPluginIdentifier()) { log.info(`Plugin '${this.currentInitializingPlugin.getPluginIdentifier()}' tried to register with an incorrect plugin identifier: '${pluginIdentifier}'. Please report this to the developer!`); this.pluginIdentifierTranslation.set(pluginIdentifier, this.currentInitializingPlugin.getPluginIdentifier()); } this.currentInitializingPlugin.registerAccessory(name, constructor); let plugins = this.accessoryToPluginMap.get(name); if (!plugins) { plugins = []; this.accessoryToPluginMap.set(name, plugins); } plugins.push(this.currentInitializingPlugin); } handleRegisterPlatform(name, constructor, pluginIdentifier) { if (!this.currentInitializingPlugin) { throw new Error(`Unexpected platform registration. Plugin ${pluginIdentifier ? `'${pluginIdentifier}' ` : ""}tried to register outside the initializer function!`); } if (pluginIdentifier && pluginIdentifier !== this.currentInitializingPlugin.getPluginIdentifier()) { log.debug(`Plugin '${this.currentInitializingPlugin.getPluginIdentifier()}' tried to register with an incorrect plugin identifier: '${pluginIdentifier}'. Please report this to the developer!`); this.pluginIdentifierTranslation.set(pluginIdentifier, this.currentInitializingPlugin.getPluginIdentifier()); } this.currentInitializingPlugin.registerPlatform(name, constructor); let plugins = this.platformToPluginMap.get(name); if (!plugins) { plugins = []; this.platformToPluginMap.set(name, plugins); } plugins.push(this.currentInitializingPlugin); } getPluginForAccessory(accessoryIdentifier) { let plugin; if (accessoryIdentifier.indexOf(".") === -1) { // see if it matches exactly one accessory let found = this.accessoryToPluginMap.get(accessoryIdentifier); if (!found) { throw new Error(`No plugin was found for the accessory "${accessoryIdentifier}" in your config.json. Please make sure the corresponding plugin is installed correctly.`); } if (found.length > 1) { const options = found.map(plugin => plugin.getPluginIdentifier() + "." + accessoryIdentifier).join(", "); // check if only one of the multiple platforms is not disabled found = found.filter(plugin => !plugin.disabled); if (found.length !== 1) { throw new Error(`The requested accessory '${accessoryIdentifier}' has been registered multiple times. Please be more specific by writing one of: ${options}`); } } plugin = found[0]; accessoryIdentifier = plugin.getPluginIdentifier() + "." + accessoryIdentifier; } else { const pluginIdentifier = PluginManager.getPluginIdentifier(accessoryIdentifier); if (!this.hasPluginRegistered(pluginIdentifier)) { throw new Error(`The requested plugin '${pluginIdentifier}' was not registered.`); } plugin = this.getPlugin(pluginIdentifier); } return plugin; } getPluginForPlatform(platformIdentifier) { let plugin; if (platformIdentifier.indexOf(".") === -1) { // see if it matches exactly one platform let found = this.platformToPluginMap.get(platformIdentifier); if (!found) { throw new Error(`No plugin was found for the platform "${platformIdentifier}" in your config.json. Please make sure the corresponding plugin is installed correctly.`); } if (found.length > 1) { const options = found.map(plugin => plugin.getPluginIdentifier() + "." + platformIdentifier).join(", "); // check if only one of the multiple platforms is not disabled found = found.filter(plugin => !plugin.disabled); if (found.length !== 1) { throw new Error(`The requested platform '${platformIdentifier}' has been registered multiple times. Please be more specific by writing one of: ${options}`); } } plugin = found[0]; platformIdentifier = plugin.getPluginIdentifier() + "." + platformIdentifier; } else { const pluginIdentifier = PluginManager.getPluginIdentifier(platformIdentifier); if (!this.hasPluginRegistered(pluginIdentifier)) { throw new Error(`The requested plugin '${pluginIdentifier}' was not registered.`); } plugin = this.getPlugin(pluginIdentifier); } return plugin; } hasPluginRegistered(pluginIdentifier) { return this.plugins.has(pluginIdentifier) || this.pluginIdentifierTranslation.has(pluginIdentifier); } getPlugin(pluginIdentifier) { const plugin = this.plugins.get(pluginIdentifier); if (plugin) { return plugin; } else { const translation = this.pluginIdentifierTranslation.get(pluginIdentifier); if (translation) { return this.plugins.get(translation); } } return undefined; } getPluginByActiveDynamicPlatform(platformName) { const found = (this.platformToPluginMap.get(platformName) || []) .filter(plugin => !!plugin.getActiveDynamicPlatform(platformName)); if (found.length === 0) { return undefined; } else if (found.length > 1) { const plugins = found.map(plugin => plugin.getPluginIdentifier()).join(", "); throw new Error(`'${platformName}' is an ambiguous platform name. It was registered by multiple plugins: ${plugins}`); } else { return found[0]; } } /** * Gets all plugins installed on the local system */ loadInstalledPlugins() { this.loadDefaultPaths(); this.searchPaths.forEach(searchPath => { if (!fs_1.default.existsSync(searchPath)) { // just because this path is in require.main.paths doesn't mean it necessarily exists! return; } if (fs_1.default.existsSync(path_1.default.join(searchPath, "package.json"))) { // does this path point inside a single plugin and not a directory containing plugins? try { this.loadPlugin(searchPath); } catch (error) { log.warn(error.message); return; } } else { // read through each directory in this node_modules folder const relativePluginPaths = fs_1.default.readdirSync(searchPath) // search for directories only .filter(relativePath => { try { return fs_1.default.statSync(path_1.default.resolve(searchPath, relativePath)).isDirectory(); } catch (e) { log.debug(`Ignoring path ${path_1.default.resolve(searchPath, relativePath)} - ${e.message}`); return false; } }); // expand out @scoped plugins relativePluginPaths.slice() .filter(path => path.charAt(0) === "@") // is it a scope directory? .forEach(scopeDirectory => { // remove scopeDirectory from the path list const index = relativePluginPaths.indexOf(scopeDirectory); relativePluginPaths.splice(index, 1); const absolutePath = path_1.default.join(searchPath, scopeDirectory); fs_1.default.readdirSync(absolutePath) .filter(name => PluginManager.isQualifiedPluginIdentifier(name)) .filter(name => { try { return fs_1.default.statSync(path_1.default.resolve(absolutePath, name)).isDirectory(); } catch (e) { log.debug(`Ignoring path ${path_1.default.resolve(absolutePath, name)} - ${e.message}`); return false; } }) .forEach(name => relativePluginPaths.push(scopeDirectory + "/" + name)); }); relativePluginPaths .filter(pluginIdentifier => { return PluginManager.isQualifiedPluginIdentifier(pluginIdentifier) // needs to be a valid homebridge plugin name && (!this.activePlugins || this.activePlugins.includes(pluginIdentifier)); // check if activePlugins is restricted and if so is the plugin is contained }) .forEach(pluginIdentifier => { try { const absolutePath = path_1.default.resolve(searchPath, pluginIdentifier); this.loadPlugin(absolutePath); } catch (error) { log.warn(error.message); return; } }); } }); if (this.plugins.size === 0) { log.warn("No plugins found."); } } loadPlugin(absolutePath) { const packageJson = PluginManager.loadPackageJSON(absolutePath); const identifier = packageJson.name; const name = PluginManager.extractPluginName(identifier); const scope = PluginManager.extractPluginScope(identifier); // possibly undefined const alreadyInstalled = this.plugins.get(identifier); // check if there is already a plugin with the same Identifier if (alreadyInstalled) { throw new Error(`Warning: skipping plugin found at '${absolutePath}' since we already loaded the same plugin from '${alreadyInstalled.getPluginPath()}'.`); } const plugin = new plugin_1.Plugin(name, absolutePath, packageJson, scope); this.plugins.set(identifier, plugin); return plugin; } static loadPackageJSON(pluginPath) { const packageJsonPath = path_1.default.join(pluginPath, "package.json"); let packageJson; if (!fs_1.default.existsSync(packageJsonPath)) { throw new Error(`Plugin ${pluginPath} does not contain a package.json.`); } try { packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, { encoding: "utf8" })); // attempt to parse package.json } catch (err) { throw new Error(`Plugin ${pluginPath} contains an invalid package.json. Error: ${err}`); } if (!packageJson.name || !PluginManager.isQualifiedPluginIdentifier(packageJson.name)) { throw new Error(`Plugin ${pluginPath} does not have a package name that begins with 'homebridge-' or '@scope/homebridge-.`); } // verify that it's tagged with the correct keyword if (!packageJson.keywords || !packageJson.keywords.includes("homebridge-plugin")) { throw new Error(`Plugin ${pluginPath} package.json does not contain the keyword 'homebridge-plugin'.`); } return packageJson; } loadDefaultPaths() { if (this.strictPluginResolution) { // if strict plugin resolution is enabled: // * only use custom plugin path, if set; // * otherwise add the current npm global prefix (e.g. /usr/local/lib/node_modules) if (this.searchPaths.size === 0) { this.addNpmPrefixToSearchPaths(); } return; } if (require.main) { // add the paths used by require() require.main.paths.forEach(path => this.searchPaths.add(path)); } // THIS SECTION FROM: https://github.com/yeoman/environment/blob/master/lib/resolver.js // Adding global npm directories // We tried using npm to get the global modules path, but it hasn't work out // because of bugs in the parsable implementation of `ls` command and mostly // performance issues. So, we go with our best bet for now. if (process.env.NODE_PATH) { process.env.NODE_PATH .split(path_1.default.delimiter) .filter(path => !!path) // trim out empty values .forEach(path => this.searchPaths.add(path)); } else { // Default paths for non-windows systems if (process.platform !== "win32") { this.searchPaths.add("/usr/local/lib/node_modules"); this.searchPaths.add("/usr/lib/node_modules"); } this.addNpmPrefixToSearchPaths(); } } addNpmPrefixToSearchPaths() { if (process.platform === "win32") { this.searchPaths.add(path_1.default.join(process.env.APPDATA, "npm/node_modules")); } else { this.searchPaths.add((0, child_process_1.execSync)("/bin/echo -n \"$(npm -g prefix)/lib/node_modules\"", { env: Object.assign({ npm_config_loglevel: "silent", npm_update_notifier: "false", }, process.env), }).toString("utf8")); } } } exports.PluginManager = PluginManager; //# sourceMappingURL=pluginManager.js.map