'use strict' ; import { makeRequire , resolveSymlink } from 'pandora-dollar' ; import { GlobalConfigProcessor } from '../universal/GlobalConfigProcessor' ; import { Entry , EntryClass , ApplicationRepresentation , ApplicationStructureRepresentation , ProcessRepresentation , ServiceRepresentation , CategoryReg } from '../domain' ; import assert = require ( 'assert' ) ; import { join , dirname , basename , extname } from 'path' ; import { existsSync , writeFileSync } from 'fs' ; import { defaultWorkerCount , PROCFILE_NAMES } from '../const' ; import { ProcfileReconcilerAccessor } from './ProcfileReconcilerAccessor' ; import { exec } from 'child_process' ; import uuid = require ( 'uuid' ) ; import mzFs = require ( 'mz/fs' ) ; import * as os from 'os' ; const tmpdir = process . env . PANDORA_TMP_DIR || os . tmpdir ( ) ; 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 = 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 ) { this . appRepresentation = appRepresentation ; this . procfileReconcilerAccessor = new ProcfileReconcilerAccessor ( this ) ; const { procfile : defaultProcfile } = GlobalConfigProcessor . getInstance ( ) . getAllProperties ( ) ; this . callProcfile ( defaultProcfile ) ; } 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 ; } 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 ; } } param entry return { EntryClass } normalizeEntry ( entry ) { 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 ) { 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 ; } param { CategoryReg } name setDefaultServiceCategory ( name ) { this . defaultServiceCategory = name ; } return { CategoryReg } getDefaultServiceCategory ( ) { if ( ! this . defaultServiceCategory ) { throw new Error ( 'Should ProcfileReconciler.setDefaultServiceCategory() before ProcfileReconciler.getDefaultServiceCategory().' ) ; } return this . defaultServiceCategory ; } param processRepresentation return { ProcessRepresentation } defineProcess ( processRepresentation ) { const processRepresentation2nd = { env : { } , execArgv : [ ] , args : [ ] , scale : 1 , ... this . appRepresentation , ... processRepresentation , entryFileBaseDir : this . procfileBasePath } ; this . processes . push ( processRepresentation2nd ) ; return processRepresentation2nd ; } param lookingFor return { ProcessRepresentation } getProcessByName ( lookingFor ) { 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` ) ; } param { Entry } entry injectEnvironment ( entry ) { this . environmentClass = this . normalizeEntry ( entry ) ; } return { EntryClass } getEnvironment ( ) { if ( ! this . environmentClass ) { throw new Error ( 'Should ProcfileReconciler.injectEnvironment() before ProcfileReconciler.getEnvironment().' ) ; } return this . environmentClass ; } param serviceRepresentation return { ServiceRepresentation } injectService ( 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 ; } param lookingFor return { ServiceRepresentation } getServiceByName ( lookingFor ) { 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` ) ; } param { string } category return { ServiceRepresentation [ ] } getServicesByCategory ( category , simple ? ) { 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 ; service . dependencies = < string [ ] > [ ... new Set ( ( serviceEntry . dependencies || [ ] ) . concat ( service . dependencies || [ ] ) ) ] ; retSet . push ( service ) ; } } return retSet ; } return { any } 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 ; } for ( const process of this . processes ) { if ( process . entryFile && ! availableProcessMap . hasOwnProperty ( process . processName ) ) { availableProcessMap [ process . processName ] = true ; } } return availableProcessMap ; } return { ApplicationStructureRepresentation } getApplicationStructure ( ) { const availableProcessMap = this . getAvailableProcessMap ( ) ; const processRepresentations = [ ] ; let offset = 0 ; for ( const process of this . processes ) { if ( foundAll === availableProcessMap || availableProcessMap . hasOwnProperty ( process . processName ) ) { const newProcess = this . makeupProcess ( process ) ; newProcess . offset = offset ; offset += < number > newProcess . scale > 1 ? < number > newProcess . scale + 1 : 1 ; processRepresentations . push ( newProcess ) ; } } const processRepresentationSet2nd = processRepresentations . sort ( ( a , b ) => { return a . order - b . order ; } ) ; return { ... this . appRepresentation , process : processRepresentationSet2nd } ; } param { ApplicationRepresentation } appRepresentation param { string } writeTo public static echoStructure ( appRepresentation , writeTo ) { const procfileReconciler = new ProcfileReconciler ( appRepresentation ) ; procfileReconciler . discover ( ) ; const structure = procfileReconciler . getApplicationStructure ( ) ; writeFileSync ( writeTo , JSON . stringify ( structure ) ) ; } param { ApplicationRepresentation } appRepresentation return { Promise < ApplicationStructureRepresentation > } public static async getStructureViaNewProcess ( appRepresentation ) { 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.echoStructure(${ JSON . stringify ( appRepresentation ) }, ${ JSON . stringify ( tmpFile ) }); process.exit()'` , { cwd : appRepresentation . appDir || process . cwd ( ) } , ( error ) => { if ( error ) { reject ( error ) ; return ; } resolve ( ) ; } ) ; } ) ; const fileBuffer = await mzFs . readFile ( tmpFile ) ; await mzFs . unlink ( tmpFile ) ; const fileContent = fileBuffer . toString ( ) ; const structure = JSON . parse ( fileContent ) ; return structure ; } param process return { ProcessRepresentation } private makeupProcess ( process ) { const execArgv = process . globalExecArgv ? ( process . execArgv || [ ] ) . concat ( process . globalExecArgv ) : process . execArgv ; const args = process . globalArgs ? ( process . args || [ ] ) . concat ( process . globalArgs ) : process . args ; const env = process . globalEnv ? { ... process . env , ... process . globalEnv } : process . env ; const scale = process . scale === 'auto' ? defaultWorkerCount : process . scale ; return { ... process , execArgv , args , env , scale } ; } } param tagetPath return { any } function getProcfileBasePath ( tagetPath ) { const resolvedTarget = resolveSymlink ( tagetPath ) ; return dirname ( resolvedTarget ) ; }