UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

167 lines (146 loc) 5.78 kB
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')