mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 18:19:40 +00:00
Split out 'socket' hosting model into a separate optional NuGet package, since most developers won't need it
This commit is contained in:
3
src/Microsoft.AspNetCore.NodeServices.Sockets/.gitignore
vendored
Normal file
3
src/Microsoft.AspNetCore.NodeServices.Sockets/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/bin/
|
||||
/node_modules/
|
||||
yarn.lock
|
||||
@@ -0,0 +1,524 @@
|
||||
(function(e, a) { for(var i in a) e[i] = a[i]; }(exports, /******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId])
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ exports: {},
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false
|
||||
/******/ };
|
||||
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
|
||||
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
/* 0 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
module.exports = __webpack_require__(1);
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 1 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
||||
// but simplifies things for the consumer of this module.
|
||||
__webpack_require__(2);
|
||||
var net = __webpack_require__(3);
|
||||
var path = __webpack_require__(4);
|
||||
var readline = __webpack_require__(5);
|
||||
var ArgsUtil_1 = __webpack_require__(6);
|
||||
var ExitWhenParentExits_1 = __webpack_require__(7);
|
||||
var virtualConnectionServer = __webpack_require__(8);
|
||||
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
|
||||
// reference to Node's runtime 'require' function.
|
||||
var dynamicRequire = eval('require');
|
||||
// Signal to the .NET side when we're ready to accept invocations
|
||||
var server = net.createServer().on('listening', function () {
|
||||
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
|
||||
});
|
||||
// Each virtual connection represents a separate invocation
|
||||
virtualConnectionServer.createInterface(server).on('connection', function (connection) {
|
||||
readline.createInterface(connection, null).on('line', function (line) {
|
||||
try {
|
||||
// Get a reference to the function to invoke
|
||||
var invocation = JSON.parse(line);
|
||||
var invokedModule = dynamicRequire(path.resolve(process.cwd(), invocation.moduleName));
|
||||
var invokedFunction = invocation.exportedFunctionName ? invokedModule[invocation.exportedFunctionName] : invokedModule;
|
||||
// Prepare a callback for accepting non-streamed JSON responses
|
||||
var hasInvokedCallback_1 = false;
|
||||
var invocationCallback = function (errorValue, successValue) {
|
||||
if (hasInvokedCallback_1) {
|
||||
throw new Error('Cannot supply more than one result. The callback has already been invoked,'
|
||||
+ ' or the result stream has already been accessed');
|
||||
}
|
||||
hasInvokedCallback_1 = true;
|
||||
connection.end(JSON.stringify({
|
||||
result: successValue,
|
||||
errorMessage: errorValue && (errorValue.message || errorValue),
|
||||
errorDetails: errorValue && (errorValue.stack || null)
|
||||
}));
|
||||
};
|
||||
// Also support streamed binary responses
|
||||
Object.defineProperty(invocationCallback, 'stream', {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
hasInvokedCallback_1 = true;
|
||||
return connection;
|
||||
}
|
||||
});
|
||||
// Actually invoke it, passing through any supplied args
|
||||
invokedFunction.apply(null, [invocationCallback].concat(invocation.args));
|
||||
}
|
||||
catch (ex) {
|
||||
connection.end(JSON.stringify({
|
||||
errorMessage: ex.message,
|
||||
errorDetails: ex.stack
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
// Begin listening now. The underlying transport varies according to the runtime platform.
|
||||
// On Windows it's Named Pipes; on Linux/OSX it's Domain Sockets.
|
||||
var useWindowsNamedPipes = /^win/.test(process.platform);
|
||||
var parsedArgs = ArgsUtil_1.parseArgs(process.argv);
|
||||
var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
|
||||
server.listen(listenAddress);
|
||||
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 2 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
// When Node writes to stdout/strerr, we capture that and convert the lines into calls on the
|
||||
// active .NET ILogger. But by default, stdout/stderr don't have any way of distinguishing
|
||||
// linebreaks inside log messages from the linebreaks that delimit separate log messages,
|
||||
// so multiline strings will end up being written to the ILogger as multiple independent
|
||||
// log messages. This makes them very hard to make sense of, especially when they represent
|
||||
// something like stack traces.
|
||||
//
|
||||
// To fix this, we intercept stdout/stderr writes, and replace internal linebreaks with a
|
||||
// marker token. When .NET receives the lines, it converts the marker tokens back to regular
|
||||
// linebreaks within the logged messages.
|
||||
//
|
||||
// Note that it's better to do the interception at the stdout/stderr level, rather than at
|
||||
// the console.log/console.error (etc.) level, because this takes place after any native
|
||||
// message formatting has taken place (e.g., inserting values for % placeholders).
|
||||
var findInternalNewlinesRegex = /\n(?!$)/g;
|
||||
var encodedNewline = '__ns_newline__';
|
||||
encodeNewlinesWrittenToStream(process.stdout);
|
||||
encodeNewlinesWrittenToStream(process.stderr);
|
||||
function encodeNewlinesWrittenToStream(outputStream) {
|
||||
var origWriteFunction = outputStream.write;
|
||||
outputStream.write = function (value) {
|
||||
// Only interfere with the write if it's definitely a string
|
||||
if (typeof value === 'string') {
|
||||
var argsClone = Array.prototype.slice.call(arguments, 0);
|
||||
argsClone[0] = encodeNewlinesInString(value);
|
||||
origWriteFunction.apply(this, argsClone);
|
||||
}
|
||||
else {
|
||||
origWriteFunction.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
function encodeNewlinesInString(str) {
|
||||
return str.replace(findInternalNewlinesRegex, encodedNewline);
|
||||
}
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 3 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("net");
|
||||
|
||||
/***/ },
|
||||
/* 4 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("path");
|
||||
|
||||
/***/ },
|
||||
/* 5 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("readline");
|
||||
|
||||
/***/ },
|
||||
/* 6 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
"use strict";
|
||||
function parseArgs(args) {
|
||||
// Very simplistic parsing which is sufficient for the cases needed. We don't want to bring in any external
|
||||
// dependencies (such as an args-parsing library) to this file.
|
||||
var result = {};
|
||||
var currentKey = null;
|
||||
args.forEach(function (arg) {
|
||||
if (arg.indexOf('--') === 0) {
|
||||
var argName = arg.substring(2);
|
||||
result[argName] = undefined;
|
||||
currentKey = argName;
|
||||
}
|
||||
else if (currentKey) {
|
||||
result[currentKey] = arg;
|
||||
currentKey = null;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
exports.parseArgs = parseArgs;
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 7 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
/*
|
||||
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
|
||||
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
|
||||
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
|
||||
|
||||
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
|
||||
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
|
||||
up to the child process to detect this has happened and terminate itself.
|
||||
|
||||
There are many possible approaches to detecting when a parent process has exited, most of which behave
|
||||
differently between Windows and Linux/OS X:
|
||||
|
||||
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
|
||||
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
|
||||
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
|
||||
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
|
||||
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
|
||||
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
|
||||
causes the process to terminate prematurely.
|
||||
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
|
||||
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
|
||||
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
|
||||
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
|
||||
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
|
||||
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
|
||||
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
|
||||
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
|
||||
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
|
||||
value changes until you actually try to write to it.
|
||||
|
||||
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
|
||||
to check whether the parent PID is still running. So that's what we do here.
|
||||
*/
|
||||
"use strict";
|
||||
var pollIntervalMs = 1000;
|
||||
function exitWhenParentExits(parentPid) {
|
||||
setInterval(function () {
|
||||
if (!processExists(parentPid)) {
|
||||
// Can't log anything at this point, because out stdout was connected to the parent,
|
||||
// but the parent is gone.
|
||||
process.exit();
|
||||
}
|
||||
}, pollIntervalMs);
|
||||
}
|
||||
exports.exitWhenParentExits = exitWhenParentExits;
|
||||
function processExists(pid) {
|
||||
try {
|
||||
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
|
||||
// throw, that means it does exist.
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
}
|
||||
catch (ex) {
|
||||
// If the reason for the error is that we don't have permission to ask about this process,
|
||||
// report that as a separate problem.
|
||||
if (ex.code === 'EPERM') {
|
||||
throw new Error("Attempted to check whether process " + pid + " was running, but got a permissions error.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 8 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
var events_1 = __webpack_require__(9);
|
||||
var VirtualConnection_1 = __webpack_require__(10);
|
||||
// Keep this in sync with the equivalent constant in the .NET code. Both sides split up their transmissions into frames with this max length,
|
||||
// and both will reject longer frames.
|
||||
var MaxFrameBodyLength = 16 * 1024;
|
||||
/**
|
||||
* Accepts connections to a net.Server and adapts them to behave as multiplexed connections. That is, for each physical socket connection,
|
||||
* we track a list of 'virtual connections' whose API is a Duplex stream. The remote clients may open and close as many virtual connections
|
||||
* as they wish, reading and writing to them independently, without the overhead of establishing new physical connections each time.
|
||||
*/
|
||||
function createInterface(server) {
|
||||
var emitter = new events_1.EventEmitter();
|
||||
server.on('connection', function (socket) {
|
||||
// For each physical socket connection, maintain a set of virtual connections. Issue a notification whenever
|
||||
// a new virtual connections is opened.
|
||||
var childSockets = new VirtualConnectionsCollection(socket, function (virtualConnection) {
|
||||
emitter.emit('connection', virtualConnection);
|
||||
});
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
exports.createInterface = createInterface;
|
||||
/**
|
||||
* Tracks the 'virtual connections' associated with a single physical socket connection.
|
||||
*/
|
||||
var VirtualConnectionsCollection = (function () {
|
||||
function VirtualConnectionsCollection(_socket, _onVirtualConnectionCallback) {
|
||||
var _this = this;
|
||||
this._socket = _socket;
|
||||
this._onVirtualConnectionCallback = _onVirtualConnectionCallback;
|
||||
this._currentFrameHeader = null;
|
||||
this._virtualConnections = {};
|
||||
// If the remote end closes the physical socket, treat all the virtual connections as being closed remotely too
|
||||
this._socket.on('close', function () {
|
||||
Object.getOwnPropertyNames(_this._virtualConnections).forEach(function (id) {
|
||||
// A 'null' frame signals that the connection was closed remotely
|
||||
_this._virtualConnections[id].onReceivedData(null);
|
||||
});
|
||||
});
|
||||
this._socket.on('readable', this._onIncomingDataAvailable.bind(this));
|
||||
}
|
||||
/**
|
||||
* This is called whenever the underlying socket signals that it may have some data available to read. It will synchronously read as many
|
||||
* message frames as it can from the underlying socket, opens virtual connections as needed, and dispatches data to them.
|
||||
*/
|
||||
VirtualConnectionsCollection.prototype._onIncomingDataAvailable = function () {
|
||||
var exhaustedAllData = false;
|
||||
while (!exhaustedAllData) {
|
||||
// We might already have a pending frame header from the previous time this method ran, but if not, that's the next thing we need to read
|
||||
if (this._currentFrameHeader === null) {
|
||||
this._currentFrameHeader = this._readNextFrameHeader();
|
||||
}
|
||||
if (this._currentFrameHeader === null) {
|
||||
// There's not enough data to fill a frameheader, so wait until more arrives later
|
||||
// The next attempt to read from the socket will start from the same place this one did (incomplete reads don't consume any data)
|
||||
exhaustedAllData = true;
|
||||
}
|
||||
else {
|
||||
var frameBodyLength = this._currentFrameHeader.bodyLength;
|
||||
var frameBodyOrNull = frameBodyLength > 0 ? this._socket.read(this._currentFrameHeader.bodyLength) : null;
|
||||
if (frameBodyOrNull !== null || frameBodyLength === 0) {
|
||||
// We have a complete frame header+body pair, so we can now dispatch this to a virtual connection. We set _currentFrameHeader back to null
|
||||
// so that the next thing we try to read is the next frame header.
|
||||
var headerCopy = this._currentFrameHeader;
|
||||
this._currentFrameHeader = null;
|
||||
this._onReceivedCompleteFrame(headerCopy, frameBodyOrNull);
|
||||
}
|
||||
else {
|
||||
// There's not enough data to fill the pending frame body, so wait until more arrives later
|
||||
// The next attempt to read from the socket will start from the same place this one did (incomplete reads don't consume any data)
|
||||
exhaustedAllData = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
VirtualConnectionsCollection.prototype._onReceivedCompleteFrame = function (header, bodyIfNotEmpty) {
|
||||
// An incoming zero-length frame signals that there's no more data to read.
|
||||
// Signal this to the Node stream APIs by pushing a 'null' chunk to it.
|
||||
var virtualConnection = this._getOrOpenVirtualConnection(header);
|
||||
virtualConnection.onReceivedData(header.bodyLength > 0 ? bodyIfNotEmpty : null);
|
||||
};
|
||||
VirtualConnectionsCollection.prototype._getOrOpenVirtualConnection = function (header) {
|
||||
if (this._virtualConnections.hasOwnProperty(header.connectionIdString)) {
|
||||
// It's an existing virtual connection
|
||||
return this._virtualConnections[header.connectionIdString];
|
||||
}
|
||||
else {
|
||||
// It's a new one
|
||||
return this._openVirtualConnection(header);
|
||||
}
|
||||
};
|
||||
VirtualConnectionsCollection.prototype._openVirtualConnection = function (header) {
|
||||
var _this = this;
|
||||
var beginWriteCallback = function (data, writeCompletedCallback) {
|
||||
// Only send nonempty frames, since empty ones are a signal to close the virtual connection
|
||||
if (data.length > 0) {
|
||||
_this._sendFrame(header.connectionIdBinary, data, writeCompletedCallback);
|
||||
}
|
||||
};
|
||||
var newVirtualConnection = new VirtualConnection_1.VirtualConnection(beginWriteCallback);
|
||||
newVirtualConnection.on('end', function () {
|
||||
// The virtual connection was closed remotely. Clean up locally.
|
||||
_this._onVirtualConnectionWasClosed(header.connectionIdString);
|
||||
});
|
||||
newVirtualConnection.on('finish', function () {
|
||||
// The virtual connection was closed locally. Clean up locally, and notify the remote that we're done.
|
||||
_this._onVirtualConnectionWasClosed(header.connectionIdString);
|
||||
_this._sendFrame(header.connectionIdBinary, new Buffer(0));
|
||||
});
|
||||
this._virtualConnections[header.connectionIdString] = newVirtualConnection;
|
||||
this._onVirtualConnectionCallback(newVirtualConnection);
|
||||
return newVirtualConnection;
|
||||
};
|
||||
/**
|
||||
* Attempts to read a complete frame header, synchronously, from the underlying socket.
|
||||
* If not enough data is available synchronously, returns null without consuming any data from the socket.
|
||||
*/
|
||||
VirtualConnectionsCollection.prototype._readNextFrameHeader = function () {
|
||||
var headerBuf = this._socket.read(12);
|
||||
if (headerBuf !== null) {
|
||||
// We have enough data synchronously
|
||||
var connectionIdBinary = headerBuf.slice(0, 8);
|
||||
var connectionIdString = connectionIdBinary.toString('hex');
|
||||
var bodyLength = headerBuf.readInt32LE(8);
|
||||
if (bodyLength < 0 || bodyLength > MaxFrameBodyLength) {
|
||||
// Throwing here is going to bring down the whole process, so this cannot be allowed to happen in real use.
|
||||
// But it won't happen in real use, because this is only used with our .NET client, which doesn't violate this rule.
|
||||
throw new Error('Illegal frame body length: ' + bodyLength);
|
||||
}
|
||||
return { connectionIdBinary: connectionIdBinary, connectionIdString: connectionIdString, bodyLength: bodyLength };
|
||||
}
|
||||
else {
|
||||
// Not enough bytes are available synchronously, so none were consumed
|
||||
return null;
|
||||
}
|
||||
};
|
||||
VirtualConnectionsCollection.prototype._sendFrame = function (connectionIdBinary, data, callback) {
|
||||
// For all sends other than the last one, only invoke the callback if it failed.
|
||||
// Also, only invoke the callback at most once.
|
||||
var hasInvokedCallback = false;
|
||||
var finalCallback = callback && (function (error) {
|
||||
if (!hasInvokedCallback) {
|
||||
hasInvokedCallback = true;
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
var notFinalCallback = callback && (function (error) {
|
||||
if (error) {
|
||||
finalCallback(error);
|
||||
}
|
||||
});
|
||||
// The amount of data we're writing might exceed MaxFrameBodyLength, so split into frames as needed.
|
||||
// Note that we always send at least one frame, even if it's empty (because that's the close-virtual-connection signal).
|
||||
// If needed, this could be changed to send frames asynchronously, so that large sends could proceed in parallel
|
||||
// (though that would involve making a clone of 'data', to avoid the risk of it being mutated during the send).
|
||||
var bytesSent = 0;
|
||||
do {
|
||||
var nextFrameBodyLength = Math.min(MaxFrameBodyLength, data.length - bytesSent);
|
||||
var isFinalChunk = (bytesSent + nextFrameBodyLength) === data.length;
|
||||
this._socket.write(connectionIdBinary, notFinalCallback);
|
||||
this._sendInt32LE(nextFrameBodyLength, notFinalCallback);
|
||||
this._socket.write(data.slice(bytesSent, bytesSent + nextFrameBodyLength), isFinalChunk ? finalCallback : notFinalCallback);
|
||||
bytesSent += nextFrameBodyLength;
|
||||
} while (bytesSent < data.length);
|
||||
};
|
||||
/**
|
||||
* Sends a number serialized in the correct format for .NET to receive as a System.Int32
|
||||
*/
|
||||
VirtualConnectionsCollection.prototype._sendInt32LE = function (value, callback) {
|
||||
var buf = new Buffer(4);
|
||||
buf.writeInt32LE(value, 0);
|
||||
this._socket.write(buf, callback);
|
||||
};
|
||||
VirtualConnectionsCollection.prototype._onVirtualConnectionWasClosed = function (id) {
|
||||
if (this._virtualConnections.hasOwnProperty(id)) {
|
||||
delete this._virtualConnections[id];
|
||||
}
|
||||
};
|
||||
return VirtualConnectionsCollection;
|
||||
}());
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 9 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("events");
|
||||
|
||||
/***/ },
|
||||
/* 10 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
var __extends = (this && this.__extends) || function (d, b) {
|
||||
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
var stream_1 = __webpack_require__(11);
|
||||
/**
|
||||
* Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection.
|
||||
*/
|
||||
var VirtualConnection = (function (_super) {
|
||||
__extends(VirtualConnection, _super);
|
||||
function VirtualConnection(_beginWriteCallback) {
|
||||
var _this = _super.call(this) || this;
|
||||
_this._beginWriteCallback = _beginWriteCallback;
|
||||
_this._flowing = false;
|
||||
_this._receivedDataQueue = [];
|
||||
return _this;
|
||||
}
|
||||
VirtualConnection.prototype._read = function () {
|
||||
this._flowing = true;
|
||||
// Keep pushing data until we run out, or the underlying framework asks us to stop.
|
||||
// When we finish, the 'flowing' state is detemined by whether more data is still being requested.
|
||||
while (this._flowing && this._receivedDataQueue.length > 0) {
|
||||
var nextChunk = this._receivedDataQueue.shift();
|
||||
this._flowing = this.push(nextChunk);
|
||||
}
|
||||
};
|
||||
VirtualConnection.prototype._write = function (chunk, encodingIfString, callback) {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = new Buffer(chunk, encodingIfString);
|
||||
}
|
||||
this._beginWriteCallback(chunk, callback);
|
||||
};
|
||||
VirtualConnection.prototype.onReceivedData = function (dataOrNullToSignalEOF) {
|
||||
if (this._flowing) {
|
||||
this._flowing = this.push(dataOrNullToSignalEOF);
|
||||
}
|
||||
else {
|
||||
this._receivedDataQueue.push(dataOrNullToSignalEOF);
|
||||
}
|
||||
};
|
||||
return VirtualConnection;
|
||||
}(stream_1.Duplex));
|
||||
exports.VirtualConnection = VirtualConnection;
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 11 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("stream");
|
||||
|
||||
/***/ }
|
||||
/******/ ])));
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
||||
{
|
||||
internal class NamedPipeConnection : StreamConnection
|
||||
{
|
||||
private bool _disposedValue = false;
|
||||
private NamedPipeClientStream _namedPipeClientStream;
|
||||
|
||||
#pragma warning disable 1998 // Because in the NET451 code path, there's nothing to await
|
||||
public override async Task<Stream> Open(string address)
|
||||
{
|
||||
_namedPipeClientStream = new NamedPipeClientStream(
|
||||
".",
|
||||
address,
|
||||
PipeDirection.InOut,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
#if NET451
|
||||
_namedPipeClientStream.Connect();
|
||||
#else
|
||||
await _namedPipeClientStream.ConnectAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
return _namedPipeClientStream;
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (_namedPipeClientStream != null)
|
||||
{
|
||||
_namedPipeClientStream.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
||||
{
|
||||
internal abstract class StreamConnection : IDisposable
|
||||
{
|
||||
public abstract Task<Stream> Open(string address);
|
||||
public abstract void Dispose();
|
||||
|
||||
public static StreamConnection Create()
|
||||
{
|
||||
#if NET451
|
||||
return new NamedPipeConnection();
|
||||
#else
|
||||
var useNamedPipes = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
|
||||
System.Runtime.InteropServices.OSPlatform.Windows);
|
||||
if (useNamedPipes)
|
||||
{
|
||||
return new NamedPipeConnection();
|
||||
}
|
||||
else
|
||||
{
|
||||
return new UnixDomainSocketConnection();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
||||
{
|
||||
internal class UnixDomainSocketConnection : StreamConnection
|
||||
{
|
||||
private bool _disposedValue = false;
|
||||
private NetworkStream _networkStream;
|
||||
private Socket _socket;
|
||||
|
||||
#if NET451
|
||||
public override Task<Stream> Open(string address)
|
||||
{
|
||||
// The 'null' assignments avoid the compiler warnings about unassigned fields.
|
||||
// Note that this whole class isn't supported on .NET 4.5.1, since that's not cross-platform.
|
||||
_networkStream = null;
|
||||
_socket = null;
|
||||
throw new System.PlatformNotSupportedException();
|
||||
}
|
||||
#else
|
||||
public override async Task<Stream> Open(string address)
|
||||
{
|
||||
var endPoint = new UnixDomainSocketEndPoint("/tmp/" + address);
|
||||
_socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Unspecified);
|
||||
await _socket.ConnectAsync(endPoint).ConfigureAwait(false);
|
||||
_networkStream = new NetworkStream(_socket);
|
||||
return _networkStream;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (_networkStream != null)
|
||||
{
|
||||
_networkStream.Dispose();
|
||||
}
|
||||
|
||||
if (_socket != null)
|
||||
{
|
||||
_socket.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
||||
{
|
||||
// From System.IO.Pipes/src/System/Net/Sockets/UnixDomainSocketEndPoint.cs (an internal class in System.IO.Pipes)
|
||||
internal sealed class UnixDomainSocketEndPoint : EndPoint
|
||||
{
|
||||
private const AddressFamily EndPointAddressFamily = AddressFamily.Unix;
|
||||
|
||||
private static readonly Encoding s_pathEncoding = Encoding.UTF8;
|
||||
private static readonly int s_nativePathOffset = 2; // = offsetof(struct sockaddr_un, sun_path). It's the same on Linux and OSX
|
||||
private static readonly int s_nativePathLength = 91; // sockaddr_un.sun_path at http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_un.h.html, -1 for terminator
|
||||
private static readonly int s_nativeAddressSize = s_nativePathOffset + s_nativePathLength;
|
||||
|
||||
private readonly string _path;
|
||||
private readonly byte[] _encodedPath;
|
||||
|
||||
public UnixDomainSocketEndPoint(string path)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
_path = path;
|
||||
_encodedPath = s_pathEncoding.GetBytes(_path);
|
||||
|
||||
if (path.Length == 0 || _encodedPath.Length > s_nativePathLength)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(path));
|
||||
}
|
||||
}
|
||||
|
||||
internal UnixDomainSocketEndPoint(SocketAddress socketAddress)
|
||||
{
|
||||
if (socketAddress == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socketAddress));
|
||||
}
|
||||
|
||||
if (socketAddress.Family != EndPointAddressFamily ||
|
||||
socketAddress.Size > s_nativeAddressSize)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(socketAddress));
|
||||
}
|
||||
|
||||
if (socketAddress.Size > s_nativePathOffset)
|
||||
{
|
||||
_encodedPath = new byte[socketAddress.Size - s_nativePathOffset];
|
||||
for (int i = 0; i < _encodedPath.Length; i++)
|
||||
{
|
||||
_encodedPath[i] = socketAddress[s_nativePathOffset + i];
|
||||
}
|
||||
|
||||
_path = s_pathEncoding.GetString(_encodedPath, 0, _encodedPath.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if NET451
|
||||
_encodedPath = new byte[0];
|
||||
#else
|
||||
_encodedPath = Array.Empty<byte>();
|
||||
#endif
|
||||
_path = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override SocketAddress Serialize()
|
||||
{
|
||||
var result = new SocketAddress(AddressFamily.Unix, s_nativeAddressSize);
|
||||
|
||||
for (int index = 0; index < _encodedPath.Length; index++)
|
||||
{
|
||||
result[s_nativePathOffset + index] = _encodedPath[index];
|
||||
}
|
||||
result[s_nativePathOffset + _encodedPath.Length] = 0; // path must be null-terminated
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override EndPoint Create(SocketAddress socketAddress) => new UnixDomainSocketEndPoint(socketAddress);
|
||||
|
||||
public override AddressFamily AddressFamily => EndPointAddressFamily;
|
||||
|
||||
public override string ToString() => _path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.NodeServices.HostingModels;
|
||||
using Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections;
|
||||
using Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// A specialisation of the OutOfProcessNodeInstance base class that uses a lightweight binary streaming protocol
|
||||
/// to perform RPC invocations. The physical transport is Named Pipes on Windows, or Domain Sockets on Linux/Mac.
|
||||
/// For details on the binary streaming protocol, see
|
||||
/// Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections.VirtualConnectionClient.
|
||||
/// The advantage versus using HTTP for RPC is that this is faster (not surprisingly - there's much less overhead
|
||||
/// because we don't need most of the functionality of HTTP.
|
||||
///
|
||||
/// The address of the pipe/socket is selected randomly here on the .NET side and sent to the child process as a
|
||||
/// command-line argument (the address space is wide enough that there's no real risk of a clash, unlike when
|
||||
/// selecting TCP port numbers).
|
||||
/// </summary>
|
||||
/// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels.OutOfProcessNodeInstance" />
|
||||
internal class SocketNodeInstance : OutOfProcessNodeInstance
|
||||
{
|
||||
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
TypeNameHandling = TypeNameHandling.None
|
||||
};
|
||||
|
||||
private readonly static int streamBufferSize = 16 * 1024;
|
||||
private readonly static UTF8Encoding utf8EncodingWithoutBom = new UTF8Encoding(false);
|
||||
|
||||
private readonly SemaphoreSlim _connectionCreationSemaphore = new SemaphoreSlim(1);
|
||||
private bool _connectionHasFailed;
|
||||
private StreamConnection _physicalConnection;
|
||||
private string _socketAddress;
|
||||
private VirtualConnectionClient _virtualConnectionClient;
|
||||
|
||||
public SocketNodeInstance(NodeServicesOptions options, string socketAddress)
|
||||
: base(
|
||||
EmbeddedResourceReader.Read(
|
||||
typeof(SocketNodeInstance),
|
||||
"/Content/Node/entrypoint-socket.js"),
|
||||
options.ProjectPath,
|
||||
options.WatchFileExtensions,
|
||||
MakeNewCommandLineOptions(socketAddress),
|
||||
options.NodeInstanceOutputLogger,
|
||||
options.EnvironmentVariables,
|
||||
options.InvocationTimeoutMilliseconds,
|
||||
options.LaunchWithDebugging,
|
||||
options.DebuggingPort)
|
||||
{
|
||||
_socketAddress = socketAddress;
|
||||
}
|
||||
|
||||
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_connectionHasFailed)
|
||||
{
|
||||
// This special exception type forces NodeServicesImpl to restart the Node instance
|
||||
throw new NodeInvocationException(
|
||||
"The SocketNodeInstance socket connection failed. See logs to identify the reason.",
|
||||
null,
|
||||
nodeInstanceUnavailable: true);
|
||||
}
|
||||
|
||||
if (_virtualConnectionClient == null)
|
||||
{
|
||||
// Although we could pass the cancellationToken into EnsureVirtualConnectionClientCreated and
|
||||
// have it signal cancellations upstream, that would be a bad thing to do, because all callers
|
||||
// wait for the same connection task. There's no reason why the first caller should have the
|
||||
// special ability to cancel the connection process in a way that would affect subsequent
|
||||
// callers. So, each caller just independently stops awaiting connection if that call is cancelled.
|
||||
await ThrowOnCancellation(EnsureVirtualConnectionClientCreated(), cancellationToken);
|
||||
}
|
||||
|
||||
// For each invocation, we open a new virtual connection. This gives an API equivalent to opening a new
|
||||
// physical connection to the child process, but without the overhead of doing so, because it's really
|
||||
// just multiplexed into the existing physical connection stream.
|
||||
bool shouldDisposeVirtualConnection = true;
|
||||
Stream virtualConnection = null;
|
||||
try
|
||||
{
|
||||
virtualConnection = _virtualConnectionClient.OpenVirtualConnection();
|
||||
|
||||
// Send request
|
||||
WriteJsonLine(virtualConnection, invocationInfo);
|
||||
|
||||
// Determine what kind of response format is expected
|
||||
if (typeof(T) == typeof(Stream))
|
||||
{
|
||||
// Pass through streamed binary response
|
||||
// It is up to the consumer to dispose this stream, so don't do so here
|
||||
shouldDisposeVirtualConnection = false;
|
||||
return (T)(object)virtualConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse and return non-streamed JSON response
|
||||
var response = await ReadJsonAsync<RpcJsonResponse<T>>(virtualConnection, cancellationToken);
|
||||
if (response.ErrorMessage != null)
|
||||
{
|
||||
throw new NodeInvocationException(response.ErrorMessage, response.ErrorDetails);
|
||||
}
|
||||
|
||||
return response.Result;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shouldDisposeVirtualConnection)
|
||||
{
|
||||
virtualConnection.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureVirtualConnectionClientCreated()
|
||||
{
|
||||
// Asynchronous equivalent to a 'lock(...) { ... }'
|
||||
await _connectionCreationSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_virtualConnectionClient == null)
|
||||
{
|
||||
_physicalConnection = StreamConnection.Create();
|
||||
|
||||
var connection = await _physicalConnection.Open(_socketAddress);
|
||||
_virtualConnectionClient = new VirtualConnectionClient(connection);
|
||||
_virtualConnectionClient.OnError += (ex) =>
|
||||
{
|
||||
// This callback is fired only if there's a protocol-level failure (e.g., child process disconnected
|
||||
// unexpectedly). It does *not* fire when RPC calls return errors. Since there's been a protocol-level
|
||||
// failure, this Node instance is no longer usable and should be discarded.
|
||||
_connectionHasFailed = true;
|
||||
|
||||
OutputLogger.LogError(0, ex, ex.Message);
|
||||
};
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionCreationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_virtualConnectionClient != null)
|
||||
{
|
||||
_virtualConnectionClient.Dispose();
|
||||
_virtualConnectionClient = null;
|
||||
}
|
||||
|
||||
if (_physicalConnection != null)
|
||||
{
|
||||
_physicalConnection.Dispose();
|
||||
_physicalConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private static void WriteJsonLine(Stream stream, object serializableObject)
|
||||
{
|
||||
using (var streamWriter = new StreamWriter(stream, utf8EncodingWithoutBom, streamBufferSize, true))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter))
|
||||
{
|
||||
jsonWriter.CloseOutput = false;
|
||||
|
||||
var serializer = JsonSerializer.Create(jsonSerializerSettings);
|
||||
serializer.Serialize(jsonWriter, serializableObject);
|
||||
jsonWriter.Flush();
|
||||
|
||||
streamWriter.WriteLine();
|
||||
streamWriter.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<T> ReadJsonAsync<T>(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream, cancellationToken));
|
||||
return JsonConvert.DeserializeObject<T>(json, jsonSerializerSettings);
|
||||
}
|
||||
|
||||
private static async Task<byte[]> ReadAllBytesAsync(Stream input, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = new byte[streamBufferSize];
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
int read;
|
||||
while ((read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
||||
{
|
||||
ms.Write(buffer, 0, read);
|
||||
}
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static string MakeNewCommandLineOptions(string listenAddress)
|
||||
{
|
||||
return $"--listenAddress {listenAddress}";
|
||||
}
|
||||
|
||||
private static Task ThrowOnCancellation(Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
return task.IsCompleted
|
||||
? task // If the task is already completed, no need to wrap it in a further layer of task
|
||||
: task.ContinueWith(
|
||||
_ => {}, // If the task completes, allow execution to continue
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
#pragma warning disable 649 // These properties are populated via JSON deserialization
|
||||
private class RpcJsonResponse<TResult>
|
||||
{
|
||||
public TResult Result { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string ErrorDetails { get; set; }
|
||||
}
|
||||
#pragma warning restore 649
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods that help with populating a <see cref="NodeServicesOptions"/> object.
|
||||
/// </summary>
|
||||
public static class NodeServicesOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the <see cref="INodeServices"/> service so that it will use out-of-process
|
||||
/// Node.js instances and perform RPC calls over binary sockets (on Windows, this is
|
||||
/// implemented as named pipes; on other platforms it uses domain sockets).
|
||||
/// </summary>
|
||||
public static void UseSocketHosting(this NodeServicesOptions options)
|
||||
{
|
||||
var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string
|
||||
options.NodeInstanceFactory = () => new SocketNodeInstance(options, pipeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
||||
// but simplifies things for the consumer of this module.
|
||||
import '../../Microsoft.AspNetCore.NodeServices/TypeScript/Util/OverrideStdOutputs';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as readline from 'readline';
|
||||
import { Duplex } from 'stream';
|
||||
import { parseArgs } from '../../Microsoft.AspNetCore.NodeServices/TypeScript/Util/ArgsUtil';
|
||||
import { exitWhenParentExits } from '../../Microsoft.AspNetCore.NodeServices/TypeScript/Util/ExitWhenParentExits';
|
||||
import * as virtualConnectionServer from './VirtualConnections/VirtualConnectionServer';
|
||||
|
||||
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
|
||||
// reference to Node's runtime 'require' function.
|
||||
const dynamicRequire: (name: string) => any = eval('require');
|
||||
|
||||
// Signal to the .NET side when we're ready to accept invocations
|
||||
const server = net.createServer().on('listening', () => {
|
||||
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
|
||||
});
|
||||
|
||||
// Each virtual connection represents a separate invocation
|
||||
virtualConnectionServer.createInterface(server).on('connection', (connection: Duplex) => {
|
||||
readline.createInterface(connection, null).on('line', line => {
|
||||
try {
|
||||
// Get a reference to the function to invoke
|
||||
const invocation = JSON.parse(line) as RpcInvocation;
|
||||
const invokedModule = dynamicRequire(path.resolve(process.cwd(), invocation.moduleName));
|
||||
const invokedFunction = invocation.exportedFunctionName ? invokedModule[invocation.exportedFunctionName] : invokedModule;
|
||||
|
||||
// Prepare a callback for accepting non-streamed JSON responses
|
||||
let hasInvokedCallback = false;
|
||||
const invocationCallback = (errorValue, successValue) => {
|
||||
if (hasInvokedCallback) {
|
||||
throw new Error('Cannot supply more than one result. The callback has already been invoked,'
|
||||
+ ' or the result stream has already been accessed');
|
||||
}
|
||||
|
||||
hasInvokedCallback = true;
|
||||
connection.end(JSON.stringify({
|
||||
result: successValue,
|
||||
errorMessage: errorValue && (errorValue.message || errorValue),
|
||||
errorDetails: errorValue && (errorValue.stack || null)
|
||||
}));
|
||||
};
|
||||
|
||||
// Also support streamed binary responses
|
||||
Object.defineProperty(invocationCallback, 'stream', {
|
||||
enumerable: true,
|
||||
get: (): Duplex => {
|
||||
hasInvokedCallback = true;
|
||||
return connection;
|
||||
}
|
||||
});
|
||||
|
||||
// Actually invoke it, passing through any supplied args
|
||||
invokedFunction.apply(null, [invocationCallback].concat(invocation.args));
|
||||
} catch (ex) {
|
||||
connection.end(JSON.stringify({
|
||||
errorMessage: ex.message,
|
||||
errorDetails: ex.stack
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Begin listening now. The underlying transport varies according to the runtime platform.
|
||||
// On Windows it's Named Pipes; on Linux/OSX it's Domain Sockets.
|
||||
const useWindowsNamedPipes = /^win/.test(process.platform);
|
||||
const parsedArgs = parseArgs(process.argv);
|
||||
const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
|
||||
server.listen(listenAddress);
|
||||
|
||||
exitWhenParentExits(parseInt(parsedArgs.parentPid));
|
||||
|
||||
interface RpcInvocation {
|
||||
moduleName: string;
|
||||
exportedFunctionName: string;
|
||||
args: any[];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Duplex } from 'stream';
|
||||
|
||||
export type EndWriteCallback = (error?: any) => void;
|
||||
export type BeginWriteCallback = (data: Buffer, callback: EndWriteCallback) => void;
|
||||
|
||||
/**
|
||||
* Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection.
|
||||
*/
|
||||
export class VirtualConnection extends Duplex {
|
||||
private _flowing = false;
|
||||
private _receivedDataQueue: Buffer[] = [];
|
||||
|
||||
constructor(private _beginWriteCallback: BeginWriteCallback) {
|
||||
super();
|
||||
}
|
||||
|
||||
public _read() {
|
||||
this._flowing = true;
|
||||
|
||||
// Keep pushing data until we run out, or the underlying framework asks us to stop.
|
||||
// When we finish, the 'flowing' state is detemined by whether more data is still being requested.
|
||||
while (this._flowing && this._receivedDataQueue.length > 0) {
|
||||
const nextChunk = this._receivedDataQueue.shift();
|
||||
this._flowing = this.push(nextChunk);
|
||||
}
|
||||
}
|
||||
|
||||
public _write(chunk: Buffer | string, encodingIfString: string, callback: EndWriteCallback) {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = new Buffer(chunk as string, encodingIfString);
|
||||
}
|
||||
|
||||
this._beginWriteCallback(chunk as Buffer, callback);
|
||||
}
|
||||
|
||||
public onReceivedData(dataOrNullToSignalEOF: Buffer) {
|
||||
if (this._flowing) {
|
||||
this._flowing = this.push(dataOrNullToSignalEOF);
|
||||
} else {
|
||||
this._receivedDataQueue.push(dataOrNullToSignalEOF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { Server, Socket } from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Duplex } from 'stream';
|
||||
import { VirtualConnection, EndWriteCallback } from './VirtualConnection';
|
||||
|
||||
// Keep this in sync with the equivalent constant in the .NET code. Both sides split up their transmissions into frames with this max length,
|
||||
// and both will reject longer frames.
|
||||
const MaxFrameBodyLength = 16 * 1024;
|
||||
|
||||
/**
|
||||
* Accepts connections to a net.Server and adapts them to behave as multiplexed connections. That is, for each physical socket connection,
|
||||
* we track a list of 'virtual connections' whose API is a Duplex stream. The remote clients may open and close as many virtual connections
|
||||
* as they wish, reading and writing to them independently, without the overhead of establishing new physical connections each time.
|
||||
*/
|
||||
export function createInterface(server: Server): EventEmitter {
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
server.on('connection', (socket: Socket) => {
|
||||
// For each physical socket connection, maintain a set of virtual connections. Issue a notification whenever
|
||||
// a new virtual connections is opened.
|
||||
const childSockets = new VirtualConnectionsCollection(socket, virtualConnection => {
|
||||
emitter.emit('connection', virtualConnection);
|
||||
});
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the 'virtual connections' associated with a single physical socket connection.
|
||||
*/
|
||||
class VirtualConnectionsCollection {
|
||||
private _currentFrameHeader: FrameHeader = null;
|
||||
private _virtualConnections: { [id: string]: VirtualConnection } = {};
|
||||
|
||||
constructor(private _socket: Socket, private _onVirtualConnectionCallback: (virtualConnection: Duplex) => void) {
|
||||
// If the remote end closes the physical socket, treat all the virtual connections as being closed remotely too
|
||||
this._socket.on('close', () => {
|
||||
Object.getOwnPropertyNames(this._virtualConnections).forEach(id => {
|
||||
// A 'null' frame signals that the connection was closed remotely
|
||||
this._virtualConnections[id].onReceivedData(null);
|
||||
});
|
||||
});
|
||||
|
||||
this._socket.on('readable', this._onIncomingDataAvailable.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called whenever the underlying socket signals that it may have some data available to read. It will synchronously read as many
|
||||
* message frames as it can from the underlying socket, opens virtual connections as needed, and dispatches data to them.
|
||||
*/
|
||||
private _onIncomingDataAvailable() {
|
||||
let exhaustedAllData = false;
|
||||
|
||||
while (!exhaustedAllData) {
|
||||
// We might already have a pending frame header from the previous time this method ran, but if not, that's the next thing we need to read
|
||||
if (this._currentFrameHeader === null) {
|
||||
this._currentFrameHeader = this._readNextFrameHeader();
|
||||
}
|
||||
|
||||
if (this._currentFrameHeader === null) {
|
||||
// There's not enough data to fill a frameheader, so wait until more arrives later
|
||||
// The next attempt to read from the socket will start from the same place this one did (incomplete reads don't consume any data)
|
||||
exhaustedAllData = true;
|
||||
} else {
|
||||
const frameBodyLength = this._currentFrameHeader.bodyLength;
|
||||
const frameBodyOrNull: Buffer = frameBodyLength > 0 ? this._socket.read(this._currentFrameHeader.bodyLength) : null;
|
||||
if (frameBodyOrNull !== null || frameBodyLength === 0) {
|
||||
// We have a complete frame header+body pair, so we can now dispatch this to a virtual connection. We set _currentFrameHeader back to null
|
||||
// so that the next thing we try to read is the next frame header.
|
||||
const headerCopy = this._currentFrameHeader;
|
||||
this._currentFrameHeader = null;
|
||||
this._onReceivedCompleteFrame(headerCopy, frameBodyOrNull);
|
||||
} else {
|
||||
// There's not enough data to fill the pending frame body, so wait until more arrives later
|
||||
// The next attempt to read from the socket will start from the same place this one did (incomplete reads don't consume any data)
|
||||
exhaustedAllData = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onReceivedCompleteFrame(header: FrameHeader, bodyIfNotEmpty: Buffer) {
|
||||
// An incoming zero-length frame signals that there's no more data to read.
|
||||
// Signal this to the Node stream APIs by pushing a 'null' chunk to it.
|
||||
const virtualConnection = this._getOrOpenVirtualConnection(header);
|
||||
virtualConnection.onReceivedData(header.bodyLength > 0 ? bodyIfNotEmpty : null);
|
||||
}
|
||||
|
||||
private _getOrOpenVirtualConnection(header: FrameHeader) {
|
||||
if (this._virtualConnections.hasOwnProperty(header.connectionIdString)) {
|
||||
// It's an existing virtual connection
|
||||
return this._virtualConnections[header.connectionIdString];
|
||||
} else {
|
||||
// It's a new one
|
||||
return this._openVirtualConnection(header);
|
||||
}
|
||||
}
|
||||
|
||||
private _openVirtualConnection(header: FrameHeader) {
|
||||
const beginWriteCallback = (data, writeCompletedCallback) => {
|
||||
// Only send nonempty frames, since empty ones are a signal to close the virtual connection
|
||||
if (data.length > 0) {
|
||||
this._sendFrame(header.connectionIdBinary, data, writeCompletedCallback);
|
||||
}
|
||||
};
|
||||
|
||||
const newVirtualConnection = new VirtualConnection(beginWriteCallback);
|
||||
newVirtualConnection.on('end', () => {
|
||||
// The virtual connection was closed remotely. Clean up locally.
|
||||
this._onVirtualConnectionWasClosed(header.connectionIdString);
|
||||
});
|
||||
newVirtualConnection.on('finish', () => {
|
||||
// The virtual connection was closed locally. Clean up locally, and notify the remote that we're done.
|
||||
this._onVirtualConnectionWasClosed(header.connectionIdString);
|
||||
this._sendFrame(header.connectionIdBinary, new Buffer(0));
|
||||
});
|
||||
|
||||
this._virtualConnections[header.connectionIdString] = newVirtualConnection;
|
||||
this._onVirtualConnectionCallback(newVirtualConnection);
|
||||
return newVirtualConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read a complete frame header, synchronously, from the underlying socket.
|
||||
* If not enough data is available synchronously, returns null without consuming any data from the socket.
|
||||
*/
|
||||
private _readNextFrameHeader(): FrameHeader {
|
||||
const headerBuf: Buffer = this._socket.read(12);
|
||||
if (headerBuf !== null) {
|
||||
// We have enough data synchronously
|
||||
const connectionIdBinary = headerBuf.slice(0, 8);
|
||||
const connectionIdString = connectionIdBinary.toString('hex');
|
||||
const bodyLength = headerBuf.readInt32LE(8);
|
||||
if (bodyLength < 0 || bodyLength > MaxFrameBodyLength) {
|
||||
// Throwing here is going to bring down the whole process, so this cannot be allowed to happen in real use.
|
||||
// But it won't happen in real use, because this is only used with our .NET client, which doesn't violate this rule.
|
||||
throw new Error('Illegal frame body length: ' + bodyLength);
|
||||
}
|
||||
|
||||
return { connectionIdBinary, connectionIdString, bodyLength };
|
||||
} else {
|
||||
// Not enough bytes are available synchronously, so none were consumed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _sendFrame(connectionIdBinary: Buffer, data: Buffer, callback?: EndWriteCallback) {
|
||||
// For all sends other than the last one, only invoke the callback if it failed.
|
||||
// Also, only invoke the callback at most once.
|
||||
let hasInvokedCallback = false;
|
||||
const finalCallback: EndWriteCallback = callback && (error => {
|
||||
if (!hasInvokedCallback) {
|
||||
hasInvokedCallback = true;
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
const notFinalCallback: EndWriteCallback = callback && (error => {
|
||||
if (error) {
|
||||
finalCallback(error);
|
||||
}
|
||||
});
|
||||
|
||||
// The amount of data we're writing might exceed MaxFrameBodyLength, so split into frames as needed.
|
||||
// Note that we always send at least one frame, even if it's empty (because that's the close-virtual-connection signal).
|
||||
// If needed, this could be changed to send frames asynchronously, so that large sends could proceed in parallel
|
||||
// (though that would involve making a clone of 'data', to avoid the risk of it being mutated during the send).
|
||||
let bytesSent = 0;
|
||||
do {
|
||||
const nextFrameBodyLength = Math.min(MaxFrameBodyLength, data.length - bytesSent);
|
||||
const isFinalChunk = (bytesSent + nextFrameBodyLength) === data.length;
|
||||
this._socket.write(connectionIdBinary, notFinalCallback);
|
||||
this._sendInt32LE(nextFrameBodyLength, notFinalCallback);
|
||||
this._socket.write(data.slice(bytesSent, bytesSent + nextFrameBodyLength), isFinalChunk ? finalCallback : notFinalCallback);
|
||||
bytesSent += nextFrameBodyLength;
|
||||
} while (bytesSent < data.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a number serialized in the correct format for .NET to receive as a System.Int32
|
||||
*/
|
||||
private _sendInt32LE(value: number, callback?: EndWriteCallback) {
|
||||
const buf = new Buffer(4);
|
||||
buf.writeInt32LE(value, 0);
|
||||
this._socket.write(buf, callback);
|
||||
}
|
||||
|
||||
private _onVirtualConnectionWasClosed(id: string) {
|
||||
if (this._virtualConnections.hasOwnProperty(id)) {
|
||||
delete this._virtualConnections[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FrameHeader {
|
||||
connectionIdBinary: Buffer;
|
||||
connectionIdString: string;
|
||||
bodyLength: number;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es3",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Dataflow;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections
|
||||
{
|
||||
/// <summary>
|
||||
/// A virtual read/write connection, typically to a remote process. Multiple virtual connections can be
|
||||
/// multiplexed over a single physical connection (e.g., a named pipe, domain socket, or TCP socket).
|
||||
/// </summary>
|
||||
internal class VirtualConnection : Stream
|
||||
{
|
||||
#if NET451
|
||||
private readonly static Task CompletedTask = Task.FromResult((object)null);
|
||||
#else
|
||||
private readonly static Task CompletedTask = Task.CompletedTask;
|
||||
#endif
|
||||
private VirtualConnectionClient _host;
|
||||
private readonly BufferBlock<byte[]> _receivedDataQueue = new BufferBlock<byte[]>();
|
||||
private ArraySegment<byte> _receivedDataNotYetUsed;
|
||||
private bool _wasClosedByRemote;
|
||||
private bool _isDisposed;
|
||||
|
||||
public VirtualConnection(long id, VirtualConnectionClient host)
|
||||
{
|
||||
Id = id;
|
||||
_host = host;
|
||||
}
|
||||
|
||||
public long Id { get; }
|
||||
|
||||
public override bool CanRead { get { return true; } }
|
||||
public override bool CanSeek { get { return false; } }
|
||||
public override bool CanWrite { get { return true; } }
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
set { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
// We're auto-flushing, so this is a no-op.
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_wasClosedByRemote)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var bytesRead = 0;
|
||||
while (true)
|
||||
{
|
||||
// Pull as many applicable bytes as we can out of receivedDataNotYetUsed, then update its offset/length
|
||||
int bytesToExtract = Math.Min(count - bytesRead, _receivedDataNotYetUsed.Count);
|
||||
if (bytesToExtract > 0)
|
||||
{
|
||||
Buffer.BlockCopy(_receivedDataNotYetUsed.Array, _receivedDataNotYetUsed.Offset, buffer, bytesRead, bytesToExtract);
|
||||
_receivedDataNotYetUsed = new ArraySegment<byte>(_receivedDataNotYetUsed.Array, _receivedDataNotYetUsed.Offset + bytesToExtract, _receivedDataNotYetUsed.Count - bytesToExtract);
|
||||
bytesRead += bytesToExtract;
|
||||
}
|
||||
|
||||
// If we've completely filled the output buffer, we're done
|
||||
if (bytesRead == count)
|
||||
{
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
// We haven't yet filled the output buffer, so we must have exhausted receivedDataNotYetUsed instead.
|
||||
// We want to get the next block of data from the underlying queue.
|
||||
byte[] nextReceivedBlock;
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
if (!_receivedDataQueue.TryReceive(null, out nextReceivedBlock))
|
||||
{
|
||||
// No more data is available synchronously, and we already have some data, so we can stop now
|
||||
return bytesRead;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since we don't yet have anything, wait for the underlying source
|
||||
nextReceivedBlock = await _receivedDataQueue.ReceiveAsync(cancellationToken);
|
||||
}
|
||||
|
||||
if (nextReceivedBlock.Length == 0)
|
||||
{
|
||||
// A zero-length block signals that the remote regards this virtual connection as closed
|
||||
_wasClosedByRemote = true;
|
||||
return bytesRead;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We got some more data, so can continue trying to fill the output buffer
|
||||
_receivedDataNotYetUsed = new ArraySegment<byte>(nextReceivedBlock, 0, nextReceivedBlock.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_wasClosedByRemote)
|
||||
{
|
||||
throw new InvalidOperationException("The connection was already closed by the remote party");
|
||||
}
|
||||
|
||||
return count > 0 ? _host.WriteAsync(Id, buffer, offset, count, cancellationToken) : CompletedTask;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return ReadAsync(buffer, offset, count, CancellationToken.None).Result;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count, CancellationToken.None).Wait();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_host.CloseInnerStream(Id, _wasClosedByRemote);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddDataToQueue(byte[] data)
|
||||
{
|
||||
await _receivedDataQueue.SendAsync(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections
|
||||
{
|
||||
/// <summary>
|
||||
/// A callback that will be invoked if the <see cref="VirtualConnectionClient"/> encounters a read error.
|
||||
/// </summary>
|
||||
/// <param name="ex"></param>
|
||||
public delegate void VirtualConnectionReadErrorHandler(Exception ex);
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an underlying physical read/write stream (e.g., named pipes, domain sockets, or TCP sockets) and
|
||||
/// exposes an API for making 'virtual connections', which act as independent read/write streams.
|
||||
/// Traffic over these virtual connections is multiplexed over the underlying physical stream. This is useful
|
||||
/// for fast stream-based inter-process communication because it avoids the overhead of opening a new physical
|
||||
/// connection each time a new communication channel is needed.
|
||||
/// </summary>
|
||||
internal class VirtualConnectionClient : IDisposable
|
||||
{
|
||||
internal const int MaxFrameBodyLength = 16 * 1024;
|
||||
|
||||
public event VirtualConnectionReadErrorHandler OnError;
|
||||
|
||||
private Stream _underlyingTransport;
|
||||
private Dictionary<long, VirtualConnection> _activeInnerStreams;
|
||||
private long _nextInnerStreamId;
|
||||
private readonly SemaphoreSlim _streamWriterSemaphore = new SemaphoreSlim(1);
|
||||
private readonly object _readControlLock = new object();
|
||||
private Exception _readLoopExitedWithException;
|
||||
private readonly CancellationTokenSource _disposalCancellatonToken = new CancellationTokenSource();
|
||||
private bool _disposedValue = false;
|
||||
|
||||
public VirtualConnectionClient(Stream underlyingTransport)
|
||||
{
|
||||
_underlyingTransport = underlyingTransport;
|
||||
_activeInnerStreams = new Dictionary<long, VirtualConnection>();
|
||||
|
||||
RunReadLoop();
|
||||
}
|
||||
|
||||
public Stream OpenVirtualConnection()
|
||||
{
|
||||
// Improve discoverability of read-loop errors (in case the developer doesn't add an OnError listener)
|
||||
ThrowIfReadLoopFailed();
|
||||
|
||||
var id = Interlocked.Increment(ref _nextInnerStreamId);
|
||||
var newInnerStream = new VirtualConnection(id, this);
|
||||
lock (_activeInnerStreams)
|
||||
{
|
||||
_activeInnerStreams.Add(id, newInnerStream);
|
||||
}
|
||||
|
||||
return newInnerStream;
|
||||
}
|
||||
|
||||
// It's async void because nothing waits for it to finish (it continues indefinitely). It signals any errors via
|
||||
// a separate channel.
|
||||
private async void RunReadLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_disposalCancellatonToken.IsCancellationRequested)
|
||||
{
|
||||
var remoteIsStillConnected = await ProcessNextFrameAsync();
|
||||
if (!remoteIsStillConnected)
|
||||
{
|
||||
CloseAllActiveStreams();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not all underlying transports correctly honor cancellation tokens. For example,
|
||||
// DomainSocketStreamTransport's ReadAsync ignores them, so we only know to stop
|
||||
// the read loop when the underlying stream is disposed and then it throws ObjectDisposedException.
|
||||
if (!(ex is TaskCanceledException || ex is ObjectDisposedException))
|
||||
{
|
||||
_readLoopExitedWithException = ex;
|
||||
|
||||
var evt = OnError;
|
||||
if (evt != null)
|
||||
{
|
||||
evt(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessNextFrameAsync()
|
||||
{
|
||||
// First read frame header
|
||||
var frameHeaderBuffer = await ReadExactLength(12);
|
||||
if (frameHeaderBuffer == null)
|
||||
{
|
||||
return false; // Underlying stream was closed
|
||||
}
|
||||
|
||||
// Parse frame header, then read the frame body
|
||||
long streamId = BitConverter.ToInt64(frameHeaderBuffer, 0);
|
||||
int frameBodyLength = BitConverter.ToInt32(frameHeaderBuffer, 8);
|
||||
if (frameBodyLength < 0 || frameBodyLength > MaxFrameBodyLength)
|
||||
{
|
||||
throw new InvalidDataException("Illegal frame length: " + frameBodyLength);
|
||||
}
|
||||
|
||||
var frameBody = await ReadExactLength(frameBodyLength);
|
||||
if (frameBody == null)
|
||||
{
|
||||
return false; // Underlying stream was closed
|
||||
}
|
||||
|
||||
// Dispatch the frame to the relevant inner stream
|
||||
VirtualConnection innerStream;
|
||||
lock (_activeInnerStreams)
|
||||
{
|
||||
_activeInnerStreams.TryGetValue(streamId, out innerStream);
|
||||
}
|
||||
|
||||
if (innerStream != null)
|
||||
{
|
||||
await innerStream.AddDataToQueue(frameBody);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<byte[]> ReadExactLength(int lengthToRead) {
|
||||
byte[] buffer = new byte[lengthToRead];
|
||||
var totalBytesRead = 0;
|
||||
var ct = _disposalCancellatonToken.Token;
|
||||
while (totalBytesRead < lengthToRead)
|
||||
{
|
||||
var chunkLengthRead = await _underlyingTransport.ReadAsync(buffer, totalBytesRead, lengthToRead - totalBytesRead, ct);
|
||||
if (chunkLengthRead == 0)
|
||||
{
|
||||
// Underlying stream was closed
|
||||
return null;
|
||||
}
|
||||
|
||||
totalBytesRead += chunkLengthRead;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private void CloseAllActiveStreams()
|
||||
{
|
||||
IList<VirtualConnection> innerStreamsCopy;
|
||||
|
||||
// Only hold the lock while cloning the list of inner streams. Release the lock before
|
||||
// actually disposing them, because each 'dispose' call will try to take another lock
|
||||
// so it can remove that inner stream from activeInnerStreams.
|
||||
lock (_activeInnerStreams)
|
||||
{
|
||||
innerStreamsCopy = _activeInnerStreams.Values.ToList();
|
||||
}
|
||||
|
||||
foreach (var stream in innerStreamsCopy)
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
_disposedValue = true;
|
||||
|
||||
_disposalCancellatonToken.Cancel(); // Stops the read loop
|
||||
CloseAllActiveStreams();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteAsync(long innerStreamId, byte[] data, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
// In case the amount of data to be sent exceeds the max frame length, split it into separate frames
|
||||
// Note that we always send at least one frame, even if it's empty, because the zero-length frame is the signal to close a virtual connection
|
||||
// (hence 'do..while' instead of just 'while').
|
||||
int bytesWritten = 0;
|
||||
do {
|
||||
// Improve discoverability of read-loop errors (in case the developer doesn't add an OnError listener)
|
||||
ThrowIfReadLoopFailed();
|
||||
|
||||
// Hold the write lock only for the time taken to send a single frame, not all frames, to allow large sends to be proceed in parallel
|
||||
await _streamWriterSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Write stream ID, then length prefix, then chunk payload, then flush
|
||||
var nextChunkBodyLength = Math.Min(MaxFrameBodyLength, count - bytesWritten);
|
||||
await _underlyingTransport.WriteAsync(BitConverter.GetBytes(innerStreamId), 0, 8, cancellationToken).ConfigureAwait(false);
|
||||
await _underlyingTransport.WriteAsync(BitConverter.GetBytes(nextChunkBodyLength), 0, 4, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (nextChunkBodyLength > 0)
|
||||
{
|
||||
await _underlyingTransport.WriteAsync(data, offset + bytesWritten, nextChunkBodyLength, cancellationToken).ConfigureAwait(false);
|
||||
bytesWritten += nextChunkBodyLength;
|
||||
}
|
||||
|
||||
await _underlyingTransport.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_streamWriterSemaphore.Release();
|
||||
}
|
||||
} while (bytesWritten < count);
|
||||
}
|
||||
|
||||
public void CloseInnerStream(long innerStreamId, bool isAlreadyClosedRemotely)
|
||||
{
|
||||
lock (_activeInnerStreams)
|
||||
{
|
||||
if (_activeInnerStreams.ContainsKey(innerStreamId))
|
||||
{
|
||||
_activeInnerStreams.Remove(innerStreamId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAlreadyClosedRemotely) {
|
||||
// Also notify the remote that this innerstream is closed
|
||||
WriteAsync(innerStreamId, new byte[0], 0, 0, new CancellationToken()).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfReadLoopFailed()
|
||||
{
|
||||
if (_readLoopExitedWithException != null)
|
||||
{
|
||||
throw new AggregateException("The connection failed - see InnerException for details.", _readLoopExitedWithException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Microsoft.AspNetCore.NodeServices.Sockets/package.json
Normal file
18
src/Microsoft.AspNetCore.NodeServices.Sockets/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "nodeservices.sockets",
|
||||
"version": "1.0.0",
|
||||
"description": "This is not really an NPM package and will not be published. This file exists only to reference compilation tools.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "./node_modules/.bin/webpack"
|
||||
},
|
||||
"author": "Microsoft",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^6.0.42",
|
||||
"ts-loader": "^0.8.2",
|
||||
"typescript": "^2.0.0",
|
||||
"webpack": "^1.13.1"
|
||||
}
|
||||
}
|
||||
39
src/Microsoft.AspNetCore.NodeServices.Sockets/project.json
Normal file
39
src/Microsoft.AspNetCore.NodeServices.Sockets/project.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"description": "Socket-based RPC for Microsoft.AspNetCore.NodeServices",
|
||||
"version": "1.1.0-*",
|
||||
"packOptions": {
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/aspnet/javascriptservices"
|
||||
},
|
||||
"tags": [
|
||||
"aspnetcore",
|
||||
"aspnetcoremvc",
|
||||
"nodeservices"
|
||||
]
|
||||
},
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"embed": [
|
||||
"Content/**/*"
|
||||
],
|
||||
"xmlDoc": true
|
||||
},
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.NodeServices": "1.1.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"net451": {
|
||||
"dependencies": {
|
||||
"Microsoft.Tpl.Dataflow": "4.5.24"
|
||||
}
|
||||
},
|
||||
"netstandard1.6": {
|
||||
"dependencies": {
|
||||
"System.IO.Pipes": "4.3.0",
|
||||
"System.Threading.Tasks.Dataflow": "4.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
externals: ['fs', 'net', 'events', 'readline', 'stream'],
|
||||
resolve: {
|
||||
extensions: [ '.ts' ]
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.ts$/, loader: 'ts-loader' },
|
||||
]
|
||||
},
|
||||
entry: {
|
||||
'entrypoint-socket': ['./TypeScript/SocketNodeInstanceEntryPoint'],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: 'commonjs',
|
||||
path: './Content/Node',
|
||||
filename: '[name].js'
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user