import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; import { Injectable } from '@nestjs/common'; import { promisify } from 'util'; import * as pm2 from 'pm2'; import { Proc, ProcessDescription, StartOptions } from 'pm2'; import { clone, last } from 'ramda'; @Injectable() export class DeployByPm2Service { constructor( @InjectPinoLogger(DeployByPm2Service.name) private readonly logger: PinoLogger, ) {} async deploy(filePath: string, workspace: string) { const baseConfig: { apps: StartOptions[] } = await import(filePath); const appOptionsList: StartOptions[] = clone(baseConfig.apps); await promisify(pm2.connect.bind(pm2))(); const allApps = await promisify(pm2.list.bind(pm2))(); try { if (!Array.isArray(baseConfig.apps)) { this.logger.error( 'the "apps" in the PM2 ecosystem configuration is not array', ); throw new Error('apps is not array'); } const oldApps = this.filterOldApps(appOptionsList, allApps); this.replaceAppName(appOptionsList, oldApps); for (const appOptions of appOptionsList) { const proc = await promisify(pm2.start.bind(pm2))({ ...appOptions, cwd: workspace, }); this.logger.info({ proc }, `start ${appOptions.name}`); } await this.stopApps(oldApps); } catch (err) { await this.stopApps(appOptionsList); throw err; } finally { pm2.disconnect(); } } private async stopApps(apps: ProcessDescription[] | StartOptions[]) { await Promise.all( apps.map(async (app: ProcessDescription | StartOptions) => { let procAtStop: ProcessDescription; let procAtDelete: ProcessDescription; try { const idOrName = 'pm_id' in app ? app.pm_id : app.name; procAtStop = await promisify(pm2.stop.bind(pm2))(idOrName); procAtDelete = await promisify(pm2.delete.bind(pm2))(idOrName); this.logger.info('stop & delete %s success', app.name); } catch (error) { this.logger.error( { error, procAtStop, procAtDelete }, 'stop & delete %s error', app.name, ); } }), ); } private replaceAppName( optionsList: StartOptions[], oldApps: ProcessDescription[], ) { const appSn = this.getAppsSn(oldApps); optionsList.forEach((options) => { if (!options.name) { this.logger.error('please give a name for application'); throw new Error('app name is not given'); } options.name = `${options.name}#${appSn}`; }); } private filterOldApps( optionsList: StartOptions[], apps: ProcessDescription[], ) { return apps.filter((app) => optionsList.some((options) => app.name.split('#')[0] === options.name), ); } private getAppsSn(oldApps: ProcessDescription[]) { const appsSn: number[] = oldApps.map( (app) => +(app.name.split('#')?.[1] ?? 0), ); return (last(appsSn.sort()) ?? 0) + 1; } }