2018-01-08 13:06:01 -08:00
import { Socket } from 'net'
2018-03-02 08:34:10 -08:00
import path from 'path'
2019-03-17 13:51:36 -07:00
import { promisify } from 'util'
2019-08-19 17:57:10 -07:00
import pause from 'delay'
2018-01-08 13:06:01 -08:00
import btc from 'better-try-catch'
2018-02-03 13:33:25 -08:00
import JsonStream from './json-stream'
2018-02-17 18:26:17 -08:00
// import logger from '../../uci-logger/src/logger'
2019-02-14 14:01:14 -08:00
import logger from '@uci-utils/logger'
2018-02-17 18:26:17 -08:00
2018-02-01 16:58:17 -08:00
let log = { }
2018-03-02 08:34:10 -08:00
// TODO change default pipe dir for windows and mac os
2019-01-01 16:53:12 -08:00
const DEFAULT _PIPE _DIR = process . env . SOCKETS _DIR || '/tmp/UCI'
2018-03-02 08:34:10 -08:00
const DEFAULT _SOCKET _NAME = 'uci-sock'
2018-01-08 13:06:01 -08:00
2019-01-01 16:53:12 -08:00
/ * *
* Socket Consumer - connects to UCI TCP or Named Pipe Sockets and coummunicates with uci packet . < br / >
* Extends { @ link https : //nodejs.org/api/net.html#net_class_net_socket | nodejs net.Socket}
* @ extends Socket
* /
class SocketConsumer extends Socket {
/ * *
* constructor - Description
*
* @ param { object } [ opts = { } ] test
* /
constructor ( opts = { } ) {
2018-01-08 13:06:01 -08:00
super ( )
2019-01-01 16:53:12 -08:00
log = logger ( {
file : 'src/consumer.js' ,
class : 'Consumer' ,
name : 'socket' ,
id : this . id
} )
this . id = opts . id || opts . name || 'socket:' + new Date ( ) . getTime ( )
2018-02-03 13:33:25 -08:00
if ( ! opts . path ) {
2019-04-26 10:14:57 -07:00
if ( ! opts . host ) log . warn ( { method : 'constructor' , line : 38 , opts : opts , msg : 'no host supplied using localhost...use named piped instead - opts.path' } )
2018-02-03 13:33:25 -08:00
opts . host = opts . host || '127.0.0.1'
opts . port = opts . port || 8080
2018-03-02 08:34:10 -08:00
} else {
2019-01-01 16:53:12 -08:00
if ( typeof opts . path === 'boolean' )
opts . path = path . join ( DEFAULT _PIPE _DIR , DEFAULT _SOCKET _NAME )
if ( path . dirname ( opts . path ) === '.' )
opts . path = path . join ( DEFAULT _PIPE _DIR , opts . path )
2018-03-02 08:34:10 -08:00
}
2019-01-01 16:53:12 -08:00
this . opts = opts
2019-04-11 20:57:45 -07:00
// default is keepAlive true, must set to false to explicitly disable
// if keepAlive is true then consumer will also be reconnecting consumer
2019-11-21 09:35:09 -08:00
this . initTimeout = opts . initTimeout == null ? 60000 : opts . initTimeout * 1000
this . retryWait = opts . retryWait == null ? 5000 : opts . retryWait * 1000
2018-07-30 19:07:03 -07:00
this . keepAlive = 'keepAlive' in opts ? opts . keepAlive : true
2019-08-23 15:48:39 -07:00
this . _connected = false
this . _authenticated = false
2018-01-22 12:18:34 -08:00
this . stream = new JsonStream ( )
2018-01-19 20:43:16 -08:00
// bind to class for other class functions
this . connect = this . connect . bind ( this )
2019-03-17 13:51:36 -07:00
this . close = promisify ( this . end ) . bind ( this )
2019-08-19 17:57:10 -07:00
// this.__ready = this.__ready.bind(this)
this . _conAttempt = 1
this . _aborted = false
2019-06-26 08:54:35 -07:00
this . _reconnect = false
2019-11-21 09:35:09 -08:00
this . retryPause = { } // timeout that may need to be cancelled if init timeout throws
2019-08-23 15:48:39 -07:00
// this._packetProcess = this._packetProcess.bind(this)
2018-01-08 13:06:01 -08:00
}
2019-08-23 15:48:39 -07:00
get connected ( ) { return this . _connected }
2019-11-21 09:35:09 -08:00
get active ( ) { return ! ! this . _authenticated }
2019-08-20 10:52:59 -07:00
2019-01-01 16:53:12 -08:00
async connect ( ) {
return new Promise ( ( resolve , reject ) => {
2018-01-30 16:59:57 -08:00
2019-06-26 08:54:35 -07:00
if ( this . opts . host === '127.0.0.1' || this . opts . host === 'localhost' )
log . warn ( { method : 'connect' , line : 107 , msg : 'tcp consumer on same machine as host, use named Pipe(Unix) Socket Instead' } )
2018-01-08 13:06:01 -08:00
2019-09-13 18:59:22 -07:00
log . debug ( 'first connnect attempt for' , this . opts . id )
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'info' , msg : 'attempting initial connection' , id : this . id , opts : this . opts , ready : false } )
2019-04-11 20:57:45 -07:00
2019-11-21 09:35:09 -08:00
console . log ( 'TIMEOUT IN SOCKE CONNECT' , this . initTimeout )
2019-08-23 15:48:39 -07:00
let initTimeout = { }
if ( this . initTimeout > 499 ) {
initTimeout = setTimeout ( ( ) => {
2019-11-21 09:35:09 -08:00
clearTimeout ( this . retryPause )
this . emit ( 'status' , { level : 'error' , msg : 'initial connection timed out' , id : this . id , timeout : this . initTimeout , wait : this . retryWait , opts : this . opts , ready : false } )
2019-08-23 15:48:39 -07:00
this . removeAllListeners ( )
log . fatal ( { method : 'connect' , line : 69 , opts : this . opts , msg : ` unable to initially connect to ${ this . opts . name } in ${ this . initTimeout / 1000 } secs no more attempts! ` } )
this . stream . removeAllListeners ( )
reject ( { opts : this . opts , msg : ` unable to connect initially to socket server in ${ this . initTimeout / 1000 } secs, giving up no more attempts ` } )
}
, this . initTimeout )
2019-04-11 20:57:45 -07:00
}
2019-08-19 17:57:10 -07:00
const initialHandshake = async ( packet ) => {
2019-08-23 15:48:39 -07:00
2019-08-19 17:57:10 -07:00
if ( packet . _handshake ) {
clearTimeout ( initTimeout )
2019-08-23 15:48:39 -07:00
this . _connected = true
2019-08-28 09:02:12 -07:00
let authPacket = this . _authenticate ( ) || { }
2019-08-23 15:48:39 -07:00
authPacket . _authenticate = true
authPacket . clientName = this . id
let res = ( await this . _authenticateSend ( authPacket ) ) || { }
2019-08-28 09:02:12 -07:00
if ( ! res . authenticated ) {
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'info' , msg : ` authentication failed: ${ res . reason } ` , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-08-28 09:02:12 -07:00
reject ( 'unable to authenticate' )
}
2019-08-23 15:48:39 -07:00
else {
this . _authenticated = res . authenticated
this . removeListener ( 'error' , initialErrorHandler )
this . _listen ( ) // setup for active connection
log . info ( { method : 'connect' , line : 87 , msg : 'initial connect/authentication complete ready for communication' } )
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'info' , msg : 'authentication succesfull' , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-09-13 18:59:22 -07:00
this . emit ( 'consumer-connection' , { state : 'connected' , name : this . id } )
if ( this . opts . conPacket ) ( this . send ( this . conPacket ) )
2019-08-23 15:48:39 -07:00
resolve ( 'initial connection successful' )
}
2018-01-30 16:59:57 -08:00
}
2019-08-19 17:57:10 -07:00
}
2019-04-11 20:57:45 -07:00
2019-08-23 15:48:39 -07:00
2019-08-19 17:57:10 -07:00
const initialConnectHandler = async ( ) => {
this . on ( 'data' , this . stream . onData )
2019-08-23 15:48:39 -07:00
this . stream . once ( 'message' , initialHandshake . bind ( this ) )
2019-08-19 17:57:10 -07:00
log . debug ( { method : 'connect' , line : 113 , msg : 'connected waiting for socket ready handshake' } )
2019-11-21 09:35:09 -08:00
this . emit ( 'status' , { level : 'debug' , msg : 'consumer connected' } )
2019-08-19 17:57:10 -07:00
}
const initialErrorHandler = async ( err ) => {
2019-11-21 09:35:09 -08:00
let msg = { level : 'error' , method : 'connect' , line : 101 , error : err , msg : ` error during initial connect, trying again in ${ this . retryWait / 1000 } secs ` }
log . error ( msg )
this . emit ( 'status' , msg )
let connect = ( ) => { super . connect ( this . opts ) }
this . retryPause = setTimeout ( connect . bind ( this ) , this . retryWait )
2019-08-19 17:57:10 -07:00
}
2019-04-11 20:57:45 -07:00
2018-01-10 15:03:32 -08:00
2019-08-19 17:57:10 -07:00
this . once ( 'connect' , initialConnectHandler )
this . on ( 'error' , initialErrorHandler )
super . connect ( this . opts )
} ) // end initial promise
2018-01-08 13:06:01 -08:00
}
2018-02-13 13:51:58 -08:00
async send ( ipacket ) {
2019-01-01 16:53:12 -08:00
return new Promise ( async resolve => {
2019-08-23 15:48:39 -07:00
if ( ! this . _connected ) {
resolve ( { error : 'socket consumer not connected, aborting send' } )
}
2019-04-11 20:57:45 -07:00
let packet = Object . assign ( { } , ipacket ) // need to avoid mutuation for different consumers using same packet instance
setTimeout ( ( ) => { resolve ( { error : 'no response from socket in 10sec' } ) } , 10000 )
2019-01-01 16:53:12 -08:00
packet . _header = {
id : Math . random ( )
. toString ( )
. slice ( 2 ) , // need this for when multiple sends for different consumers use same packet instanceack
sender : { name : this . name , instanceID : this . id } ,
2018-02-13 13:51:58 -08:00
path : this . opts . path ,
port : this . opts . port ,
host : this . opts . host
}
2018-02-12 14:41:06 -08:00
let [ err , res ] = await btc ( this . stream . serialize ) ( packet )
2019-01-01 16:53:12 -08:00
if ( err )
2019-04-11 20:57:45 -07:00
resolve ( { error : 'unable to serialize packet for sending' , packet : packet } )
2018-07-31 09:45:32 -07:00
await this . _ _write ( res )
2019-01-01 16:53:12 -08:00
this . once ( packet . _header . id , async function ( reply ) {
2018-02-12 14:41:06 -08:00
let res = await this . _packetProcess ( reply )
2019-04-11 20:57:45 -07:00
if ( ! res ) { // if packetProcess was not promise
2018-02-12 14:41:06 -08:00
res = reply
2019-04-26 10:14:57 -07:00
log . debug ( { method : 'send' , line : 180 , msg : 'consumer function was not promise returning further processing may be out of sequence' } )
2018-02-13 13:51:58 -08:00
}
2019-08-19 17:57:10 -07:00
resolve ( res ) // resolves processed packet not return packet
2018-02-12 14:41:06 -08:00
} ) //end listener
} )
2018-01-25 18:07:45 -08:00
}
2019-04-11 20:57:45 -07:00
// TODO register user alt stream processor (emit 'message' with JSON, serialize function, onData method for raw socket chucks)
2018-01-30 16:59:57 -08:00
2019-01-01 16:53:12 -08:00
registerPacketProcessor ( func ) {
2018-02-03 13:33:25 -08:00
this . _packetProcess = func
2018-01-25 18:07:45 -08:00
}
2019-08-28 09:02:12 -07:00
// func should return an object the server expects
registerAuthenticator ( func ) {
this . _authenticate = func
}
2018-02-03 13:33:25 -08:00
// PRIVATE METHODS
2019-08-28 09:02:12 -07:00
// default authentication using a simple token
2019-08-23 15:48:39 -07:00
_authenticate ( ) {
2019-08-28 09:02:12 -07:00
return { token : process . env . UCI _CLIENT _TOKEN || this . token || 'default' }
2019-08-23 15:48:39 -07:00
}
2019-08-28 09:02:12 -07:00
async _authenticateSend ( authPacket = { } ) {
2019-08-23 15:48:39 -07:00
return new Promise ( async resolve => {
setTimeout ( ( ) => { resolve ( { error : 'no response from socket in 10sec' } ) } , 10000 )
let [ err , res ] = await btc ( this . stream . serialize ) ( authPacket )
if ( err )
resolve ( { error : 'unable to serialize packet for sending' , packet : authPacket } )
this . stream . on ( 'message' , ( res ) => {
this . stream . removeAllListeners ( 'message' )
resolve ( res )
} )
await this . _ _write ( res )
} )
}
2018-02-03 13:33:25 -08:00
2019-08-19 17:57:10 -07:00
// set up incoming message listening and error/reonnect handling
async _listen ( ) {
// Define Handlers and Other Functions
const reconnectHandler = ( ) => {
this . stream . once ( 'message' , handshake . bind ( this ) )
log . debug ( { method : 'connect' , line : 113 , msg : 'connected waiting for socket ready handshake' } )
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'info' , msg : 'attemping reconnect' , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-08-19 17:57:10 -07:00
}
const handshake = async ( packet ) => {
if ( packet . _handshake ) {
2019-08-23 15:48:39 -07:00
this . _connected = true
2019-08-28 09:02:12 -07:00
let authPacket = this . _authenticate ( ) || { }
2019-08-23 15:48:39 -07:00
authPacket . _authenticate = true
authPacket . clientName = this . id
let res = ( await this . _authenticateSend ( authPacket ) ) || { }
if ( ! res . authenticated ) {
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'error' , msg : ` authentication failed: ${ res . reason } ` , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-08-28 09:02:12 -07:00
this . emit ( 'error' , { code : 'authentification failed' } )
2019-08-23 15:48:39 -07:00
}
else {
this . _authenticated = res . authenticated
log . info ( { method : 'connect' , line : 87 , msg : 'initial connect/authentication complete ready for communication' } )
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'info' , msg : 'authentication successful' , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-08-23 15:48:39 -07:00
if ( this . keepAlive ) { // only attempt reconnect if keepAlive is set which it is by default
this . on ( 'ping' , pingHandler )
this . setKeepAlive ( this . keepAlive , 3000 ) // keep connection alive unless disabled
}
this . stream . on ( 'message' , messageHandler . bind ( this ) ) // reset default message handler
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'info' , msg : 'reconnected' , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-09-13 18:59:22 -07:00
this . emit ( 'consumer-connection' , { state : 'reconnected' , name : this . id } )
if ( this . opts . conPacket ) ( this . send ( this . conPacket ) )
2019-08-19 17:57:10 -07:00
}
2018-02-12 14:41:06 -08:00
}
2019-08-19 17:57:10 -07:00
}
2018-02-12 14:41:06 -08:00
2019-08-19 17:57:10 -07:00
const errorHandler = async ( err ) => {
log . debug ( { msg : 'connection error emitted ' , error : err } )
2019-08-23 15:48:39 -07:00
this . _connected = false
this . _authenticated = false
2019-09-08 19:49:05 -07:00
this . emit ( 'status' , { level : 'error' , msg : 'connection error' , id : this . id , opts : this . opts , authenticated : this . _authenticated , connected : this . _connected } )
2019-08-23 15:48:39 -07:00
log . debug ( { method : 'connect' , line : 130 , error : err . code , msg : ` connect error ${ err . code } , attempting reconnect after ${ this . retryWait / 1000 } secs ` } )
2019-09-13 18:59:22 -07:00
this . emit ( 'consumer-connection' , { state : 'disconnected' , name : this . id } )
2019-08-23 15:48:39 -07:00
await pause ( this . retryWait )
this . stream . removeAllListeners ( 'message' ) // remove regular message handler in prep for reconnect
this . removeAllListeners ( 'connect' )
this . removeAllListeners ( 'ping' )
this . once ( 'connect' , reconnectHandler )
super . connect ( this . opts )
2019-08-19 17:57:10 -07:00
}
const pushHandler = async ( packet ) => {
2018-05-24 12:28:30 -07:00
// TODO do some extra security here?
2019-08-19 17:57:10 -07:00
log . debug ( 'packed was pushed from socket sever, processing' , packet )
2018-05-24 12:28:30 -07:00
let res = await this . _packetProcess ( packet )
2019-01-01 16:53:12 -08:00
if ( ! res ) {
2019-04-11 20:57:45 -07:00
// if process was not promise returning then res will be undefined
2019-04-26 10:14:57 -07:00
log . debug ( 'consumer packet processing function was not promise returning' )
2018-05-24 12:28:30 -07:00
}
2019-08-19 17:57:10 -07:00
}
const pingHandler = async ( packet ) => {
clearTimeout ( pingTimeout )
2019-09-13 18:59:22 -07:00
log . trace ( { method : 'connect' , line : 191 , msg : 'received ping, restting timeout' } )
2019-08-19 17:57:10 -07:00
this . _pingTimeout = packet . pingInterval + 1000
monitorPing . call ( this )
}
let pingTimeout = { }
function monitorPing ( ) {
pingTimeout = setTimeout ( ( ) => {
log . error ( { method : 'connect' , line : 142 , msg : 'socket (server) not availabe' } )
this . removeAllListeners ( 'ping' )
2019-08-23 15:48:39 -07:00
this . _connected = false
2019-08-19 17:57:10 -07:00
this . emit ( 'error' , { code : 'PING-FAILED' } )
} , this . _pingTimeout )
}
2019-08-23 15:48:39 -07:00
// general handler
function messageHandler ( packet ) {
2019-09-13 18:59:22 -07:00
if ( packet . _header . id !== 'ping' ) log . debug ( 'incoming packet from socket sever' , packet )
2019-08-23 15:48:39 -07:00
this . emit ( packet . _header . id , packet )
}
2019-08-19 17:57:10 -07:00
// Start Message Listening and Error/Reconnect Handling
log . debug ( 'listening for incoming packets from socket' )
2019-08-23 15:48:39 -07:00
this . stream . on ( 'message' , messageHandler . bind ( this ) )
2019-08-19 17:57:10 -07:00
this . setKeepAlive ( this . keepAlive , 3000 ) // keep connection alive unless disabled
this . on ( 'pushed' , pushHandler )
this . on ( 'error' , errorHandler )
if ( this . keepAlive ) { // keepAlive also activates ping Monitor
this . on ( 'ping' , pingHandler )
}
}
2018-05-24 12:28:30 -07:00
2019-08-19 17:57:10 -07:00
async _ _write ( packet ) {
// timeout already set if sockect can't be drained in 10 secs
return new Promise ( resolve => {
const cb = ( ) => resolve ( 'packet written to consumer side socket stream ' )
if ( ! super . write ( packet ) ) {
this . once ( 'drain' , cb )
} else {
process . nextTick ( cb )
2019-01-01 16:53:12 -08:00
}
2019-08-19 17:57:10 -07:00
} )
2018-02-01 16:58:17 -08:00
}
2018-02-12 14:41:06 -08:00
// default packet process just a simple console logger. ignores any cmd: prop
2019-01-01 16:53:12 -08:00
_packetProcess ( packet ) {
2019-04-11 20:57:45 -07:00
console . log ( 'default consumer processor -- log packet from socket to console' )
console . log ( 'replace by calling .registerPacketProcessor(func) with your function' )
2018-02-03 13:33:25 -08:00
console . dir ( packet )
}
2019-08-19 17:57:10 -07:00
2019-01-01 16:53:12 -08:00
} // end class
2018-02-03 13:33:25 -08:00
2019-01-01 16:53:12 -08:00
export default SocketConsumer