/* jshint undef: true, unused: true */
/* globals Promise */
var net = require('net');
var tls = require('tls');
var events = require('events');
var crypto = require('crypto');
var util = require('util');
var dbg = require('debug');
var utils = require('./utils');
var Channel = require('./channel');
var Trap = require('./trap');
module.exports = (function() {
var debugSocket = dbg('mikronode:socket');
var debugSocketData = dbg('mikronode:socket:data');
var debugLogin = dbg('mikronode:login');
var debugSentence = dbg('mikronode:sentence');
var debugConnection = dbg('mikronode:connection');
var debugPromise = dbg('mikronode:promise');
var _ = require('private-parts').createKey();
var emptyString = String.fromCharCode(0);
/**
* Emitted when a non-recoverable error has occurred on the socket. No further commands
* can be processed on any channel.
* @event mikronode.Connection#event:error
* @property {error} error - The error object
* @property {mikronode.Connection} connection - The connection originating the event
*/
/**
* Emitted when a socket has been idle too long.
* @event mikronode.Connection#event:timeout
* @property {string} message - 'Socket Timeout'
* @property {boolean} socketStillOpen - If true, communications can continue
* @property {mikronode.Connection} connection - The connection originating the event
*/
/**
* Emitted when the connection is closed either by an explicit call to
* {@link mikronode.Connection#close} or when the connection is closed automatically
* via {@link mikronode.Connection#closeOnDone}
* @event mikronode.Connection#event:close
* @property {mikronode.Connection} connection - The connection originating the event
*/
/**
* Emitted when a login has failed. No further commands can be processed on any
* channel.
* @event mikronode.Connection#event:trap
* @property {mikronode.Trap} trap - The trap object
*/
/**
* <strong>An instance of Connection is fully self-contained. You can have as many open
* in parallel in the same node process as your environment can handle.</strong>
* <p>
* @exports mikronode.Connection
* @class
* @implements {EventEmitter}
* @param {string} host - The host name or ip address
* @param {string} user - The user name
* @param {string} password - The users password
* @param {object} [options]
* @param {number} [options.port=8728] - Sets the port if not the standard 8728 (8729
* for TLS).
* @param {boolean} [options.closeOnDone=false] - If set, when the last channel closes,
* the connection will automatically close.
* @param {number} [options.timeout=0] - Sets the socket inactivity timeout. A timeout
* does not necessarily mean that an error has occurred, especially if you're
* only listening for events.
* @param {boolean} [options.closeOnTimeout=false] - If set, when a socket timeout
* happens the connection will automatically close.
* @param {(object|boolean)} [options.tls] - Set to true to use TLS for this
* connection. Set to an object to use TLS and pass the object to tls.connect
* as the tls options. If your device uses self-signed certificates, you'll
* either have to set 'rejectUnauthorized : false' or supply the proper CA
* certificate. See the options for
* {@link https://nodejs.org/api/tls.html#tls_tls_connect_port_host_options_callback|tls.connect()}
* for more info.
* @fires mikronode.Connection#event:trap
* @fires mikronode.Connection#event:error
* @throws <strong>WARNING: If you do not listen for 'error' or 'timeout' events and
* one occurrs during the initial connection (host unreachable, connection
* refused, etc.), an "Unhandled 'error' event" exception will be thrown.</strong>
* @fires mikronode.Connection#event:timeout
* @fires mikronode.Connection#event:close
*
* @example
*
* <pre>
* var MikroNode = require('mikronode');
*
* var connection = new MikroNode.Connection('192.168.88.1', 'admin', 'mypassword', {
* timeout : 4,
* closeOnDone : true,
* closeOnTimeout : true,
* });
*
* connection.on('error', function(err) {
* console.error('Error: ', err);
* });
*
* </pre>
*
*/
function Connection(host, user, password, options) {
this.hash = crypto.createHash('md5').update(host + user).digest('hex');
// If we already have a connection, return the same one.
options = options || {};
// if (api._conn[this.hash]) return api._conn[this.hash];
/**
* @public
* @readonly
* @instance
* @member {string} host - Hostname or ip address
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'host', host, false, _);
/**
* @public
* @readonly
* @instance
* @member {string} user - User ID
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'user', user, false, _);
/**
* @public
* @readonly
* @instance
* @member {string} password - Password
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'password', password, false, _);
/**
* @public
* @readonly
* @instance
* @member {number} [port=8728] - Port
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'port', options.port || (options.tls ? 8729 : 8728), false, _);
/**
* @public
* @readonly
* @instance
* @member {number} [timeout=0] - Socket inactivity timeout
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'timeout', options.timeout, false, _);
/**
* @public
* @readonly
* @instance
* @member {(object|boolean)} [tls] - Set to true to use TLS for this connection
* with default options. Set to an object to use TLS and pass the object to
* tls.connect as the tls options. If your device uses self-signed
* certificates, you'll either need to set 'rejectUnauthorized : false' or
* supply the proper CA certificate. See the options for
* {@link https://nodejs.org/api/tls.html#tls_tls_connect_port_host_options_callback|tls.connect()}
* for more info.
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'tls', options.tls, false, _);
/**
* @public
* @readonly
* @instance
* @member {string} status - Connection status
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'status', 'New', false, _);
/**
* @public
* @readonly
* @instance
* @member {object} channels - Active channels keyed by channel id
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'channels', {}, false, _, true);
/**
* @public
* @instance
* @member {boolean} [closeOnDone=false] - If set, when the last channel closes, the
* connection will automatically close.
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'closeOnDone', !!options.closeOnDone, true, _);
/**
* @public
* @instance
* @readonly
* @member {boolean} [options.closeOnTimeout=false] - If set, when a socket timeout
* happens the connection will automatically close.
* @memberof mikronode.Connection
*/
utils.createProperty(this, 'closeOnTimeout', !!options.closeOnTimeout, true, _);
/* The following properties are all private */
_(this).connected = false;
_(this).connecting = false;
_(this).socket = null; // socket connection
_(this).line = ''; // current line. When the line is built, the sentence event is called.
_(this).buffer = []; // buffer holding incoming stream from socket
_(this).packet = []; // current packet
_(this).currentChannelId = -1;
_(this).currentReply = '';
_(this).currentProgress = '';
_(this).traps = {}; // we encountered a trap.
_(this).error = {}; // Buffer errors
_(this).datalen = 0; // Used to look-ahead to see if more data is available
_(this).connectionCallback = null;
}
util.inherits(Connection, events.EventEmitter);
/**
* Parse !re return records into an array of objects
* @function
* @static
* @param {string[]} data - The data[] returned from Channel.on('done')
* @returns {object[]}
*/
Connection.parseItems = function parseItems(data) {
var db = [];
var idx = 0;
var record = {};
// util.puts('parseItems: '+JSON.stringify(data));
data.forEach(function(data) {
while (data.length) {
var l = data.shift().split(/=/);
if (l[0] === '!re') {
if (db.length) {
record = {};
}
db.push(record);
idx++;
continue;
}
l.shift(); // remove empty first element
record[l.shift()] = l.join('='); // next element is key. All the
// rest is value.
}
if (data.length === 1 && (data[0] !== record)) {
db.push(record);
}
});
return db;
};
/**
* Triggered by the 'sentence' event
* @private
* @param {string} data - Sentence
* @param {boolean} more - There's data left to read
* @this mikronode.Connection
*/
Connection.prototype.sentence = function sentence(data, more) {
debugSentence('Sentence2:(' + more + ') data: ' + data);
if (_(this).currentReply === '!fatal') { // our last message was a fatal error.
_(this).packet.push(data);
this.emit('fatal', _(this).packet, this);
if (!_(this).closing) {
this.close();
}
return;
} else if (data === '!fatal') {
_(this).currentReply = data;
} else if (data === '!re') {
_(this).currentReply = data;
_(this).currentProgress = '';
_(this).buffer[_(this).buffer.length] = data;
} else if (data.match(/\.tag/)) {
var tagChannelId = data.substring(5);
_(this).currentChannelId = tagChannelId;
if ((((_(this).currentProgress === '!done' && (_(this).currentReply === '!re'))) || !more)) {
debugSentence('Sentence2: Done channel %s Trapped? %o', tagChannelId, !!_(this).traps[tagChannelId]);
_(this).packet = _(this).buffer; // backup up the packet
_(this).buffer = [];
if (_(this).channels[tagChannelId]) {
_(this).channels[tagChannelId]._done(_(this).packet, _(this).traps[tagChannelId]);
_(this).currentChannelId = -1;
}
} else if (_(this).currentProgress === '!trap') {
if (_(this).traps[tagChannelId]) {
debugSentence('Sentence2: caught second trap');
_(this).traps[tagChannelId].addTrapError();
}
debugSentence('Sentence2: caught a trap for channel %s ', tagChannelId);
var trap = new Trap();
trap.channelId = tagChannelId;
trap.channel = _(this).channels[tagChannelId];
_(this).traps[tagChannelId] = trap;
} else if (_(this).currentReply === '!re') {
_(this).currentChannelId = tagChannelId;
}
} else if (data === '!done') {
debugSentence('Sentence2: Done Signal.');
_(this).currentProgress = data;
if (!more) {
_(this).packet = _(this).buffer;
_(this).buffer = [];
debugSentence('Sentence2: No more data in packet. Done.');
if (Object.keys(_(this).traps).length > 0) {
this.emit('trap', _(this).traps, this);
_(this).trap = false;
} else {
this.emit('done', _(this).packet);
}
}
} else if (/=ret=/.test(data)) {
debugSentence('Sentence2: Single return: ' + data);
_(this).buffer.push('!re');
_(this).buffer.push(data);
_(this).packet = _(this).buffer;
_(this).buffer = [];
if (!more) {
if (_(this).channels[_(this).currentChannelId]) {
_(this).channels[_(this).currentChannelId]
._done(_(this).packet, _(this).traps[_(this).currentChannelId]);
_(this).currentChannelId = -1;
}
}
} else if (data === '!trap') {
_(this).currentProgress = data;
_(this).buffer[_(this).buffer.length] = data;
} else {
if (_(this).currentProgress === '!trap') {
var m = data.match(/^=(category|message)=(.+)/);
if (m) {
var ct = _(this).traps[_(this).currentChannelId];
ct.errors[ct.errors.length - 1][m[1]] = m[2];
}
}
debugSentence('Sentence2: adding data: ' + data);
_(this).buffer[_(this).buffer.length] = data;
if (!more && _(this).currentReply === '!re' && (((_(this).currentChannelId >= 0)))) {
_(this).packet = _(this).buffer;
_(this).buffer = [];
_(this).channels[_(this).currentChannelId]._data(_(this).packet);
_(this).currentChannelId = -1;
}
}
};
/**
* Triggered by a 'data' event on teh socket
* @private
* @param {string} data - Sentence
* @this mikronode.Connection
*/
Connection.prototype._read = function _read(data) {
if (debugSocketData.enabled) {
utils.hexDump(data, debugSocketData);
}
while (data.length) {
debugSocket('read: data-len:' + data.length);
if (_(this).len) { // maintain the current data length. What if the data
// comes in 2 separate packets?
// I am hopping that the API on the other end doesn't send more than
// one channel
// at a time if more than one packet is required.
// if (this.debug>3) debug('read: data:'+data);
if (data.length <= _(this).len) {
_(this).len -= data.length;
_(this).line += data.toString();
debugSocketData('read:consume-all: data:' + data);
if (_(this).len === 0) {
this.sentence(_(this).line, (data.length !== _(this).len));
_(this).line = '';
}
break;
} else {
debugSocketData('read:consume len:(' + _(this).len + ') data: ' + data);
_(this).line += data.toString('utf8', 0, _(this).len);
var l = _(this).line;
_(this).line = '';
data = data.slice(_(this).len);
var x = utils.decodeLength(data);
_(this).len = x[1];
data = data.slice(x[0]); // get rid of excess buffer
if (_(this).len === 1 && data[0] === "\x00") {
_(this).len = 0;
data = data.slice(1); // get rid of excess buffer
}
this.sentence(l, data.length);
}
} else {
var y = utils.decodeLength(data);
_(this).len = y[1];
data = data.slice(y[0]);
if (_(this).len === 1 && data[0] === "\x00") {
_(this).len = 0;
data = data.slice(1); // get rid of excess buffer
}
}
}
};
/**
* Send data
* @private
* @param {string} data - Sentence
* @this mikronode.Connection
*/
Connection.prototype._write = function write(data) {
var _this = this;
if (!_(this).connected && !_(this).connecting) {
debugSocket('write: not connected ');
return;
}
if (typeof (data) === 'string') {
data = [ data ];
} else if (!Array.isArray(data)) {
return;
}
data.forEach(function(i) {
debugSocket('write: sending ' + i);
_(_this).socket.write(utils.encodeString(i));
});
_(this).socket.write(emptyString);
};
/**
* Called when the connection is established and authenticated
* @callback mikronode.Connection.connectCallback
* @param {mikronode.Connection}
*/
/**
* Opens the socket and performs authentication
* @param {mikronode.Connection.connectCallback} callback - Called when authentication
* succeeds and the connection is ready for channel activity
* @returns {mikronode.Connection}
* @fires mikronode.Connection#event:trap
* @fires mikronode.Connection#event:error
* @throws <strong>WARNING: If you do not listen for 'error' or 'timeout' events and an
* error occurrs during the initial connection (host unreachable, connection
* refused, etc.), an "Unhandled 'error' event" exception will be thrown.</strong>
* @fires mikronode.Connection#event:timeout
* @fires mikronode.Connection#event:close
*/
Connection.prototype.connect = function connect(callBack) {
if (_(this).connected) {
return;
}
var _this = this;
_(this).connectionCallback = callBack;
_(this).status = "Connecting";
var tempSocket = new net.Socket({
type : 'tcp4'
});
if (_(this).tls) {
var tlsopts = ((typeof _(this).tls === 'boolean') ? {} : _(this).tls);
_(this).socket = new tls.TLSSocket(tempSocket, tlsopts);
} else {
_(this).socket = tempSocket;
}
debugSocket('Created%s socket to %s:%d', _(this).tls ? ' TLS' : '', _(this).host, _(this).port);
_(this).connecting = true;
_(this).socket.on('data', function(a) {
_this._read(a);
});
_(this).socket.on('error', function(a) {
debugSocket('Connection error: ' + a);
_(_this).socket.destroy();
_(_this).connected = false;
_this.emit('error', a, _this);
_this.emit('close', _this);
_this.removeAllListeners();
});
/**
* timeout is triggered if the socket goes inactive for a set period of time. This
* may be normal if we're listening for events that may not occur often.
*/
if (_(this).timeout) {
debugConnection('Setting timeout to %d seconds', _(this).timeout);
/**
* Calling setTimeout with a callback is safer than socket.on('timeout') for TLS
* sockets
*/
_(this).socket.setTimeout(_(this).timeout * 1000, function() {
debugSocket('Timeout');
if (_(_this).closeOnTimeout) {
_this.emit('timeout', 'Socket Timeout', false, _this);
_(_this).socket.destroy();
_(_this).connected = false;
_this.emit('close', _this);
_this.removeAllListeners();
} else {
_this.emit('timeout', 'Socket Timeout', true, _this);
}
});
}
_(this).socket.setKeepAlive(true);
/**
* Setup is done. Call connect on the NON TLS socket to start the login state
* machine. _this.loginStateMachine() can't be passed directly to connect as the
* callback because it's 'this' will be the socket instead of our connection.
*/
tempSocket.connect(_(this).port, _(this).host, function lsm() {
_this.loginStateMachine();
});
return this;
};
Connection.prototype.loginStateMachine = function loginStateMachine() {
/**
* We save the value of closeOnDone as set by the caller, force it to false, then
* restore it after successful login. Otherwise, if set set to true, when the login
* channel closes, the connection would also be closed and be of no further use.
*/
var tempcOD = _(this).closeOnDone;
_(this).closeOnDone = false;
var lc = this.openChannel('login');
/**
* lc.closeOnDone has to be false because we're expecting multiple done events.
*/
lc.closeOnDone = false;
_(this).status = 'Sending Login';
debugLogin(_(this).status);
lc.write('/login');
_(this).status = 'Waiting for challenge';
debugLogin(_(this).status);
var _this = this;
lc.on('trap', function loginTrap(data) {
/** @this is now mikronode.Channel */
_(_this).status = 'Received Trap';
debugLogin('%s %o', _(_this).status, data);
_this.emit('trap', data);
this.close(true);
_(_this).closeOnDone = true;
});
lc.on('done', function loginDone(data) {
/** @this is now mikronode.Channel */
var parsed = Connection.parseItems(data);
switch (_(_this).status) {
case 'Waiting for challenge':
_(_this).status = 'Received challenge';
debugLogin('%s %o', _(_this).status, parsed);
var challenge = '';
var a = parsed[0].ret.split('');
while (a.length) {
challenge += String.fromCharCode(parseInt("0x" + a.shift() + a.shift()));
}
if (challenge.length !== 16) {
this.emit('trap', 'Bad challenge: %o Challenge length: %d', data, challenge.length);
return;
}
_(_this).status = 'Sending Credentials';
debugLogin('%s %s', _(_this).status, _(_this).user);
this.write('/login', {
"=name" : _(_this).user,
"=response" : "00"
+ crypto.createHash('md5').update(emptyString + _(_this).password + challenge).digest("hex")
});
_(_this).status = 'Waiting for Response';
debugLogin(_(_this).status);
return;
case 'Waiting for Response':
_(_this).status = 'Received Response';
debugLogin('%s %o', _(_this).status, parsed);
this.close(true);
_(_this).status = 'Connected';
debugLogin(_(_this).status);
_(_this).connected = true;
if (_(_this).connectionCallback) {
_(_this).connectionCallback(_this);
_(_this).connectionCallback = null;
}
_(_this).closeOnDone = tempcOD;
}
});
};
/**
* Opens a new Channel
* @public
* @param {string} [id=next available] - Automatically assigned ids are numbers but you
* can specify any string.
* @returns {mikronode.Channel}
*/
Connection.prototype.openChannel = function openChannel(id) {
var _this = this;
if (!id) {
id = Object.keys(_(this).channels).length + 1;
while (_(this).channels[id]) {
id++;
}
} else if (_(this).channels[id]) {
throw ('Channel already exists for ID ' + id);
}
debugConnection('Opening channel: ' + id);
_(this).channels[id] = new Channel(id, this);
_(this).channels[id].addListener('close', function(channel) {
_this.closeChannel(channel.id);
});
return _(this).channels[id];
};
/**
* Returns the channel specified by id.
* @public
* @param {number} id - The id of the channel desired
* @returns {mikronode.Channel}
*/
Connection.prototype.getChannel = function getChannel(id) {
if (!id && id !== 0) {
throw ('Missing channel ID parameter' + id);
}
if (!_(this).channels[id]) {
throw ('Channel does not exist for ID ' + id);
}
debugConnection('Getting channel: ' + id);
return _(this).channels[id];
};
/**
* Closes the channel specified by id.
* @public
* @param {number} id - The id of the channel to close
*/
Connection.prototype.closeChannel = function closeChannel(id) {
if (!id) {
throw ("Missing ID for stream channel to close.");
}
if (!_(this).channels[id]) {
throw ('Channel does not exist for ID ' + id);
}
// Make sure that the channel closes itself... so that remaining
// commands will execute.
if (!_(this).channels[id].closed) {
return _(this).channels[id].close();
}
delete _(this).channels[id];
var channelsLeft = Object.keys(_(this).channels).length;
debugConnection('Closing channel: %s Channels left: %d', id, channelsLeft);
if (channelsLeft === 0 && ((_(this).closing || _(this).closeOnDone))) {
debugConnection('Last channel closed');
this.close();
}
};
Connection.prototype.close = function close(force) {
var _this = this;
if (!_(this).connected) {
debugConnection('Connection already disconnected: ' + _(this).host);
_(this).socket.destroy();
_(this).connected = false;
this.removeAllListeners();
this.emit('close', this);
this.removeAllListeners();
return;
}
if (!force && ((Object.keys(_(this).channels).length > 0))) {
_(this).closing = true;
debugConnection('deferring closing connection');
return;
}
debugConnection('Connection disconnecting: ' + _(this).host);
this.removeAllListeners('done');
this.removeAllListeners('error');
this.removeAllListeners('timeout');
if (force) {
Object.keys(_(this).channels).forEach(function(e) {
_(_this).channel[e].close(true);
});
}
this.once('fatal', function() { // quit command ends with a fatal.
debugConnection('Connection disconnected: ' + _(this).host);
_(_this).socket.destroy();
_(_this).connected = false;
_this.removeAllListeners();
_this.emit('close', _this);
});
_(this).closing = false;
// delete api._conn[_(this).hash];
this._write([ '/quit' ]);
_(this).closing = true;
};
Connection.prototype.finalize = function finalize() {
_(this).close(true);
};
/**
* Returns a Promise for an open connection.
* <p>
* The promise will resolve when the connection is ready for use or reject if there's
* an error or trap. If resolved, the result object will be the
* {@link mikronode.Connection} with authentication completed and ready for channels.
* If rejected, the result object will be an Error if there was a socket error or
* timeout during connection or login or a {@link mikronode.Trap} if there was a
* problem with the login credentials.
* <p>
* @returns {Promise}
*
* @example
*
* <pre>
* var MikroNode = require('mikronode');
*
* var connection = new MikroNode.Connection(process.argv[2], process.argv[3], process.argv[4], {
* closeOnDone : true
* });
*
* var connPromise = connection.getConnectPromise().then(function resolve(conn) {
* // You now have an open, authenticated connection
* // To issue some commands see {@link mikronode.Connection#getCommandPromise getCommandPromise}
* });
* </pre>
*/
Connection.prototype.getConnectPromise = function connectPromise() {
var _this = this;
return new Promise(function(resolve, reject) {
try {
_this.on('error', function(err) {
debugConnection('Error: %o', err);
reject(err);
_this.close();
});
_this.on('trap', function(err) {
debugConnection('Trap: %o', err);
reject(err);
_this.close();
});
_this.on('timeout', function(err) {
debugConnection('Timeout: %o', err);
reject(err);
_this.close();
});
_this.connect(function connect(connection) {
debugConnection('Resolved');
resolve(connection);
});
} catch (err) {
reject(err);
}
});
};
/**
* ** Returnes a Promise of a completed command.
* <p>
* The promise will resolve when the command completes or reject if there's an error or
* trap. If resolved, the result will be an array of instances of DestinationClass (or
* Object, if no destination class was specified). If rejected, the result will be an
* Error if there was a socket error or timeout, or a {@link mikronode.Trap} if the
* command failed on the device.
*
* @param {(string|string[])} data - Can be a single string with the command and
* optional parameters separated by '\n' or an array of strings with the
* command in the first position and the parameters in the rest.
* @param {(object|string[])} [parameters] - If the first parameter is a command
* string, this object will be treated as the parameters for the command.
* <p>
* It can be an array or strings...
*
* <pre>
* ['name=value','name=value'...]
* </pre>
*
* or an Object...
*
* <pre>
* {'name': 'value', 'name': 'value'...}
* </pre>
*
* @param {object} [options] - A set of options that determine what to do with the
* return data (if any). If neither dataClass nor itemClass are provided, the
* default behavior will be as though itemClass were set to Object. This will
* result in Promise.resolve() being called with an array of plain Objects,
* one for each parsed item.
* @param {boolean} [options.closeOnDone=true] - If true, the channel will
* automatically close when the command completes.
* @param {boolean} [options.dontParse=false] - If true, Promise.resolve() will be
* called with the unaltered data array provided by the channel's 'done'
* event. with the unaltered data array provided by the channel's 'done'
* event.
* @param {class} [options.dataClass] - If provided, this class will be instantiated
* with the data array provided by the channel's 'done' event as the
* constructor's sole argument. Promise.resolve() will then be called with
* this object.
* @param {class} [options.itemClass] - If provided, {mikronode.parseItems} will be
* called on the returned data and this class will be instantiated once for
* each resulting item. The item object will be passed as the sole argument
* to the constructor. An array of itemClass objects will be passed to
* Promise.resolve().
* @param {string} [options.itemKey] - If provided, instead of an array of parsed
* objects being passed to Promise.resolve(), the parsed objects will be
* added to a wrapper object using the value of itemKey as the property name.
*
* @return {Promise} The promise will have a channel property added which will be set
* to the channel used to fulfill the promise.
*
* @example
*
* <pre>
*
* function Interface(intf) {
* var _this = this;
* Object.keys(intf).forEach(function(key) {
* _this[key] = intf[key];
* });
* }
*
* var chan1Promise = conn.getCommandPromise('/interface/print', {
* itemClass : Interface,
* itemKey : 'name'
* });
*
* chan1Promise.then(function(values){
* // It succeeded. You'll have a hash of Interfaces keyed by interface name.
* });
*
* chan1Promise.catch(function(result){
* // It failed. result will tell you why.
* });
* </pre>
*/
Connection.prototype.getCommandPromise = function commandPromise(data, parameters, options) {
var _this = this;
debugConnection('getCommandPromise');
if (parameters
&& !Array.isArray(parameters)
&& (parameters.hasOwnProperty('closeOnDone') || parameters.hasOwnProperty('dontParse')
|| parameters.dataClass || parameters.itemClass || parameters.itemKey)) {
options = parameters;
parameters = null;
}
options = options || {};
var chan = _this.openChannel();
chan.closeOnDone = options.hasOwnProperty('closeOnDone') ? options.closeOnDone : true;
var p1 = new Promise(function(resolve, reject) {
try {
chan.write(data, parameters, function() {
chan.on('error', function(err) {
debugPromise('Channel %d error: %o', chan.id, err);
chan.close();
reject(err);
});
chan.on('trap', function(err) {
debugPromise('Channel %d trap: %o', chan.id, err);
chan.close();
reject(err);
});
chan.on('timeout', function(err) {
debugPromise('Channel %d timeout', chan.id);
chan.close();
reject(err);
});
chan.on('done', function chanDone(data) {
debugPromise('Channel %d done: %o', chan.id, data);
if (options.dontParse) {
resolve(data);
return;
}
if (typeof options.dataClass === 'function') {
resolve(new options.dataClass(data));
return;
}
var items;
if (options.itemKey) {
items = {};
} else {
items = [];
}
var parsed = Connection.parseItems(data);
parsed.forEach(function(item) {
var o;
if (typeof options.itemClass === 'function') {
o = new options.itemClass(item);
} else {
o = {};
Object.keys(item).forEach(function(k) {
o[k] = item[k];
});
}
if (options.itemKey) {
items[item[options.itemKey]] = o;
} else {
items.push(o);
}
});
resolve(items);
});
});
} catch (err) {
reject(err);
}
});
p1.channel = chan;
return p1;
};
return Connection;
})();