UNPKG

pandora

Version:

A powerful and lightweight application manager for Node.js applications powered by TypeScript.

208 lines (175 loc) 6.37 kB
'use strict'; import {fork} from 'child_process'; import Base = require('sdk-base'); import { MASTER, APP_START_SUCCESS, APP_START_ERROR, RELOAD, RELOAD_SUCCESS, RELOAD_ERROR, PANDORA_CWD, State } from '../const'; import {existsSync} from 'fs'; import assert = require('assert'); import {getDaemonLogger, createAppLogger, getAppLogPath, removeEOL} from '../universal/LoggerBroker'; import {ApplicationRepresentation, ProcessRepresentation} from '../domain'; const pathProcessMaster = require.resolve('./ProcessMaster'); const pathProcessBootstrap = require.resolve('./ProcessBootstrap'); const daemonLogger = getDaemonLogger(); /** * Class ApplicationHandler */ export class ApplicationHandler extends Base { public state: State; public appRepresentation: ApplicationRepresentation & ProcessRepresentation; private nodejsStdout: any; private proc: any; public get name() { return this.appRepresentation.appName; } public get appDir() { return this.appRepresentation.appDir; } public get mode() { return this.appRepresentation.mode; } public get pid() { return this.proc && this.proc.pid; } public startCount: number = 0; constructor(applicationRepresentation: ApplicationRepresentation) constructor(applicationRepresentation: ProcessRepresentation) { super(); this.state = State.pending; this.appRepresentation = applicationRepresentation; assert(existsSync(this.appDir), `AppDir ${this.appDir} does not exist`); this.nodejsStdout = createAppLogger(applicationRepresentation.appName, 'nodejs_stdout'); } /** * Start application through fork * @return {Promise<void>} */ async start(): Promise<void> { const {mode, entryFile} = this.appRepresentation; const args = []; if ('procfile.js' === mode || 'cluster' === mode) { args.push('--entry', pathProcessMaster); args.push('--params', JSON.stringify(Object.assign({name: MASTER}, this.appRepresentation))); } else if ('fork' === mode) { args.push('--entry', entryFile); args.push('--params', JSON.stringify(Object.assign({name: MASTER}, this.appRepresentation))); } else { throw new Error(`Unknown start mode ${mode} when start an application`); } this.startCount++; await this.doFork(args); } protected doFork(args): Promise<void> { const nodejsStdout = this.nodejsStdout; const execArgv: any = process.execArgv.slice(0); // Handing typeScript file,just for testing if (/\.ts$/.test(module.filename) && execArgv.indexOf('ts-node/register') === -1) { execArgv.push('-r', 'ts-node/register', '-r', 'nyc-ts-patch'); } const userArgv = (<ProcessRepresentation> this.appRepresentation).argv; if(userArgv && userArgv.length) { execArgv.push.apply(execArgv, userArgv); } const env = { ...process.env, ...this.appRepresentation.env, [PANDORA_CWD]: process.cwd(), // require.main === module maybe be 'false' after patched spawn wrap RUN_PROCESS_BOOTSTRAP_BY_FORCE: true }; return new Promise((resolve, reject) => { // Fork it const proc = fork(pathProcessBootstrap, args, <any> { cwd: this.appRepresentation.appDir, execArgv, stdio: ['ipc', 'pipe', 'pipe'], env }); proc.once('message', (message) => { if (message.action === APP_START_SUCCESS) { const msg = `Application [appName = ${this.appRepresentation.appName}, processName = ${(<ProcessRepresentation> this.appRepresentation).processName || 'null'}, dir = ${this.appDir}, pid = ${proc.pid}] started successfully!`; daemonLogger.info(msg); nodejsStdout.info(msg); this.state = State.complete; resolve(); } else if (message.action === APP_START_ERROR) { this.stop().catch((err) => { daemonLogger.error(err); nodejsStdout.error(err); }).then(() => { reject(new Error(`Application [name = ${this.appRepresentation.appName}, dir = ${this.appDir}, pid = ${proc.pid}] start error!`)); }); } }); // TODO: enhance performance proc.stdout.on('data', (data) => { nodejsStdout.write(removeEOL(data.toString())); }); proc.stderr.on('data', (err) => { nodejsStdout.write(removeEOL(err.toString())); }); // Here just to distinguish normal exits and exceptional exits, exceptional exits needs to restart proc.once('exit', (code, signal) => { const msg = `Application [name = ${this.appRepresentation.appName}, dir = ${this.appDir}, pid = ${proc.pid}] exit with code ${code} and signal ${signal}`; daemonLogger.info(msg); nodejsStdout.info(msg); switch (this.state) { case State.complete: // Restart it automatically when it exceptional exits after it start successful this.start().catch(err => { daemonLogger.error('Restart application error'); nodejsStdout.error('Restart application error'); daemonLogger.error(err); nodejsStdout.error(err); }); break; case State.pending: default: const err = new Error('Start failed, log file: ' + getAppLogPath(this.name, 'nodejs_stdout')); reject(err); break; } }); this.proc = proc; }); } /** * Stop application through kill * @return {Promise<void>} */ stop(): Promise<void> { if (this.state === State.stopped) { return Promise.resolve(); } this.state = State.stopped; return new Promise((resolve) => { this.proc.once('exit', () => { this.proc = null; resolve(); }); this.proc.kill('SIGTERM'); }); } /** * Reload application through process message * @param processName * @return {Promise<void>} */ reload(processName?): Promise<void> { return new Promise((resolve, reject) => { this.proc.once('message', (message) => { if (message.action === RELOAD_SUCCESS) { resolve(); } if (message.action === RELOAD_ERROR) { reject(); } }); this.proc.send({ action: RELOAD, name: processName, }); }); } }