"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DarwinInstaller = void 0;
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 fs_extra_1 = require("fs-extra");
const semver_1 = require("semver");
const base_platform_1 = require("../base-platform");
class DarwinInstaller extends base_platform_1.BasePlatform {
    get plistName() {
        return `com.${this.hbService.serviceName.toLowerCase()}.server`;
    }
    get plistPath() {
        return (0, node_path_1.resolve)('/Library/LaunchDaemons/', `${this.plistName}.plist`);
    }
    async install() {
        this.checkForRoot();
        this.fixStoragePath();
        await this.hbService.portCheck();
        await this.checkGlobalNpmAccess();
        await this.hbService.storagePathCheck();
        await this.hbService.configCheck();
        try {
            await this.createLaunchAgent();
            await this.start();
            await this.hbService.printPostInstallInstructions();
        }
        catch (e) {
            console.error(e.toString());
            this.hbService.logger('ERROR: Failed Operation', 'fail');
        }
    }
    async uninstall() {
        this.checkForRoot();
        await this.stop();
        try {
            if ((0, fs_extra_1.existsSync)(this.plistPath)) {
                this.hbService.logger(`Removed ${this.hbService.serviceName} Service`, 'succeed');
                (0, fs_extra_1.unlinkSync)(this.plistPath);
            }
            else {
                this.hbService.logger(`Could not find installed ${this.hbService.serviceName} Service.`, 'fail');
            }
        }
        catch (e) {
            console.error(e.toString());
            this.hbService.logger('ERROR: Failed Operation', 'fail');
        }
    }
    async start() {
        this.checkForRoot();
        try {
            this.hbService.logger(`Starting ${this.hbService.serviceName} Service...`);
            (0, node_child_process_1.execFileSync)('launchctl', ['load', '-w', this.plistPath]);
            this.hbService.logger(`${this.hbService.serviceName} Started`, 'succeed');
        }
        catch (e) {
            this.hbService.logger(`Failed to start ${this.hbService.serviceName}`, 'fail');
        }
    }
    async stop() {
        this.checkForRoot();
        try {
            this.hbService.logger(`Stopping ${this.hbService.serviceName} Service...`);
            (0, node_child_process_1.execFileSync)('launchctl', ['unload', '-w', this.plistPath]);
            this.hbService.logger(`${this.hbService.serviceName} Stopped`, 'succeed');
        }
        catch (e) {
            this.hbService.logger(`Failed to stop ${this.hbService.serviceName}`, 'fail');
        }
    }
    async restart() {
        this.checkForRoot();
        await this.stop();
        setTimeout(async () => {
            await this.start();
        }, 2000);
    }
    async rebuild(all = false) {
        try {
            if (!this.isPackage()) {
                this.checkForRoot();
            }
            const targetNodeVersion = (0, node_child_process_1.execSync)('node -v').toString('utf8').trim();
            const npmGlobalPath = (0, node_child_process_1.execSync)('/bin/echo -n "$(npm -g prefix)/lib/node_modules"', {
                env: Object.assign({
                    npm_config_loglevel: 'silent',
                    npm_update_notifier: 'false',
                }, node_process_1.default.env),
            }).toString('utf8');
            (0, node_child_process_1.execSync)('npm rebuild --unsafe-perm', {
                cwd: node_process_1.default.env.UIX_BASE_PATH,
                stdio: 'inherit',
            });
            this.hbService.logger(`Rebuilt homebridge-config-ui-x for Node.js ${targetNodeVersion}.`, 'succeed');
            if (all === true) {
                try {
                    (0, node_child_process_1.execSync)('npm rebuild --unsafe-perm', {
                        cwd: npmGlobalPath,
                        stdio: 'inherit',
                    });
                    this.hbService.logger(`Rebuilt plugins in ${npmGlobalPath} for Node.js ${targetNodeVersion}.`, 'succeed');
                }
                catch (e) {
                    this.hbService.logger('Could not rebuild all modules - check Homebridge logs.', 'warn');
                }
            }
            await this.setNpmPermissions(npmGlobalPath);
        }
        catch (e) {
            console.error(e.toString());
            this.hbService.logger('ERROR: Failed Operation', 'fail');
        }
    }
    async getId() {
        if ((node_process_1.default.getuid() === 0 && this.hbService.asUser) || node_process_1.default.env.SUDO_USER) {
            const uid = (0, node_child_process_1.execSync)(`id -u ${this.hbService.asUser || node_process_1.default.env.SUDO_USER}`).toString('utf8');
            const gid = (0, node_child_process_1.execSync)(`id -g ${this.hbService.asUser || node_process_1.default.env.SUDO_USER}`).toString('utf8');
            return {
                uid: Number.parseInt(uid, 10),
                gid: Number.parseInt(gid, 10),
            };
        }
        else {
            return {
                uid: (0, node_os_1.userInfo)().uid,
                gid: (0, node_os_1.userInfo)().gid,
            };
        }
    }
    getPidOfPort(port) {
        try {
            return (0, node_child_process_1.execSync)(`lsof -n -iTCP:${port} -sTCP:LISTEN -t 2> /dev/null`).toString('utf8').trim();
        }
        catch (e) {
            return null;
        }
    }
    checkForRoot() {
        if (node_process_1.default.getuid() !== 0) {
            this.hbService.logger('ERROR: This command must be executed using sudo on macOS', 'fail');
            this.hbService.logger(`sudo hb-service ${this.hbService.action}`, 'fail');
            node_process_1.default.exit(1);
        }
        if (!node_process_1.default.env.SUDO_USER && !this.hbService.asUser) {
            this.hbService.logger('ERROR: Could not detect user. Pass in the user you want to run Homebridge as using the --user flag eg.', 'fail');
            this.hbService.logger(`sudo hb-service ${this.hbService.action} --user your-user`, 'fail');
            node_process_1.default.exit(1);
        }
        this.user = this.hbService.asUser || node_process_1.default.env.SUDO_USER;
    }
    fixStoragePath() {
        if (!this.hbService.usingCustomStoragePath) {
            this.hbService.storagePath = (0, node_path_1.resolve)(this.getUserHomeDir(), `.${this.hbService.serviceName.toLowerCase()}`);
        }
    }
    getUserHomeDir() {
        try {
            const realHomeDir = (0, node_child_process_1.execSync)(`eval echo "~${this.user}"`).toString('utf8').trim();
            if (realHomeDir.charAt(0) === '~') {
                throw new Error('Could not resolve user home directory');
            }
            return realHomeDir;
        }
        catch (e) {
            return (0, node_os_1.homedir)();
        }
    }
    async updateNodejs(job) {
        this.checkForRoot();
        if (!['x64', 'arm64'].includes(node_process_1.default.arch)) {
            this.hbService.logger(`Architecture not supported: ${node_process_1.default.arch}.`, 'fail');
            node_process_1.default.exit(1);
        }
        if (node_process_1.default.arch === 'arm64' && (0, semver_1.lt)(job.target, '18.0.0')) {
            this.hbService.logger('macOS M1 / arm64 support is only available from Node.js v18 or later', 'fail');
            node_process_1.default.exit(1);
        }
        if ((0, semver_1.lt)((0, node_os_1.release)(), '19.0.0') && (0, semver_1.gte)(job.target, '18.0.0')) {
            this.hbService.logger('macOS Catalina 10.15 or later is required to install Node.js v18 or later', 'fail');
            node_process_1.default.exit(1);
        }
        const downloadUrl = `https://nodejs.org/dist/${job.target}/node-${job.target}-darwin-${node_process_1.default.arch}.tar.gz`;
        const targetPath = (0, node_path_1.dirname)((0, node_path_1.dirname)(node_process_1.default.execPath));
        if (targetPath !== '/usr/local' && !targetPath.startsWith('/Library/Application Support/Homebridge/node-')) {
            this.hbService.logger(`Cannot update Node.js on your system. Non-standard installation path detected: ${targetPath}`, 'fail');
            node_process_1.default.exit(1);
        }
        this.hbService.logger(`Target: ${targetPath}`);
        try {
            const archivePath = await this.hbService.downloadNodejs(downloadUrl);
            const extractConfig = {
                file: archivePath,
                cwd: targetPath,
                strip: 1,
                preserveOwner: false,
                unlink: true,
            };
            await this.hbService.removeNpmPackage((0, node_path_1.resolve)(targetPath, 'lib', 'node_modules', 'npm'));
            await this.hbService.extractNodejs(job.target, extractConfig);
            await (0, fs_extra_1.remove)(archivePath);
            await this.rebuild(true);
            if (await (0, fs_extra_1.pathExists)(this.plistPath)) {
                await this.restart();
            }
            else {
                this.hbService.logger('Please restart Homebridge for the changes to take effect.', 'warn');
            }
        }
        catch (e) {
            this.hbService.logger(`Failed to update Node.js: ${e.message}`, 'fail');
            node_process_1.default.exit(1);
        }
    }
    async checkGlobalNpmAccess() {
        const npmGlobalPath = (0, node_child_process_1.execSync)('/bin/echo -n "$(npm -g prefix)/lib/node_modules"', {
            env: Object.assign({
                npm_config_loglevel: 'silent',
                npm_update_notifier: 'false',
            }, node_process_1.default.env),
        }).toString('utf8');
        const { uid, gid } = await this.getId();
        try {
            (0, node_child_process_1.execSync)(`test -w "${npmGlobalPath}"`, {
                uid,
                gid,
            });
            (0, node_child_process_1.execSync)('test -w "$(dirname $(which npm))"', {
                uid,
                gid,
            });
        }
        catch (e) {
            await this.setNpmPermissions(npmGlobalPath);
        }
    }
    async setNpmPermissions(npmGlobalPath) {
        if (this.isPackage()) {
            return;
        }
        try {
            (0, node_child_process_1.execSync)(`chown -R ${this.user}:admin "${npmGlobalPath}"`);
            (0, node_child_process_1.execSync)(`chown -R ${this.user}:admin "$(dirname $(which npm))"`);
        }
        catch (e) {
            this.hbService.logger(`ERROR: User "${this.user}" does not have write access to the global npm modules path.`, 'fail');
            this.hbService.logger('You can fix this issue by running the following commands:', 'fail');
            console.log('');
            console.log(`sudo chown -R ${this.user}:admin "${npmGlobalPath}"`);
            console.log(`sudo chown -R ${this.user}:admin "$(dirname $(which npm))"`);
            console.log('');
            this.hbService.logger('Once you have done this run the hb-service install command again to complete your installation.', 'fail');
            node_process_1.default.exit(1);
        }
    }
    isPackage() {
        return (Boolean(node_process_1.default.env.HOMEBRIDGE_MACOS_PACKAGE === '1'));
    }
    async createLaunchAgent() {
        const plistFileContents = [
            '<?xml version="1.0" encoding="UTF-8"?>',
            '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
            '<plist version="1.0">',
            '<dict>',
            '    <key>RunAtLoad</key>',
            '        <true/>',
            '    <key>KeepAlive</key>',
            '        <true/>',
            '    <key>Label</key>',
            `        <string>${this.plistName}</string>`,
            '    <key>ProgramArguments</key>',
            '        <array>',
            `             <string>${node_process_1.default.execPath}</string>`,
            `             <string>${this.hbService.selfPath}</string>`,
            '             <string>run</string>',
            '             <string>-I</string>',
            '             <string>-U</string>',
            `             <string>${this.hbService.storagePath}</string>`,
            '        </array>',
            '    <key>WorkingDirectory</key>',
            `         <string>${this.hbService.storagePath}</string>`,
            '    <key>StandardOutPath</key>',
            `        <string>${this.hbService.storagePath}/homebridge.log</string>`,
            '    <key>StandardErrorPath</key>',
            `        <string>${this.hbService.storagePath}/homebridge.log</string>`,
            '    <key>UserName</key>',
            `        <string>${this.user}</string>`,
            '    <key>EnvironmentVariables</key>',
            '        <dict>',
            '            <key>PATH</key>',
            `                <string>${node_process_1.default.env.PATH}</string>`,
            '            <key>HOME</key>',
            `                <string>${this.getUserHomeDir()}</string>`,
            '            <key>UIX_STORAGE_PATH</key>',
            `                <string>${this.hbService.storagePath}</string>`,
            '            <key>HOMEBRIDGE_CONFIG_UI_TERMINAL</key>',
            '                <string>1</string>',
            '        </dict>',
            '</dict>',
            '</plist>',
        ].filter(x => x).join('\n');
        await (0, fs_extra_1.writeFile)(this.plistPath, plistFileContents);
    }
}
exports.DarwinInstaller = DarwinInstaller;
//# sourceMappingURL=darwin.js.map