Implement SocketNodeInstance

This commit is contained in:
SteveSandersonMS
2016-06-01 16:05:29 +01:00
parent 32ebaecdd8
commit 341cd4f1cb
23 changed files with 3982 additions and 8 deletions

View File

@@ -1 +1,2 @@
/bin/ /bin/
/node_modules/

View File

@@ -39,6 +39,8 @@ namespace Microsoft.AspNetCore.NodeServices
{ {
case NodeHostingModel.Http: case NodeHostingModel.Http:
return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions); return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions);
case NodeHostingModel.Socket:
return new SocketNodeInstance(options.ProjectPath, watchFileExtensions);
case NodeHostingModel.InputOutputStream: case NodeHostingModel.InputOutputStream:
return new InputOutputStreamNodeInstance(options.ProjectPath); return new InputOutputStreamNodeInstance(options.ProjectPath);
default: default:

View File

@@ -0,0 +1,408 @@
(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.
var fs = __webpack_require__(2);
var net = __webpack_require__(3);
var path = __webpack_require__(4);
var readline = __webpack_require__(5);
var virtualConnectionServer = __webpack_require__(6);
// 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');
var parsedArgs = parseArgs(process.argv);
if (parsedArgs.watch) {
autoQuitOnFileChange(process.cwd(), parsedArgs.watch.split(','));
}
// 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;
// Actually invoke it, passing the callback followed by any supplied args
var invocationCallback = function (errorValue, successValue) {
connection.end(JSON.stringify({
result: successValue,
errorMessage: errorValue && (errorValue.message || errorValue),
errorDetails: errorValue && (errorValue.stack || null)
}));
};
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 listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.pipename;
server.listen(listenAddress);
function autoQuitOnFileChange(rootDir, extensions) {
// Note: This will only work on Windows/OS X, because the 'recursive' option isn't supported on Linux.
// Consider using a different watch mechanism (though ideally without forcing further NPM dependencies).
fs.watch(rootDir, { persistent: false, recursive: true }, function (event, filename) {
var ext = path.extname(filename);
if (extensions.indexOf(ext) >= 0) {
console.log('Restarting due to file change: ' + filename);
process.exit(0);
}
});
}
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;
}
/***/ },
/* 2 */
/***/ function(module, exports) {
module.exports = require("fs");
/***/ },
/* 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, __webpack_require__) {
"use strict";
var events_1 = __webpack_require__(7);
var VirtualConnection_1 = __webpack_require__(8);
// 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)
.on('end', function () {
// The virtual connection was closed remotely. Clean up locally.
_this._onVirtualConnectionWasClosed(header.connectionIdString);
}).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;
}());
/***/ },
/* 7 */
/***/ function(module, exports) {
module.exports = require("events");
/***/ },
/* 8 */
/***/ 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__(9);
/**
* 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) {
_super.call(this);
this._beginWriteCallback = _beginWriteCallback;
this._flowing = false;
this._receivedDataQueue = [];
}
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;
/***/ },
/* 9 */
/***/ function(module, exports) {
module.exports = require("stream");
/***/ }
/******/ ])));

View File

@@ -0,0 +1,12 @@
using System;
namespace Microsoft.AspNetCore.NodeServices
{
public class NodeInvocationException : Exception
{
public NodeInvocationException(string message, string details)
: base(message + Environment.NewLine + details)
{
}
}
}

View File

