import * as path from "path" ; import * as glob from "glob" ; import * as fs from "fs" ; import { config } from ".." ; import { log } from "../modules/log" ; import { Initializer } from "./initializer" ; import { Initializers } from "./initializers" ; import { utils } from "../modules/utils" ; import { id } from "./process/id" ; import { env } from "./process/env" ; import { writePidFile , clearPidFile } from "./process/pid" ; import { api } from "../index" ; import { rebuildConfig } from "../modules/config" ; export const fatalErrorCode = "FATAL_ACTIONHERO_ERROR" ; export class Process { running : boolean ; initialized : boolean ; started : boolean ; stopped : boolean ; stopReasons ? : string [ ] ; shuttingDown : boolean ; bootTime : number ; initializers : Initializers ; startCount : number ; loadInitializers : Initializer [ "initialize" ] [ ] ; startInitializers : Initializer [ "start" ] [ ] ; stopInitializers : Initializer [ "stop" ] [ ] ; constructor ( ) { this . initialized = false ; this . started = false ; this . stopped = false ; this . initializers = { } ; this . loadInitializers = [ ] ; this . startInitializers = [ ] ; this . stopInitializers = [ ] ; this . stopReasons = [ ] ; this . startCount = 0 ; api . process = this ; api . commands . initialize = this . initialize ; api . commands . start = this . start ; api . commands . stop = this . stop ; api . commands . restart = this . restart ; } async initialize ( ) { const loadInitializerRankings = { } ; const startInitializerRankings = { } ; const stopInitializerRankings = { } ; let initializerFiles = [ ] ; initializerFiles = initializerFiles . concat ( glob . sync ( path . join ( __dirname , ".." , "initializers" , "**" , "**/*(*.js|*.ts)" ) ) ) ; config . general . paths . initializer . forEach ( ( startPath ) => { initializerFiles = initializerFiles . concat ( glob . sync ( path . join ( startPath , "**" , "**/*(*.js|*.ts)" ) ) ) ; } ) ; for ( const plugin of Object . values ( config . plugins ) ) { const pluginPath = path . normalize ( plugin . path ) ; if ( ! fs . existsSync ( pluginPath ) ) { throw new Error ( `plugin path does not exist: ${ pluginPath }` ) ; } if ( plugin . initializers !== false ) { initializerFiles = initializerFiles . concat ( glob . sync ( path . join ( pluginPath , "initializers" , "**" , "*.js" ) ) ) ; initializerFiles = initializerFiles . concat ( glob . sync ( path . join ( pluginPath , "dist" , "initializers" , "**" , "*.js" ) ) ) ; } } initializerFiles = utils . arrayUnique ( initializerFiles ) ; initializerFiles = utils . ensureNoTsHeaderFiles ( initializerFiles ) ; for ( const i in initializerFiles ) { const f = initializerFiles [ i ] ; const file = path . normalize ( f ) ; if ( require . cache [ require . resolve ( file ) ] ) { delete require . cache [ require . resolve ( file ) ] ; } let exportedClasses = await import ( file ) ; if ( typeof exportedClasses === "function" ) { exportedClasses = { default : exportedClasses } ; } if ( Object . keys ( exportedClasses ) . length === 0 ) { this . fatalError ( new Error ( `no exported initializers found in ${ file }` ) , file ) ; } for ( const exportKey in exportedClasses ) { let initializer ; let InitializerClass = exportedClasses [ exportKey ] ; try { initializer = new InitializerClass ( ) ; if ( this . initializers [ initializer . name ] ) { const warningMessage = `an existing initializer with the same name \`${ initializer . name }\` will be overridden by the file ${ file }` ; log ( warningMessage , "warning" ) ; } else { initializer . validate ( ) ; this . initializers [ initializer . name ] = initializer ; } } catch ( error ) { this . fatalError ( error , file ) ; } function decorateInitError ( error , type ) { error [ "data" ] = error [ "data" ] ?? { } ; error [ "data" ] . name = initializer . name ; error [ "data" ] . file = file ; error [ "data" ] . type = type ; } const initializeFunction = async ( ) => { if ( typeof initializer . initialize === "function" ) { log ( `Loading initializer: ${ initializer . name }` , "debug" , file ) ; try { await initializer . initialize ( ) ; log ( `Loaded initializer: ${ initializer . name }` , "debug" , file ) ; } catch ( error ) { decorateInitError ( error , "initialize" ) ; throw error ; } } } ; const startFunction = async ( ) => { if ( typeof initializer . start === "function" ) { log ( `Starting initializer: ${ initializer . name }` , "debug" , file ) ; try { await initializer . start ( ) ; log ( `Started initializer: ${ initializer . name }` , "debug" , file ) ; } catch ( error ) { decorateInitError ( error , "start" ) ; throw error ; } } } ; const stopFunction = async ( ) => { if ( typeof initializer . stop === "function" ) { log ( `Stopping initializer: ${ initializer . name }` , "debug" , file ) ; try { await initializer . stop ( ) ; log ( `Stopped initializer: ${ initializer . name }` , "debug" , file ) ; } catch ( error ) { decorateInitError ( error , "stop" ) ; throw error ; } } } ; if ( ! loadInitializerRankings [ initializer . loadPriority ] ) { loadInitializerRankings [ initializer . loadPriority ] = [ ] ; } if ( ! startInitializerRankings [ initializer . startPriority ] ) { startInitializerRankings [ initializer . startPriority ] = [ ] ; } if ( ! stopInitializerRankings [ initializer . stopPriority ] ) { stopInitializerRankings [ initializer . stopPriority ] = [ ] ; } if ( initializer . loadPriority > 0 ) { loadInitializerRankings [ initializer . loadPriority ] . push ( initializeFunction ) ; } if ( initializer . startPriority > 0 ) { startInitializerRankings [ initializer . startPriority ] . push ( startFunction ) ; } if ( initializer . stopPriority > 0 ) { stopInitializerRankings [ initializer . stopPriority ] . push ( stopFunction ) ; } } } this . loadInitializers = this . flattenOrderedInitializer ( loadInitializerRankings ) ; this . startInitializers = this . flattenOrderedInitializer ( startInitializerRankings ) ; this . stopInitializers = this . flattenOrderedInitializer ( stopInitializerRankings ) ; try { for ( const loader of this . loadInitializers ) await loader ( ) ; } catch ( error ) { return this . fatalError ( error , "initialize" ) ; } this . initialized = true ; } /**
     * Start the Actionhero Process
     */ async start ( ) { if ( ! this . initialized ) await this . initialize ( ) ; const serverName = config . general . serverName ; writePidFile ( ) ; this . running = true ; api . running = true ; log ( `environment: ${ env }` , "notice" ) ; log ( `*** Starting ${ serverName } ***` , "info" ) ; this . startInitializers . push ( async ( ) => { this . bootTime = new Date ( ) . getTime ( ) ; if ( this . startCount === 0 ) { log ( `server ID: ${ id }` , "notice" ) ; log ( `*** ${ serverName } Started ***` , "notice" ) ; this . startCount ++ ; } else { log ( `*** ${ serverName } Restarted ***` , "notice" ) ; } } ) ; try { for ( const starter of this . startInitializers ) await starter ( ) ; } catch ( error ) { return this . fatalError ( error , "start" ) ; } this . started = true ; } /**
     * Stop the Actionhero Process
     */ async stop ( stopReasons = [ ] ) { const serverName = config . general . serverName ; if ( this . running ) { this . shuttingDown = true ; this . running = false ; this . initialized = false ; this . started = false ; this . stopReasons = Array . isArray ( stopReasons ) ? stopReasons : [ stopReasons ] ; log ( "stopping process..." , "notice" ) ; if ( this . stopReasons ?. length > 0 ) { log ( `stop reasons: ${ this . stopReasons . join ( ", " ) }` , "debug" ) ; } await utils . sleep ( 100 ) ; this . stopInitializers . push ( async ( ) => { clearPidFile ( ) ; log ( `*** ${ serverName } Stopped ***` , "notice" ) ; delete this . shuttingDown ; this . initializers = { } ; api . running = false ; await utils . sleep ( 100 ) ; } ) ; try { for ( const stopper of this . stopInitializers ) await stopper ( ) ; } catch ( error ) { return this . fatalError ( error , "stop" ) ; } this . stopped = true ; } else if ( this . shuttingDown === true ) { } else { const message = `Cannot shut down ${ serverName }, not running` ; log ( message , "crit" ) ; } } /**
     * Restart the Actionhero Process
     */ async restart ( ) { if ( this . running === true ) { await this . stop ( ) ; rebuildConfig ( ) ; await this . start ( ) ; } else { await this . start ( ) ; } } /**
     * Register listeners for process signals and uncaught exceptions & rejections.
     * Try to gracefully shut down when signaled to do so
     */ registerProcessSignals ( stopCallback = ( exitCode ? ) => { } ) { const timeout = process . env . ACTIONHERO_SHUTDOWN_TIMEOUT ? parseInt ( process . env . ACTIONHERO_SHUTDOWN_TIMEOUT ) : 1000 * 30 ; function awaitHardStop ( ) { return setTimeout ( ( ) => { console . error ( `Process did not terminate within ${ timeout }ms. Stopping now!` ) ; process . nextTick ( process . exit ( 1 ) ) ; } , timeout ) ; } process . once ( "uncaughtException" , async ( error ) => { if ( error . code !== fatalErrorCode ) { if ( api . exceptionHandlers ) { api . exceptionHandlers . report ( error , "uncaught" , "Exception" , { } , "emerg" ) ; } else { console . error ( error ) ; } } if ( this . shuttingDown !== true ) { let timer = awaitHardStop ( ) ; if ( this . running ) await this . stop ( ) ; clearTimeout ( timer ) ; stopCallback ( 1 ) ; } } ) ; process . once ( "unhandledRejection" , async ( rejection ) => { if ( rejection . code !== fatalErrorCode ) { if ( api . exceptionHandlers ) { api . exceptionHandlers . report ( rejection , "uncaught" , "Rejection" , { } , "emerg" ) ; } else { console . error ( rejection ) ; } } if ( ! this . shuttingDown ) { let timer = awaitHardStop ( ) ; if ( this . running ) await this . stop ( ) ; clearTimeout ( timer ) ; stopCallback ( 1 ) ; } } ) ; process . on ( "SIGINT" , async ( ) => { log ( `[ SIGNAL ] - SIGINT` , "notice" ) ; let timer = awaitHardStop ( ) ; if ( this . running ) await this . stop ( ) ; if ( ! this . shuttingDown ) { clearTimeout ( timer ) ; stopCallback ( 0 ) ; } } ) ; process . on ( "SIGTERM" , async ( ) => { log ( `[ SIGNAL ] - SIGTERM` , "notice" ) ; let timer = awaitHardStop ( ) ; if ( this . running ) await this . stop ( ) ; if ( ! this . shuttingDown ) { clearTimeout ( timer ) ; stopCallback ( 0 ) ; } } ) ; process . on ( "SIGUSR2" , async ( ) => { log ( `[ SIGNAL ] - SIGUSR2` , "notice" ) ; let timer = awaitHardStop ( ) ; await this . restart ( ) ; clearTimeout ( timer ) ; } ) ; } async fatalError ( errors = [ ] , type ) { if ( ! ( errors instanceof Array ) ) errors = [ errors ] ; if ( errors ) { const showStack = process . env . ACTIONHERO_FATAL_ERROR_STACK_DISPLAY ? process . env . ACTIONHERO_FATAL_ERROR_STACK_DISPLAY === "true" : true ; errors . forEach ( ( error ) => { if ( ! showStack ) delete error . stack ; if ( api . exceptionHandlers ) { api . exceptionHandlers . report ( error , "initializer" , type ) ; } else { console . error ( error ) ; } } ) ; if ( this . running ) { await this . stop ( errors . map ( ( e ) => e . message ?? e . toString ( ) ) ) ; } await utils . sleep ( 100 ) ; if ( ! errors [ 0 ] . code ) errors [ 0 ] . code = fatalErrorCode ; throw errors [ 0 ] ; } } flattenOrderedInitializer < T > ( collection ) { const output = [ ] ; const keys = [ ] ; for ( const key in collection ) keys . push ( parseInt ( key , 10 ) ) ; keys . sort ( sortNumber ) ; keys . forEach ( ( key ) => { collection [ key ] . forEach ( ( d ) => { output . push ( d ) ; } ) ; } ) ; return output ; } } function sortNumber ( a , b ) { return a - b ; }