pandora
Version:
534 lines (466 loc) • 15.8 kB
text/typescript
'use strict';
import {makeRequire, resolveSymlink} from 'pandora-dollar';
import {GlobalConfigProcessor} from '../universal/GlobalConfigProcessor';
import {
Entry, EntryClass, AppletRepresentation, ApplicationRepresentation, ApplicationStructureRepresentation
, ProcessRepresentation, ServiceRepresentation, ComplexApplicationStructureRepresentation, MountRepresentation,
CategoryReg
} from '../domain';
import assert = require('assert');
import {join, dirname, basename, extname} from 'path';
import {existsSync} from 'fs';
import {PROCFILE_NAMES} from '../const';
import {consoleLogger} from '../universal/LoggerBroker';
import {ProcfileReconcilerAccessor} from './ProcfileReconcilerAccessor';
import {exec} from 'child_process';
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 defaultAppletCategory: CategoryReg;
protected defaultServiceCategory: CategoryReg;
protected configuratorClass: EntryClass = null;
protected environmentClass: EntryClass = null;
protected processes: Array<ProcessRepresentation> = [];
protected services: Array<ServiceRepresentation> = [];
protected applets: Array<AppletRepresentation> = [];
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 uniqApplets(): Array<AppletRepresentation> {
const nameMap: Map<string, boolean> = new Map;
const ret = [];
for (const applet of this.applets.reverse()) {
if (!nameMap.has(applet.appletName)) {
nameMap.set(applet.appletName, true);
ret.push(applet);
}
}
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) {
try {
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));
} catch (err) {
consoleLogger.error('Fail to load procfile from path ' + target);
throw err;
}
}
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 applet class, service class and configurator 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;
}
/**
* Convert class name to instance name
* @param {string} name
* @return {string}
*/
normalizeName(name: string): string {
return name;
}
/**
* setDefaultAppletCategory
* @param {CategoryReg} name
*/
setDefaultAppletCategory (name: CategoryReg) {
this.defaultAppletCategory = name;
}
/**
* getDefaultAppletCategory
* @return {CategoryReg}
*/
getDefaultAppletCategory () {
if(!this.defaultAppletCategory) {
throw new Error('Should ProcfileReconciler.setDefaultAppletCategory() before ProcfileReconciler.getDefaultAppletCategory().');
}
return this.defaultAppletCategory;
}
/**
* 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 = {
...this.appRepresentation,
...processRepresentation,
entryFileBaseDir: this.procfileBasePath
};
this.processes.push(processRepresentation);
return processRepresentation;
}
/**
* Get a process representation by name
* @param processName
* @return {ProcessRepresentation}
*/
getProcessByName(processName): ProcessRepresentation {
for(const process of this.processes) {
if(process.processName === processName) {
return process;
}
}
return null;
}
/**
* Inject configurator class
* @param {Entry} entry
*/
injectConfigurator(entry: Entry) {
this.configuratorClass = this.normalizeEntry(entry);
}
/**
* Get configurator class
* @return {EntryClass}
*/
getConfigurator(): EntryClass {
if(!this.configuratorClass) {
throw new Error('Should ProcfileReconciler.injectConfigurator() before ProcfileReconciler.getConfigurator().');
}
return this.configuratorClass;
}
/**
* 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 = {
...serviceRepresentation,
serviceName: this.normalizeName(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 entry string or class
* @param lookingFor
* @return {ServiceRepresentation}
*/
getServiceByEntry (lookingFor): ServiceRepresentation {
for(const service of this.services) {
if(matchEntry(lookingFor, service.serviceEntry)) {
return service;
}
}
return null;
}
/**
* Get services by category
* @param {string} category
* @return {ServiceRepresentation[]}
*/
getServicesByCategory(category: string): 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') {
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;
}
/**
* Inject applet class
* @param {AppletRepresentation} appletRepresentation
* @return {{appletName: string; category: (CategoryReg | any); appletEntry: EntryClass}}
*/
injectApplet(appletRepresentation) {
const appletEntry = this.normalizeEntry(appletRepresentation.appletEntry);
const ret = {
...appletRepresentation,
appletName: this.normalizeName(appletRepresentation.appletName || (<any> appletEntry).lazyName
|| (<any> appletEntry).appletName || (<any> appletEntry).name),
category: appletRepresentation.category || this.getDefaultAppletCategory(),
appletEntry
};
this.applets.push(ret);
return ret;
}
/**
* Get a applet representation by entry string or class
* @param lookingFor
* @return {ApplicationRepresentation}
*/
getAppletByEntry (lookingFor): AppletRepresentation {
for(const applet of this.applets) {
if(matchEntry(lookingFor, applet.appletEntry)) {
return applet;
}
}
return null;
}
/**
* Get applets by category
* @param {string} category
* @return {AppletRepresentation[]}
*/
getAppletsByCategory(category: string): AppletRepresentation[] {
const appletFullSet = this.uniqApplets;
const retSet = [];
for (const applet of appletFullSet) {
if (applet.category === category || category === 'all'
|| applet.category === 'all' || applet.category === 'weak-all') {
retSet.push(applet);
}
}
return retSet;
}
protected getAvailableProcessMap () {
const availableProcessMap = {};
/**
* Allocate applets
*/
for (const applet of this.getAppletsByCategory('all')) {
if(applet.category === 'all') {
return foundAll;
}
if (applet.category === 'weak-all') {
continue;
}
const process = this.getProcessByName(applet.category);
if (!process) {
throw new Error(`Can't allocate applet ${applet.appletName} at category ${applet.category} to any process.`);
}
availableProcessMap[applet.category] = true;
}
/**
* Allocate services
*/
for (const service of this.getServicesByCategory('all')) {
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(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(process);
}
}
return { mount };
}
public static echoComplex(appRepresentation: ApplicationRepresentation) {
const procfileReconciler = new ProcfileReconciler(appRepresentation);
procfileReconciler.discover();
const complex = procfileReconciler.getComplexApplicationStructureRepresentation();
// PLS Keep console.log below, it is useful
console.log(JSON.stringify(complex, null, 2));
}
public static async getComplexViaNewProcess(appRepresentation: ApplicationRepresentation): Promise<ComplexApplicationStructureRepresentation> {
return <Promise<ComplexApplicationStructureRepresentation>> new Promise((resolve, reject) => {
exec(`${process.execPath} ${/\.ts$/.test(__filename) ? '-r ts-node/register' : ''} -e 'require("${__filename}").ProcfileReconciler.echoComplex(${JSON.stringify(appRepresentation)})'`,
(error, stdout) => {
if(error) {
reject(error);
return;
}
try {
const complex: ComplexApplicationStructureRepresentation = JSON.parse(stdout.toString());
resolve(complex);
} catch (err) {
reject(err);
}
});
});
}
}
/**
* Get procfile's dirname through resolved symlink
* @param tagetPath
* @return {any}
*/
function getProcfileBasePath(tagetPath) {
const resolvedTarget = resolveSymlink(tagetPath);
return dirname(resolvedTarget);
}
function matchEntry(userLooking: any, entry: any) {
return !!(userLooking === entry || userLooking === entry.lazyEntryMadeBy);
}