2022-11-06 18:35:53 +01:00
const ChildProcess = require ( 'node:child_process' ) ;
const { EventEmitter } = require ( 'node:events' ) ;
const { Util } = require ( '../util' ) ;
class Shard extends EventEmitter {
constructor ( controller , id , options = { } ) {
super ( ) ;
this . controller = controller ;
if ( typeof id !== 'number' || isNaN ( id ) ) throw new Error ( 'Missing ID' ) ;
this . id = id ;
if ( ! options . path ) throw new Error ( 'Missing path to file to fork' ) ;
this . filePath = options . path ;
this . args = options . args || [ ] ;
this . execArgv = options . execArgv || [ ] ;
this . env = options . env || { } ;
this . _respawn = options . respawn || false ;
this . serverOptions = options . serverOptions || { } ;
this . ready = false ;
this . process = null ;
2022-11-06 19:31:41 +01:00
this . fatal = false ;
2022-11-06 18:35:53 +01:00
}
async spawn ( waitForReady = false ) {
2022-11-06 19:31:41 +01:00
if ( this . fatal ) throw new Error ( ` [shard- ${ this . id } ] Process died fatally and cannot be restarted. Fix the issue before trying again. ` ) ;
2022-11-06 18:35:53 +01:00
if ( this . process ) throw new Error ( ` [shard- ${ this . id } ] A process for this shard already exists! ` ) ;
this . process = ChildProcess . fork ( this . filePath , this . args , { env : { ... this . env , SHARD _ID : this . id } , execArgv : this . execArgv } )
. on ( 'message' , this . _handleMessage . bind ( this ) )
. on ( 'exit' , ( ) => this . _handleExit ( ) )
. on ( 'disconnect' , this . _handleDisconnect . bind ( this ) ) ; // Don't know if this is going to help, but monitoring whether this gets called whenever a process on its own closes the IPC channel
this . process . once ( 'spawn' , ( ) => {
this . emit ( 'spawn' ) ;
this . process . send ( { _start : this . serverOptions } ) ;
} ) ;
if ( ! waitForReady ) return ;
return new Promise ( ( resolve , reject ) => {
this . once ( 'ready' , resolve ) ;
this . once ( 'disconnect' , ( ) => reject ( new Error ( ` [shard- ${ this . id } ] Shard disconnected while starting up ` ) ) ) ;
this . once ( 'death' , ( ) => reject ( new Error ( ` [shard- ${ this . id } ] Shard died while starting ` ) ) ) ;
setTimeout ( ( ) => reject ( new Error ( ` [shard- ${ this . id } ] Shard timed out while starting ` ) ) , 30_000 ) ;
} ) ;
}
async respawn ( delay = 500 ) {
await this . kill ( ) ;
if ( delay ) await Util . wait ( delay ) ;
return this . spawn ( ) ;
}
/ * *
* Sends a shutdown command to the shard , if it doesn ' t respond within 5 seconds it gets killed
* TODO : Add a check to see if the process actually ends and print out a warning if it hasn ' t
*
* @ return { * }
* @ memberof Shard
* /
kill ( ) {
if ( this . process ) {
return new Promise ( ( resolve ) => {
// Clear out all other exit listeners so they don't accidentally start the process up again
this . process . removeAllListeners ( 'exit' ) ;
// Set timeout for force kill
const to = setTimeout ( ( ) => {
this . process . kill ( ) ;
resolve ( ) ;
} , 5000 ) ;
// Gracefully handle exit
this . process . once ( 'exit' , ( ) => {
clearTimeout ( to ) ;
this . _handleExit ( false ) ;
resolve ( ) ;
} ) ;
// Clear the force kill timeout if the process responds with a shutdown echo, allowing it time to gracefully close all connections
this . once ( 'shutdown' , ( ) => {
clearTimeout ( to ) ;
} ) ;
this . process . send ( { _shutdown : true } ) ;
} ) ;
}
this . _handleExit ( false ) ;
}
send ( message ) {
return new Promise ( ( resolve , reject ) => {
if ( this . ready && this . process ) {
this . process . send ( message , err => {
if ( err ) reject ( err ) ;
else resolve ( ) ;
} ) ;
} else reject ( new Error ( ` [shard- ${ this . id } ] Cannot send message to dead shard. ` ) ) ;
} ) ;
}
_handleMessage ( message ) {
if ( message ) {
if ( message . _ready ) {
this . ready = true ;
this . emit ( 'ready' ) ;
return ;
} else if ( message . _shutdown ) {
this . ready = false ;
this . emit ( 'shutdown' ) ;
return ;
2022-11-06 19:31:41 +01:00
} else if ( message . _fatal ) {
this . process . removeAllListeners ( ) ;
this . ready = false ;
this . fatal = true ;
this . _handleExit ( false ) ;
this . emit ( 'fatal' ) ;
2022-11-06 18:35:53 +01:00
}
}
2022-11-09 10:20:06 +01:00
this . emit ( 'message' , message ) ;
2022-11-06 18:35:53 +01:00
}
_handleDisconnect ( ) {
this . emit ( 'disconnect' ) ;
}
_handleExit ( respawn = this . _respawn ) {
this . process . removeAllListeners ( ) ;
this . emit ( 'death' ) ;
this . ready = false ;
this . process = null ;
if ( respawn ) this . spawn ( ) . catch ( err => this . emit ( 'error' , err ) ) ;
}
}
module . exports = Shard ;