Skip to content

Commit

Permalink
use common ipc service for hb-service homebridge control
Browse files Browse the repository at this point in the history
  • Loading branch information
oznu committed Feb 2, 2021
1 parent d9cfaa9 commit 290ae5a
Show file tree
Hide file tree
Showing 18 changed files with 267 additions and 143 deletions.
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"ignore": [
"src/**/*.spec.ts"
],
"exec": "sleep 2 && UIX_INSECURE_MODE=1 UIX_SERVICE_MODE=1 HOMEBRIDGE_CONFIG_UI_TERMINAL=1 ts-node -r tsconfig-paths/register src/main.ts",
"exec": "sleep 2 && UIX_INSECURE_MODE=1 UIX_SERVICE_MODE=1 HOMEBRIDGE_CONFIG_UI_TERMINAL=1 ts-node -r tsconfig-paths/register src/bin/hb-service.ts run --stdout",
"signal": "SIGTERM"
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "homebridge-config-ui-x",
"displayName": "Homebridge UI",
"version": "4.37.1-test.3",
"version": "4.37.1-test.5",
"description": "A web based management, configuration and control platform for Homebridge",
"license": "MIT",
"author": "oznu <[email protected]>",
Expand Down
93 changes: 19 additions & 74 deletions src/bin/hb-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { Win32Installer } from './platforms/win32';
import { LinuxInstaller } from './platforms/linux';
import { DarwinInstaller } from './platforms/darwin';

import type { HomebridgeIpcService } from '../core/homebridge-ipc/homebridge-ipc.service';

