UNPKG

@swizzyweb/swerve-manager

Version:

swizzy-swerve is a bootstrapper for swizzy web services. This package will bootstrap and run independent swizzy web services.

462 lines (419 loc) 13.3 kB
// @ts-ignore import express, { Application } from "@swizzyweb/express"; import { getLoggerForService, installWebService, SwerveArgs, } from "./utils/index.js"; import { AnyServer, IWebService, SwizzyWinstonLogger, WebService, } from "@swizzyweb/swizzy-web-service"; import os from "node:os"; import process from "node:process"; import { ILogger } from "@swizzyweb/swizzy-common"; import path from "node:path"; import { mkdirSync } from "node:fs"; import { getFullImportPath, resolvePackageEntry, } from "./utils/getFullImportPath.js"; export interface ISwerveManager { run(request: RunRequest): Promise<RunResponse>; stop(request: StopRequest): Promise<void>; getRunningWebServices( props: GetRunningWebServiceRequest, ): Promise<GetRunningWebServiceResponse>; } export interface GetRunningWebServiceRequest {} export interface GetRunningWebServiceResponse { webServices: { [instanceId: string]: { webService: any; serviceConfig: any; }; }; } export enum InstanceType { webservice = "webservice", stack = "stack", } export interface InstanceDetails { instanceType: InstanceType; instanceId: string; } export interface StopRequest { instanceDetails: InstanceDetails; } export interface RunRequest { args?: SwerveArgs; } export interface RunResponse { webServices: WebService<any>[]; } export type Apps = { [key: number]: { app: Application; server?: AnyServer; services: { [instanceId: string]: { webService: IWebService; serviceArgs: SwerveArgs; runRequest: RunRequest; }; }; }; }; export interface SwerveManagerProps { apps?: Apps; webServices?: WebService<any>[]; nodeModulesPath?: string; logger?: ILogger<any> | undefined; } export interface WebServiceConfiguration {} type WebServiceConfigurations = { [instanceId: string]: WebServiceConfiguration; }; export class SwerveManager implements ISwerveManager { apps: Apps; webServices: WebService<any>[]; configurations: WebServiceConfigurations; nodeModulesPath: string; logger: ILogger<any>; constructor(props: SwerveManagerProps) { this.apps = props.apps ?? {}; this.webServices = props.webServices ?? []; this.nodeModulesPath = props.nodeModulesPath; this.logger = props.logger ?? new SwizzyWinstonLogger({ appName: "swerve-manager", port: -1, hostName: os.hostname(), }); } async run(request: RunRequest): Promise<RunResponse> { const { args } = request; const newWebServices = await this.runWithArgs({ args, }); this.webServices.push(...newWebServices); return { webServices: newWebServices }; } async runWithArgs(request: RunRequest) { const { args } = request; const logLevel: string = process.env.LOG_LEVEL ?? args.logLevel ?? "info"; let gLogger = new SwizzyWinstonLogger({ port: 0, logLevel, appDataRoot: args.appDataRoot ?? ".", appName: `swerve`, hostName: os.hostname(), ownerName: "swerve", pid: process.pid, }); try { const webServices: WebService<any>[] = []; const newApps: { [port: number]: Application } = {}; for (const serviceEntry of Object.entries(args.services)) { const port = serviceEntry[1].port ?? args.port; if (!this.apps[`${port}`]) { this.apps[`${port}`] = { app: await express(), services: {} }; newApps[`${port}`] = this.apps[`${port}`]; } const app = this.apps[`${port}`].app; const service = serviceEntry[1]; const serviceName = serviceEntry[0]; const packageName = service.packageName; gLogger.debug(`servicePath: ${service.servicePath}`); const serviceArgs: SwerveArgs = { ...service, ...service.serviceConfiguration, ...args.serviceArgs, }; const webservice = await this.installWebService({ serviceKey: serviceName, packageName, servicePath: service.servicePath, port, app, appDataRoot: args.appDataRoot, serviceArgs, gLogger, }); this.apps[`${port}`].services[webservice.instanceId] = { webService: webservice, serviceArgs, runRequest: request, }; webServices.push(webservice); } for (const newAppEntry of Object.entries(newApps)) { const [port, appRecord] = newAppEntry; const newApp = appRecord.app; const server = await newApp.listen(port, () => { // this.apps[port].services[newApp.instanceId] = {}; gLogger.debug(`New app listening on port ${port}`); }); this.apps[`${port}`].server = server; } for (const webService of webServices) { gLogger.info(`${webService.name} running on port ${webService.port}`); } return webServices; } catch (e) { gLogger.error( `Error occurred initializing service\n ${e.message}\n ${e.stack ?? {}}`, ); } } private async runWithApp(props: RunWithAppArgs) { const { app, args } = props; let gLogger = new SwizzyWinstonLogger({ port: 0, logLevel: process.env.LOG_LEVEL ?? "info", appDataRoot: args.appDataRoot, appName: `swerve`, hostName: os.hostname(), pid: process.pid, }); try { gLogger = new SwizzyWinstonLogger({ logLevel: args.serviceArgs.logLevel ?? process.env.LOG_LEVEL ?? "info", port: args.port, logDir: args.appDataRoot, appName: `swerve`, hostName: os.hostname(), pid: process.pid, }); gLogger.debug(`Swerve Args: ${JSON.stringify(args)}`); const PORT = args.port ?? 3005; const webServices = []; for (const serviceEntry of Object.entries(args.services)) { const service = serviceEntry[1]; const packageName = service.packageName; const webservice = await this.installWebService({ serviceKey: serviceEntry[0], packageName, servicePath: service.servicePath, port: PORT, app, appDataRoot: args.appDataRoot, serviceArgs: { ...service, ...service.serviceConfiguration, ...args.serviceArgs, }, gLogger, }); webServices.push(webservice); } return webServices; } catch (e) { gLogger.error( `Error occurred initializing service\n ${e.message}\n ${e.stack ?? {}}`, ); } } async installWebService(props: { // serviceKey: string; servicePath: string; app: Application; appDataRoot: string; packageName: string; port: number; gLogger: ILogger<any>; serviceArgs: { [key: string]: any }; }) { const { app, appDataRoot, packageName, port, gLogger, serviceArgs, servicePath, serviceKey, } = props; try { gLogger.info( `Getting webservice package ${packageName} and will run on port ${port}`, ); if (packageName) { gLogger.debug(`Getting web service with name ${packageName}`); } else { gLogger.debug(`Getting webservice with path: ${servicePath}`); } let firstTool; try { firstTool = await import(packageName ?? servicePath); } catch (e: any) { gLogger.warn( `Unable to import from raw tool path ${packageName ?? servicePath}, trying with import name. Err: ${e?.message}. Stack: ${e?.stack}`, ); } const fullPath = await this.getImportName(packageName, servicePath); const tool = firstTool ?? (await import(fullPath)); gLogger.debug(`Got service with require: ${JSON.stringify(tool)}`); gLogger.debug(`Getting web service from tool...`); const appDataPath = path.join(appDataRoot, "appdata", serviceKey); mkdirSync(appDataPath, { recursive: true }); const logger = getLoggerForService( serviceArgs, serviceKey, port, gLogger, ); gLogger.debug(`serviceArgs for ${packageName}: ${serviceArgs}`); const service = await tool.getWebservice({ appDataPath, ...serviceArgs, port, app, packageName, servicePath, serviceArgs: { ...serviceArgs }, logger, }); logger.debug(`Got web service`); gLogger.debug(`Installing web service...`); await service.install({}); gLogger.debug(`Installed web service ${packageName}`); return service; } catch (e) { const exceptionMessage = `exception: ${e} Failed to install web service, is it installed with NPM? Check package exists in node_modules To add, run: npm install ${packageName ?? "packageName"} args: packageName: ${packageName} port: ${port} `; // ${getHelpText}`; gLogger.error( `Failed to install web service, error: ${e?.message} stack: ${e?.stack}`, ); throw e; //new Error(exceptionMessage); } } async getImportName(packageName: string, servicePath: string) { const nodeModulesPath = this.nodeModulesPath; this.logger.debug(`getImportPathName: modulesPath: ${nodeModulesPath}`); if (servicePath) { return await getFullImportPath(servicePath); } else { if (nodeModulesPath) { this.logger.debug( `Getting path with nodeModulesPath and packageName ${nodeModulesPath} ${packageName}`, ); return await resolvePackageEntry( path.join(nodeModulesPath, packageName), ); } else { return packageName; } } } async stop(request: StopRequest) { const { instanceDetails } = request; const { instanceId, instanceType } = instanceDetails; const instanceIds = []; if (!instanceId) { throw new Error(`Instance id required to stop web service`); } if (instanceType === InstanceType.webservice) { // console.log(`Matched instance type`); instanceIds.push(`${instanceId}`); } else if (instanceType === InstanceType.stack) { // TODO: implement throw new Error(`Stack instance type is not yet supported`); } else { // this.logger.error(`Invalid instance details ${instanceDetails}`); throw new Error(`Invalid instance details provided`); } // console.log(`instanceIds: ${instanceIds}`); const webServices = this.webServices.filter((service) => { // console.log(`instanceId ${service.instanceId}`); return instanceIds.includes(`${service.instanceId}`); }); if (!webServices || webServices.length < 1) { //console.error(webServices); //this.webServices); throw new Error( `WebService with instanceId ${instanceId} not found while attempting to stop`, ); } const ports = []; for (const webService of webServices) { const port = webService.port; ports.push(port); //console.log(webService); await webService.uninstall({}); const indexes = this.webServices .map((val, index, array) => { //console.log( // `instanceInSwerve: ${val.instanceId} instanceInWebService ${webService.instanceId}`, // ); if (val.instanceId == webService.instanceId) { return index; } return undefined; }) .filter((val) => val != undefined); //.filter((val) => val); if (indexes.length > 1) { throw new Error( `Found multiple indexes for webservice instance ${webService.instanceId} ${indexes}`, ); } else if (indexes.length == 0) { throw new Error( `No indexes for webservice instance ${webService.instanceId}`, ); } const index = indexes[0]; this.webServices.splice(index, 1); if (!this.apps[`${port}`]?.services[webService.instanceId]) { //console.log(`Apps does not contain service`); // TODO: DO SOMETHING, log, throw maybe. } else { delete this.apps[`${port}`].services[webService.instanceId]; } } //cleanup apps if no more services for (const port of ports) { const { services, server, app } = this.apps[`${port}`]; if (!services || Object.keys(services).length == 0) { if (server) { server.close(); } else { continue; // TODO: do something, log throw etc } delete this.apps[`${port}`]; } } } async getRunningWebServices( props: GetRunningWebServiceRequest, ): Promise<GetRunningWebServiceResponse> { const webservices = {}; for (const webservice of this.webServices) { const instanceId = webservice.instanceId; const { runRequest, serviceArgs } = this.apps[webservice.port].services[instanceId]; webservices[instanceId] = { webService: webservice.toJson(), serviceConfig: runRequest?.args, }; } return { webServices: webservices, }; } } interface RunWithAppArgs { app: Application; args: SwerveArgs; }