import child_process from "child_process" ; import path from "path" ; import fs from "fs-extra" ; import { MacAddress } from "hap-nodejs" ; import { IpcOutgoingEvent , IpcService } from "./ipcService" ; import { ExternalPortService } from "./externalPortService" ; import { HomebridgeAPI , PluginType } from "./api" ; import { HomebridgeOptions } from "./server" ; import { Logger , Logging } from "./logger" ; import { Plugin } from "./plugin" ; import { User } from "./user" ; import { AccessoryConfig , BridgeConfiguration , BridgeOptions , HomebridgeConfig , PlatformConfig , } from "./bridgeService" ; export const enum ChildProcessMessageEventType { /**
     * Sent from the child process when it is ready to accept config
     */ READY = "ready" , /**
     * Sent to the child process with a ChildProcessLoadEventData payload
     */ LOAD = "load" , /**
     * Sent from the child process once it has loaded the plugin
     */ LOADED = "loaded" , /**
     * Sent to the child process telling it to start
     */ START = "start" , /**
     * Sent from the child process when the bridge is online
     */ ONLINE = "online" , /**
     * Sent from the child when it wants to request port allocation for an external accessory
     */ PORT_REQUEST = "portRequest" , /**
     * Sent from the parent with the port allocation response
     */ PORT_ALLOCATED = "portAllocated" } export const enum ChildBridgeStatus { /**
     * When the child bridge is loading, or restarting
     */ PENDING = "pending" , /**
     * The child bridge is online and has published it's accessory
     */ OK = "ok" , /**
     * The bridge is shutting down, or the process ended unexpectedly
     */ DOWN = "down" } export interface ChildProcessMessageEvent < T > { id : ChildProcessMessageEventType ; data ? : T ; } export interface ChildProcessLoadEventData { type : PluginType ; identifier : string ; pluginPath : string ; pluginConfig : Array < PlatformConfig | AccessoryConfig > ; bridgeConfig : BridgeConfiguration ; homebridgeConfig : HomebridgeConfig ; bridgeOptions : BridgeOptions ; } export interface ChildProcessPluginLoadedEventData { version : string ; } export interface ChildProcessPortRequestEventData { username : MacAddress ; } export interface ChildProcessPortAllocatedEventData { username : MacAddress ; port ? : number ; } export interface ChildMetadata { status : ChildBridgeStatus ; username : MacAddress ; name : string ; plugin : string ; identifier : string ; pid ? : number ; } /**
 * Manages the child processes of platforms/accessories being exposed as seperate forked bridges.
 * A child bridge runs a single platform or accessory.
 */ export class ChildBridgeService { private child ? : child_process . ChildProcess ; private args : string [ ] = [ ] ; private shuttingDown = false ; private lastBridgeStatus : ChildBridgeStatus = ChildBridgeStatus . PENDING ; private pluginConfig : Array < PlatformConfig | AccessoryConfig > = [ ] ; private log : Logging = Logger . withPrefix ( this . plugin . getPluginIdentifier ( ) ) ; private displayName ? : string ; constructor ( public type , public identifier , private plugin , private bridgeConfig , private homebridgeConfig , private homebridgeOptions , private api , private ipcService , private externalPortService ) { this . api . on ( "shutdown" , ( ) => { this . shuttingDown = true ; this . teardown ( ) ; } ) ; this . api . setMaxListeners ( this . api . getMaxListeners ( ) + 1 ) ; } /**
     * Start the child bridge service
     */ public start ( ) { this . setProcessFlags ( ) ; this . startChildProcess ( ) ; if ( this . pluginConfig . length > 1 || this . pluginConfig . length === 0 ) { this . displayName = this . plugin . getPluginIdentifier ( ) ; } else { this . displayName = this . pluginConfig [ 0 ] ?. name || this . plugin . getPluginIdentifier ( ) ; } this . log = Logger . withPrefix ( this . displayName ) ; } param config public addConfig ( config ) { this . pluginConfig . push ( config ) ; } private get bridgeStatus ( ) : ChildBridgeStatus { return this . lastBridgeStatus ; } private set bridgeStatus ( value ) { this . lastBridgeStatus = value ; this . ipcService . sendMessage ( IpcOutgoingEvent . CHILD_BRIDGE_STATUS_UPDATE , this . getMetadata ( ) ) ; } /**
     * Start the child bridge process
     */ private startChildProcess ( ) { this . bridgeStatus = ChildBridgeStatus . PENDING ; this . child = child_process . fork ( path . resolve ( __dirname , "childBridgeFork.js" ) , this . args , { silent : true , } ) ; this . child . stdout . on ( "data" , ( data ) => { process . stdout . write ( data ) ; } ) ; this . child . stderr . on ( "data" , ( data ) => { process . stderr . write ( data ) ; } ) ; this . child . on ( "exit" , ( ) => { this . log . warn ( "Child bridge process ended" ) ; } ) ; this . child . on ( "error" , ( e ) => { this . bridgeStatus = ChildBridgeStatus . DOWN ; this . log . error ( "Child process error" , e ) ; } ) ; this . child . on ( "close" , ( code , signal ) => { this . bridgeStatus = ChildBridgeStatus . DOWN ; this . handleProcessClose ( code , signal ) ; } ) ; this . child . on ( "message" , ( message ) => { if ( typeof message !== "object" || ! message . id ) { return ; } switch ( message . id ) { case ChildProcessMessageEventType . READY : { this . log ( `Launched child bridge with PID ${ this . child ?. pid }` ) ; this . loadPlugin ( ) ; break ; } case ChildProcessMessageEventType . LOADED : { const version = ( message . data as ChildProcessPluginLoadedEventData ) . version ; if ( this . pluginConfig . length > 1 ) { this . log ( `Loaded ${ this . plugin . getPluginIdentifier ( ) } v${ version } child bridge successfully with ${ this . pluginConfig . length } accessories` ) ; } else { this . log ( `Loaded ${ this . plugin . getPluginIdentifier ( ) } v${ version } child bridge successfully` ) ; } this . startBridge ( ) ; break ; } case ChildProcessMessageEventType . ONLINE : { this . bridgeStatus = ChildBridgeStatus . OK ; break ; } case ChildProcessMessageEventType . PORT_REQUEST : { this . handlePortRequest ( message . data as ChildProcessPortRequestEventData ) ; break ; } } } ) ; } param code param signal private handleProcessClose ( code , signal ) { this . log ( `Process Ended. Code: ${ code }, Signal: ${ signal }` ) ; setTimeout ( ( ) => { if ( ! this . shuttingDown ) { this . log ( "Restarting Process..." ) ; this . startChildProcess ( ) ; } } , 7000 ) ; } param type param data private sendMessage < T = unknown > ( type , data ? ) { if ( this . child && this . child . connected ) { this . child . send ( { id : type , data , } ) ; } } /**
     * Some plugins may make use of the homebridge process flags
     * These will be passed through to the forked process
     */ private setProcessFlags ( ) { if ( this . homebridgeOptions . debugModeEnabled ) { this . args . push ( "-D" ) ; } if ( this . homebridgeOptions . forceColourLogging ) { this . args . push ( "-C" ) ; } if ( this . homebridgeOptions . insecureAccess ) { this . args . push ( "-I" ) ; } if ( this . homebridgeOptions . noLogTimestamps ) { this . args . push ( "-T" ) ; } if ( this . homebridgeOptions . keepOrphanedCachedAccessories ) { this . args . push ( "-K" ) ; } if ( this . homebridgeOptions . customStoragePath ) { this . args . push ( "-U" , this . homebridgeOptions . customStoragePath ) ; } if ( this . homebridgeOptions . customPluginPath ) { this . args . push ( "-P" , this . homebridgeOptions . customPluginPath ) ; } } /**
     * Tell the child process to load the given plugin
     */ private loadPlugin ( ) { const bridgeConfig = { name : this . bridgeConfig . name || this . displayName || this . plugin . getPluginIdentifier ( ) , port : this . bridgeConfig . port , username : this . bridgeConfig . username , advertiser : this . homebridgeConfig . bridge . advertiser , pin : this . bridgeConfig . pin || this . homebridgeConfig . bridge . pin , bind : this . homebridgeConfig . bridge . bind , setupID : this . bridgeConfig . setupID , manufacturer : this . bridgeConfig . manufacturer || this . homebridgeConfig . bridge . manufacturer , model : this . bridgeConfig . model || this . homebridgeConfig . bridge . model , } ; const bridgeOptions = { cachedAccessoriesDir : User . cachedAccessoryPath ( ) , cachedAccessoriesItemName : "cachedAccessories." + this . bridgeConfig . username . replace ( /:/g , "" ) . toUpperCase ( ) , } ; Object . assign ( bridgeOptions , this . homebridgeOptions ) ; this . sendMessage < ChildProcessLoadEventData > ( ChildProcessMessageEventType . LOAD , { type : this . type , identifier : this . identifier , pluginPath : this . plugin . getPluginPath ( ) , pluginConfig : this . pluginConfig , bridgeConfig , bridgeOptions , homebridgeConfig : { bridge : this . homebridgeConfig . bridge , mdns : this . homebridgeConfig . mdns , ports : this . homebridgeConfig . ports , disabledPlugins : [ ] , accessories : [ ] , platforms : [ ] , } , } ) ; } /**
     * Tell the child bridge to start broadcasting
     */ private startBridge ( ) { this . sendMessage ( ChildProcessMessageEventType . START ) ; } /**
     * Handle external port requests from child
     */ private async handlePortRequest ( request ) { const port = await this . externalPortService . requestPort ( request . username ) ; this . sendMessage < ChildProcessPortAllocatedEventData > ( ChildProcessMessageEventType . PORT_ALLOCATED , { username : request . username , port : port , } ) ; } /**
     * Send sigterm to the child bridge
     */ private teardown ( ) { if ( this . child && this . child . connected ) { this . bridgeStatus = ChildBridgeStatus . DOWN ; this . child . kill ( "SIGTERM" ) ; } } /**
     * Restarts the child bridge process
     */ public restartBridge ( ) { this . log . warn ( "Restarting child bridge..." ) ; this . refreshConfig ( ) ; this . teardown ( ) ; } /**
     * Read the config.json file from disk and refresh the plugin config block for just this plugin
     */ public async refreshConfig ( ) { try { const homebridgeConfig = await fs . readJson ( User . configPath ( ) ) ; if ( this . type === PluginType . PLATFORM ) { const config = homebridgeConfig . platforms ?. filter ( x => x . platform === this . identifier && x . _bridge ?. username === this . bridgeConfig . username ) ; if ( config . length ) { this . pluginConfig = config ; this . bridgeConfig = this . pluginConfig [ 0 ] . _bridge || this . bridgeConfig ; } else { this . log . warn ( "Platform config could not be found, using existing config." ) ; } } else if ( this . type === PluginType . ACCESSORY ) { const config = homebridgeConfig . accessories ?. filter ( x => x . accessory === this . identifier && x . _bridge ?. username === this . bridgeConfig . username ) ; if ( config . length ) { this . pluginConfig = config ; this . bridgeConfig = this . pluginConfig [ 0 ] . _bridge || this . bridgeConfig ; } else { this . log . warn ( "Accessory config could not be found, using existing config." ) ; } } } catch ( e ) { this . log . error ( "Failed to refresh plugin config:" , e . message ) ; } } /**
     * Returns metadata about this child bridge
     */ public getMetadata ( ) { return { status : this . bridgeStatus , username : this . bridgeConfig . username , name : this . bridgeConfig . name || this . displayName || this . plugin . getPluginIdentifier ( ) , plugin : this . plugin . getPluginIdentifier ( ) , identifier : this . identifier , pid : this . child ?. pid , } ; } }