#!/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 += 1) {
            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, '20.19.0')) {
                    this.logger('Refusing to install Node.js version lower than v20.19.0.', '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 = `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