@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
171 lines (147 loc) • 5.19 kB
JavaScript
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
}