homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge.
1,013 lines • 46.1 kB
JavaScript
"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