usocket
Version:
unix local sockets with descriptor passing
312 lines (254 loc) • 7.2 kB
JavaScript
"use strict";
var util = require('util');
var stream = require('stream');
var EventEmitter = require('events');
var uwrap = require('bindings')('uwrap');
var debug = require('debug')("usocket");
//-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
//--
exports.USocket = USocket;
function USocket(opts, cb) {
if (!opts || typeof opts !== 'object')
opts = { path: opts };
var duplexOpts = { writableObjectMode: true };
if ("allowHalfOpen" in opts)
duplexOpts = opts.allowHalfOpen;
stream.Duplex.call(this, duplexOpts);
debug("new USocket", opts);
if (opts.fd || opts.path)
this.connect(opts, cb);
}
util.inherits(USocket, stream.Duplex);
USocket.prototype._read = function(size) {
debug("USocket._read", size);
if (this._wrap)
this._wrap.resume();
};
USocket.prototype._write = function(chunk, encoding, callback) {
if (!this._wrap)
return callback(new Error("USocket not connected"));
var data, fds, cb;
if (Buffer.isBuffer(chunk)) {
data = chunk;
}
else if (Array.isArray(chunk)) {
fds = chunk;
}
else {
cb = chunk.callback;
data = chunk.data;
fds = chunk.fds;
}
if (data && !Buffer.isBuffer(data))
return callback(new Error("USocket data needs to be a buffer"));
if (fds && !Array.isArray(fds))
return callback(new Error("USocket fds needs to be an array"));
if (cb && typeof cb !== 'function')
return callback(new Error("USocket write callback needs to be a function"));
if (!data && !fds)
return callback(new Error("USocket write needs a data or array"));
debug("USocket._write", data && data.length, fds);
var r = this._wrap.write(data, fds);
if (util.isError(r)) {
debug("USocket._write error", r);
return callback(r);
}
else if (!data || r == data.length) {
if (cb) cb(chunk);
return callback();
}
debug("USocket._write waiting");
this._wrap.drain = this._write.bind(this,
{ data: data.subarray(r), callback: cb }, encoding, callback);
};
USocket.prototype.connect = function(opts, cb) {
if (this._wrap)
throw new Error("connect on already connected USocket");
if (typeof opts === 'string') {
opts = { path: opts };
}
if (typeof cb === 'function')
this.once('connected', cb);
debug("USocket connect", opts);
this._wrap = new uwrap.USocketWrap(this._wrapEvent.bind(this));
this._wrap.shutdownCalled = false;
this._wrap.endReceived = false;
this._wrap.drain = null;
this._wrap.fds = [];
if (typeof opts.fd === 'number') {
this._wrap.adopt(opts.fd);
this.fd = opts.fd;
return;
}
if (typeof opts.path !== 'string')
throw new Error("USocket#connect expects string path");
this._wrap.connect(opts.path);
};
USocket.prototype._wrapEvent = function(event, a0, a1) {
if (event === "connect") {
this.fd = a0;
debug("USocket connected on " + this.fd);
this.emit('connected');
}
if (event === "error") {
debug("USocket error", a0);
this._wrap.close();
this._wrap = null;
this.emit("error", a0);
this.emit('close', a0);
}
if (event === "data") {
if (a1 && a1.length > 0) {
debug("USocket received file descriptors", a1);
this._wrap.fds = this._wrap.fds.concat(a1);
this.emit('fds', a1);
}
if (a0) {
if (!this.push(a0))
this._wrap.pause();
}
if (a1 && !a0) {
this.emit('readable');
}
if (!a0 && !a1 && !this._wrap.endReceived) {
debug("USocket end of stream received");
this._wrap.endReceived = true;
this._wrap.pause();
this.push(null);
this.maybeClose();
}
}
if (event === "drain") {
var d = this._wrap.drain;
this._wrap.drain = null;
if (d) d();
}
};
USocket.prototype.read = function(size, fdSize) {
if (!this._wrap) return null;
if (typeof fdSize === 'undefined')
return stream.Duplex.prototype.read.apply(this, arguments);
if (fdSize === null)
fdSize = this._wrap.fds.length;
else if (this._wrap.fds.length < fdSize)
return null;
var data = stream.Duplex.prototype.read.call(this, size);
if (size && !data) return data;
var fds = this._wrap.fds.splice(0, fdSize);
return { data: data, fds: fds };
};
USocket.prototype.unshift = function(chunk, fds) {
if (chunk) {
stream.Duplex.prototype.unshift.call(this, chunk);
}
if (Array.isArray(fds) && this._wrap) {
while (fds.length > 0)
this._wrap.fds.unshift(fds.pop);
}
};
USocket.prototype.write = function(chunk, encoding, callback) {
if (typeof encoding === 'function') {
callback = encoding;
encoding = null;
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk);
}
if (Buffer.isBuffer(chunk)) {
chunk = { data: chunk };
}
stream.Duplex.prototype.write.call(this, chunk, null, callback);
};
USocket.prototype.end = function(data, encoding, callback) {
stream.Duplex.prototype.end.call(this, data, encoding, callback);
if (this._wrap) {
debug("USocket shutdown");
this._wrap.shutdownCalled = true;
this._wrap.shutdown();
this.read(0);
this.maybeClose();
}
};
USocket.prototype.destroy = function() {
if (!this._wrap)
return;
this._wrap.close();
this._wrap = null;
};
USocket.prototype.maybeClose = function() {
if (!this._wrap || !this._wrap.shutdownCalled || !this._wrap.endReceived)
return;
debug("USocket closing socket at end");
this.destroy();
this.emit('close');
};
//-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----
//--
exports.UServer = UServer;
function UServer() {
this.paused = false;
this.listening = false;
EventEmitter.call(this);
}
util.inherits(UServer, EventEmitter);
UServer.prototype.listen = function(path, backlog, cb) {
if (this._wrap || this.listening)
throw new Error("listen on already listened UServer");
if (typeof path === 'object') {
backlog = path.backlog;
path = path.path;
cb = backlog;
}
else if (typeof backlog === 'function') {
cb = backlog;
backlog = 0;
}
backlog = backlog || 16;
if (typeof path !== 'string')
throw new Error("UServer expects valid path");
if (typeof backlog !== 'number')
throw new Error("UServer expects valid path");
if (typeof cb === 'function')
this.once('listening', cb);
debug("creating UServerWrap");
this._wrap = new uwrap.UServerWrap(this._wrapEvent.bind(this));
debug("start UServerWrap#listen");
this._wrap.listen(path, backlog);
};
UServer.prototype.pause = function() {
this.paused = true;
if (this.listening && this._wrap)
this._wrap.pause();
};
UServer.prototype.resume = function() {
this.paused = false;
if (this.listening && this._wrap)
this._wrap.resume();
};
UServer.prototype.close = function() {
if (!this._wrap)
return;
this._wrap.close();
this._wrap = undefined;
};
UServer.prototype._wrapEvent = function(event, a0, a1) {
if (event === "listening") {
this.fd = a0;
debug("UServer listening on " + this.fd);
this.emit('listening');
this.listening = true;
if (!this.paused)
this._wrap.resume();
}
if (event === "error") {
debug("UServer error", a0);
this._wrap.close();
this._wrap = null;
this.emit("error", a0);
}
if (event === "accept") {
debug("UServer accepted socket " + a0);
var n = new USocket({ fd: a0 });
this.emit('connection', n);
}
};