UNPKG

homebridge-config-ui-x

Version:

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

251 lines 10.2 kB
import { execSync } from 'node:child_process'; import { existsSync, readFileSync, unlinkSync } from 'node:fs'; import { chmod } from 'node:fs/promises'; import { userInfo } from 'node:os'; import { resolve } from 'node:path'; import process from 'node:process'; import { outputFile } from 'fs-extra/esm'; import { RE_OS_USERNAME } from '../../core/regex.constants.js'; import { BasePlatform } from '../base-platform.js'; export class FreeBSDInstaller extends BasePlatform { get rcServiceName() { return this.hbService.serviceName.toLowerCase(); } get rcServicePath() { return resolve('/usr/local/etc/rc.d', this.rcServiceName); } async install() { this.checkForRoot(); await this.checkUser(); this.setupSudo(); await this.hbService.portCheck(); await this.hbService.storagePathCheck(); await this.hbService.configCheck(); try { await this.createRCService(); await this.enableService(); 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(); await this.disableService(); try { if (existsSync(this.rcServicePath)) { this.hbService.logger(`Removed ${this.rcServiceName} Service`, 'succeed'); unlinkSync(this.rcServicePath); } else { this.hbService.logger(`Could not find installed ${this.rcServiceName} 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.rcServiceName} Service...`); execSync(`service ${this.rcServiceName} start`, { stdio: 'inherit' }); this.hbService.logger(`${this.rcServiceName} Started`, 'succeed'); } catch (e) { this.hbService.logger(`Failed to start ${this.rcServiceName}`, 'fail'); } } async stop() { this.checkForRoot(); try { this.hbService.logger(`Stopping ${this.rcServiceName} Service...`); execSync(`service ${this.rcServiceName} stop`, { stdio: 'inherit' }); this.hbService.logger(`${this.rcServiceName} Stopped`, 'succeed'); } catch (e) { this.hbService.logger(`Failed to stop ${this.rcServiceName}`, 'fail'); } } async restart() { this.checkForRoot(); try { this.hbService.logger(`Restarting ${this.rcServiceName} Service...`); execSync(`service ${this.rcServiceName} restart`, { stdio: 'inherit' }); this.hbService.logger(`${this.rcServiceName} Restarted`, 'succeed'); } catch (e) { this.hbService.logger(`Failed to restart ${this.rcServiceName}`, 'fail'); } } async rebuild(all = false) { try { this.checkForRoot(); const npmGlobalPath = execSync('/bin/echo -n "$(npm -g prefix)/lib/node_modules"', { env: { npm_config_loglevel: 'silent', npm_update_notifier: 'false', ...process.env, }, }).toString('utf8'); const targetNodeVersion = execSync('node -v').toString('utf8').trim(); execSync('npm rebuild --unsafe-perm', { cwd: process.env.UIX_BASE_PATH, stdio: 'inherit', }); if (all === true) { try { execSync('npm rebuild --unsafe-perm', { cwd: npmGlobalPath, stdio: 'inherit', }); } catch (e) { this.hbService.logger('Could not rebuild all modules - check Homebridge logs.', 'warn'); } } this.hbService.logger(`Rebuilt modules in ${process.env.UIX_BASE_PATH} for Node.js ${targetNodeVersion}.`, 'succeed'); } catch (e) { console.error(e.toString()); this.hbService.logger('ERROR: Failed Operation', 'fail'); } } async getId() { if ((process.getuid() === 0 && this.hbService.asUser) || process.env.SUDO_USER) { const uid = execSync(`id -u ${this.hbService.asUser || process.env.SUDO_USER}`).toString('utf8'); const gid = execSync(`id -g ${this.hbService.asUser || process.env.SUDO_USER}`).toString('utf8'); return { uid: Number.parseInt(uid, 10), gid: Number.parseInt(gid, 10), }; } else { return { uid: userInfo().uid, gid: userInfo().gid, }; } } getPidOfPort(port) { try { return execSync(`sockstat -P tcp -p ${port} -l -q 2> /dev/null | awk '{print $3}' | head -n 1`).toString('utf8').trim(); } catch (e) { return null; } } async enableService() { try { execSync(`sysrc ${this.rcServiceName}_enable="YES" 2> /dev/null`); } catch (e) { this.hbService.logger(`WARNING: failed to run "sysrc ${this.rcServiceName}_enable=\"YES\"`, 'warn'); } } async disableService() { try { execSync(`sysrc ${this.rcServiceName}_enable="NO" 2> /dev/null`); } catch (e) { this.hbService.logger(`WARNING: failed to run "sysrc ${this.rcServiceName}_enable=\"NO\"`, 'warn'); } } checkForRoot() { if (process.getuid() !== 0) { this.hbService.logger('ERROR: This command must be executed using sudo on FreeBSD', 'fail'); this.hbService.logger(`EXAMPLE: sudo hb-service ${this.hbService.action}`, 'fail'); process.exit(1); } if (this.hbService.action === 'install' && !process.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(`EXAMPLE: sudo hb-service ${this.hbService.action} --user your-user`, 'fail'); process.exit(1); } } async checkUser() { try { execSync(`id ${this.hbService.asUser} 2> /dev/null`); } catch (e) { execSync(`pw useradd -q -n ${this.hbService.asUser} -s /usr/sbin/nologin 2> /dev/null`); this.hbService.logger(`Created service user: ${this.hbService.asUser}`, 'info'); } } setupSudo() { try { if (!RE_OS_USERNAME.test(this.hbService.asUser)) { this.hbService.logger(`WARNING: Refusing to write /usr/local/etc/sudoers entry — invalid username "${this.hbService.asUser}".`, 'warn'); return; } const npmPath = execSync('which npm').toString('utf8').trim(); const sudoersEntry = `${this.hbService.asUser} ALL=(ALL) NOPASSWD:SETENV: ${npmPath}, /usr/local/bin/npm`; const sudoers = readFileSync('/usr/local/etc/sudoers', 'utf-8'); if (sudoers.includes(sudoersEntry)) { return; } execSync(`echo '${sudoersEntry}' | sudo EDITOR='tee -a' visudo`); } catch (e) { this.hbService.logger('WARNING: Failed to setup /etc/sudoers, you may not be able to shutdown/restart your server from the Homebridge UI.', 'warn'); } } async updateNodejs(job) { this.hbService.logger('Update Node.js using pkg manually.', 'fail'); process.exit(1); } async createRCService() { const rcFileContents = [ '#!/bin/sh', '#', `# PROVIDE: ${this.rcServiceName}`, '# REQUIRE: NETWORKING SYSLOG', '# KEYWORD: shutdown', '#', `# Add the following lines to /etc/rc.conf to enable ${this.rcServiceName}:`, '#', `#${this.rcServiceName}_enable="YES"`, '', '. /etc/rc.subr', '', `name="${this.rcServiceName}"`, `rcvar="${this.rcServiceName}_enable"`, '', 'load_rc_config $name', '', `: \${${this.rcServiceName}_user:="${this.hbService.asUser}"}`, `: \${${this.rcServiceName}_enable:="NO"}`, `: \${${this.rcServiceName}_facility:="daemon"}`, `: \${${this.rcServiceName}_priority:="debug"}`, `: \${${this.rcServiceName}_storage_path:="${this.hbService.storagePath}"}`, '', `export HOME="$(eval echo ~\${${this.rcServiceName}_user})"`, 'export PATH=/usr/local/bin:${PATH}', 'export HOMEBRIDGE_CONFIG_UI_TERMINAL=1', `export UIX_STORAGE_PATH="\${${this.rcServiceName}_storage_path}"`, '', 'pidfile="/var/run/${name}.pid"', 'command="/usr/sbin/daemon"', 'procname="daemon"', `command_args=" -c -f -R 3 -P \${pidfile} ${this.hbService.selfPath} run -U \${${this.rcServiceName}_storage_path}"`, 'start_precmd="homebridge_precmd"', '', 'homebridge_precmd()', '{', ' sleep 10', ` chown -R \${${this.rcServiceName}_user}: \${${this.rcServiceName}_storage_path}`, ` install -o \${${this.rcServiceName}_user} /dev/null \${pidfile}`, '}', '', 'run_rc_command "$1"', ].filter(x => x).join('\n'); await outputFile(this.rcServicePath, rcFileContents); await chmod(this.rcServicePath, '755'); } } //# sourceMappingURL=freebsd.js.map