pandora
Version:
A powerful and lightweight application manager for Node.js applications powered by TypeScript.
450 lines (383 loc) • 13.2 kB
text/typescript
'use strict';
import {makeRequire, resolveSymlink} from 'pandora-dollar';
import {GlobalConfigProcessor} from '../universal/GlobalConfigProcessor';
import {
Entry, EntryClass, ApplicationRepresentation, ApplicationStructureRepresentation
, ProcessRepresentation, ServiceRepresentation, ComplexApplicationStructureRepresentation, MountRepresentation,
CategoryReg
} from '../domain';
import assert = require('assert');
import {join, dirname, basename, extname} from 'path';
import {existsSync, writeFileSync} from 'fs';
import {PROCFILE_NAMES} from '../const';
import {ProcfileReconcilerAccessor} from './ProcfileReconcilerAccessor';
import {exec} from 'child_process';
import {tmpdir} from 'os';
import uuid = require('uuid');
import mzFs = require('mz/fs');
const foundAll = Symbol();
/**
* Class ProcfileReconciler
* TODO: Add more description
*/
export class ProcfileReconciler {
public appRepresentation: ApplicationRepresentation = null;
public procfileBasePath: string = null;
protected discovered = false;
protected procfileReconcilerAccessor: ProcfileReconcilerAccessor = null;
protected defaultServiceCategory: CategoryReg;
protected environmentClass: EntryClass = null;
protected processes: Array<ProcessRepresentation> = [];
protected services: Array<ServiceRepresentation> = [];
protected get uniqServices(): Array<ServiceRepresentation> {
const nameMap: Map<string, boolean> = new Map;
const ret = [];
for (const service of this.services.reverse()) {
if (!nameMap.has(service.serviceName)) {
nameMap.set(service.serviceName, true);
ret.push(service);
}
}
return ret.reverse();
}
protected get appDir() {
assert(
this.appRepresentation && this.appRepresentation.appDir,
'Can not get appDir from ProcfileReconciler.appRepresentation, it should passed from time of constructing ProcfileReconciler'
);
return this.appRepresentation.appDir;
}
constructor(appRepresentation: ApplicationRepresentation) {
this.appRepresentation = appRepresentation;
this.procfileReconcilerAccessor = new ProcfileReconcilerAccessor(this);
// Attach default procfile
const {procfile: defaultProcfile} = GlobalConfigProcessor.getInstance().getAllProperties();
this.callProcfile(defaultProcfile);
}
/**
* Find out all possibly profile.js paths
* @return {Array}
*/
resovle() {
const retSet = [];
const appDir = this.appDir;
const findBasePath = [
join(appDir, 'node_modules/.bin'),
appDir
];
for (const basePath of findBasePath) {
for (const alias of PROCFILE_NAMES) {
const targetPath = join(basePath, alias);
if (existsSync(targetPath)) {
retSet.push(targetPath);
}
}
}
return retSet;
}
/**
* Discover procfile.js in appDir, and apply them.
*/
discover() {
if (this.discovered) {
return;
}
const procfileTargets = this.resovle();
for (const target of procfileTargets) {
const targetMod = require(target);
const entryFn = 'function' === typeof targetMod ? targetMod : targetMod.default;
assert('function' === typeof entryFn, 'The procfile should export a function, during loading ' + target);
this.callProcfile(entryFn, getProcfileBasePath(target));
}
this.discovered = true;
}
/**
* callProcfile required a argument as typed function, then call that function, pass ProcfileReconcilerAccessor as the first argument of that function.
* @param entryFn
* @param path
*/
callProcfile(entryFn, path?) {
try {
this.procfileBasePath = path || null;
/**
* inject a pandora object
* example: exports = (pandora) => {}
*/
entryFn(this.procfileReconcilerAccessor);
} finally {
this.procfileBasePath = null;
}
}
/**
* Normalize entry class, entry class such as service class
* Those classes have a lazy way to represent, it can get a relative path
* this method will wrap that relative path to a real class
* @param entry
* @return {EntryClass}
*/
normalizeEntry(entry): EntryClass {
if ('string' === typeof entry && this.procfileBasePath) {
const procfileBasePath = this.procfileBasePath;
function getLazyClass() {
const targetMod = makeRequire(procfileBasePath)(entry);
const TargetClass = 'function' === typeof targetMod ? targetMod : targetMod.default;
return TargetClass;
}
function LazyEntry(option: any) {
const LazyClass = getLazyClass();
return new LazyClass(option);
}
(<any> LazyEntry).lazyEntryMadeBy = entry;
(<any> LazyEntry).lazyName = basename(entry, extname(entry));
(<any> LazyEntry).getLazyClass = getLazyClass;
return <EntryClass> <any> LazyEntry;
}
return entry;
}
/**
* setDefaultServiceCategory
* @param {CategoryReg} name
*/
setDefaultServiceCategory (name: CategoryReg) {
this.defaultServiceCategory = name;
}
/**
* getDefaultServiceCategory
* @return {CategoryReg}
*/
getDefaultServiceCategory () {
if(!this.defaultServiceCategory) {
throw new Error('Should ProcfileReconciler.setDefaultServiceCategory() before ProcfileReconciler.getDefaultServiceCategory().');
}
return this.defaultServiceCategory;
}
/**
* Define process representation
* @param processRepresentation
* @return {ProcessRepresentation}
*/
defineProcess(processRepresentation): ProcessRepresentation {
processRepresentation = {
env: {}, argv: [],
...this.appRepresentation,
...processRepresentation,
entryFileBaseDir: this.procfileBasePath
};
this.processes.push(processRepresentation);
return processRepresentation;
}
/**
* Get a process representation by name
* @param lookingFor
* @return {ProcessRepresentation}
*/
getProcessByName(lookingFor): ProcessRepresentation {
for(const process of this.processes) {
if(process.processName === lookingFor) {
return process;
}
}
return null;
}
/**
* Drop a process representation by name
*/
dropProcessByName (lookingFor) {
for(let idx = 0, len = this.processes.length; idx < len; idx++) {
const process = this.processes[idx];
if(lookingFor === process.processName) {
this.processes.splice(idx, 1);
return;
}
}
throw new Error(`Can\'t drop a process named ${lookingFor} it not exist`);
}
/**
* Inject environment class
* @param {Entry} entry
*/
injectEnvironment(entry: Entry) {
this.environmentClass = this.normalizeEntry(entry);
}
/**
* Get environment class
* @return {EntryClass}
*/
getEnvironment(): EntryClass {
if (!this.environmentClass) {
throw new Error('Should ProcfileReconciler.injectEnvironment() before ProcfileReconciler.getEnvironment().');
}
return this.environmentClass;
}
/**
* Inject service class
* @param serviceRepresentation
* @return {ServiceRepresentation}
*/
injectService(serviceRepresentation): ServiceRepresentation {
const serviceEntry = this.normalizeEntry(serviceRepresentation.serviceEntry);
const ret = {
config: {},
...serviceRepresentation,
serviceName: serviceRepresentation.serviceName || (<any> serviceEntry).lazyName || (<any> serviceEntry).serviceName || (<any> serviceEntry).name,
category: serviceRepresentation.category || this.getDefaultServiceCategory(),
serviceEntry: serviceEntry
};
this.services.push(ret);
return ret;
}
/**
* Get a service representation by name
* @param lookingFor
* @return {ServiceRepresentation}
*/
getServiceByName (lookingFor): ServiceRepresentation {
for(const service of this.services) {
if(lookingFor === service.serviceName) {
return service;
}
}
return null;
}
/**
* Drop a service representation by name
*/
dropServiceByName (lookingFor) {
for(let idx = 0, len = this.services.length; idx < len; idx++) {
const service = this.services[idx];
if(lookingFor === service.serviceName) {
this.services.splice(idx, 1);
return;
}
}
throw new Error(`Can\'t drop a service named ${lookingFor} it not exist`);
}
/**
* Get services by category
* @param {string} category
* @return {ServiceRepresentation[]}
*/
getServicesByCategory(category: string, simple?): ServiceRepresentation[] {
const serviceFullSet = this.uniqServices;
const retSet = [];
for (const service of serviceFullSet) {
if (service.category === category || category === 'all'
|| service.category === 'all' || service.category === 'weak-all') {
if(simple) {
retSet.push(service);
continue;
}
const serviceEntry = (<any> service.serviceEntry).getLazyClass
? (<any> service.serviceEntry).getLazyClass() : service.serviceEntry;
// Sucks code below, just unique the dependencies array...
service.dependencies = <string[]> [...new Set((serviceEntry.dependencies || []).concat(service.dependencies || []))];
retSet.push(service);
}
}
return retSet;
}
protected getAvailableProcessMap () {
const availableProcessMap = {};
/**
* Allocate services
*/
for (const service of this.getServicesByCategory('all', true)) {
if(service.category === 'all') {
return foundAll;
}
if (service.category === 'weak-all') {
continue;
}
const process = this.getProcessByName(service.category);
if (!process) {
throw new Error(`Can't allocate service ${service.serviceName} at category ${service.category} to any process.`);
}
availableProcessMap[service.category] = true;
}
return availableProcessMap;
}
/**
* Get the application's structure
* @returns {ApplicationStructureRepresentation}
*/
getApplicationStructure(): ApplicationStructureRepresentation {
const availableProcessMap = this.getAvailableProcessMap();
const processRepresentations: ProcessRepresentation[] = [];
for(const process of this.processes) {
if(
process.mode === 'profile.js' &&
foundAll === availableProcessMap || availableProcessMap.hasOwnProperty(process.processName)
) {
processRepresentations.push(this.processGlobalForProcess(process));
}
}
const processRepresentationSet2nd = processRepresentations.sort((a, b) => {
return a.order - b.order;
});
return {
...this.appRepresentation,
mode: 'procfile.js',
process: processRepresentationSet2nd
};
}
/**
* Get the complex application's structure
* @returns {ApplicationStructureRepresentation}
*/
getComplexApplicationStructureRepresentation(): ComplexApplicationStructureRepresentation {
const processes: ProcessRepresentation[] = this.processes;
const mount: MountRepresentation[] = [];
const applicationStructure = this.getApplicationStructure();
if(applicationStructure.process.length) {
mount.push(applicationStructure);
}
for(const process of processes) {
if( process.mode === 'fork' ) {
mount.push(this.processGlobalForProcess(process));
}
}
return { mount };
}
public static echoComplex(appRepresentation: ApplicationRepresentation, writeTo: string) {
const procfileReconciler = new ProcfileReconciler(appRepresentation);
procfileReconciler.discover();
const complex = procfileReconciler.getComplexApplicationStructureRepresentation();
writeFileSync(writeTo, JSON.stringify(complex));
}
public static async getComplexViaNewProcess(appRepresentation: ApplicationRepresentation): Promise<ComplexApplicationStructureRepresentation> {
const tmpFile = join(tmpdir(), uuid.v4());
const isTs = /\.ts$/.test(__filename);
await new Promise((resolve, reject) => {
exec(`${process.execPath} ${ isTs ? '-r ts-node/register' : ''} -e 'require("${__filename}").ProcfileReconciler.echoComplex(${JSON.stringify(appRepresentation)}, ${JSON.stringify(tmpFile)})'`,
(error) => {
if(error) {
reject(error);
return;
}
resolve();
});
});
const fileBuffer = await mzFs.readFile(tmpFile);
await mzFs.unlink(tmpFile);
const fileContent = fileBuffer.toString();
const complex: ComplexApplicationStructureRepresentation = JSON.parse(fileContent);
return complex;
}
private processGlobalForProcess (process): ProcessRepresentation {
const argv = process.globalArgv ? (process.argv || []).concat(process.globalArgv) : process.argv;
const env = process.globalEnv ? {...process.env, ...process.globalEnv} : process.env;
return {
...process,
argv, env
};
}
}
/**
* Get procfile's dirname through resolved symlink
* @param tagetPath
* @return {any}
*/
function getProcfileBasePath(tagetPath) {
const resolvedTarget = resolveSymlink(tagetPath);
return dirname(resolvedTarget);
}