UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge.

1,013 lines • 46.1 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HomebridgeServiceHelper = void 0; const node_buffer_1 = require("node:buffer"); const node_child_process_1 = require("node:child_process"); const node_os_1 = require("node:os"); const node_path_1 = require("node:path"); const node_process_1 = __importDefault(require("node:process")); const axios_1 = __importDefault(require("axios")); const commander_1 = require("commander"); const fs_extra_1 = require("fs-extra"); const ora_1 = __importDefault(require("ora")); const semver_1 = require("semver"); const systeminformation_1 = require("systeminformation"); const tail_1 = require("tail"); const tar_1 = require("tar"); const tcp_port_used_1 = require("tcp-port-used"); const darwin_1 = require("./platforms/darwin"); const freebsd_1 = require("./platforms/freebsd"); const linux_1 = require("./platforms/linux"); const win32_1 = require("./platforms/win32"); node_process_1.default.title = 'hb-service'; class HomebridgeServiceHelper { get logPath() { return (0, node_path_1.resolve)(this.storagePath, 'homebridge.log'); } constructor() { this.selfPath = __filename; this.serviceName = 'Homebridge'; this.usingCustomStoragePath = false; this.allowRunRoot = false; this.enableHbServicePluginManagement = false; this.homebridgeOpts = ['-I']; this.homebridgeCustomEnv = {}; this.uiPort = 8581; this.nodeVersionCheck(); switch ((0, node_os_1.platform)()) { case 'linux': this.installer = new linux_1.LinuxInstaller(this); break; case 'win32': this.installer = new win32_1.Win32Installer(this); break; case 'darwin': this.installer = new darwin_1.DarwinInstaller(this); break; case 'freebsd': this.installer = new freebsd_1.FreeBSDInstaller(this); break; default: this.logger(`ERROR: This command is not supported on ${(0, node_os_1.platform)()}.`, 'fail'); node_process_1.default.exit(1); } commander_1.program .allowUnknownOption() .allowExcessArguments() .storeOptionsAsProperties(true) .arguments('[install|uninstall|start|stop|restart|rebuild|run|logs|view|add|remove]') .option('-P, --plugin-path <path>', '', (p) => { node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH = p; this.homebridgeOpts.push('-P', p); }) .option('-U, --user-storage-path <path>', '', (p) => { this.storagePath = p; this.usingCustomStoragePath = true; }) .option('-S, --service-name <service name>', 'The name of the homebridge service to install or control', p => this.serviceName = p) .option('-T, --no-timestamp', '', () => this.homebridgeOpts.push('-T')) .option('--strict-plugin-resolution', '', () => { node_process_1.default.env.UIX_STRICT_PLUGIN_RESOLUTION = '1'; }) .option('--port <port>', 'The port to set to the Homebridge UI when installing as a service', p => this.uiPort = Number.parseInt(p, 10)) .option('--user <user>', 'The user account the Homebridge service will be installed as (Linux, FreeBSD, macOS only)', p => this.asUser = p) .option('--group <group>', 'The group the Homebridge service will be added to (Linux, FreeBSD, macOS only)', p => this.addGroup = p) .option('--stdout', '', () => this.stdout = true) .option('--allow-root', '', () => this.allowRunRoot = true) .option('--docker', '', () => this.docker = true) .option('--uid <number>', '', i => this.uid = Number.parseInt(i, 10)) .option('--gid <number>', '', i => this.gid = Number.parseInt(i, 10)) .option('-v, --version', 'output the version number', () => this.showVersion()) .action((cmd) => { this.action = cmd; }) .parse(node_process_1.default.argv); this.setEnv(); switch (this.action) { case 'install': { this.nvmCheck(); this.logger(`Installing ${this.serviceName} service...`); this.installer.install(); break; } case 'uninstall': { this.logger(`Removing ${this.serviceName} service...`); this.installer.uninstall(); break; } case 'start': { this.installer.start(); break; } case 'stop': { this.installer.stop(); break; } case 'restart': { this.logger(`Restarting ${this.serviceName} service...`); this.installer.restart(); break; } case 'rebuild': { this.logger(`Rebuilding for Node.js ${node_process_1.default.version}...`); this.installer.rebuild(commander_1.program.args.includes('--all')); break; } case 'run': { this.launch(); break; } case 'logs': { this.tailLogs(); break; } case 'view': { this.viewLogs(); break; } case 'add': { this.npmPluginManagement(commander_1.program.args); break; } case 'remove': { this.npmPluginManagement(commander_1.program.args); break; } case 'update-node': { this.checkForNodejsUpdates(commander_1.program.args.length === 2 ? commander_1.program.args[1] : null); break; } case 'before-start': { this.installer.beforeStart(); break; } case 'status': { this.checkStatus(); break; } default: { commander_1.program.outputHelp(); console.log('\nThe hb-service command is provided by homebridge-config-ui-x\n'); console.log('Please provide a command:'); console.log(' install install homebridge as a service'); console.log(' uninstall remove the homebridge service'); console.log(' start start the homebridge service'); console.log(' stop stop the homebridge service'); console.log(' restart restart the homebridge service'); if (this.enableHbServicePluginManagement) { console.log(' add <plugin>@<version> install a plugin'); console.log(' remove <plugin>@<version> remove a plugin'); } console.log(' rebuild rebuild ui'); console.log(' rebuild --all rebuild all npm modules (use after updating Node.js)'); console.log(' run run homebridge daemon'); console.log(' logs tails the homebridge service logs'); console.log(' view views the homebridge service logs for 30 seconds'); console.log(' update-node [version] update Node.js'); console.log('\nSee the wiki for help with hb-service: https://homebridge.io/w/JTtHK \n'); node_process_1.default.exit(1); } } } logger(msg, level = 'info') { if (this.action === 'run') { msg = `\x1B[37m[${new Date().toLocaleString()}]\x1B[0m ` + `\x1B[36m[HB Supervisor]\x1B[0m ${msg}`; if (this.log) { this.log.write(`${msg}\n`); } else { console.log(msg); } } else { (0, ora_1.default)()[level](msg); } } setEnv() { if (!this.serviceName.match(/^[a-z0-9-]+$/i)) { this.logger('Service name must not contain spaces or special characters.', 'fail'); node_process_1.default.exit(1); } if (!this.storagePath) { if ((0, node_os_1.platform)() === 'linux' || (0, node_os_1.platform)() === 'freebsd') { this.storagePath = (0, node_path_1.resolve)('/var/lib', this.serviceName.toLowerCase()); } else { this.storagePath = (0, node_path_1.resolve)((0, node_os_1.homedir)(), `.${this.serviceName.toLowerCase()}`); } } if (node_process_1.default.env.CONFIG_UI_VERSION && node_process_1.default.env.HOMEBRIDGE_VERSION && node_process_1.default.env.QEMU_ARCH) { if ((0, node_os_1.platform)() === 'linux' && ['install', 'uninstall', 'start', 'stop', 'restart', 'logs'].includes(this.action)) { this.logger(`Sorry, the ${this.action} command is not supported in Docker.`, 'fail'); node_process_1.default.exit(1); } } this.enableHbServicePluginManagement = (node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH && (Boolean(node_process_1.default.env.HOMEBRIDGE_SYNOLOGY_PACKAGE === '1') || Boolean(node_process_1.default.env.HOMEBRIDGE_APT_PACKAGE === '1'))); node_process_1.default.env.UIX_STORAGE_PATH = this.storagePath; node_process_1.default.env.UIX_CONFIG_PATH = (0, node_path_1.resolve)(this.storagePath, 'config.json'); node_process_1.default.env.UIX_BASE_PATH = node_process_1.default.env.UIX_BASE_PATH_OVERRIDE || (0, node_path_1.resolve)(__dirname, '../../'); node_process_1.default.env.UIX_SERVICE_MODE = '1'; node_process_1.default.env.UIX_INSECURE_MODE = '1'; } showVersion() { const pjson = (0, fs_extra_1.readJsonSync)((0, node_path_1.resolve)(__dirname, '../../', 'package.json')); console.log(`v${pjson.version}`); node_process_1.default.exit(0); } async startLog() { if (this.stdout === true) { this.log = node_process_1.default.stdout; return; } this.logger(`Logging to ${this.logPath}.`); this.log = (0, fs_extra_1.createWriteStream)(this.logPath, { flags: 'a' }); node_process_1.default.stdout.write = node_process_1.default.stderr.write = this.log.write.bind(this.log); } async readConfig() { return (0, fs_extra_1.readJson)(node_process_1.default.env.UIX_CONFIG_PATH); } async truncateLog() { if (!(await (0, fs_extra_1.pathExists)(this.logPath))) { return; } try { const currentConfig = await this.readConfig(); const uiConfigBlock = currentConfig.platforms?.find((x) => x.platform === 'config'); const maxSize = uiConfigBlock?.log?.maxSize ?? 1000000; const truncateSize = uiConfigBlock?.log?.truncateSize ?? 200000; if (maxSize < 0) { return; } const logStats = await (0, fs_extra_1.stat)(this.logPath); if (logStats.size < maxSize) { return; } const logStartPosition = logStats.size - truncateSize; const logBuffer = node_buffer_1.Buffer.alloc(truncateSize); const logFileHandle = await (0, fs_extra_1.open)(this.logPath, 'a+'); await (0, fs_extra_1.read)(logFileHandle, logBuffer, 0, truncateSize, logStartPosition); await (0, fs_extra_1.ftruncate)(logFileHandle); await (0, fs_extra_1.write)(logFileHandle, logBuffer); await (0, fs_extra_1.close)(logFileHandle); } catch (e) { this.logger(`Failed to truncate log file: ${e.message}.`, 'fail'); } } async launch() { if ((0, node_os_1.platform)() !== 'win32' && node_process_1.default.getuid() === 0 && !this.allowRunRoot) { this.logger('The hb-service run command should not be executed as root.'); this.logger('Use the --allow-root flag to force the service to run as the root user.'); node_process_1.default.exit(0); } this.logger(`Homebridge storage path: ${this.storagePath}.`); this.logger(`Homebridge config path: ${node_process_1.default.env.UIX_CONFIG_PATH}.`); setInterval(() => { this.truncateLog(); }, (1000 * 60 * 60) * 2); try { await this.storagePathCheck(); await this.startLog(); await this.configCheck(); this.logger(`OS: ${(0, node_os_1.type)()} ${(0, node_os_1.release)()} ${(0, node_os_1.arch)()}.`); this.logger(`Node.js ${node_process_1.default.version} ${node_process_1.default.execPath}.`); this.homebridgeBinary = await this.findHomebridgePath(); this.logger(`Homebridge path: ${this.homebridgeBinary}.`); await this.loadHomebridgeStartupOptions(); this.uiBinary = (0, node_path_1.resolve)(node_process_1.default.env.UIX_BASE_PATH, 'dist', 'bin', 'standalone.js'); this.logger(`UI path: ${this.uiBinary}.`); } catch (e) { this.logger(e.message); node_process_1.default.exit(1); } this.startExitHandler(); await this.runUi(); if (this.ipcService && this.homebridgePackage) { this.ipcService.setHomebridgeVersion(this.homebridgePackage.version); } if ((0, node_os_1.cpus)().length === 1 && (0, node_os_1.arch)() === 'arm') { this.logger('Delaying Homebridge startup by 20 seconds on low powered server.'); setTimeout(() => { this.runHomebridge(); }, 20000); } else { this.runHomebridge(); } } startExitHandler() { const exitHandler = () => { this.logger('Stopping services...'); try { this.homebridge.kill(); } catch (e) { } setTimeout(() => { try { this.homebridge.kill('SIGKILL'); } catch (e) { } node_process_1.default.exit(1282); }, 7000); }; node_process_1.default.on('SIGTERM', exitHandler); node_process_1.default.on('SIGINT', exitHandler); } runHomebridge() { if (!this.homebridgeBinary || !(0, fs_extra_1.pathExistsSync)(this.homebridgeBinary)) { this.logger('Could not find Homebridge. Make sure you have installed Homebridge using the -g flag then restart.', 'fail'); this.logger('npm install -g --unsafe-perm homebridge', 'fail'); return; } if (node_process_1.default.env.UIX_STRICT_PLUGIN_RESOLUTION === '1') { if (!this.homebridgeOpts.includes('--strict-plugin-resolution')) { this.homebridgeOpts.push('--strict-plugin-resolution'); } } if (this.homebridgeOpts.length) { this.logger(`Starting Homebridge with extra flags: ${this.homebridgeOpts.join(' ')}.`); } if (Object.keys(this.homebridgeCustomEnv).length) { this.logger(`Starting Homebridge with custom env: ${JSON.stringify(this.homebridgeCustomEnv)}.`); } const env = {}; Object.assign(env, node_process_1.default.env); Object.assign(env, this.homebridgeCustomEnv); const childProcessOpts = { env, silent: true, }; if (this.allowRunRoot && this.uid && this.gid) { childProcessOpts.uid = this.uid; childProcessOpts.gid = this.gid; } if (this.docker) { this.fixDockerPermissions(); } this.homebridge = (0, node_child_process_1.fork)(this.homebridgeBinary, [ '-C', '-Q', '-U', this.storagePath, ...this.homebridgeOpts, ], childProcessOpts); if (this.ipcService) { this.ipcService.setHomebridgeProcess(this.homebridge); this.ipcService.setHomebridgeVersion(this.homebridgePackage.version); } this.logger(`Started Homebridge v${this.homebridgePackage.version} with PID: ${this.homebridge.pid}.`); this.homebridge.stdout.on('data', (data) => { this.log.write(data); }); this.homebridge.stderr.on('data', (data) => { this.log.write(data); }); this.homebridge.on('close', (code, signal) => { this.handleHomebridgeClose(code, signal); }); } handleHomebridgeClose(code, signal) { this.logger(`Homebridge process ended. Code: ${code}, signal: ${signal}.`); this.checkForStaleHomebridgeProcess(); this.refreshHomebridgePackage(); setTimeout(() => { this.logger('Restarting Homebridge...'); this.runHomebridge(); }, 5000); } async runUi() { try { const main = await Promise.resolve().then(() => __importStar(require('../main'))); const ui = await main.app; this.ipcService = ui.get(main.HomebridgeIpcService); } catch (e) { this.logger('The user interface threw an unhandled error.'); console.error(e); setTimeout(() => { node_process_1.default.exit(1); }, 4500); if (this.homebridge) { this.homebridge.kill(); } } } async getNpmGlobalModulesDirectory() { try { const npmPrefix = (0, node_child_process_1.execSync)('npm -g prefix', { env: Object.assign({ npm_config_loglevel: 'silent', npm_update_notifier: 'false', }, node_process_1.default.env), }).toString('utf8').trim(); return (0, node_os_1.platform)() === 'win32' ? (0, node_path_1.join)(npmPrefix, 'node_modules') : (0, node_path_1.join)(npmPrefix, 'lib', 'node_modules'); } catch (e) { return null; } } async findHomebridgePath() { const nodeModules = (0, node_path_1.resolve)(node_process_1.default.env.UIX_BASE_PATH, '..'); if (await (0, fs_extra_1.pathExists)((0, node_path_1.resolve)(nodeModules, 'homebridge', 'package.json'))) { this.homebridgeModulePath = (0, node_path_1.resolve)(nodeModules, 'homebridge'); } if (!this.homebridgeModulePath && !(node_process_1.default.env.UIX_STRICT_PLUGIN_RESOLUTION === '1' && node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH)) { const globalModules = await this.getNpmGlobalModulesDirectory(); if (globalModules && await (0, fs_extra_1.pathExists)((0, node_path_1.resolve)(globalModules, 'homebridge'))) { this.homebridgeModulePath = (0, node_path_1.resolve)(globalModules, 'homebridge'); } } if (!this.homebridgeModulePath && node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH) { if (await (0, fs_extra_1.pathExists)((0, node_path_1.resolve)(node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH, 'homebridge', 'package.json'))) { this.homebridgeModulePath = (0, node_path_1.resolve)(node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH, 'homebridge'); } } if (this.homebridgeModulePath) { try { await this.refreshHomebridgePackage(); return (0, node_path_1.resolve)(this.homebridgeModulePath, this.homebridgePackage.bin.homebridge); } catch (e) { console.log(e); } } return null; } async refreshHomebridgePackage() { try { if (await (0, fs_extra_1.pathExists)(this.homebridgeModulePath)) { this.homebridgePackage = await (0, fs_extra_1.readJson)((0, node_path_1.join)(this.homebridgeModulePath, 'package.json')); } else { this.logger(`Homebridge not longer found at ${this.homebridgeModulePath}.`, 'fail'); this.homebridgeModulePath = undefined; this.homebridgeBinary = await this.findHomebridgePath(); this.logger(`Found new Homebridge path: ${this.homebridgeBinary}.`); } } catch (e) { console.log(e); } } nodeVersionCheck() { if (Number.parseInt(node_process_1.default.versions.modules, 10) < 64) { this.logger(`Node.js v10.13.0 or greater is required, current: ${node_process_1.default.version}.`, 'fail'); node_process_1.default.exit(1); } } nvmCheck() { if (node_process_1.default.execPath.includes('nvm') && (0, node_os_1.platform)() === 'linux') { this.logger('WARNING: It looks like you are running Node.js via NVM (Node Version Manager).\n' + ' Using hb-service with NVM may not work unless you have configured NVM for the\n' + ' user this service will run as. See https://homebridge.io/w/JUZ2g for instructions on how\n' + ' to remove NVM, then follow the wiki instructions to install Node.js and Homebridge.', 'warn'); } } async printPostInstallInstructions() { const defaultAdapter = await (0, systeminformation_1.networkInterfaceDefault)(); const defaultInterface = (await (0, systeminformation_1.networkInterfaces)()).find((x) => x.iface === defaultAdapter); console.log('\nManage Homebridge by going to one of the following in your browser:\n'); console.log(`* http://localhost:${this.uiPort}`); if (defaultInterface && defaultInterface.ip4) { console.log(`* http://${defaultInterface.ip4}:${this.uiPort}`); } if (defaultInterface && defaultInterface.ip6) { console.log(`* http://[${defaultInterface.ip6}]:${this.uiPort}`); } console.log(''); this.logger('Homebridge setup complete.', 'succeed'); } async portCheck() { const inUse = await (0, tcp_port_used_1.check)(this.uiPort); if (inUse) { this.logger(`Port ${this.uiPort} is already in use by another process on this host.`, 'fail'); this.logger('You can specify another port using the --port flag, e.g.:', 'fail'); this.logger(`hb-service ${this.action} --port 8581`, 'fail'); node_process_1.default.exit(1); } } async storagePathCheck() { if ((0, node_os_1.platform)() === 'darwin' && !await (0, fs_extra_1.pathExists)((0, node_path_1.dirname)(this.storagePath))) { this.logger(`Cannot create Homebridge storage directory, base path does not exist: ${(0, node_path_1.dirname)(this.storagePath)}.`, 'fail'); node_process_1.default.exit(1); } if (!await (0, fs_extra_1.pathExists)(this.storagePath)) { this.logger(`Creating Homebridge directory: ${this.storagePath}.`); await (0, fs_extra_1.mkdirp)(this.storagePath); await this.chownPath(this.storagePath); } } async configCheck() { let saveRequired = false; let restartRequired = false; if (!await (0, fs_extra_1.pathExists)(node_process_1.default.env.UIX_CONFIG_PATH)) { this.logger(`Creating default config.json: ${node_process_1.default.env.UIX_CONFIG_PATH}.`); await this.createDefaultConfig(); restartRequired = true; } try { const currentConfig = await this.readConfig(); if (!Array.isArray(currentConfig.platforms)) { currentConfig.platforms = []; } let uiConfigBlock = currentConfig.platforms.find((x) => x.platform === 'config'); if (!uiConfigBlock) { this.logger(`Adding missing UI platform block to ${node_process_1.default.env.UIX_CONFIG_PATH}.`, 'info'); uiConfigBlock = await this.createDefaultUiConfig(); currentConfig.platforms.push(uiConfigBlock); saveRequired = true; restartRequired = true; } if (this.action !== 'install' && typeof uiConfigBlock.port !== 'number') { uiConfigBlock.port = await this.getLastKnownUiPort(); this.logger(`Added missing port number to UI config: ${uiConfigBlock.port}.`, 'info'); saveRequired = true; restartRequired = true; } if (this.action === 'install') { if (uiConfigBlock.port !== this.uiPort) { uiConfigBlock.port = this.uiPort; this.logger(`Homebridge UI port in ${node_process_1.default.env.UIX_CONFIG_PATH} changed to: ${this.uiPort}.`, 'warn'); } delete uiConfigBlock.restart; delete uiConfigBlock.sudo; delete uiConfigBlock.log; saveRequired = true; } if (typeof uiConfigBlock.port !== 'number') { uiConfigBlock.port = await this.getLastKnownUiPort(); this.logger(`Added missing port number to UI config: ${uiConfigBlock.port}.`, 'info'); saveRequired = true; restartRequired = true; } if (!currentConfig.bridge) { currentConfig.bridge = await this.generateBridgeConfig(); this.logger('Added missing Homebridge bridge section to the config.json.', 'info'); saveRequired = true; } if (!currentConfig.bridge.port) { currentConfig.bridge.port = await this.generatePort(); this.logger(`Added port to the Homebridge bridge section of the config.json: ${currentConfig.bridge.port}.`, 'info'); saveRequired = true; } if ((uiConfigBlock && currentConfig.bridge.port === uiConfigBlock.port) || currentConfig.bridge.port === 8080) { currentConfig.bridge.port = await this.generatePort(); this.logger(`Bridge port must not be the same as the UI port. Changing bridge port to: ${currentConfig.bridge.port}.`, 'info'); saveRequired = true; } if (currentConfig.plugins && Array.isArray(currentConfig.plugins)) { if (!currentConfig.plugins.includes('homebridge-config-ui-x')) { currentConfig.plugins.push('homebridge-config-ui-x'); this.logger('Added Homebridge UI to the plugins array in the config.json.', 'info'); saveRequired = true; } } if (saveRequired) { await (0, fs_extra_1.writeJson)(node_process_1.default.env.UIX_CONFIG_PATH, currentConfig, { spaces: 4 }); } } catch (e) { const backupFile = (0, node_path_1.resolve)(this.storagePath, `config.json.invalid.${new Date().getTime().toString()}`); this.logger(`${node_process_1.default.env.UIX_CONFIG_PATH} does not contain valid JSON.`, 'warn'); this.logger(`Invalid config.json file has been backed up to ${backupFile}.`, 'warn'); await (0, fs_extra_1.rename)(node_process_1.default.env.UIX_CONFIG_PATH, backupFile); await this.createDefaultConfig(); restartRequired = true; } if (restartRequired && this.action === 'run' && await this.isRaspbianImage()) { this.logger('Restarting process after port number update.', 'info'); node_process_1.default.exit(1); } } async createDefaultConfig() { await (0, fs_extra_1.writeJson)(node_process_1.default.env.UIX_CONFIG_PATH, { bridge: await this.generateBridgeConfig(), accessories: [], platforms: [ await this.createDefaultUiConfig(), ], }, { spaces: 4 }); await this.chownPath(node_process_1.default.env.UIX_CONFIG_PATH); } async generateBridgeConfig() { const username = this.generateUsername(); const port = await this.generatePort(); const name = `Homebridge ${username.substring(username.length - 5).replace(/:/g, '')}`; const pin = this.generatePin(); const advertiser = await this.isAvahiDaemonRunning() ? 'avahi' : 'bonjour-hap'; return { name, username, port, pin, advertiser, }; } async createDefaultUiConfig() { return { name: 'Config', port: this.action === 'install' ? this.uiPort : await this.getLastKnownUiPort(), platform: 'config', }; } async isRaspbianImage() { return (0, node_os_1.platform)() === 'linux' && await (0, fs_extra_1.pathExists)('/etc/hb-ui-port'); } async getLastKnownUiPort() { if (await this.isRaspbianImage()) { const lastPort = Number.parseInt((await (0, fs_extra_1.readFile)('/etc/hb-ui-port', 'utf8')), 10); if (!Number.isNaN(lastPort) && lastPort <= 65535) { return lastPort; } } const envPort = Number.parseInt(node_process_1.default.env.HOMEBRIDGE_CONFIG_UI_PORT, 10); if (!Number.isNaN(envPort) && envPort <= 65535) { return envPort; } return this.uiPort; } generatePin() { let code = `${Math.floor(10000000 + Math.random() * 90000000)}`; code = code.split(''); code.splice(3, 0, '-'); code.splice(6, 0, '-'); code = code.join(''); return code; } generateUsername() { const hexDigits = '0123456789ABCDEF'; let username = '0E:'; for (let i = 0; i < 5; i++) { username += hexDigits.charAt(Math.round(Math.random() * 15)); username += hexDigits.charAt(Math.round(Math.random() * 15)); if (i !== 4) { username += ':'; } } return username; } async generatePort() { const randomPort = () => Math.floor(Math.random() * (52000 - 51000 + 1) + 51000); let port = randomPort(); while (await (0, tcp_port_used_1.check)(port)) { port = randomPort(); } return port; } async isAvahiDaemonRunning() { if ((0, node_os_1.platform)() !== 'linux') { return false; } if (!await (0, fs_extra_1.pathExists)('/etc/avahi/avahi-daemon.conf') || !await (0, fs_extra_1.pathExists)('/usr/bin/systemctl')) { return false; } try { if (await (0, fs_extra_1.pathExists)('/usr/lib/systemd/system/avahi.service')) { (0, node_child_process_1.execSync)('systemctl is-active --quiet avahi 2> /dev/null'); return true; } else if (await (0, fs_extra_1.pathExists)('/lib/systemd/system/avahi-daemon.service')) { (0, node_child_process_1.execSync)('systemctl is-active --quiet avahi-daemon 2> /dev/null'); return true; } else { return false; } } catch (e) { return false; } } async chownPath(pathToChown) { if ((0, node_os_1.platform)() !== 'win32' && node_process_1.default.getuid() === 0) { const { uid, gid } = await this.installer.getId(); (0, fs_extra_1.chownSync)(pathToChown, uid, gid); } } async checkForStaleHomebridgeProcess() { if ((0, node_os_1.platform)() === 'win32') { return; } try { const currentConfig = await this.readConfig(); if (!currentConfig.bridge || !currentConfig.bridge.port) { return; } if (!await (0, tcp_port_used_1.check)(Number.parseInt(currentConfig.bridge.port.toString(), 10))) { return; } const pid = Number.parseInt(this.installer.getPidOfPort(Number.parseInt(currentConfig.bridge.port.toString(), 10)), 10); if (!pid) { return; } this.logger(`Found stale Homebridge process running on port: ${currentConfig.bridge.port}, with PID: ${pid}, killing...`); node_process_1.default.kill(pid, 'SIGKILL'); } catch (e) { } } async tailLogs() { if (!(0, fs_extra_1.existsSync)(this.logPath)) { this.logger(`Log file does not exist at expected location: ${this.logPath}.`, 'fail'); node_process_1.default.exit(1); } const logStats = await (0, fs_extra_1.stat)(this.logPath); const logStartPosition = logStats.size <= 200000 ? 0 : logStats.size - 200000; const logStream = (0, fs_extra_1.createReadStream)(this.logPath, { start: logStartPosition }); logStream.on('data', (buffer) => { node_process_1.default.stdout.write(buffer); }); logStream.on('end', () => { logStream.close(); }); const tail = new tail_1.Tail(this.logPath, { fromBeginning: false, useWatchFile: true, fsWatchOptions: { interval: 200, }, }); tail.on('line', console.log); } async viewLogs() { this.installer.viewLogs(); if (!(0, fs_extra_1.existsSync)(this.logPath)) { this.logger(`Log file does not exist at expected location: ${this.logPath}.`, 'fail'); node_process_1.default.exit(1); } const logStats = await (0, fs_extra_1.stat)(this.logPath); const logStartPosition = logStats.size <= 200000 ? 0 : logStats.size - 200000; const logStream = (0, fs_extra_1.createReadStream)(this.logPath, { start: logStartPosition }); logStream.on('data', (buffer) => { node_process_1.default.stdout.write(buffer); }); logStream.on('end', () => { logStream.close(); }); const tail = new tail_1.Tail(this.logPath, { fromBeginning: false, useWatchFile: true, fsWatchOptions: { interval: 200, }, }); tail.on('line', console.log); setTimeout(() => { tail.unwatch(); }, 30000); } get homebridgeStartupOptionsPath() { return (0, node_path_1.resolve)(this.storagePath, '.uix-hb-service-homebridge-startup.json'); } async loadHomebridgeStartupOptions() { try { if (await (0, fs_extra_1.pathExists)(this.homebridgeStartupOptionsPath)) { const homebridgeStartupOptions = await (0, fs_extra_1.readJson)(this.homebridgeStartupOptionsPath); if (homebridgeStartupOptions.debugMode && !this.homebridgeOpts.includes('-D')) { this.homebridgeOpts.push('-D'); } if (homebridgeStartupOptions.keepOrphans && !this.homebridgeOpts.includes('-K')) { this.homebridgeOpts.push('-K'); } if (homebridgeStartupOptions.insecureMode === false && this.homebridgeOpts.includes('-I')) { this.homebridgeOpts.splice(this.homebridgeOpts.findIndex(x => x === '-I'), 1); node_process_1.default.env.UIX_INSECURE_MODE = '0'; } Object.assign(this.homebridgeCustomEnv, homebridgeStartupOptions.env); } else if (this.docker) { if (node_process_1.default.env.HOMEBRIDGE_DEBUG === '1' && !this.homebridgeOpts.includes('-D')) { this.homebridgeOpts.push('-D'); } if (node_process_1.default.env.HOMEBRIDGE_INSECURE !== '1' && this.homebridgeOpts.includes('-I')) { this.homebridgeOpts.splice(this.homebridgeOpts.findIndex(x => x === '-I'), 1); node_process_1.default.env.UIX_INSECURE_MODE = '0'; } } } catch (e) { this.logger(`Failed to load startup options as ${e.message}.`); } } fixDockerPermissions() { try { (0, node_child_process_1.execSync)(`chown -R ${this.uid}:${this.gid} "${this.storagePath}"`); } catch (e) { } } async checkForNodejsUpdates(requestedVersion) { const versionList = (await axios_1.default.get('https://nodejs.org/dist/index.json')).data; if (!Array.isArray(versionList)) { this.logger('Failed to check for Node.js updates.', 'fail'); return { update: false }; } const currentLts = versionList.filter(x => x.lts)[0]; if (requestedVersion) { const wantedVersion = versionList.find(x => x.version.startsWith(`v${requestedVersion}`)); if (wantedVersion) { if (!(0, semver_1.gte)(wantedVersion.version, '16.18.2')) { this.logger('Refusing to install Node.js version lower than v16.18.2.', 'fail'); return { update: false }; } this.logger(`Installing Node.js ${wantedVersion.version} over ${node_process_1.default.version}...`, 'info'); return this.installer.updateNodejs({ target: wantedVersion.version, rebuild: wantedVersion.modules !== node_process_1.default.versions.modules, }); } else { this.logger(`v${requestedVersion} is not a valid Node.js version.`, 'info'); return { update: false }; } } if ((0, semver_1.gt)(currentLts.version, node_process_1.default.version)) { this.logger(`Updating Node.js from ${node_process_1.default.version} to ${currentLts.version}...`, 'info'); return this.installer.updateNodejs({ target: currentLts.version, rebuild: currentLts.modules !== node_process_1.default.versions.modules, }); } const currentMajor = (0, semver_1.parse)(node_process_1.default.version).major; const latestVersion = versionList.filter(x => (0, semver_1.parse)(x.version).major === currentMajor)[0]; if ((0, semver_1.gt)(latestVersion.version, node_process_1.default.version)) { this.logger(`Updating Node.js from ${node_process_1.default.version} to ${latestVersion.version}...`, 'info'); return this.installer.updateNodejs({ target: latestVersion.version, rebuild: latestVersion.modules !== node_process_1.default.versions.modules, }); } this.logger(`Node.js ${node_process_1.default.version} already up-to-date.`); return { update: false }; } async downloadNodejs(downloadUrl) { const spinner = (0, ora_1.default)(`Downloading ${downloadUrl}`).start(); try { const tempDir = await (0, fs_extra_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'node')); const tempFilePath = (0, node_path_1.join)(tempDir, 'node.tar.gz'); const tempFile = (0, fs_extra_1.createWriteStream)(tempFilePath); await axios_1.default.get(downloadUrl, { responseType: 'stream' }) .then((response) => { return new Promise((res, rej) => { response.data.pipe(tempFile).on('finish', () => { return res(tempFile); }).on('error', (err) => { return rej(err); }); }); }); spinner.succeed('Download complete.'); return tempFilePath; } catch (e) { spinner.fail(e.message); node_process_1.default.exit(1); } } async extractNodejs(targetVersion, extractConfig) { const spinner = (0, ora_1.default)(`Installing Node.js ${targetVersion}`).start(); try { await (0, tar_1.extract)(extractConfig); spinner.succeed(`Installed Node.js ${targetVersion}`); } catch (e) { spinner.fail(e.message); node_process_1.default.exit(1); } } async removeNpmPackage(npmInstallPath) { if (!await (0, fs_extra_1.pathExists)(npmInstallPath)) { return; } const spinner = (0, ora_1.default)(`Cleaning up npm at ${npmInstallPath}...`).start(); try { await (0, fs_extra_1.remove)(npmInstallPath); spinner.succeed(`Cleaned up npm at ${npmInstallPath}`); } catch (e) { spinner.fail(e.message); } } async checkStatus() { this.logger(`Testing hb-service is running on port ${this.uiPort}...`); try { const res = await axios_1.default.get(`http://localhost:${this.uiPort}/api`); if (res.data === 'Hello World!') { this.logger('Homebridge UI running.', 'succeed'); } else { this.logger('Unexpected response.', 'fail'); node_process_1.default.exit(1); } } catch (e) { this.logger('Homebridge UI not running.', 'fail'); node_process_1.default.exit(1); } } parseNpmPackageString(input) { const RE_SCOPED = /^(@[^/]+\/[^@/]+)(?:@([^/]+))?(\/.*)?$/; const RE_NON_SCOPED = /^([^@/]+)(?:@([^/]+))?(\/.*)?$/; const m = RE_SCOPED.exec(input) || RE_NON_SCOPED.exec(input); if (!m) { this.logger('Invalid plugin name.', 'fail'); node_process_1.default.exit(1); } return { name: m[1] || '', version: m[2] || 'latest', path: m[3] || '', }; } async npmPluginManagement(args) { if (!this.enableHbServicePluginManagement) { this.logger('Plugin management is not supported on your platform using hb-service.', 'fail'); node_process_1.default.exit(1); } if (args.length === 1) { this.logger('Plugin name required.', 'fail'); node_process_1.default.exit(1); } const action = args[0]; const target = this.parseNpmPackageString(args[args.length - 1]); if (!target.name) { this.logger('Invalid plugin name.', 'fail'); node_process_1.default.exit(1); } if (!target.name.match(/^(@[\w-]+(\.[\w-]+)*\/)?homebridge-[\w-]+$/)) { this.logger('Invalid plugin name.', 'fail'); node_process_1.default.exit(1); } const cwd = (0, node_path_1.dirname)(node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH); if (!await (0, fs_extra_1.pathExists)(cwd)) { this.logger(`Path does not exist: ${cwd}.`, 'fail'); } let cmd; if (node_process_1.default.env.UIX_USE_PNPM === '1') { cmd = `pnpm -C "${cwd}" ${action} ${target.name}`; } else { cmd = `npm --prefix "${cwd}" ${action} ${target.name}`; } if (action === 'add') { cmd += `@${target.version}`; } this.logger(`CMD: ${cmd}`, 'info'); try { (0, node_child_process_1.execSync)(cmd, { cwd, stdio: 'inherit', }); this.logger(`Installed ${target.name}@${target.version}.`, 'succeed'); } catch (e) { this.logger(`Plugin installation failed as ${e.message}.`, 'fail'); } } } exports.HomebridgeServiceHelper = HomebridgeServiceHelper; function bootstrap() { return new HomebridgeServiceHelper(); } bootstrap(); //# sourceMappingURL=hb-service.js.map