@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.NodeServices
public abstract class OutOfProcessNodeInstance : INodeServices public abstract class OutOfProcessNodeInstance : INodeServices
{ {
private readonly object _childProcessLauncherLock; private readonly object _childProcessLauncherLock;
private readonly string _commandLineArguments; private string _commandLineArguments;
private readonly StringAsTempFile _entryPointScript; private readonly StringAsTempFile _entryPointScript;
private Process _nodeProcess; private Process _nodeProcess;
private TaskCompletionSource<bool> _nodeProcessIsReadySource; private TaskCompletionSource<bool> _nodeProcessIsReadySource;
@@ -27,6 +27,12 @@ namespace Microsoft.AspNetCore.NodeServices
_projectPath = projectPath; _projectPath = projectPath;
_commandLineArguments = commandLineArguments ?? string.Empty; _commandLineArguments = commandLineArguments ?? string.Empty;
} }
public string CommandLineArguments
{
get { return _commandLineArguments; }
set { _commandLineArguments = value; }
}
protected Process NodeProcess protected Process NodeProcess
{ {
@@ -59,12 +65,23 @@ namespace Microsoft.AspNetCore.NodeServices
public abstract Task<T> Invoke<T>(NodeInvocationInfo invocationInfo); public abstract Task<T> Invoke<T>(NodeInvocationInfo invocationInfo);
protected void ExitNodeProcess()
{
if (_nodeProcess != null && !_nodeProcess.HasExited)
{
// TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
_nodeProcess.Kill();
}
}
protected async Task EnsureReady() protected async Task EnsureReady()
{ {
lock (_childProcessLauncherLock) lock (_childProcessLauncherLock)
{ {
if (_nodeProcess == null || _nodeProcess.HasExited) if (_nodeProcess == null || _nodeProcess.HasExited)
{ {
this.OnBeforeLaunchProcess();
var startInfo = new ProcessStartInfo("node") var startInfo = new ProcessStartInfo("node")
{ {
Arguments = "\"" + _entryPointScript.FileName + "\" " + _commandLineArguments, Arguments = "\"" + _entryPointScript.FileName + "\" " + _commandLineArguments,
@@ -89,7 +106,6 @@ namespace Microsoft.AspNetCore.NodeServices
startInfo.Environment.Add("NODE_PATH", nodePathValue); startInfo.Environment.Add("NODE_PATH", nodePathValue);
#endif #endif
OnBeforeLaunchProcess();
_nodeProcess = Process.Start(startInfo); _nodeProcess = Process.Start(startInfo);
ConnectToInputOutputStreams(); ConnectToInputOutputStreams();
} }
@@ -162,11 +178,7 @@ namespace Microsoft.AspNetCore.NodeServices
_entryPointScript.Dispose(); _entryPointScript.Dispose();
} }
if (_nodeProcess != null && !_nodeProcess.HasExited) ExitNodeProcess();
{
_nodeProcess.Kill();
// TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
}
_disposed = true; _disposed = true;
} }

View File

@@ -0,0 +1,32 @@
using System.IO;
using System.IO.Pipes;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections
{
internal class NamedPipeConnection : StreamConnection
{
private bool _disposedValue = false;
private NamedPipeClientStream _namedPipeClientStream;
public override async Task<Stream> Open(string address)
{
_namedPipeClientStream = new NamedPipeClientStream(".", address, PipeDirection.InOut);
await _namedPipeClientStream.ConnectAsync().ConfigureAwait(false);
return _namedPipeClientStream;
}
public override void Dispose()
{
if (!_disposedValue)
{
if (_namedPipeClientStream != null)
{
_namedPipeClientStream.Dispose();
}
_disposedValue = true;
}
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections
{
internal abstract class StreamConnection : IDisposable
{
public abstract Task<Stream> Open(string address);
public abstract void Dispose();
public static StreamConnection Create()
{
var useNamedPipes = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (useNamedPipes)
{
return new NamedPipeConnection();
}
else
{
return new UnixDomainSocketConnection();
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections
{
internal class UnixDomainSocketConnection : StreamConnection
{
private bool _disposedValue = false;
private NetworkStream _networkStream;
private Socket _socket;
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;
}
public override void Dispose()
{
if (!_disposedValue)
{
if (_networkStream != null)
{
_networkStream.Dispose();
}
if (_socket != null)
{
_socket.Dispose();
}
_disposedValue = true;
}
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Microsoft.AspNetCore.NodeServices.HostingModels.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
{
_encodedPath = Array.Empty<byte>();
_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;
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections;
using Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.NodeServices
{
internal class SocketNodeInstance : OutOfProcessNodeInstance
{
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
private string _addressForNextConnection;
private readonly SemaphoreSlim _clientModificationSemaphore = new SemaphoreSlim(1);
private StreamConnection _currentPhysicalConnection;
private VirtualConnectionClient _currentVirtualConnectionClient;
private readonly string[] _watchFileExtensions;
public SocketNodeInstance(string projectPath, string[] watchFileExtensions = null): base(
EmbeddedResourceReader.Read(
typeof(SocketNodeInstance),
"/Content/Node/entrypoint-socket.js"),
projectPath)
{
_watchFileExtensions = watchFileExtensions;
}
public override async Task<T> Invoke<T>(NodeInvocationInfo invocationInfo)
{
await EnsureReady();
var virtualConnectionClient = await GetOrCreateVirtualConnectionClientAsync();
using (var virtualConnection = _currentVirtualConnectionClient.OpenVirtualConnection())
{
// Send request
await WriteJsonLineAsync(virtualConnection, invocationInfo);
// Receive response
var response = await ReadJsonAsync<RpcResponse<T>>(virtualConnection);
if (response.ErrorMessage != null)
{
throw new NodeInvocationException(response.ErrorMessage, response.ErrorDetails);
}
return response.Result;
}
}
private async Task<VirtualConnectionClient> GetOrCreateVirtualConnectionClientAsync()
{
var client = _currentVirtualConnectionClient;
if (client == null)
{
await _clientModificationSemaphore.WaitAsync();
try
{
if (_currentVirtualConnectionClient == null)
{
var address = _addressForNextConnection;
if (string.IsNullOrEmpty(address))
{
// This shouldn't happen, because we always await 'EnsureReady' before getting here.
throw new InvalidOperationException("Cannot open connection to Node process until it has signalled that it is ready");
}
_currentPhysicalConnection = StreamConnection.Create();
var connection = await _currentPhysicalConnection.Open(address);
_currentVirtualConnectionClient = new VirtualConnectionClient(connection);
_currentVirtualConnectionClient.OnError += (ex) =>
{
// TODO: Log the exception properly. Need to change the chain of calls up to this point to supply
// an ILogger or IServiceProvider etc.
Console.WriteLine(ex.Message);
ExitNodeProcess(); // We'll restart it next time there's a request to it
};
}
return _currentVirtualConnectionClient;
}
finally
{
_clientModificationSemaphore.Release();
}
}
else
{
return client;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
EnsurePipeRpcClientDisposed();
}
base.Dispose(disposing);
}
protected override void OnBeforeLaunchProcess()
{
// Either we've never yet launched the Node process, or we did but the old one died.
// Stop waiting for any outstanding requests and prepare to launch the new process.
EnsurePipeRpcClientDisposed();
_addressForNextConnection = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string
CommandLineArguments = MakeNewCommandLineOptions(_addressForNextConnection, _watchFileExtensions);
}
private static async Task WriteJsonLineAsync(Stream stream, object serializableObject)
{
var json = JsonConvert.SerializeObject(serializableObject, jsonSerializerSettings);
var bytes = Encoding.UTF8.GetBytes(json + '\n');
await stream.WriteAsync(bytes, 0, bytes.Length);
}
private static async Task<T> ReadJsonAsync<T>(Stream stream)
{
var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream));
return JsonConvert.DeserializeObject<T>(json, jsonSerializerSettings);
}
private static async Task<byte[]> ReadAllBytesAsync(Stream input)
{
byte[] buffer = new byte[16*1024];
using (var ms = new MemoryStream())
{
int read;
while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
private static string MakeNewCommandLineOptions(string pipeName, string[] watchFileExtensions)
{
var result = "--pipename " + pipeName;
if (watchFileExtensions != null && watchFileExtensions.Length > 0)
{
result += " --watch " + string.Join(",", watchFileExtensions);
}
return result;
}
private void EnsurePipeRpcClientDisposed()
{
_clientModificationSemaphore.Wait();
try
{
if (_currentVirtualConnectionClient != null)
{
_currentVirtualConnectionClient.Dispose();
_currentVirtualConnectionClient = null;
}
if (_currentPhysicalConnection != null)
{
_currentPhysicalConnection.Dispose();
_currentPhysicalConnection = null;
}
}
finally
{
_clientModificationSemaphore.Release();
}
}
#pragma warning disable 649 // These properties are populated via JSON deserialization
private class RpcResponse<TResult>
{
public TResult Result { get; set; }
public string ErrorMessage { get; set; }
public string ErrorDetails { get; set; }
}
#pragma warning restore 649
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
namespace Microsoft.AspNetCore.NodeServices.HostingModels.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
{
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) : Task.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);
}
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections
{
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);
_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);
}
}
}
}

View File

@@ -4,5 +4,6 @@ namespace Microsoft.AspNetCore.NodeServices
{ {
Http, Http,
InputOutputStream, InputOutputStream,
Socket,
} }
} }

View File

@@ -0,0 +1,91 @@
// 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 * as fs from 'fs';
import * as net from 'net';
import * as path from 'path';
import * as readline from 'readline';
import { Duplex } from 'stream';
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');
const parsedArgs = parseArgs(process.argv);
if (parsedArgs.watch) {
autoQuitOnFileChange(process.cwd(), parsedArgs.watch.split(','));
}
// 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;
// Actually invoke it, passing the callback followed by any supplied args
const invocationCallback = (errorValue, successValue) => {
connection.end(JSON.stringify({
result: successValue,
errorMessage: errorValue && (errorValue.message || errorValue),
errorDetails: errorValue && (errorValue.stack || null)
}));
};
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 listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.pipename;
server.listen(listenAddress);
function autoQuitOnFileChange(rootDir: string, extensions: string[]) {
// Note: This will only work on Windows/OS X, because the 'recursive' option isn't supported on Linux.
// Consider using a different watch mechanism (though ideally without forcing further NPM dependencies).
fs.watch(rootDir, { persistent: false, recursive: true } as any, (event, filename) => {
var ext = path.extname(filename);
if (extensions.indexOf(ext) >= 0) {
console.log('Restarting due to file change: ' + filename);
process.exit(0);
}
});
}
function parseArgs(args: string[]): any {
// 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.
const result = {};
let currentKey = null;
args.forEach(arg => {
if (arg.indexOf('--') === 0) {
const argName = arg.substring(2);
result[argName] = undefined;
currentKey = argName;
} else if (currentKey) {
result[currentKey] = arg;
currentKey = null;
}
});
return result;
}
interface RpcInvocation {
moduleName: string;
exportedFunctionName: string;
args: any[];
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,198 @@
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)
.on('end', () => {
// The virtual connection was closed remotely. Clean up locally.
this._onVirtualConnectionWasClosed(header.connectionIdString);
}).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;
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node"
},
"exclude": [
"node_modules"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
/// <reference path="node/node.d.ts" />

View File

@@ -0,0 +1,17 @@
{
"name": "nodeservices",
"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": {
"ts-loader": "^0.8.2",
"typescript": "^1.8.10",
"webpack": "^1.13.1"
}
}

View File

@@ -10,6 +10,7 @@
"version": "1.0.0-rc2-*", "version": "1.0.0-rc2-*",
"type": "platform" "type": "platform"
}, },
"System.IO.Pipes": "4.0.0-*",
"Microsoft.AspNetCore.Hosting.Abstractions": "1.0.0-*", "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.0-*",
"Microsoft.Extensions.Configuration.Json": "1.0.0-*", "Microsoft.Extensions.Configuration.Json": "1.0.0-*",
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0-*", "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0-*",
@@ -17,7 +18,7 @@
"Newtonsoft.Json": "8.0.3" "Newtonsoft.Json": "8.0.3"
}, },
"frameworks": { "frameworks": {
"netcoreapp1.0": { "netstandard1.3": {
"imports": [ "imports": [
"dotnet5.6", "dotnet5.6",
"dnxcore50", "dnxcore50",

View File

@@ -0,0 +1,12 @@
{
"version": "v4",
"repo": "borisyankov/DefinitelyTyped",
"ref": "master",
"path": "TypeScript/typings",
"bundle": "TypeScript/typings/tsd.d.ts",
"installed": {
"node/node.d.ts": {
"commit": "edb64e4a35896510ce02b93c0bca5ec3878db738"
}
}
}

View File

@@ -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'
}
};