mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 01:58:29 +00:00
Move logic for restarting Node child process into NodeServicesImpl. Tidy up lots.
This commit is contained in:
@@ -48,7 +48,8 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
case NodeHostingModel.Http:
|
case NodeHostingModel.Http:
|
||||||
return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions);
|
return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions);
|
||||||
case NodeHostingModel.Socket:
|
case NodeHostingModel.Socket:
|
||||||
return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions);
|
var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string
|
||||||
|
return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName);
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
|
throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
// Begin listening now. The underlying transport varies according to the runtime platform.
|
// 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.
|
// On Windows it's Named Pipes; on Linux/OSX it's Domain Sockets.
|
||||||
var useWindowsNamedPipes = /^win/.test(process.platform);
|
var useWindowsNamedPipes = /^win/.test(process.platform);
|
||||||
var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.pipename;
|
var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
|
||||||
server.listen(listenAddress);
|
server.listen(listenAddress);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ using Newtonsoft.Json.Serialization;
|
|||||||
|
|
||||||
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A specialisation of the OutOfProcessNodeInstance base class that uses HTTP to perform RPC invocations.
|
||||||
|
///
|
||||||
|
/// The Node child process starts an HTTP listener on an arbitrary available port (except where a nonzero
|
||||||
|
/// port number is specified as a constructor parameter), and signals which port was selected using the same
|
||||||
|
/// input/output-based mechanism that the base class uses to determine when the child process is ready to
|
||||||
|
/// accept RPC invocations.
|
||||||
|
///
|
||||||
|
/// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing
|
||||||
|
/// the actual watching in .NET code (not Node), for consistency across platforms.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels.OutOfProcessNodeInstance" />
|
||||||
internal class HttpNodeInstance : OutOfProcessNodeInstance
|
internal class HttpNodeInstance : OutOfProcessNodeInstance
|
||||||
{
|
{
|
||||||
private static readonly Regex PortMessageRegex =
|
private static readonly Regex PortMessageRegex =
|
||||||
@@ -19,7 +31,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||||
};
|
};
|
||||||
|
|
||||||
private HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private int _portNumber;
|
private int _portNumber;
|
||||||
|
|
||||||
@@ -47,9 +59,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
|
|
||||||
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
|
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
|
||||||
{
|
{
|
||||||
await EnsureReady();
|
|
||||||
|
|
||||||
// TODO: Use System.Net.Http.Formatting (PostAsJsonAsync etc.)
|
|
||||||
var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings);
|
var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings);
|
||||||
var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json");
|
var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json");
|
||||||
var response = await _client.PostAsync("http://localhost:" + _portNumber, payload);
|
var response = await _client.PostAsync("http://localhost:" + _portNumber, payload);
|
||||||
@@ -97,6 +106,9 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
|
|
||||||
protected override void OnOutputDataReceived(string outputData)
|
protected override void OnOutputDataReceived(string outputData)
|
||||||
{
|
{
|
||||||
|
// Watch for "port selected" messages, and when observed, store the port number
|
||||||
|
// so we can use it when making HTTP requests. The child process will always send
|
||||||
|
// one of these messages before it sends a "ready for connections" message.
|
||||||
var match = _portNumber != 0 ? null : PortMessageRegex.Match(outputData);
|
var match = _portNumber != 0 ? null : PortMessageRegex.Match(outputData);
|
||||||
if (match != null && match.Success)
|
if (match != null && match.Success)
|
||||||
{
|
{
|
||||||
@@ -108,12 +120,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnBeforeLaunchProcess()
|
|
||||||
{
|
|
||||||
// Prepare to receive a new port number
|
|
||||||
_portNumber = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing) {
|
protected override void Dispose(bool disposing) {
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,17 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
{
|
{
|
||||||
public class NodeInvocationException : Exception
|
public class NodeInvocationException : Exception
|
||||||
{
|
{
|
||||||
|
public bool NodeInstanceUnavailable { get; private set; }
|
||||||
|
|
||||||
public NodeInvocationException(string message, string details)
|
public NodeInvocationException(string message, string details)
|
||||||
: base(message + Environment.NewLine + details)
|
: base(message + Environment.NewLine + details)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NodeInvocationException(string message, string details, bool nodeInstanceUnavailable)
|
||||||
|
: this(message, details)
|
||||||
|
{
|
||||||
|
NodeInstanceUnavailable = nodeInstanceUnavailable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,37 +6,43 @@ using System.Threading.Tasks;
|
|||||||
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class responsible for launching the Node child process, determining when it is ready to accept invocations,
|
/// Class responsible for launching a Node child process on the local machine, determining when it is ready to
|
||||||
/// and finally killing it when the parent process exits. Also it restarts the child process if it dies.
|
/// accept invocations, detecting if it dies on its own, and finally terminating it on disposal.
|
||||||
|
///
|
||||||
|
/// This abstract base class uses the input/output streams of the child process to perform a simple handshake
|
||||||
|
/// to determine when the child process is ready to accept invocations. This is agnostic to the mechanism that
|
||||||
|
/// derived classes use to actually perform the invocations (e.g., they could use HTTP-RPC, or a binary TCP
|
||||||
|
/// protocol, or any other RPC-type mechanism).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="Microsoft.AspNetCore.NodeServices.INodeInstance" />
|
/// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels.INodeInstance" />
|
||||||
public abstract class OutOfProcessNodeInstance : INodeInstance
|
public abstract class OutOfProcessNodeInstance : INodeInstance
|
||||||
{
|
{
|
||||||
private readonly object _childProcessLauncherLock;
|
private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]";
|
||||||
private string _commandLineArguments;
|
private readonly TaskCompletionSource<object> _connectionIsReadySource = new TaskCompletionSource<object>();
|
||||||
private readonly StringAsTempFile _entryPointScript;
|
|
||||||
private Process _nodeProcess;
|
|
||||||
private TaskCompletionSource<bool> _nodeProcessIsReadySource;
|
|
||||||
private readonly string _projectPath;
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
private readonly StringAsTempFile _entryPointScript;
|
||||||
|
private readonly Process _nodeProcess;
|
||||||
|
|
||||||
public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null)
|
public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null)
|
||||||
{
|
{
|
||||||
_childProcessLauncherLock = new object();
|
|
||||||
_entryPointScript = new StringAsTempFile(entryPointScript);
|
_entryPointScript = new StringAsTempFile(entryPointScript);
|
||||||
_projectPath = projectPath;
|
_nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments);
|
||||||
_commandLineArguments = commandLineArguments ?? string.Empty;
|
ConnectToInputOutputStreams();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CommandLineArguments
|
public async Task<T> InvokeExportAsync<T>(string moduleName, string exportNameOrNull, params object[] args)
|
||||||
{
|
{
|
||||||
get { return _commandLineArguments; }
|
// Wait until the connection is established. This will throw if the connection fails to initialize.
|
||||||
set { _commandLineArguments = value; }
|
await _connectionIsReadySource.Task;
|
||||||
}
|
|
||||||
|
|
||||||
public Task<T> InvokeExportAsync<T>(string moduleName, string exportNameOrNull, params object[] args)
|
if (_nodeProcess.HasExited)
|
||||||
{
|
{
|
||||||
return InvokeExportAsync<T>(new NodeInvocationInfo
|
// This special kind of exception triggers a transparent retry - NodeServicesImpl will launch
|
||||||
|
// a new Node instance and pass the invocation to that one instead.
|
||||||
|
throw new NodeInvocationException("The Node process has exited", null, nodeInstanceUnavailable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await InvokeExportAsync<T>(new NodeInvocationInfo
|
||||||
{
|
{
|
||||||
ModuleName = moduleName,
|
ModuleName = moduleName,
|
||||||
ExportedFunctionName = exportNameOrNull,
|
ExportedFunctionName = exportNameOrNull,
|
||||||
@@ -52,100 +58,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
|
|
||||||
protected abstract Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo);
|
protected abstract Task<T> InvokeExportAsync<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()
|
|
||||||
{
|
|
||||||
lock (_childProcessLauncherLock)
|
|
||||||
{
|
|
||||||
if (_nodeProcess == null || _nodeProcess.HasExited)
|
|
||||||
{
|
|
||||||
this.OnBeforeLaunchProcess();
|
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo("node")
|
|
||||||
{
|
|
||||||
Arguments = "\"" + _entryPointScript.FileName + "\" " + _commandLineArguments,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardInput = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
WorkingDirectory = _projectPath
|
|
||||||
};
|
|
||||||
|
|
||||||
// Append projectPath to NODE_PATH so it can locate node_modules
|
|
||||||
var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty;
|
|
||||||
if (existingNodePath != string.Empty)
|
|
||||||
{
|
|
||||||
existingNodePath += ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodePathValue = existingNodePath + Path.Combine(_projectPath, "node_modules");
|
|
||||||
#if NET451
|
|
||||||
startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue;
|
|
||||||
#else
|
|
||||||
startInfo.Environment["NODE_PATH"] = nodePathValue;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_nodeProcess = Process.Start(startInfo);
|
|
||||||
ConnectToInputOutputStreams();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var task = _nodeProcessIsReadySource.Task;
|
|
||||||
var initializationSucceeded = await task;
|
|
||||||
|
|
||||||
if (!initializationSucceeded)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConnectToInputOutputStreams()
|
|
||||||
{
|
|
||||||
var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.)
|
|
||||||
_nodeProcessIsReadySource = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
_nodeProcess.OutputDataReceived += (sender, evt) =>
|
|
||||||
{
|
|
||||||
if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted)
|
|
||||||
{
|
|
||||||
_nodeProcessIsReadySource.SetResult(true);
|
|
||||||
initializationIsCompleted = true;
|
|
||||||
}
|
|
||||||
else if (evt.Data != null)
|
|
||||||
{
|
|
||||||
OnOutputDataReceived(evt.Data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_nodeProcess.ErrorDataReceived += (sender, evt) =>
|
|
||||||
{
|
|
||||||
if (evt.Data != null)
|
|
||||||
{
|
|
||||||
OnErrorDataReceived(evt.Data);
|
|
||||||
if (!initializationIsCompleted)
|
|
||||||
{
|
|
||||||
_nodeProcessIsReadySource.SetResult(false);
|
|
||||||
initializationIsCompleted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_nodeProcess.BeginOutputReadLine();
|
|
||||||
_nodeProcess.BeginErrorReadLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnBeforeLaunchProcess()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnOutputDataReceived(string outputData)
|
protected virtual void OnOutputDataReceived(string outputData)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[Node] " + outputData);
|
Console.WriteLine("[Node] " + outputData);
|
||||||
@@ -165,12 +77,84 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
_entryPointScript.Dispose();
|
_entryPointScript.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
ExitNodeProcess();
|
// Make sure the Node process is finished
|
||||||
|
// TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
|
||||||
|
if (!_nodeProcess.HasExited)
|
||||||
|
{
|
||||||
|
_nodeProcess.Kill();
|
||||||
|
}
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments)
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo("node")
|
||||||
|
{
|
||||||
|
Arguments = "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty),
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
WorkingDirectory = projectPath
|
||||||
|
};
|
||||||
|
|
||||||
|
// Append projectPath to NODE_PATH so it can locate node_modules
|
||||||
|
var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty;
|
||||||
|
if (existingNodePath != string.Empty)
|
||||||
|
{
|
||||||
|
existingNodePath += ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules");
|
||||||
|
#if NET451
|
||||||
|
startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue;
|
||||||
|
#else
|
||||||
|
startInfo.Environment["NODE_PATH"] = nodePathValue;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return Process.Start(startInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectToInputOutputStreams()
|
||||||
|
{
|
||||||
|
var initializationIsCompleted = false;
|
||||||
|
|
||||||
|
_nodeProcess.OutputDataReceived += (sender, evt) =>
|
||||||
|
{
|
||||||
|
if (evt.Data == ConnectionEstablishedMessage && !initializationIsCompleted)
|
||||||
|
{
|
||||||
|
_connectionIsReadySource.SetResult(null);
|
||||||
|
initializationIsCompleted = true;
|
||||||
|
}
|
||||||
|
else if (evt.Data != null)
|
||||||
|
{
|
||||||
|
OnOutputDataReceived(evt.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_nodeProcess.ErrorDataReceived += (sender, evt) =>
|
||||||
|
{
|
||||||
|
if (evt.Data != null)
|
||||||
|
{
|
||||||
|
if (!initializationIsCompleted)
|
||||||
|
{
|
||||||
|
_connectionIsReadySource.SetException(
|
||||||
|
new InvalidOperationException("The Node.js process failed to initialize: " + evt.Data));
|
||||||
|
initializationIsCompleted = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnErrorDataReceived(evt.Data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_nodeProcess.BeginOutputReadLine();
|
||||||
|
_nodeProcess.BeginErrorReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
~OutOfProcessNodeInstance()
|
~OutOfProcessNodeInstance()
|
||||||
{
|
{
|
||||||
Dispose(false);
|
Dispose(false);
|
||||||
|
|||||||
@@ -10,6 +10,22 @@ using Newtonsoft.Json.Serialization;
|
|||||||
|
|
||||||
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||||
{
|
{
|
||||||
|
/// <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).
|
||||||
|
///
|
||||||
|
/// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing
|
||||||
|
/// the actual watching in .NET code (not Node), for consistency across platforms.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="Microsoft.AspNetCore.NodeServices.HostingModels.OutOfProcessNodeInstance" />
|
||||||
internal class SocketNodeInstance : OutOfProcessNodeInstance
|
internal class SocketNodeInstance : OutOfProcessNodeInstance
|
||||||
{
|
{
|
||||||
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
||||||
@@ -17,31 +33,48 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||||
};
|
};
|
||||||
|
|
||||||
private string _addressForNextConnection;
|
private readonly SemaphoreSlim _connectionCreationSemaphore = new SemaphoreSlim(1);
|
||||||
private readonly SemaphoreSlim _clientModificationSemaphore = new SemaphoreSlim(1);
|
private bool _connectionHasFailed;
|
||||||
private StreamConnection _currentPhysicalConnection;
|
private StreamConnection _physicalConnection;
|
||||||
private VirtualConnectionClient _currentVirtualConnectionClient;
|
private string _socketAddress;
|
||||||
|
private VirtualConnectionClient _virtualConnectionClient;
|
||||||
private readonly string[] _watchFileExtensions;
|
private readonly string[] _watchFileExtensions;
|
||||||
|
|
||||||
public SocketNodeInstance(string projectPath, string[] watchFileExtensions = null): base(
|
public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress): base(
|
||||||
EmbeddedResourceReader.Read(
|
EmbeddedResourceReader.Read(
|
||||||
typeof(SocketNodeInstance),
|
typeof(SocketNodeInstance),
|
||||||
"/Content/Node/entrypoint-socket.js"),
|
"/Content/Node/entrypoint-socket.js"),
|
||||||
projectPath)
|
projectPath,
|
||||||
|
MakeNewCommandLineOptions(socketAddress, watchFileExtensions))
|
||||||
{
|
{
|
||||||
_watchFileExtensions = watchFileExtensions;
|
_watchFileExtensions = watchFileExtensions;
|
||||||
|
_socketAddress = socketAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
|
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo)
|
||||||
{
|
{
|
||||||
await EnsureReady();
|
if (_connectionHasFailed)
|
||||||
var virtualConnectionClient = await GetOrCreateVirtualConnectionClientAsync();
|
{
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
await EnsureVirtualConnectionClientCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
bool shouldDisposeVirtualConnection = true;
|
||||||
Stream virtualConnection = null;
|
Stream virtualConnection = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
virtualConnection = _currentVirtualConnectionClient.OpenVirtualConnection();
|
virtualConnection = _virtualConnectionClient.OpenVirtualConnection();
|
||||||
|
|
||||||
// Send request
|
// Send request
|
||||||
await WriteJsonLineAsync(virtualConnection, invocationInfo);
|
await WriteJsonLineAsync(virtualConnection, invocationInfo);
|
||||||
@@ -75,46 +108,34 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<VirtualConnectionClient> GetOrCreateVirtualConnectionClientAsync()
|
private async Task EnsureVirtualConnectionClientCreated()
|
||||||
{
|
{
|
||||||
var client = _currentVirtualConnectionClient;
|
// Asynchronous equivalent to a 'lock(...) { ... }'
|
||||||
if (client == null)
|
await _connectionCreationSemaphore.WaitAsync();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await _clientModificationSemaphore.WaitAsync();
|
if (_virtualConnectionClient == null)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (_currentVirtualConnectionClient == null)
|
_physicalConnection = StreamConnection.Create();
|
||||||
|
|
||||||
|
var connection = await _physicalConnection.Open(_socketAddress);
|
||||||
|
_virtualConnectionClient = new VirtualConnectionClient(connection);
|
||||||
|
_virtualConnectionClient.OnError += (ex) =>
|
||||||
{
|
{
|
||||||
var address = _addressForNextConnection;
|
// This callback is fired only if there's a protocol-level failure (e.g., child process disconnected
|
||||||
if (string.IsNullOrEmpty(address))
|
// 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.
|
||||||
// This shouldn't happen, because we always await 'EnsureReady' before getting here.
|
_connectionHasFailed = true;
|
||||||
throw new InvalidOperationException("Cannot open connection to Node process until it has signalled that it is ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentPhysicalConnection = StreamConnection.Create();
|
// TODO: Log the exception properly. Need to change the chain of calls up to this point to supply
|
||||||
|
// an ILogger or IServiceProvider etc.
|
||||||
var connection = await _currentPhysicalConnection.Open(address);
|
Console.WriteLine(ex.Message);
|
||||||
_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
|
finally
|
||||||
{
|
{
|
||||||
return client;
|
_connectionCreationSemaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,21 +143,22 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
EnsurePipeRpcClientDisposed();
|
if (_virtualConnectionClient != null)
|
||||||
|
{
|
||||||
|
_virtualConnectionClient.Dispose();
|
||||||
|
_virtualConnectionClient = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_physicalConnection != null)
|
||||||
|
{
|
||||||
|
_physicalConnection.Dispose();
|
||||||
|
_physicalConnection = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(disposing);
|
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)
|
private static async Task WriteJsonLineAsync(Stream stream, object serializableObject)
|
||||||
{
|
{
|
||||||
var json = JsonConvert.SerializeObject(serializableObject, jsonSerializerSettings);
|
var json = JsonConvert.SerializeObject(serializableObject, jsonSerializerSettings);
|
||||||
@@ -166,9 +188,9 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string MakeNewCommandLineOptions(string pipeName, string[] watchFileExtensions)
|
private static string MakeNewCommandLineOptions(string listenAddress, string[] watchFileExtensions)
|
||||||
{
|
{
|
||||||
var result = "--pipename " + pipeName;
|
var result = "--listenAddress " + listenAddress;
|
||||||
if (watchFileExtensions != null && watchFileExtensions.Length > 0)
|
if (watchFileExtensions != null && watchFileExtensions.Length > 0)
|
||||||
{
|
{
|
||||||
result += " --watch " + string.Join(",", watchFileExtensions);
|
result += " --watch " + string.Join(",", watchFileExtensions);
|
||||||
@@ -177,30 +199,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
return result;
|
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
|
#pragma warning disable 649 // These properties are populated via JSON deserialization
|
||||||
private class RpcJsonResponse<TResult>
|
private class RpcJsonResponse<TResult>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,9 +37,44 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args)
|
public Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args)
|
||||||
|
{
|
||||||
|
return InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, allowRetry: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> InvokeExportWithPossibleRetryAsync<T>(string moduleName, string exportedFunctionName, object[] args, bool allowRetry)
|
||||||
{
|
{
|
||||||
var nodeInstance = GetOrCreateCurrentNodeInstance();
|
var nodeInstance = GetOrCreateCurrentNodeInstance();
|
||||||
return nodeInstance.InvokeExportAsync<T>(moduleName, exportedFunctionName, args);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await nodeInstance.InvokeExportAsync<T>(moduleName, exportedFunctionName, args);
|
||||||
|
}
|
||||||
|
catch (NodeInvocationException ex)
|
||||||
|
{
|
||||||
|
// If the Node instance can't complete the invocation because it needs to restart (e.g., because the underlying
|
||||||
|
// Node process has exited, or a file it depends on has changed), then we make one attempt to restart transparently.
|
||||||
|
if (allowRetry && ex.NodeInstanceUnavailable)
|
||||||
|
{
|
||||||
|
// Perform the retry after clearing away the old instance
|
||||||
|
lock (_currentNodeInstanceAccessLock)
|
||||||
|
{
|
||||||
|
if (_currentNodeInstance == nodeInstance)
|
||||||
|
{
|
||||||
|
DisposeNodeInstance(_currentNodeInstance);
|
||||||
|
_currentNodeInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One the next call, don't allow retries, because we could get into an infinite retry loop, or a long retry
|
||||||
|
// loop that masks an underlying problem. A newly-created Node instance should be able to accept invocations,
|
||||||
|
// or something more serious must be wrong.
|
||||||
|
return await InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, allowRetry: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -48,12 +83,19 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
{
|
{
|
||||||
if (_currentNodeInstance != null)
|
if (_currentNodeInstance != null)
|
||||||
{
|
{
|
||||||
_currentNodeInstance.Dispose();
|
DisposeNodeInstance(_currentNodeInstance);
|
||||||
_currentNodeInstance = null;
|
_currentNodeInstance = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void DisposeNodeInstance(INodeInstance nodeInstance)
|
||||||
|
{
|
||||||
|
// TODO: Implement delayed disposal for connection draining
|
||||||
|
// Or consider having the delayedness of it being a responsibility of the INodeInstance
|
||||||
|
nodeInstance.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private INodeInstance GetOrCreateCurrentNodeInstance()
|
private INodeInstance GetOrCreateCurrentNodeInstance()
|
||||||
{
|
{
|
||||||
var instance = _currentNodeInstance;
|
var instance = _currentNodeInstance;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ virtualConnectionServer.createInterface(server).on('connection', (connection: Du
|
|||||||
// Begin listening now. The underlying transport varies according to the runtime platform.
|
// 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.
|
// On Windows it's Named Pipes; on Linux/OSX it's Domain Sockets.
|
||||||
const useWindowsNamedPipes = /^win/.test(process.platform);
|
const useWindowsNamedPipes = /^win/.test(process.platform);
|
||||||
const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.pipename;
|
const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
|
||||||
server.listen(listenAddress);
|
server.listen(listenAddress);
|
||||||
|
|
||||||
interface RpcInvocation {
|
interface RpcInvocation {
|
||||||
|
|||||||
Reference in New Issue
Block a user