From 4d1badb07028f840aea33e53e97425af3baba76d Mon Sep 17 00:00:00 2001 From: Cameron Aaron Date: Tue, 12 Nov 2024 17:39:33 -0800 Subject: [PATCH] Add ServiceManager to manage services with dependencies Fixes #468 --- packages/server/src/server/index.ts | 71 ++++++------------- .../server/src/server/lib/ServiceManager.ts | 58 +++++++++++++++ 2 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 packages/server/src/server/lib/ServiceManager.ts diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 2b1820d2..2aec84a1 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -29,7 +29,6 @@ import { UpdateService, CloudflareService, WebhookService, - ScheduledMessagesService, OauthService, ZrokService } from "@server/services"; @@ -71,6 +70,7 @@ import { MessagePoller } from "./databases/imessage/pollers/MessagePoller"; import { obfuscatedHandle } from "./utils/StringUtils"; import { AutoStartMethods } from "./databases/server/constants"; import { MacOsInterface } from "./api/interfaces/macosInterface"; +import { ServiceManager } from "./lib/ServiceManager"; const findProcess = require("find-process"); @@ -171,6 +171,8 @@ class BlueBubblesServer extends EventEmitter { typingCache: string[]; + serviceManager: ServiceManager; + get hasDiskAccess(): boolean { // As long as we've tried to initialize the DB, we know if we do/do not have access. const dbInit: boolean | null = this.iMessageRepo?.db?.isInitialized; @@ -245,6 +247,8 @@ class BlueBubblesServer extends EventEmitter { this.region = null; this.typingCache = []; this.findMyCache = null; + + this.serviceManager = new ServiceManager(); } emitToUI(event: string, data: any) { @@ -497,6 +501,18 @@ class BlueBubblesServer extends EventEmitter { } catch (ex: any) { this.logger.error(`Failed to start Scheduled Message service! ${ex?.message ?? String(ex)}}`); } + + this.serviceManager.registerService("HttpService", this.httpService); + this.serviceManager.registerService("PrivateApiService", this.privateApi); + this.serviceManager.registerService("FCMService", this.fcm); + this.serviceManager.registerService("NetworkService", this.networkChecker); + this.serviceManager.registerService("CaffeinateService", this.caffeinate); + this.serviceManager.registerService("QueueService", this.queue); + this.serviceManager.registerService("UpdateService", this.updater); + this.serviceManager.registerService("OutgoingMessageManager", this.messageManager); + this.serviceManager.registerService("WebhookService", this.webhookService); + this.serviceManager.registerService("ScheduledMessagesService", this.scheduledMessages); + this.serviceManager.registerService("OauthService", this.oauthService); } /** @@ -505,11 +521,10 @@ class BlueBubblesServer extends EventEmitter { */ async startServices(): Promise { try { - this.logger.info("Starting HTTP service..."); - this.httpService.initialize(); - this.httpService.start(); + this.logger.info("Starting services using ServiceManager..."); + await this.serviceManager.startServices(); } catch (ex: any) { - this.logger.error(`Failed to start HTTP service! ${ex?.message ?? String(ex)}}`); + this.logger.error(`Failed to start services using ServiceManager! ${ex?.message ?? String(ex)}}`); } // Only start the oauth service if the tutorial isn't done @@ -532,13 +547,6 @@ class BlueBubblesServer extends EventEmitter { this.logger.error(`Failed to connect to proxy service! ${ex?.message ?? String(ex)}`); } - try { - this.logger.info("Starting Scheduled Messages service..."); - await this.scheduledMessages.start(); - } catch (ex: any) { - this.logger.error(`Failed to start Scheduled Messages service! ${ex?.message ?? String(ex)}}`); - } - const privateApiEnabled = this.repo.getConfig("enable_private_api") as boolean; const ftPrivateApiEnabled = this.repo.getConfig("enable_ft_private_api") as boolean; if (privateApiEnabled || ftPrivateApiEnabled) { @@ -550,23 +558,16 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Starting iMessage Database listeners..."); await this.startChatListeners(); } - - try { - this.logger.info("Starting FCM service..."); - await this.fcm.start(); - } catch (ex: any) { - this.logger.error(`Failed to start FCM service! ${ex?.message ?? String(ex)}}`); - } } async stopServices(): Promise { this.isStopping = true; - this.logger.info("Stopping services..."); + this.logger.info("Stopping services using ServiceManager..."); try { - FCMService.stop(); + await this.serviceManager.stopServices(); } catch (ex: any) { - this.logger.info(`Failed to stop FCM service! ${ex?.message ?? ex}`); + this.logger.error(`Failed to stop services using ServiceManager! ${ex?.message ?? ex}`); } try { @@ -576,37 +577,11 @@ class BlueBubblesServer extends EventEmitter { this.logger.info(`Failed to stop iMessage database listeners! ${ex?.message ?? ex}`); } - try { - await this.privateApi?.stop(); - } catch (ex: any) { - this.logger.info(`Failed to stop Private API Helper service! ${ex?.message ?? ex}`); - } - try { await this.stopProxyServices(); } catch (ex: any) { this.logger.info(`Failed to stop Proxy services! ${ex?.message ?? ex}`); } - - try { - await this.httpService?.stop(); - } catch (ex: any) { - this.logger.error(`Failed to stop HTTP service! ${ex?.message ?? ex}`); - } - - try { - await this.oauthService?.stop(); - } catch (ex: any) { - this.logger.error(`Failed to stop OAuth service! ${ex?.message ?? ex}`); - } - - try { - this.scheduledMessages?.stop(); - } catch (ex: any) { - this.logger.error(`Failed to stop Scheduled Messages service! ${ex?.message ?? ex}`); - } - - this.logger.info("Finished stopping services..."); } async stopServerComponents() { diff --git a/packages/server/src/server/lib/ServiceManager.ts b/packages/server/src/server/lib/ServiceManager.ts new file mode 100644 index 00000000..2e50e026 --- /dev/null +++ b/packages/server/src/server/lib/ServiceManager.ts @@ -0,0 +1,58 @@ +import { Loggable } from "./logging/Loggable"; + +interface Service { + start(): Promise; + stop(): Promise; +} + +export class ServiceManager extends Loggable { + tag = "ServiceManager"; + + private services: Map = new Map(); + private dependencies: Map = new Map(); + + registerService(name: string, service: Service, dependencies: string[] = []) { + this.services.set(name, service); + this.dependencies.set(name, dependencies); + } + + async startServices() { + const startedServices = new Set(); + + for (const [name, service] of this.services) { + await this.startService(name, service, startedServices); + } + } + + private async startService(name: string, service: Service, startedServices: Set) { + if (startedServices.has(name)) return; + + const dependencies = this.dependencies.get(name) || []; + for (const dependency of dependencies) { + const dependencyService = this.services.get(dependency); + if (dependencyService) { + await this.startService(dependency, dependencyService, startedServices); + } + } + + this.log.info(`Starting service: ${name}`); + await service.start(); + startedServices.add(name); + } + + async stopServices() { + const stoppedServices = new Set(); + + for (const [name, service] of Array.from(this.services).reverse()) { + await this.stopService(name, service, stoppedServices); + } + } + + private async stopService(name: string, service: Service, stoppedServices: Set) { + if (stoppedServices.has(name)) return; + + this.log.info(`Stopping service: ${name}`); + await service.stop(); + stoppedServices.add(name); + } +}