import { AccessoryIdentifier , AccessoryName , AccessoryPluginConstructor , HomebridgeAPI , InternalAPIEvent , PlatformIdentifier , PlatformName , PlatformPluginConstructor , PluginIdentifier , PluginName , } from "./api" ; import path from "path" ; import fs from "fs" ; import { Plugin } from "./plugin" ; import { Logger } from "./logger" ; import { execSync } from "child_process" ; const log = Logger . internal ; export interface PackageJSON { name : string ; version : string ; keywords ? : string [ ] ; main ? : string ; /**
     * When set as module, it marks .js file to be treated as ESM.
     * See https://nodejs.org/dist/latest-v14.x/docs/api/esm.html#esm_enabling
     */ type ? : "module" | "commonjs" ; engines ? : Record < string , string > ; dependencies ? : Record < string , string > ; devDependencies ? : Record < string , string > ; peerDependencies ? : Record < string , string > ; } export interface PluginManagerOptions { /**
     * Additional path to search for plugins in. Specified relative to the current working directory.
     */ customPluginPath ? : string ; /**
     * When defined, only plugins specified here will be initialized.
     */ activePlugins ? : PluginIdentifier [ ] ; /**
     * Plugins that are marked as disabled and whos corresponding config blocks should be ignored
     */ disabledPlugins ? : PluginIdentifier [ ] ; } /**
 * Utility which exposes methods to search for installed Homebridge plugins
 */ export class PluginManager { private static readonly PLUGIN_IDENTIFIER_PATTERN = /^((@[\w-]*)\/)?(homebridge-[\w-]*)$/ ; private readonly api : HomebridgeAPI ; private readonly searchPaths : Set < string > = new Set ( ) ; private readonly activePlugins ? : PluginIdentifier [ ] ; private readonly disabledPlugins ? : PluginIdentifier [ ] ; private readonly plugins : Map < PluginIdentifier , Plugin > = new Map ( ) ; private readonly pluginIdentifierTranslation : Map < PluginIdentifier , PluginIdentifier > = new Map ( ) ; private readonly accessoryToPluginMap : Map < AccessoryName , Plugin [ ] > = new Map ( ) ; private readonly platformToPluginMap : Map < PlatformName , Plugin [ ] > = new Map ( ) ; private currentInitializingPlugin ? : Plugin ; constructor ( api , options ? ) { this . api = api ; if ( options ) { if ( options . customPluginPath ) { this . searchPaths . add ( path . resolve ( process . cwd ( ) , options . customPluginPath ) ) ; } this . activePlugins = options . activePlugins ; this . disabledPlugins = Array . isArray ( options . disabledPlugins ) ? options . disabledPlugins : undefined ; } this . api . on ( InternalAPIEvent . REGISTER_ACCESSORY , this . handleRegisterAccessory . bind ( this ) ) ; this . api . on ( InternalAPIEvent . REGISTER_PLATFORM , this . handleRegisterPlatform . bind ( this ) ) ; } public static isQualifiedPluginIdentifier ( identifier ) { return PluginManager . PLUGIN_IDENTIFIER_PATTERN . test ( identifier ) ; } public static extractPluginName ( name ) { return name . match ( PluginManager . PLUGIN_IDENTIFIER_PATTERN ) ! [ 3 ] ; } public static extractPluginScope ( name ) { return name . match ( PluginManager . PLUGIN_IDENTIFIER_PATTERN ) ! [ 2 ] ; } public static getAccessoryName ( identifier ) { if ( identifier . indexOf ( "." ) === - 1 ) { return identifier ; } return identifier . split ( "." ) [ 1 ] ; } public static getPlatformName ( identifier ) { if ( identifier . indexOf ( "." ) === - 1 ) { return identifier ; } return identifier . split ( "." ) [ 1 ] ; } public static getPluginIdentifier ( identifier ) { return identifier . split ( "." ) [ 0 ] ; } public 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 ; } public async initializePlugin ( plugin , identifier ) { try { this . currentInitializingPlugin = plugin ; await plugin . initialize ( this . api ) ; } catch ( error ) { log . error ( "====================" ) ; log . error ( `ERROR INITIALIZING PLUGIN ${ identifier }:` ) ; log . error ( error . stack ) ; log . error ( "====================" ) ; this . plugins . delete ( identifier ) ; return ; } } private 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 ) ; } private 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 ) ; } public getPluginForAccessory ( accessoryIdentifier ) { let plugin ; if ( accessoryIdentifier . indexOf ( "." ) === - 1 ) { 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 ( ", " ) ; 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 ; } public getPluginForPlatform ( platformIdentifier ) { let plugin ; if ( platformIdentifier . indexOf ( "." ) === - 1 ) { 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 ( ", " ) ; 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 ; } public hasPluginRegistered ( pluginIdentifier ) { return this . plugins . has ( pluginIdentifier ) || this . pluginIdentifierTranslation . has ( pluginIdentifier ) ; } public 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 ; } public 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
     */ private loadInstalledPlugins ( ) { this . loadDefaultPaths ( ) ; this . searchPaths . forEach ( searchPath => { if ( ! fs . existsSync ( searchPath ) ) { return ; } if ( fs . existsSync ( path . join ( searchPath , "package.json" ) ) ) { try { this . loadPlugin ( searchPath ) ; } catch ( error ) { log . warn ( error . message ) ; return ; } } else { const relativePluginPaths = fs . readdirSync ( searchPath ) . filter ( relativePath => { try { return fs . statSync ( path . resolve ( searchPath , relativePath ) ) . isDirectory ( ) ; } catch ( e ) { log . debug ( `Ignoring path ${ path . resolve ( searchPath , relativePath ) } - ${ e . message }` ) ; return false ; } } ) ; relativePluginPaths . slice ( ) . filter ( path => path . charAt ( 0 ) === "@" ) . forEach ( scopeDirectory => { const index = relativePluginPaths . indexOf ( scopeDirectory ) ; relativePluginPaths . splice ( index , 1 ) ; const absolutePath = path . join ( searchPath , scopeDirectory ) ; fs . readdirSync ( absolutePath ) . filter ( name => PluginManager . isQualifiedPluginIdentifier ( name ) ) . filter ( name => { try { return fs . statSync ( path . resolve ( absolutePath , name ) ) . isDirectory ( ) ; } catch ( e ) { log . debug ( `Ignoring path ${ path . resolve ( absolutePath , name ) } - ${ e . message }` ) ; return false ; } } ) . forEach ( name => relativePluginPaths . push ( scopeDirectory + "/" + name ) ) ; } ) ; relativePluginPaths . filter ( pluginIdentifier => { return PluginManager . isQualifiedPluginIdentifier ( pluginIdentifier ) && ( ! this . activePlugins || this . activePlugins . includes ( pluginIdentifier ) ) ; } ) . forEach ( pluginIdentifier => { try { const absolutePath = path . resolve ( searchPath , pluginIdentifier ) ; this . loadPlugin ( absolutePath ) ; } catch ( error ) { log . warn ( error . message ) ; return ; } } ) ; } } ) ; if ( this . plugins . size === 0 ) { log . warn ( "No plugins found. See the README for information on installing plugins." ) ; } } public loadPlugin ( absolutePath ) { const packageJson = PluginManager . loadPackageJSON ( absolutePath ) ; const identifier = packageJson . name ; const name = PluginManager . extractPluginName ( identifier ) ; const scope = PluginManager . extractPluginScope ( identifier ) ; const alreadyInstalled = this . plugins . get ( 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 ( name , absolutePath , packageJson , scope ) ; this . plugins . set ( identifier , plugin ) ; return plugin ; } private static loadPackageJSON ( pluginPath ) { const packageJsonPath = path . join ( pluginPath , "package.json" ) ; let packageJson ; if ( ! fs . existsSync ( packageJsonPath ) ) { throw new Error ( `Plugin ${ pluginPath } does not contain a package.json.` ) ; } try { packageJson = JSON . parse ( fs . readFileSync ( packageJsonPath , { encoding : "utf8" } ) ) ; } 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-.` ) ; } if ( ! packageJson . keywords || ! packageJson . keywords . includes ( "homebridge-plugin" ) ) { throw new Error ( `Plugin ${ pluginPath } package.json does not contain the keyword 'homebridge-plugin'.` ) ; } return packageJson ; } private loadDefaultPaths ( ) { if ( require . main ) { require . main . paths . forEach ( path => this . searchPaths . add ( path ) ) ; } if ( process . env . NODE_PATH ) { process . env . NODE_PATH . split ( path . delimiter ) . filter ( path => ! ! path ) . forEach ( path => this . searchPaths . add ( path ) ) ; } else { if ( process . platform === "win32" ) { this . searchPaths . add ( path . join ( process . env . APPDATA ! , "npm/node_modules" ) ) ; } else { this . searchPaths . add ( "/usr/local/lib/node_modules" ) ; this . searchPaths . add ( "/usr/lib/node_modules" ) ; this . searchPaths . add ( execSync ( "/bin/echo -n \"$(npm --no-update-notifier -g prefix)/lib/node_modules\"" ) . toString ( "utf8" ) ) ; } } } }