UNPKG

homebridge

Version:
431 lines 16.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChildBridgeService = exports.ChildBridgeStatus = exports.ChildProcessMessageEventType = void 0; const child_process_1 = __importDefault(require("child_process")); const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const logger_1 = require("./logger"); const user_1 = require("./user"); var ChildProcessMessageEventType; (function (ChildProcessMessageEventType) { /** * Sent from the child process when it is ready to accept config */ ChildProcessMessageEventType["READY"] = "ready"; /** * Sent to the child process with a ChildProcessLoadEventData payload */ ChildProcessMessageEventType["LOAD"] = "load"; /** * Sent from the child process once it has loaded the plugin */ ChildProcessMessageEventType["LOADED"] = "loaded"; /** * Sent to the child process telling it to start */ ChildProcessMessageEventType["START"] = "start"; /** * Sent from the child process when the bridge is online */ ChildProcessMessageEventType["ONLINE"] = "online"; /** * Sent from the child when it wants to request port allocation for an external accessory */ ChildProcessMessageEventType["PORT_REQUEST"] = "portRequest"; /** * Sent from the parent with the port allocation response */ ChildProcessMessageEventType["PORT_ALLOCATED"] = "portAllocated"; /** * Sent from the child to update its current status */ ChildProcessMessageEventType["STATUS_UPDATE"] = "status"; })(ChildProcessMessageEventType || (exports.ChildProcessMessageEventType = ChildProcessMessageEventType = {})); var ChildBridgeStatus; (function (ChildBridgeStatus) { /** * When the child bridge is loading, or restarting */ ChildBridgeStatus["PENDING"] = "pending"; /** * The child bridge is online and has published it's accessory */ ChildBridgeStatus["OK"] = "ok"; /** * The bridge is shutting down, or the process ended unexpectedly */ ChildBridgeStatus["DOWN"] = "down"; })(ChildBridgeStatus || (exports.ChildBridgeStatus = ChildBridgeStatus = {})); /** * Manages the child processes of platforms/accessories being exposed as separate forked bridges. * A child bridge runs a single platform or accessory. */ class ChildBridgeService { type; identifier; plugin; bridgeConfig; homebridgeConfig; homebridgeOptions; api; ipcService; externalPortService; child; args = []; processEnv = {}; shuttingDown = false; lastBridgeStatus = "pending" /* ChildBridgeStatus.PENDING */; pairedStatus = null; manuallyStopped = false; setupUri = null; pluginConfig = []; log; displayName; constructor(type, identifier, plugin, bridgeConfig, homebridgeConfig, homebridgeOptions, api, ipcService, externalPortService) { this.type = type; this.identifier = identifier; this.plugin = plugin; this.bridgeConfig = bridgeConfig; this.homebridgeConfig = homebridgeConfig; this.homebridgeOptions = homebridgeOptions; this.api = api; this.ipcService = ipcService; this.externalPortService = externalPortService; this.log = logger_1.Logger.withPrefix(this.plugin.getPluginIdentifier()); this.api.on("shutdown", () => { this.shuttingDown = true; this.teardown(); }); // make sure we don't hit the max listeners limit this.api.setMaxListeners(this.api.getMaxListeners() + 1); } /** * Start the child bridge service */ start() { this.setProcessFlags(); this.setProcessEnv(); this.startChildProcess(); // set display name if (this.pluginConfig.length > 1 || this.pluginConfig.length === 0) { this.displayName = this.plugin.getPluginIdentifier(); } else { this.displayName = this.pluginConfig[0]?.name || this.plugin.getPluginIdentifier(); } // re-configured log with display name this.log = logger_1.Logger.withPrefix(this.displayName); } /** * Add a config block to a child bridge. * Platform child bridges can only contain one config block. * @param config */ addConfig(config) { this.pluginConfig.push(config); } get bridgeStatus() { return this.lastBridgeStatus; } set bridgeStatus(value) { this.lastBridgeStatus = value; this.sendStatusUpdate(); } /** * Start the child bridge process */ startChildProcess() { this.bridgeStatus = "pending" /* ChildBridgeStatus.PENDING */; this.child = child_process_1.default.fork(path_1.default.resolve(__dirname, "childBridgeFork.js"), this.args, this.processEnv); 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 = "down" /* ChildBridgeStatus.DOWN */; this.log.error("Child process error", e); }); this.child.once("close", (code, signal) => { this.bridgeStatus = "down" /* ChildBridgeStatus.DOWN */; this.handleProcessClose(code, signal); }); // handle incoming ipc messages from the child process this.child.on("message", (message) => { if (typeof message !== "object" || !message.id) { return; } switch (message.id) { case "ready" /* ChildProcessMessageEventType.READY */: { this.log(`Launched child bridge with PID ${this.child?.pid}`); this.loadPlugin(); break; } case "loaded" /* ChildProcessMessageEventType.LOADED */: { const version = message.data.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 "online" /* ChildProcessMessageEventType.ONLINE */: { this.bridgeStatus = "ok" /* ChildBridgeStatus.OK */; break; } case "portRequest" /* ChildProcessMessageEventType.PORT_REQUEST */: { this.handlePortRequest(message.data); break; } case "status" /* ChildProcessMessageEventType.STATUS_UPDATE */: { this.pairedStatus = message.data.paired; this.setupUri = message.data.setupUri; this.sendStatusUpdate(); break; } } }); } /** * Called when the child bridge process exits, if Homebridge is not shutting down, it will restart the process * @param code * @param signal */ handleProcessClose(code, signal) { this.log(`Process Ended. Code: ${code}, Signal: ${signal}`); setTimeout(() => { if (!this.shuttingDown) { this.log("Restarting Process..."); this.startChildProcess(); } }, 7000); } /** * Helper function to send a message to the child process * @param type * @param data */ sendMessage(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 */ 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); } } /** * Set environment variables for the child process */ setProcessEnv() { this.processEnv = { env: { ...process.env, DEBUG: `${process.env.DEBUG || ""} ${this.bridgeConfig.env?.DEBUG || ""}`.trim(), NODE_OPTIONS: `${process.env.NODE_OPTIONS || ""} ${this.bridgeConfig.env?.NODE_OPTIONS || ""}`.trim(), }, silent: true, }; } /** * Tell the child process to load the given plugin */ 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, firmwareRevision: this.bridgeConfig.firmwareRevision || this.homebridgeConfig.bridge.firmwareRevision, serialNumber: this.bridgeConfig.serialNumber || this.bridgeConfig.username, }; const bridgeOptions = { cachedAccessoriesDir: user_1.User.cachedAccessoryPath(), cachedAccessoriesItemName: "cachedAccessories." + this.bridgeConfig.username.replace(/:/g, "").toUpperCase(), }; // shallow copy the homebridge options to the bridge options object Object.assign(bridgeOptions, this.homebridgeOptions); this.sendMessage("load" /* 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: [], // not used by child bridges accessories: [], // not used by child bridges platforms: [], // not used by child bridges }, }); } /** * Tell the child bridge to start broadcasting */ startBridge() { this.sendMessage("start" /* ChildProcessMessageEventType.START */); } /** * Handle external port requests from child */ async handlePortRequest(request) { const port = await this.externalPortService.requestPort(request.username); this.sendMessage("portAllocated" /* ChildProcessMessageEventType.PORT_ALLOCATED */, { username: request.username, port: port, }); } /** * Send sigterm to the child bridge */ teardown() { if (this.child && this.child.connected) { this.bridgeStatus = "down" /* ChildBridgeStatus.DOWN */; this.child.kill("SIGTERM"); } } /** * Trigger sending child bridge metadata to the process parent via IPC */ sendStatusUpdate() { this.ipcService.sendMessage("childBridgeStatusUpdate" /* IpcOutgoingEvent.CHILD_BRIDGE_STATUS_UPDATE */, this.getMetadata()); } /** * Restarts the child bridge process */ restartChildBridge() { if (this.manuallyStopped) { this.startChildBridge(); } else { this.log.warn("Restarting child bridge..."); this.refreshConfig(); this.teardown(); } } /** * Stops the child bridge, not starting it again */ stopChildBridge() { if (!this.shuttingDown) { this.log.warn("Stopping child bridge (will not restart)..."); this.shuttingDown = true; this.manuallyStopped = true; this.child?.removeAllListeners("close"); this.teardown(); } else { this.log.warn("Bridge already shutting down or stopped."); } } /** * Starts the child bridge, only if it was manually stopped and is no longer running */ startChildBridge() { if (this.manuallyStopped && this.bridgeStatus === "down" /* ChildBridgeStatus.DOWN */ && (!this.child || !this.child.connected)) { this.log.warn("Starting child bridge..."); this.refreshConfig(); this.startChildProcess(); this.shuttingDown = false; this.manuallyStopped = false; } else { this.log.warn("Cannot start child bridge, it is still running or was not manually stopped"); } } /** * Read the config.json file from disk and refresh the plugin config block for just this plugin */ async refreshConfig() { try { const homebridgeConfig = await fs_extra_1.default.readJson(user_1.User.configPath()); if (this.type === "platform" /* 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 === "accessory" /* 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 */ getMetadata() { return { status: this.bridgeStatus, paired: this.pairedStatus, setupUri: this.setupUri, username: this.bridgeConfig.username, port: this.bridgeConfig.port, pin: this.bridgeConfig.pin || this.homebridgeConfig.bridge.pin, name: this.bridgeConfig.name || this.displayName || this.plugin.getPluginIdentifier(), plugin: this.plugin.getPluginIdentifier(), identifier: this.identifier, pid: this.child?.pid, manuallyStopped: this.manuallyStopped, }; } } exports.ChildBridgeService = ChildBridgeService; //# sourceMappingURL=childBridgeService.js.map