@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
167 lines (146 loc) • 5.78 kB
JavaScript
const cds = require ('../cds')
const LOG = cds.log('cli|watch'), DEBUG = cds.debug('cli|watch')
const { relative } = require('path')
module.exports = ({ cwd = process.cwd(), script, ext, ignore=RegExp(), watched, env={}, delay=200, options={} })=>{
const debugOpts = _debugOpts(options)
let child
// Phoning home...
const EventEmitter = require('events')
const emitter = new EventEmitter
process.on('SIGINT', ()=>console.log()) //> newline after ^C
process.on('SIGINT', _shutdown)
process.on('SIGTERM', _shutdown)
function _shutdown (signal,n) {
DEBUG?.('⚡️', signal, n, 'received by cds watch')
process.emit('shutdown')
}
// Re-starting child process...
const { fork } = require('child_process')
const restart = _coalesceEvents(delay, (eventsAndOpts) => {
DEBUG?.('Restart', {events: eventsAndOpts})
_kill (child, () => {
const execArgv = [...process.execArgv, ...debugOpts]
eventsAndOpts.forEach(evt => { // ad-hoc options from stdin
if (evt.type?.startsWith('--')) execArgv.push(evt.type)
if (evt.name?.startsWith('--')) execArgv.push(evt.name)
})
const updated = eventsAndOpts.filter(evt => evt.type && evt.type === 'update').map(evt => evt.name)
emitter.emit ('restart', updated)
child = fork (script, { cwd, env, stdio:'inherit', execArgv }, (err)=>{
if (err) console.error (err)
})
child.on('message', msg => {
if (msg === 'restart') return restart()
emitter.emit('message', msg)
})
// child.channel.unref()
// child.unref()
// child.on('exit', process.exit)
})
})
restart()
process.on('shutdown', ()=> _kill(child))
// Watching for touched files...
const watch = require('node-watch')
const include = RegExp(`\\.(${ext.replace(/,/g,'|')})$`)
const filter = f => {
const isIn = !ignore.test(f) && include.test(f)
if (!isIn) DEBUG?. (`ignored: ${relative(cwd||process.cwd(), f)}`)
// else DEBUG?. (`NOT ignored: ${relative(cwd||process.cwd(), f)}`, include.test(f), include)
return isIn
}
const watcher = watch (watched,{ recursive:true, filter, delay:0 })
if (LOG._debug) watcher.on('close', ()=> DEBUG('⚡️', 'cds watch - file watcher closed'))
watcher.on('change', restart)
process.on('shutdown', ()=> watcher.close())
// Live commands...
const readline = require('readline').createInterface(process.stdin).on('line', (input) => {
if (input === '') restart()
else if (input === 'restart' || input === 'rs' || input.match(/^y$/i)) restart()
else if (input === 'debug' || input === 'dbg') restart('--inspect')
else if (input === 'break' || input === 'brk') restart('--inspect-brk')
else if (input === 'debug-brk') restart('--inspect-brk')
else if (input === 'ps') ps(child,env)
else if (input === 'bye' || input.match(/^n$/i)) { _kill (child) }
else console.log ('?\n')
})
if (LOG._debug) readline.on('close', ()=> DEBUG('⚡️', 'cds watch - readline closed'))
process.on('shutdown', ()=> readline.close())
emitter.restart = restart
emitter.stop = () => _kill(child)
emitter.setEnv = (name, value) => {
if (typeof value === "undefined") {
delete env[name]
} else {
env[name] // REVISIT: no value assigned ???
}
}
return emitter
}
const ps = (child,env) => console.log (`\x1b[32m
PID Process Command
${process.pid} parent cds ${process.argv.slice(2).join(' ')}
${ child.pid} child cds ${JSON.parse(env._args).join(' ')}
\x1b[0m`
)
// reduces multiple node-watch events in a time frame into a bulk
function _coalesceEvents(delay, fn) {
let timer, cache = []
return function(type, name) { // node-watch callback signature
if (type) cache.push({type, name})
if (!timer) timer = setTimeout(() => {
fn(cache)
timer = null
cache = []
}, delay).unref();
}
}
/*
* Ensures the given child process gets killed in different environments
*/
function _kill (child, cb=()=>{}) {
if (!child || !child.connected) return cb()
if (child[_pending]) return // safeguard against repeated calls for the same process
child[_pending] = true
const _done = () => { delete child[_pending]; return cb(); }
const waitTime = parseInt(process.env.CDS_WATCH_KILL_DELAY) || 2222
// On Windows, child_process.kill() abruptly kills the process (https://nodejs.org/api/child_process.html#subprocesskillsignal)
// Server has cleanup code to run, so send a custom message and give it a bit time.
if (process.platform === 'win32') {
child.send({ close: true }, () => {}) // see @sap/cds/bin/serve.js
setTimeout(()=> {
child.kill()
DEBUG?.('Killed process', child.pid)
_done()
}, waitTime).unref()
return
}
// fallback for misbehaving processes: SIGKILL it after some time
const forced = setTimeout(()=> {
DEBUG?.('⚡️', 'cds watch - killing child forcefully!')
child.kill('SIGKILL')
}, waitTime).unref()
// first kill normally (using SIGTERM)
child.on('exit',() => { clearTimeout(forced)
DEBUG?.('⚡️', 'cds watch - child exited', child.pid)
_done()
})
child.kill()
}
function _debugOpts(options={}) {
const o = []
if ('debug' in options) {
options.inspect = options.debug
delete options.debug
}
if ('inspect' in options) { // could be 0
if (typeof options.inspect === 'string') o.push(`--inspect=${options.inspect}`) // could be port and/or host
else o.push(`--inspect`)
}
if ('inspect-brk' in options) {
if (typeof options['inspect-brk'] === 'string') o.push(`--inspect-brk=${options['inspect-brk']}`)
else o.push(`--inspect-brk`)
}
return o
}
const _pending = Symbol.for('sap.cds.watch.pendingKill')