homebridge-config-ui-x
Version:
A web based management, configuration and control platform for Homebridge.
323 lines • 14.9 kB
JavaScript
"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();
if (this.isPackage() && node_process_1.default.env.UIX_USE_PNPM === '1' && node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH) {
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.hbService.logger(`Path does not exist: "${cwd}"`, 'fail');
node_process_1.default.exit(1);
}
(0, node_child_process_1.execSync)(`pnpm -C "${cwd}" rebuild`, {
cwd,
stdio: 'inherit',
});
this.hbService.logger(`Rebuilt plugins in ${node_process_1.default.env.UIX_CUSTOM_PLUGIN_PATH} for Node.js ${targetNodeVersion}.`, 'succeed');
}
else {
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>`,
' </dict>',
'</dict>',
'</plist>',
].filter(x => x).join('\n');
await (0, fs_extra_1.writeFile)(this.plistPath, plistFileContents);
}
}
exports.DarwinInstaller = DarwinInstaller;
//# sourceMappingURL=darwin.js.map