You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
9.0 KiB
323 lines
9.0 KiB
import {Container} from '../di';
|
|
import {
|
|
ErrorWithContext,
|
|
globalRegistry,
|
|
infer,
|
|
isLoggingLevel,
|
|
PathLike,
|
|
StandardLogger,
|
|
universalPath,
|
|
UniversalPath
|
|
} from '../util';
|
|
|
|
import {Logging} from '../service/Logging';
|
|
import {RunLevelErrorHandler} from "./RunLevelErrorHandler";
|
|
import {Unit, UnitStatus} from "./Unit";
|
|
import * as dotenv from 'dotenv';
|
|
import {CacheFactory} from "../support/cache/CacheFactory";
|
|
|
|
/**
|
|
* Helper function that resolves and infers environment variable values.
|
|
*
|
|
* If none is found, returns `defaultValue`.
|
|
*
|
|
* @param key
|
|
* @param defaultValue
|
|
*/
|
|
export function env(key: string, defaultValue?: any): any {
|
|
return Application.getApplication().env(key, defaultValue)
|
|
}
|
|
|
|
/**
|
|
* Helper function for fetching a universal path relative to the root of the application.
|
|
* @param parts
|
|
*/
|
|
export function basePath(...parts: PathLike[]): UniversalPath {
|
|
return Application.getApplication().path(...parts)
|
|
}
|
|
|
|
/**
|
|
* Helper function for fetching a universal path relative to the `app/` directory.
|
|
* @param parts
|
|
*/
|
|
export function appPath(...parts: PathLike[]): UniversalPath {
|
|
return Application.getApplication().appPath(...parts)
|
|
}
|
|
|
|
/**
|
|
* The main application container.
|
|
*/
|
|
export class Application extends Container {
|
|
public static getContainer(): Container {
|
|
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
|
if ( !existing ) {
|
|
const container = new Application()
|
|
globalRegistry.setGlobal('extollo/injector', container)
|
|
return container
|
|
}
|
|
|
|
return existing as Container
|
|
}
|
|
|
|
/**
|
|
* Get the global application instance.
|
|
*/
|
|
public static getApplication(): Application {
|
|
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
|
if ( existing instanceof Application ) {
|
|
return existing
|
|
} else if ( existing ) {
|
|
const app = new Application()
|
|
existing.cloneTo(app)
|
|
|
|
globalRegistry.setGlobal('extollo/injector', app)
|
|
return app
|
|
} else {
|
|
const app = new Application()
|
|
globalRegistry.setGlobal('extollo/injector', app)
|
|
return app
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The fully-qualified path to the base directory of the app.
|
|
* @protected
|
|
*/
|
|
protected baseDir!: string
|
|
|
|
/**
|
|
* Resolved universal path to the base directory of the app.
|
|
* @protected
|
|
*/
|
|
protected basePath!: UniversalPath
|
|
|
|
/**
|
|
* The Unit classes registered with the app.
|
|
* @protected
|
|
*/
|
|
protected applicationUnits: (typeof Unit)[] = []
|
|
|
|
/**
|
|
* Instances of the units registered with this app.
|
|
* @protected
|
|
*/
|
|
protected instantiatedUnits: Unit[] = []
|
|
|
|
/**
|
|
* If true, the "Starting Extollo..." messages will always
|
|
* be logged.
|
|
*/
|
|
public forceStartupMessage: boolean = true
|
|
|
|
constructor() {
|
|
super()
|
|
|
|
if ( !this.hasKey(Application) ) {
|
|
this.register(Application)
|
|
this.instances.push({
|
|
key: Application,
|
|
value: this,
|
|
})
|
|
}
|
|
|
|
if ( !this.hasKey('app') ) {
|
|
this.registerSingleton('app', this)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given unit class is registered with the application.
|
|
* @param unitClass
|
|
*/
|
|
public hasUnit(unitClass: typeof Unit) {
|
|
return this.applicationUnits.includes(unitClass)
|
|
}
|
|
|
|
/**
|
|
* Return a UniversalPath to the root of the application.
|
|
*/
|
|
get root() {
|
|
return this.basePath.concat()
|
|
}
|
|
|
|
/**
|
|
* Returns a UniversalPath to the `app/` directory in the application.
|
|
*/
|
|
get appRoot() {
|
|
return this.basePath.concat('app')
|
|
}
|
|
|
|
/**
|
|
* Resolve a path relative to the root of the application.
|
|
* @param parts
|
|
*/
|
|
path(...parts: PathLike[]) {
|
|
return this.basePath.concat(...parts)
|
|
}
|
|
|
|
/**
|
|
* Resolve a path relative to the `app/` directory in the application.
|
|
* @param parts
|
|
*/
|
|
appPath(...parts: PathLike[]) {
|
|
return this.basePath.concat('app', ...parts)
|
|
}
|
|
|
|
/**
|
|
* Get an instance of the RunLevelErrorHandler.
|
|
*/
|
|
get errorHandler() {
|
|
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
|
return rleh.handle
|
|
}
|
|
|
|
/**
|
|
* Wrap a base Error instance into an ErrorWithContext.
|
|
* @param e
|
|
* @param context
|
|
*/
|
|
errorWrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
|
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
|
return rleh.wrapContext(e, context)
|
|
}
|
|
|
|
/**
|
|
* Set up the bare essentials to get the application up and running.
|
|
* @param absolutePathToApplicationRoot
|
|
* @param applicationUnits
|
|
*/
|
|
scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]) {
|
|
this.baseDir = absolutePathToApplicationRoot
|
|
this.basePath = universalPath(absolutePathToApplicationRoot)
|
|
this.applicationUnits = applicationUnits
|
|
|
|
this.bootstrapEnvironment()
|
|
this.setupLogging()
|
|
|
|
this.registerFactory(new CacheFactory()) // FIXME move this somewhere else?
|
|
|
|
this.make<Logging>(Logging).debug(`Application root: ${this.baseDir}`)
|
|
}
|
|
|
|
/**
|
|
* Initialize the logger and load the logging level from the environment.
|
|
* @protected
|
|
*/
|
|
protected setupLogging() {
|
|
const standard: StandardLogger = this.make<StandardLogger>(StandardLogger)
|
|
const logging: Logging = this.make<Logging>(Logging)
|
|
|
|
logging.registerLogger(standard)
|
|
|
|
try {
|
|
logging.verbose('Attempting to load logging level from the environment...')
|
|
const envLevel = this.env('EXTOLLO_LOGGING_LEVEL')
|
|
logging.verbose(`Read logging level: ${envLevel}`)
|
|
|
|
if ( isLoggingLevel(envLevel) ) {
|
|
logging.verbose('Logging level is valid.')
|
|
logging.level = envLevel
|
|
logging.debug(`Set logging level from environment: ${envLevel}`)
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
/**
|
|
* Initialize the environment variable library and read from the `.env` file.
|
|
* @protected
|
|
*/
|
|
protected bootstrapEnvironment() {
|
|
dotenv.config({
|
|
path: this.basePath.concat('.env').toLocal
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Get a value from the loaded environment variables.
|
|
* If no value could be found, the default value will be returned.
|
|
* @param key
|
|
* @param defaultValue
|
|
*/
|
|
public env(key: string, defaultValue?: any): any {
|
|
return infer(process.env[key] ?? '') ?? defaultValue
|
|
}
|
|
|
|
/**
|
|
* Run the application by starting all units in order, then stopping them in reverse order.
|
|
*/
|
|
async run() {
|
|
try {
|
|
await this.up()
|
|
await this.down()
|
|
} catch (e) {
|
|
this.errorHandler(e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start all units in the application, one at a time, in order.
|
|
*/
|
|
async up() {
|
|
const logging: Logging = this.make<Logging>(Logging)
|
|
|
|
logging.info('Starting Extollo...', this.forceStartupMessage)
|
|
for ( const unitClass of this.applicationUnits ) {
|
|
const unit: Unit = this.make<Unit>(unitClass)
|
|
this.instantiatedUnits.push(unit)
|
|
await this.startUnit(unit)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all units in the application, one at a time, in reverse order.
|
|
*/
|
|
async down() {
|
|
const logging: Logging = this.make<Logging>(Logging)
|
|
|
|
logging.info('Stopping Extollo...', this.forceStartupMessage)
|
|
for ( const unit of [...this.instantiatedUnits].reverse() ) {
|
|
if ( !unit ) continue
|
|
await this.stopUnit(unit)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start a single unit, setting its status.
|
|
* @param unit
|
|
*/
|
|
public async startUnit(unit: Unit) {
|
|
const logging: Logging = this.make<Logging>(Logging)
|
|
|
|
try {
|
|
logging.debug(`Starting ${unit.constructor.name}...`)
|
|
unit.status = UnitStatus.Starting
|
|
await unit.up()
|
|
unit.status = UnitStatus.Started
|
|
logging.info(`Started ${unit.constructor.name}.`)
|
|
} catch (e) {
|
|
unit.status = UnitStatus.Error
|
|
console.log(e)
|
|
throw this.errorWrapContext(e, {unit_name: unit.constructor.name})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop a single unit, setting its status.
|
|
* @param unit
|
|
*/
|
|
public async stopUnit(unit: Unit) {
|
|
const logging: Logging = this.make<Logging>(Logging)
|
|
|
|
try {
|
|
logging.debug(`Stopping ${unit.constructor.name}...`)
|
|
unit.status = UnitStatus.Stopping
|
|
await unit.down()
|
|
unit.status = UnitStatus.Stopped
|
|
logging.info(`Stopped ${unit.constructor.name}.`)
|
|
} catch (e) {
|
|
unit.status = UnitStatus.Error
|
|
throw this.errorWrapContext(e, {unit_name: unit.constructor.name})
|
|
}
|
|
}
|
|
}
|