UNPKG

nozombie

Version:

track pocesses and make sure they die when they are supposed to

266 lines (217 loc) 6.4 kB
const tempy = require( 'tempy' ) const childProcess = require( 'child_process' ) const fs = require( 'fs' ) const path = require( 'path' ) const util = require( './util.js' ) function uuid () { uuid.counter = ( uuid.counter || 0 ) const n = ++uuid.counter const unix_time = Math.floor( Date.now() / 1000 ) return ( 'nz' + Math.random().toString( 16 ).slice( 4, 10 ) + unix_time.toString( 16 ).slice( -6 ) + String( process.pid ) + String( n ) ) } const _envs = {} Object.keys( process.env ).forEach( function ( key ) { const n = process.env[ key ] if ( n == '0' || n == 'false' || !n ) { return _envs[ key ] = false } _envs[ key ] = n } ) module.exports = nozombie module.exports.spawn = function ( opts, disable_warning ) { if ( !disable_warning ) { console.log(` nozombie: warning You should probably not be using the nozombie.spawn() function. Use nozombie() instead to spawn a shared global subprocess for you application process. `) } return nozombieFactory( opts ) } let globalInstance // you should only need 1 per process function nozombie ( namespace ) { if ( namespace && typeof namespace !== 'string' ) { throw new TypeError( 'nozombie() error: namespace should be an application wide unique name' ) } if ( !globalInstance ) { globalInstance = nozombieFactory( { main_parent_pid: process.pid } ) } // return a namespace return globalInstance.createNamespace( namespace || ( uuid() + ':' ) ) } /* * Spawns a subprocess to track parent pid and supplied children pids. * You should only need one of these per parent/node process/application. */ function nozombieFactory ( opts ) { opts = opts || {} if ( typeof opts !== 'object' ) { throw new Error( 'nozombieFactory() invalid options given' ) } let messageBuffer = [] // keep recent messages in memory for writing const sendQueue = [] const tempfile = tempy.file() // main file to communicate with subprocess const logfile = tempfile + '-debug.log' // only used/created when debugging if ( !!_envs[ 'debug_nozombie' ] ) { console.log( 'tempfile: ' + tempfile ) console.log( 'logfile: ' + logfile ) } let write_buffer = '' let ack = 1 const main_parent_pid = Number( opts.main_parent_pid || process.pid ) if ( typeof main_parent_pid !== 'number' || Number.isNaN( main_parent_pid ) ) { throw new Error( 'nozombieFactory() invalid options.main_parent_pid pid given' ) } const headerText = ( 'https://github.com/talmobi/nozombie' + '\n' + `started by pid: ${ process.pid }, main_parent_pid: ${ main_parent_pid }, date: ${ Date.now().toLocaleString() }` + '\n' ) fs.writeFileSync( tempfile, headerText, 'utf8' ) const nodeBinPath = process.execPath // spawn detached sub process to spy on the pids const spawn = childProcess.spawn( nodeBinPath, [ path.join( __dirname, './main-spawn.js' ), main_parent_pid, // main parent pid tempfile, logfile, !!_envs[ 'debug_nozombie' ], 'nozombie-spawn' // an ignored arg to help ps filtering ], { env: { // less threads if possible UV_THEADPOOL_SIZE: 1, v8_thread_pool_size: 1 }, // see: https://devdocs.io/node~10_lts/child_process#child_process_options_detached detached: true, // possible to run after parent exists on windows stdio: 'ignore', // disconnect io from parent allowing for independent running windowsHide: true // don't open console on windows } ) // see: https://devdocs.io/node~10_lts/child_process#child_process_options_detached spawn.unref() // make parent (this process) not wait for child before exiting function addParent ( opts ) { if ( typeof opts !== 'object' ) { opts = { pid: opts } } // normalize pid const n = Number( opts.pid ) if ( n <= 0 || Number.isNaN( n ) ) throw new TypeError( 'nozombie invalid parent pid: ' + opts.pid ) const msg = { type: 'parent', pid: opts.pid, date_ms: Date.now(), ack: ack++ } if ( opts.name != null ) msg.name = opts.name sendQueue.push( msg ) scheduleProcessing() } function addChild ( opts ) { if ( typeof opts !== 'object' ) { opts = { pid: opts } } // normalize pid const n = Number( opts.pid ) if ( n <= 0 || Number.isNaN( n ) ) throw new TypeError( 'nozombie invalid child pid: ' + opts.pid ) const msg = { type: 'child', pid: opts.pid, date_ms: Date.now(), ack: ack++ } if ( opts.ttl >= 0 ) msg.ttl_ms = opts.ttl if ( opts.name != null ) msg.name = opts.name sendQueue.push( msg ) scheduleProcessing() } function kill ( name ) { const msg = { type: 'kill', date_ms: Date.now(), ack: ack++ } if ( name != null ) msg.name = name sendQueue.push( msg ) scheduleProcessing() } async function processSendQueue () { return new Promise( async function ( resolve ) { while ( sendQueue.length > 0 ) { const msg = sendQueue.shift() messageBuffer.push( msg ) } const stale_time = Date.now() - 1000 * 15 messageBuffer = messageBuffer.filter( msg => msg.date_ms > stale_time ) write_buffer = headerText messageBuffer.forEach( function ( msg ) { const line = JSON.stringify( msg ) + '\n' write_buffer += line } ) if ( write_buffer !== '' ) { try { await util.writeFile( tempfile, write_buffer ) write_buffer = '' } catch ( err ) { /* ignore */ } } resolve() } ) } function scheduleProcessing ( ms ) { if ( sendQueue.length > 0 || write_buffer !== '' ) { if ( processSendQueue.timeout ) return // already in progress processSendQueue.timeout = setTimeout( async function () { await processSendQueue() processSendQueue.timeout = undefined scheduleProcessing( 0 ) }, ms || 0 ) } } function createNamespace ( namespace ) { function _addParent ( opts ) { if ( typeof opts !== 'object' ) opts = { pid: opts } opts.name = opts.name || '' opts.name = namespace + opts.name addParent( opts ) } function _addChild ( opts ) { if ( typeof opts !== 'object' ) opts = { pid: opts } opts.name = opts.name || '' opts.name = namespace + opts.name addChild( opts ) } function _kill ( name ) { name = name || '' name = namespace + name kill( name ) } return { addParent: _addParent, add: _addChild, kill: _kill } } return { createNamespace, tempfile, spawn, addParent, add: addChild, kill } }