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