UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

171 lines (147 loc) 5.19 kB
const cds = require ('..') const { exec } = require('child_process') const LOG = cds.log('serve|bindings',{label:'cds'}) const registry = '~/.cds-services.json' const fs = require('fs') class Bindings { provides = {} servers = {} #bound = {} then (r,e) { const info = ()=> LOG.info ('using bindings from:', { registry }) if (cds.watched) cds.prependOnceListener ('connect',info); else info() delete Bindings.prototype.then // only once per process cds.once('listening', server => this.export (cds.service.providers, server.url)) return this.import() .then (r,e) } bind (service) { let required = cds.requires [service] let binding = this.provides [service] if (binding?.endpoints && !required?.credentials) { // > Re-route requests to the mock service running locally const server = this.servers [binding.server] const kind = [ required?.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints) const path = binding.endpoints [kind] if (typeof required !== 'object') required = {} cds.env.requires[service] = required = { kind, ...cds.requires.kinds [kind], ...required, credentials: { ...required.credentials, ...binding.credentials, url: server.url + path }, } required.kind = kind // override kind from binding // REVISIT: temporary fix to inherit kind as well for mocked odata services // otherwise mocking with two services does not work for kind:odata-v2 if (kind === 'odata-v2' || kind === 'odata-v4') required.kind = 'odata' // Finally, ensure the changes are mirrored in cds.requires overlays as well if (required.service) cds.requires[required.service] = required } return this.#bound [service] = required } // used by cds.connect at (service) { return this.#bound [service] ??= this.bind (service) } get registry() { return Bindings.registry ??= registry.replace(/^~/, require('os').homedir()) } async load (read = fs.promises.readFile) { LOG.debug ('reading bindings from:', registry) try { let src = read (this.registry) let {cds} = JSON.parse (src.then ? await src : src) Object.assign (this, cds) } catch { /* ignored */ } return this } async store (write = fs.promises.writeFile) { LOG.debug ('writing bindings to:', registry) const json = JSON.stringify ({ cds: this },null,' ') return write (this.registry, json) } async import() { await this.load() for (let each in cds.env.requires) this.bind (each) return this } async export (services, url) { this.cleanup (url) await this.cleanupStaleServers() const { servers, provides } = this, { pid } = process // register our server servers[pid] = { root: 'file://' + cds.root, url } // register our services for (let srv of services) { // if (each.name in cds.env.requires) continue provides[srv.name] = { endpoints: Object.fromEntries (srv.endpoints.map (ep => [ ep.kind, ep.path ])), server: pid, } } process.on ('exit', ()=> this.purge()) cds.on ('shutdown', ()=> this.purge()) return this.store() } purge() { if (this.done) return; this.load (fs.readFileSync) LOG.debug ('purging bindings from:', registry) this.cleanup() this.store (fs.writeFileSync) this.done = true } /** * Remove all services served by this server or at the given url. */ cleanup (url, pid=process.pid) { const { servers, provides } = this for (let [key,srv] of Object.entries (provides)) if (srv.server === pid || url && srv.credentials?.url?.startsWith(url)) delete provides [key] delete servers [pid.toString()] return this } async cleanupStaleServers() { const { servers } = this if (!Object.keys(servers).length) return const pids = await new Promise((resolve) => { const command = process.platform === 'win32' ? 'tasklist /fo csv /nh' : 'ps -e -o pid=' exec(command, (error, stdout) => { if (error) { LOG.warn('Error fetching process IDs with command:', command, error) return resolve([]) } resolve(stdout.split('\n').map(line => line.trim()) .map(line => { if (process.platform === 'win32') { const parts = line.split(',') const pid = parts[1]?.replace(/"/g, '') return pid ? parseInt(pid, 10) : NaN } return parseInt(line, 10) }) .filter(pid => !isNaN(pid))) }) }) LOG.debug(`fetched ${pids.length} PIDs`) const pidSet = new Set(pids) for (let key of Object.keys(servers)) { const pid = parseInt(key, 10) if (!pidSet.has(pid)) { LOG.debug(`cleaning up bindings for stale PID ${pid}`) this.cleanup(undefined, pid) } } } } const {NODE_ENV} = process.env if (NODE_ENV === 'test' || global.it || cds.env.no_bindings) { Object.defineProperty (module, 'exports', { value: { at: ()=> undefined }}) } else { module.exports = new Bindings }