export class HomebridgeServiceHelper {
public action: 'install' | 'uninstall' | 'start' | 'stop' | 'restart' | 'rebuild' | 'run' | 'logs' | 'update-node' | 'before-start' | 'status';
public selfPath = __filename;
Expand Down Expand Up @@ -53,6 +55,9 @@ export class HomebridgeServiceHelper {

private installer: Win32Installer | LinuxInstaller | DarwinInstaller;

// ui services
private ipcService: HomebridgeIpcService;

get logPath(): string {
return path.resolve(this.storagePath, 'homebridge.log');
}
Expand Down Expand Up @@ -337,6 +342,9 @@ export class HomebridgeServiceHelper {
// start homebridge
this.startExitHandler();

// start the ui
await this.runUi();

// delay the launch of homebridge on Raspberry Pi 1/Zero by 20 seconds
if (os.cpus().length === 1 && os.arch() === 'arm') {
this.logger('Delaying Homebridge startup by 20 seconds on low powered server');
Expand All @@ -346,29 +354,6 @@ export class HomebridgeServiceHelper {
} else {
this.runHomebridge();
}

// start the ui
this.runUi();

process.addListener('message', (event, callback) => {
switch (event) {
case 'clearCachedAccessories': {
return this.clearHomebridgeCachedAccessories(callback);
}
case 'deleteSingleCachedAccessory': {
return this.clearHomebridgeCachedAccessories(callback);
}
case 'restartHomebridge': {
return this.restartHomebridge();
}
case 'postBackupRestoreRestart': {
return this.postBackupRestoreRestart();
}
case 'getHomebridgeChildProcess': {
return this.getHomebridgeChildProcess(callback);
}
}
});
}

/**
Expand Down Expand Up @@ -447,6 +432,9 @@ export class HomebridgeServiceHelper {
childProcessOpts,
);

// let the ipc service know of the new process
this.ipcService.setHomebridgeProcess(this.homebridge);

this.logger(`Started Homebridge v${this.homebridgePackage.version} with PID: ${this.homebridge.pid}`);

this.homebridge.stdout.on('data', (data) => {
Expand Down Expand Up @@ -485,7 +473,14 @@ export class HomebridgeServiceHelper {
*/
private async runUi() {
try {
await import('../main');
// import main module
const main = await import('../main');

// load the nest js instance
const ui = await main.app;

// extract services
this.ipcService = ui.get('HomebridgeIpcService');
} catch (e) {
this.logger('ERROR: The user interface threw an unhandled error');
console.error(e);
Expand Down Expand Up @@ -994,56 +989,6 @@ export class HomebridgeServiceHelper {
}
}

/**
* Clears the Homebridge Cached Accessories
*/
private clearHomebridgeCachedAccessories(callback) {
if (this.homebridge && !this.homebridgeStopped) {
this.homebridge.once('close', callback);
this.restartHomebridge();
} else {
callback();
}
}

/**
* Standard SIGTERM restart for Homebridge
*/
private restartHomebridge() {
if (this.homebridge) {
this.logger('Sending SIGTERM to Homebridge');
this.homebridge.kill('SIGTERM');

setTimeout(() => {
if (!this.homebridgeStopped) {
try {
this.logger('Sending SIGKILL to Homebridge');
this.homebridge.kill('SIGKILL');
} catch (e) { }
}
}, 7000);
}
}

/**
* Send SIGKILL to Homebridge after a restore is completed to prevent the
* Homebridge cached accessories being regenerated
*/
private postBackupRestoreRestart() {
if (this.homebridge) {
this.logger('Sending SIGKILL to Homebridge');
this.homebridge.kill('SIGKILL');
}

setTimeout(() => {
process.kill(process.pid, 'SIGKILL');
}, 500);
}

private getHomebridgeChildProcess(callback) {
callback(this.homebridge);
}

/**
* Fix the permission on the docker storage directory
* This is only used when running in the oznu/docker-homebridge docker container
Expand Down
16 changes: 16 additions & 0 deletions src/core/homebridge-ipc/homebridge-ipc.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { LoggerModule } from '../logger/logger.module';
import { HomebridgeIpcService } from './homebridge-ipc.service';

@Module({
imports: [
LoggerModule,
],
providers: [
HomebridgeIpcService
],
exports: [
HomebridgeIpcService
],
})
export class HomebridgeIpcModule { }
134 changes: 134 additions & 0 deletions src/core/homebridge-ipc/homebridge-ipc.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { Logger } from '../logger/logger.service';

@Injectable()
export class HomebridgeIpcService extends EventEmitter {
private homebridge: ChildProcess;

private permittedEvents = [
'childBridgeMetadataResponse',
'childBridgeStatusUpdate',
];

constructor(
private logger: Logger
) {
super();
}

/**
* Set the current homebridge process.
* This method is called from hb-service.
*/
public setHomebridgeProcess(process: ChildProcess) {
this.homebridge = process;

this.homebridge.on('message', (message: { id: string; data: unknown }) => {
if (typeof message !== 'object' || !message.id) {
return;
}
if (this.permittedEvents.includes(message.id)) {
this.emit(message.id, message.data);
}
});
}

/**
* Send a message to the homebridge child process
*/
private sendMessage(type: string, data: unknown) {
if (this.homebridge && this.homebridge.connected) {
this.homebridge.send({ id: type, data: data });
} else {
throw new ServiceUnavailableException('The Homebridge Service Is Unavailable');
}
}

/**
* Send a data request to homebridge and wait for the reply
*/
private async requestResponse(requestEvent: string, responseEvent: string) {
return new Promise((resolve, reject) => {
const actionTimeout = setTimeout(() => {
this.removeListener(responseEvent, listener);
reject('The Homebridge service did not respond');
}, 3000);

const listener = (data) => {
clearTimeout(actionTimeout);
resolve(data);
};

this.once(responseEvent, listener);
this.homebridge.send({ id: requestEvent });
});
}

/**
* Restarts the main bridge process and any child bridges
*/
public restartHomebridge(): void {
if (this.homebridge) {
this.logger.log('Sending SIGTERM to Homebridge');

// send SIGTERM command
this.homebridge.kill('SIGTERM');

// prepare a timeout to send SIGKILL after 7 seconds if not shutdown before then
const shutdownTimeout = setTimeout(() => {
try {
this.logger.warn('Sending SIGKILL to Homebridge');
this.homebridge.kill('SIGKILL');
} catch (e) { }
}, 7000);

// if homebridge ends before the timeout, clear the timeout
this.homebridge.once('close', () => {
clearTimeout(shutdownTimeout);
});
}
}

/**
* Restarts and resolves once homebridge is stopped.
*/
public async restartAndWaitForClose(): Promise<boolean> {
if (!this.homebridge || !this.homebridge.connected) {
return true;
} else {
return new Promise((resolve) => {
this.homebridge.once('close', () => {
resolve(true);
});
this.restartHomebridge();
});
}
}

/**
* Send a SIGKILL to the homebridge process
*/
public async killHomebridge() {
if (this.homebridge) {
this.logger.log('Sending SIGKILL to Homebridge');
this.homebridge.kill('SIGKILL');
}
}

/**
* Restart a Homebridge child bridge
*/
public async restartChildBridge(username: string) {
await this.sendMessage('restartChildBridge', username);
}

/**
* Request a list of child bridges from the Homebridge process
*/
public async getChildBridgeMetadata() {
return await this.requestResponse('childBridgeMetadataRequest', 'childBridgeMetadataResponse');
}

}
7 changes: 5 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { getStartupConfig } from './core/config/config.startup';

process.env.UIX_BASE_PATH = path.resolve(__dirname, '../');

async function bootstrap() {
async function bootstrap(): Promise<NestFastifyApplication> {
const startupConfig = await getStartupConfig();

const server = fastify({
Expand Down Expand Up @@ -123,5 +123,8 @@ async function bootstrap() {

logger.warn(`Homebridge Config UI X v${configService.package.version} is listening on ${startupConfig.host} port ${configService.ui.port}`);
await app.listen(configService.ui.port, startupConfig.host);

return app;
}
bootstrap();

export const app = bootstrap();
2 changes: 2 additions & 0 deletions src/modules/backup/backup.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PassportModule } from '@nestjs/passport';
import { ConfigModule } from '../../core/config/config.module';
import { LoggerModule } from '../../core/logger/logger.module';
import { SchedulerModule } from '../../core/scheduler/scheduler.module';
import { HomebridgeIpcModule } from '../../core/homebridge-ipc/homebridge-ipc.module';
import { BackupService } from './backup.service';
import { BackupGateway } from './backup.gateway';
import { BackupController } from './backup.controller';
Expand All @@ -16,6 +17,7 @@ import { PluginsModule } from '../plugins/plugins.module';
PluginsModule,
SchedulerModule,
LoggerModule,
HomebridgeIpcModule,
],
providers: [
BackupService,
Expand Down
12 changes: 11 additions & 1 deletion src/modules/backup/backup.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { FastifyReply } from 'fastify';
import { PluginsService } from '../plugins/plugins.service';
import { SchedulerService } from '../../core/scheduler/scheduler.service';
import { ConfigService, HomebridgeConfig } from '../../core/config/config.service';
import { HomebridgeIpcService } from '../..//core/homebridge-ipc/homebridge-ipc.service';
import { Logger } from '../../core/logger/logger.service';
import { HomebridgePlugin } from '../plugins/types';

Expand All @@ -24,6 +25,7 @@ export class BackupService {
private readonly configService: ConfigService,
private readonly pluginsService: PluginsService,
private readonly schedulerService: SchedulerService,
private readonly homebridgeIpcService: HomebridgeIpcService,
private readonly logger: Logger,
) {
this.scheduleInstanceBackups();
Expand Down Expand Up @@ -581,7 +583,15 @@ export class BackupService {
setTimeout(() => {
// if running in service mode
if (this.configService.serviceMode) {
return process.emit('message', 'postBackupRestoreRestart', undefined);
// kill homebridge
this.homebridgeIpcService.killHomebridge();

// kill self
setTimeout(() => {
process.kill(process.pid, 'SIGKILL');
}, 500);

return;
}

// if running in docker
Expand Down
Loading

0 comments on commit 290ae5a

Please sign in to comment.