pyconnector
Version:
Bridge the gap between Node.JS and Python applications
174 lines (143 loc) • 3.99 kB
JavaScript
'use strict'
const ZMQ = require('zeromq/v5-compat')
const ChildProcess = require('child_process')
const { EventEmitter } = require('events')
// connector class
class PyConnector {
constructor (options) {
// set options
this._opts = this._processOptions(Object.assign(
{
port: 24001,
endpoint: null,
launcher: 'python',
path: null,
cwd: null,
respawn: 0,
local: true
}, options))
// event callbacks
this._events = new EventEmitter()
this._connector = ZMQ.socket('req')
// spawn Python process
this._process = null
if (this._opts.local && this._opts.path !== null) {
this._processSpawn()
}
// query identifiers
this._qid = 0
// connect and handle messages
this._connector.connect(this._opts.endpoint)
this._connector.on('message', (data) => {
this._responseHandle(data)
})
}
_responseHandle (data) {
// response decoding
const response = JSON.parse(data.toString())
const evt = `q_${response._p}_${response._id}`
// pass data
this._events.emit(evt, response)
}
// summon handler process
_processSpawn () {
const ns = this
let launcher = this._opts.launcher
const pargs = [this._opts.path, '--pynodeport', this._opts.port]
const popts = { stdio: 'inherit' }
if (launcher == null) launcher = pargs.shift()
if (this._opts.cwd != null) popts.cwd = this._opts.cwd
// create child process
this._process = ChildProcess.spawn(
launcher,
pargs,
popts
)
// error handling
this._process.on('error', (e) => {
console.error(`PyConnector cannot start process ${this._opts.path}`)
console.error(e)
ns._process = null
})
// close event
this._process.on('close', () => {
ns._process = null
// respawn timeout option
if (ns._opts.respawn > 0) {
// hopefully restart
setTimeout(ns._processSpawn, ns._opts.respawn)
}
})
}
// set options
_processOptions (opts) {
opts.local = false
// custom endpoint
if (opts.endpoint !== null) {
// string endpoint
if (opts.endpoint.substring) {
// grab port value
let parts = opts.endpoint.split(':')
opts.port = parseInt(parts.pop())
// check if string contains protocol
if (opts.endpoint.indexOf('//') >= 0) {
parts = opts.endpoint.split('//').slice(1)
opts.endpoint = `tcp://${parts[0]}`
opts.respawn = 0
return opts
}
opts.endpoint = `tcp://${opts.endpoint}`
opts.respawn = 0
return opts
}
// numeric endpoint
opts.port = opts.endpoint
}
// local instance
opts.endpoint = `tcp://127.0.0.1:${opts.port}`
opts.local = true
return opts
}
// retrieve all available paths handled by Python
routes (callback) {
// use the query function
return this.query('__pyroutes', {}, callback)
}
// query Python endpoint
query (path, args, callback) {
// increment identifier
const qdata = {
_p: path,
_id: this._qid++,
args
}
// asyncronous response
const query = new Promise((resolve, reject) => {
// set callback
this._events.once(`q_${qdata._p}_${qdata._id}`, (response) => {
resolve(response.data)
// run callback
if (callback) callback(response.data)
})
// send JSON query
this._connector.send(JSON.stringify(qdata))
}).catch(
// handle errors
(e) => {
// log
console.error(e)
// remove listeners for erroneous event
this._events.removeAllListeners(`q_${qdata._p}_${qdata._id}`)
return e
})
return query
}
end () {
if (this._process !== null) {
this._process.kill('SIGINT')
this._process.kill()
}
this._connector.close()
}
};
module.exports = PyConnector