var path = require('path'); var fs = require('fs'); var semver = require('semver'); var User = require('./user').User; var version = require('./version'); var log = require("./logger")._system; 'use strict'; module.exports = { Plugin: Plugin } /** * Homebridge Plugin. * * Allows for discovering and loading installed Homebridge plugins. */ function Plugin(pluginPath) { this.pluginPath = pluginPath; // like "/usr/local/lib/node_modules/homebridge-lockitron" this.initializer; // exported function from the plugin that initializes it } Plugin.prototype.name = function() { return path.basename(this.pluginPath); } Plugin.prototype.load = function(options) { options = options || {}; // does this plugin exist at all? if (!fs.existsSync(this.pluginPath)) { throw new Error("Plugin " + this.pluginPath + " was not found. Make sure the module '" + this.pluginPath + "' is installed."); } // attempt to load package.json var pjson = Plugin.loadPackageJSON(this.pluginPath); // make sure the name is prefixed with 'homebridge-' if (!pjson.name || pjson.name.indexOf('homebridge-') != 0) { throw new Error("Plugin " + this.pluginPath + " does not have a package name that begins with 'homebridge-'."); } // very temporary fix for first wave of plugins if (pjson.peerDepdendencies && (!pjson.engines || !pjson.engines.homebridge)) { var engines = pjson.engines || {} engines.homebridge = pjson.peerDepdendencies.homebridge; pjson.engines = engines; } // pluck out the HomeBridge version requirement if (!pjson.engines || !pjson.engines.homebridge) { throw new Error("Plugin " + this.pluginPath + " does not contain the 'homebridge' package in 'engines'."); } var versionRequired = pjson.engines.homebridge; // make sure the version is satisfied by the currently running version of HomeBridge if (!semver.satisfies(version, versionRequired)) { throw new Error("Plugin " + this.pluginPath + " requires a HomeBridge version of " + versionRequired + " which does not satisfy the current HomeBridge version of " + version + ". You may need to upgrade your installation of HomeBridge."); } // figure out the main module - index.js unless otherwise specified var main = pjson.main || "./index.js"; var mainPath = path.join(this.pluginPath, main); // try to require() it and grab the exported initialization hook this.initializer = require(mainPath); } Plugin.loadPackageJSON = function(pluginPath) { // check for a package.json var pjsonPath = path.join(pluginPath, "package.json"); var pjson = null; if (!fs.existsSync(pjsonPath)) { throw new Error("Plugin " + pluginPath + " does not contain a package.json."); } try { // attempt to parse package.json pjson = JSON.parse(fs.readFileSync(pjsonPath)); } catch (err) { throw new Error("Plugin " + pluginPath + " contains an invalid package.json. Error: " + err); } // verify that it's tagged with the correct keyword if (!pjson.keywords || pjson.keywords.indexOf("homebridge-plugin") == -1) { throw new Error("Plugin " + pluginPath + " package.json does not contain the keyword 'homebridge-plugin'."); } return pjson; } Plugin.getDefaultPaths = function() { var win32 = process.platform === 'win32'; var paths = []; // add the paths used by require() paths = paths.concat(require.main.paths); // 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 haven't work out // because of bugs in the parseable implementation of `ls` command and mostly // performance issues. So, we go with our best bet for now. if (process.env.NODE_PATH) { paths = process.env.NODE_PATH.split(path.delimiter) .filter(function(p) { return !!p; }) // trim out empty values .concat(paths); } else { // Default paths for each system if (win32) { paths.push(path.join(process.env.APPDATA, 'npm/node_modules')); } else { paths.push('/usr/local/lib/node_modules'); paths.push('/usr/lib/node_modules'); } } return paths; } // All search paths we will use to discover installed plugins Plugin.paths = Plugin.getDefaultPaths(); Plugin.addPluginPath = function(pluginPath) { Plugin.paths.unshift(path.resolve(process.cwd(), pluginPath)); } // Gets all plugins installed on the local system Plugin.installed = function() { var plugins = []; var pluginsByName = {}; // don't add duplicate plugins var searchedPaths = {}; // don't search the same paths twice // search for plugins among all known paths, in order for (var index in Plugin.paths) { var requirePath = Plugin.paths[index]; // did we already search this path? if (searchedPaths[requirePath]) continue; searchedPaths[requirePath] = true; // just because this path is in require.main.paths doesn't mean it necessarily exists! if (!fs.existsSync(requirePath)) continue; var names = fs.readdirSync(requirePath); // does this path point inside a single plugin and not a directory containing plugins? if (fs.existsSync(path.join(requirePath, "package.json"))) names = [""]; // read through each directory in this node_modules folder for (var index2 in names) { var name = names[index2]; // reconstruct full path var pluginPath = path.join(requirePath, name); // we only care about directories if (!fs.statSync(pluginPath).isDirectory()) continue; // does this module contain a package.json? var pjson; try { // throws an Error if this isn't a homebridge plugin pjson = Plugin.loadPackageJSON(pluginPath); } catch (err) { if (name.substring(0,11) === 'homebridge-') { // show warning only if module starts with prefix log.warn("Warning: skipping plugin found at '" + pluginPath + "' because of: " + err.message); } // swallow error and skip this module continue; } // get actual name if this path points inside a single plugin if (!name) name = pjson.name; // add it to the return list if (!pluginsByName[name]) { pluginsByName[name] = pluginPath; plugins.push(new Plugin(pluginPath)); } else { log.warn("Warning: skipping plugin found at '" + pluginPath + "' since we already loaded the same plugin from '" + pluginsByName[name] + "'."); } } } return plugins; }