From 95cba7f5ddb980bc5919ade7a121eeb011aab15f Mon Sep 17 00:00:00 2001 From: Andrei Tserakhau Date: Mon, 23 May 2016 11:28:42 +0300 Subject: [PATCH 001/876] refactor: apply default vs transform to xproj refactor(spa-services): clean code refactor(node-services): clean code, extract classes nto separate files refactor(angular-services): prime cache cleanup --- samples/angular/MusicStore/MusicStore.xproj | 4 +- .../ES2015Transpilation.xproj | 4 +- samples/react/ReactGrid/ReactGrid.xproj | 4 +- ...Microsoft.AspNetCore.AngularServices.xproj | 5 +- .../PrimeCacheHelper.cs | 45 +++-- .../Configuration.cs | 46 +++-- .../HostingModels/HttpNodeInstance.cs | 82 ++++++--- .../InputOutputStreamNodeInstance.cs | 85 +++++---- .../HostingModels/NodeInvocationInfo.cs | 9 +- .../HostingModels/OutOfProcessNodeInstance.cs | 173 ++++++++++-------- .../INodeInstance.cs | 6 +- .../Microsoft.AspNetCore.NodeServices.xproj | 5 +- .../NodeHostingModel.cs | 6 +- .../NodeServicesOptions.cs | 14 ++ .../Util/EmbeddedResourceReader.cs | 14 +- .../Util/StringAsTempFile.cs | 42 +++-- .../Microsoft.AspNetCore.ReactServices.xproj | 5 +- .../Microsoft.AspNetCore.SpaServices.xproj | 5 +- .../Prerendering/JavaScriptModuleExport.cs | 14 ++ .../Prerendering/PrerenderTagHelper.cs | 77 ++++---- .../Prerendering/Prerenderer.cs | 38 ++-- .../Prerendering/RenderToStringResult.cs | 10 + .../Routing/SpaRouteConstraint.cs | 32 ++-- .../Routing/SpaRouteExtensions.cs | 46 +++-- .../Webpack/ConditionalProxyMiddleware.cs | 98 +++++----- .../ConditionalProxyMiddlewareOptions.cs | 16 ++ .../Webpack/WebpackDevMiddleware.cs | 68 ++++--- .../Webpack/WebpackDevMiddlewareOptions.cs | 8 +- templates/Angular2Spa/Angular2Spa.xproj | 4 +- templates/ReactReduxSpa/ReactReduxSpa.xproj | 4 +- templates/ReactSpa/ReactSpa.xproj | 4 +- .../WebApplicationBasic.xproj | 4 +- 32 files changed, 575 insertions(+), 402 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs diff --git a/samples/angular/MusicStore/MusicStore.xproj b/samples/angular/MusicStore/MusicStore.xproj index 2b748588..93efb5ea 100644 --- a/samples/angular/MusicStore/MusicStore.xproj +++ b/samples/angular/MusicStore/MusicStore.xproj @@ -10,11 +10,11 @@ 1a74148f-9dc0-435d-b5ac-7d1b0d3d5e0b MusicStore ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 5068 - + \ No newline at end of file diff --git a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj b/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj index 9f87ae01..ba3c0634 100644 --- a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj +++ b/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj @@ -10,11 +10,11 @@ 6d4bcdd6-7951-449b-be55-cb7f014b7430 ES2015Transpilation ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/samples/react/ReactGrid/ReactGrid.xproj b/samples/react/ReactGrid/ReactGrid.xproj index 7abf60c8..0d002a64 100644 --- a/samples/react/ReactGrid/ReactGrid.xproj +++ b/samples/react/ReactGrid/ReactGrid.xproj @@ -10,11 +10,11 @@ abf90a5b-f4e0-438c-a6e4-9549fb43690b ReactGrid ..\..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2311 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj b/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj index b037b327..701af9a5 100644 --- a/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj +++ b/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj @@ -9,11 +9,10 @@ 421807e6-b62c-417b-b901-46c5dedaa8f1 Microsoft.AspNetCore.AngularServices ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs index 48fdeca3..ca9f6030 100644 --- a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs +++ b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs @@ -3,42 +3,51 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Html; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.AngularServices { - public static class PrimeCacheHelper { - public static async Task PrimeCache(this IHtmlHelper html, string url) { +namespace Microsoft.AspNetCore.AngularServices +{ + public static class PrimeCacheHelper + { + public static async Task PrimeCache(this IHtmlHelper html, string url) + { // TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache // the same URL, only return nonempty for one of them). This will make it easier to auto-prime-cache any // HTTP requests made during server-side rendering, without risking unnecessary duplicate requests. - if (string.IsNullOrEmpty(url)) { + if (string.IsNullOrEmpty(url)) + { throw new ArgumentException("Value cannot be null or empty", nameof(url)); } - try { + try + { var request = html.ViewContext.HttpContext.Request; - var baseUri = new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent())); + var baseUri = + new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), + request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), + request.QueryString.ToUriComponent())); var fullUri = new Uri(baseUri, url); var response = await new HttpClient().GetAsync(fullUri.ToString()); var responseBody = await response.Content.ReadAsStringAsync(); return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody)); - } catch (Exception ex) { - var logger = (ILogger)html.ViewContext.HttpContext.RequestServices.GetService(typeof (ILogger)); - if (logger != null) { - logger.LogWarning("Error priming cache for URL: " + url, ex); - } + } + catch (Exception ex) + { + var logger = (ILogger)html.ViewContext.HttpContext.RequestServices.GetService(typeof(ILogger)); + logger?.LogWarning("Error priming cache for URL: " + url, ex); return new HtmlString(string.Empty); } } private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody) - { - return string.Format(@"", - JsonConvert.SerializeObject(url), - JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody }) - ); - } + => + ""; } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs index 5d85d07d..d40733a7 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs @@ -2,31 +2,37 @@ using Microsoft.Extensions.PlatformAbstractions; using Microsoft.AspNetCore.Hosting; -namespace Microsoft.AspNetCore.NodeServices { - public static class Configuration { - private readonly static string[] defaultWatchFileExtensions = new[] { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; - private readonly static NodeServicesOptions defaultOptions = new NodeServicesOptions { +namespace Microsoft.AspNetCore.NodeServices +{ + using System; + + public static class Configuration + { + private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"}; + + private static readonly NodeServicesOptions DefaultOptions = new NodeServicesOptions + { HostingModel = NodeHostingModel.Http, - WatchFileExtensions = defaultWatchFileExtensions + WatchFileExtensions = DefaultWatchFileExtensions }; - public static void AddNodeServices(this IServiceCollection serviceCollection) { - AddNodeServices(serviceCollection, defaultOptions); - } + public static void AddNodeServices(this IServiceCollection serviceCollection) + => AddNodeServices(serviceCollection, DefaultOptions); - public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) { - serviceCollection.AddSingleton(typeof(INodeServices), (serviceProvider) => { + public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) + => serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => + { var hostEnv = serviceProvider.GetRequiredService(); - if (string.IsNullOrEmpty(options.ProjectPath)) { + if (string.IsNullOrEmpty(options.ProjectPath)) + { options.ProjectPath = hostEnv.ContentRootPath; } return CreateNodeServices(options); }); - } public static INodeServices CreateNodeServices(NodeServicesOptions options) { - var watchFileExtensions = options.WatchFileExtensions ?? defaultWatchFileExtensions; + var watchFileExtensions = options.WatchFileExtensions ?? DefaultWatchFileExtensions; switch (options.HostingModel) { case NodeHostingModel.Http: @@ -34,18 +40,8 @@ public static INodeServices CreateNodeServices(NodeServicesOptions options) case NodeHostingModel.InputOutputStream: return new InputOutputStreamNodeInstance(options.ProjectPath); default: - throw new System.ArgumentException("Unknown hosting model: " + options.HostingModel.ToString()); + throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } } } - - public class NodeServicesOptions { - public NodeHostingModel HostingModel { get; set; } - public string ProjectPath { get; set; } - public string[] WatchFileExtensions { get; set; } - - public NodeServicesOptions() { - this.HostingModel = NodeHostingModel.Http; - } - } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 4671740c..8cff8830 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -7,66 +7,90 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices { - internal class HttpNodeInstance : OutOfProcessNodeInstance { - private readonly static Regex PortMessageRegex = new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); +namespace Microsoft.AspNetCore.NodeServices +{ + internal class HttpNodeInstance : OutOfProcessNodeInstance + { + private static readonly Regex PortMessageRegex = + new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); - private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings + { ContractResolver = new CamelCasePropertyNamesContractResolver() }; private int _portNumber; - public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null) - : base(EmbeddedResourceReader.Read(typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, MakeCommandLineOptions(port, watchFileExtensions)) + public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null) + : base( + EmbeddedResourceReader.Read( + typeof(HttpNodeInstance), + "/Content/Node/entrypoint-http.js"), + projectPath, + MakeCommandLineOptions(port, watchFileExtensions)) { - } + } - private static string MakeCommandLineOptions(int port, string[] watchFileExtensions) { - var result = "--port " + port.ToString(); - if (watchFileExtensions != null && watchFileExtensions.Length > 0) { + private static string MakeCommandLineOptions(int port, string[] watchFileExtensions) + { + var result = "--port " + port; + if (watchFileExtensions != null && watchFileExtensions.Length > 0) + { result += " --watch " + string.Join(",", watchFileExtensions); } + return result; } - public override async Task Invoke(NodeInvocationInfo invocationInfo) { - await this.EnsureReady(); + public override async Task Invoke(NodeInvocationInfo invocationInfo) + { + await EnsureReady(); - using (var client = new HttpClient()) { + using (var client = new HttpClient()) + { // 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 response = await client.PostAsync("http://localhost:" + this._portNumber, payload); + var response = await client.PostAsync("http://localhost:" + _portNumber, payload); var responseString = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) { + if (!response.IsSuccessStatusCode) + { throw new Exception("Call to Node module failed with error: " + responseString); } var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; - if (responseIsJson) { + if (responseIsJson) + { return JsonConvert.DeserializeObject(responseString); - } else if (typeof(T) != typeof(string)) { - throw new System.ArgumentException("Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + typeof(T).FullName); - } else { - return (T)(object)responseString; } + if (typeof(T) != typeof(string)) + { + throw new ArgumentException( + "Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + + typeof(T).FullName); + } + return (T)(object)responseString; } } - protected override void OnOutputDataReceived(string outputData) { - var match = this._portNumber != 0 ? null : PortMessageRegex.Match(outputData); - if (match != null && match.Success) { - this._portNumber = int.Parse(match.Groups[1].Captures[0].Value); - } else { + protected override void OnOutputDataReceived(string outputData) + { + var match = _portNumber != 0 ? null : PortMessageRegex.Match(outputData); + if (match != null && match.Success) + { + _portNumber = int.Parse(match.Groups[1].Captures[0].Value); + } + else + { base.OnOutputDataReceived(outputData); } } - protected override void OnBeforeLaunchProcess() { + protected override void OnBeforeLaunchProcess() + { // Prepare to receive a new port number - this._portNumber = 0; + _portNumber = 0; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs index 3f27acda..ffa2cdca 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs @@ -4,55 +4,72 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices { - // This is just to demonstrate that other transports are possible. This implementation is extremely - // dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an - // error), then it will just hang forever. So don't use this. - // - // But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine), - // versus 2-3ms for the HTTP transport. - // - // Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes - // on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the - // requests, associating them with responses, and scheduling use of the comms channel. +namespace Microsoft.AspNetCore.NodeServices +{ + /// + /// This is just to demonstrate that other transports are possible. This implementation is extremely + /// dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an + /// error), then it will just hang forever. So don't use this. + /// + /// But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine), + /// versus 2-3ms for the HTTP transport. + /// + /// Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes + /// on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the + /// requests, associating them with responses, and scheduling use of the comms channel. + /// + /// internal class InputOutputStreamNodeInstance : OutOfProcessNodeInstance { - private SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); - private TaskCompletionSource _currentInvocationResult; - - private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings + { ContractResolver = new CamelCasePropertyNamesContractResolver() }; - public InputOutputStreamNodeInstance(string projectPath) - : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeInstance), "/Content/Node/entrypoint-stream.js"), projectPath) + private TaskCompletionSource _currentInvocationResult; + private readonly SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); + + public InputOutputStreamNodeInstance(string projectPath) + : base( + EmbeddedResourceReader.Read( + typeof(InputOutputStreamNodeInstance), + "/Content/Node/entrypoint-stream.js"), + projectPath) { - } + } - public override async Task Invoke(NodeInvocationInfo invocationInfo) { - await this._invocationSemaphore.WaitAsync(); - try { - await this.EnsureReady(); + public override async Task Invoke(NodeInvocationInfo invocationInfo) + { + await _invocationSemaphore.WaitAsync(); + try + { + await EnsureReady(); - var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings); - var nodeProcess = this.NodeProcess; - this._currentInvocationResult = new TaskCompletionSource(); + var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); + var nodeProcess = NodeProcess; + _currentInvocationResult = new TaskCompletionSource(); nodeProcess.StandardInput.Write("\ninvoke:"); nodeProcess.StandardInput.WriteLine(payloadJson); // WriteLineAsync isn't supported cross-platform - var resultString = await this._currentInvocationResult.Task; + var resultString = await _currentInvocationResult.Task; return JsonConvert.DeserializeObject(resultString); - } finally { - this._invocationSemaphore.Release(); - this._currentInvocationResult = null; + } + finally + { + _invocationSemaphore.Release(); + _currentInvocationResult = null; } } - protected override void OnOutputDataReceived(string outputData) { - if (this._currentInvocationResult != null) { - this._currentInvocationResult.SetResult(outputData); - } else { + protected override void OnOutputDataReceived(string outputData) + { + if (_currentInvocationResult != null) + { + _currentInvocationResult.SetResult(outputData); + } + else + { base.OnOutputDataReceived(outputData); } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs index a4fde64a..2e196f6e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs @@ -1,8 +1,9 @@ -namespace Microsoft.AspNetCore.NodeServices { +namespace Microsoft.AspNetCore.NodeServices +{ public class NodeInvocationInfo { - public string ModuleName; - public string ExportedFunctionName; - public object[] Args; + public string ModuleName { get; set; } + public string ExportedFunctionName { get; set; } + public object[] Args { get; set; } } } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index fe3386a6..65e21a39 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,150 +3,167 @@ using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.NodeServices { - /** - * Class responsible for launching the Node child process, determining when it is ready to accept invocations, - * and finally killing it when the parent process exits. Also it restarts the child process if it dies. - */ - public abstract class OutOfProcessNodeInstance : INodeServices { - private object _childProcessLauncherLock; - private bool disposed; - private StringAsTempFile _entryPointScript; - private string _projectPath; - private string _commandLineArguments; - private Process _nodeProcess; +namespace Microsoft.AspNetCore.NodeServices +{ + /// + /// Class responsible for launching the Node child process, determining when it is ready to accept invocations, + /// and finally killing it when the parent process exits. Also it restarts the child process if it dies. + /// + /// + public abstract class OutOfProcessNodeInstance : INodeServices + { + private readonly object _childProcessLauncherLock; + private readonly string _commandLineArguments; + private readonly StringAsTempFile _entryPointScript; private TaskCompletionSource _nodeProcessIsReadySource; - - protected Process NodeProcess { - get { - // This is only exposed to support the unreliable OutOfProcessNodeRunner, which is just to verify that - // other hosting/transport mechanisms are possible. This shouldn't really be exposed. - return this._nodeProcess; - } - } + private readonly string _projectPath; + private bool _disposed; public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null) { - this._childProcessLauncherLock = new object(); - this._entryPointScript = new StringAsTempFile(entryPointScript); - this._projectPath = projectPath; - this._commandLineArguments = commandLineArguments ?? string.Empty; + _childProcessLauncherLock = new object(); + _entryPointScript = new StringAsTempFile(entryPointScript); + _projectPath = projectPath; + _commandLineArguments = commandLineArguments ?? string.Empty; } - public abstract Task Invoke(NodeInvocationInfo invocationInfo); + protected Process NodeProcess { get; private set; } - public Task Invoke(string moduleName, params object[] args) { - return this.InvokeExport(moduleName, null, args); - } + public Task Invoke(string moduleName, params object[] args) + => InvokeExport(moduleName, null, args); - public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { - return await this.Invoke(new NodeInvocationInfo { + public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) + => Invoke(new NodeInvocationInfo + { ModuleName = moduleName, ExportedFunctionName = exportedFunctionName, Args = args }); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } - protected async Task EnsureReady() { - lock (this._childProcessLauncherLock) { - if (this._nodeProcess == null || this._nodeProcess.HasExited) { - var startInfo = new ProcessStartInfo("node") { - Arguments = "\"" + this._entryPointScript.FileName + "\" " + this._commandLineArguments, + public abstract Task Invoke(NodeInvocationInfo invocationInfo); + + protected async Task EnsureReady() + { + lock (_childProcessLauncherLock) + { + if (NodeProcess == null || NodeProcess.HasExited) + { + var startInfo = new ProcessStartInfo("node") + { + Arguments = "\"" + _entryPointScript.FileName + "\" " + _commandLineArguments, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, - WorkingDirectory = this._projectPath + 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) { + if (existingNodePath != string.Empty) + { existingNodePath += ":"; } - var nodePathValue = existingNodePath + Path.Combine(this._projectPath, "node_modules"); - #if NET451 + var nodePathValue = existingNodePath + Path.Combine(_projectPath, "node_modules"); +#if NET451 startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue); - #else +#else startInfo.Environment.Add("NODE_PATH", nodePathValue); - #endif +#endif - this.OnBeforeLaunchProcess(); - this._nodeProcess = Process.Start(startInfo); - this.ConnectToInputOutputStreams(); + OnBeforeLaunchProcess(); + NodeProcess = Process.Start(startInfo); + ConnectToInputOutputStreams(); } } - var task = this._nodeProcessIsReadySource.Task; + var task = _nodeProcessIsReadySource.Task; var initializationSucceeded = await task; - if (!initializationSucceeded) { + if (!initializationSucceeded) + { throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception); } } - private void ConnectToInputOutputStreams() { + private void ConnectToInputOutputStreams() + { var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) - this._nodeProcessIsReadySource = new TaskCompletionSource(); + _nodeProcessIsReadySource = new TaskCompletionSource(); - this._nodeProcess.OutputDataReceived += (sender, evt) => { - if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted) { - this._nodeProcessIsReadySource.SetResult(true); + NodeProcess.OutputDataReceived += (sender, evt) => + { + if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted) + { + _nodeProcessIsReadySource.SetResult(true); initializationIsCompleted = true; - } else if (evt.Data != null) { - this.OnOutputDataReceived(evt.Data); + } + else if (evt.Data != null) + { + OnOutputDataReceived(evt.Data); } }; - this._nodeProcess.ErrorDataReceived += (sender, evt) => { - if (evt.Data != null) { - this.OnErrorDataReceived(evt.Data); - if (!initializationIsCompleted) { - this._nodeProcessIsReadySource.SetResult(false); + NodeProcess.ErrorDataReceived += (sender, evt) => + { + if (evt.Data != null) + { + OnErrorDataReceived(evt.Data); + if (!initializationIsCompleted) + { + _nodeProcessIsReadySource.SetResult(false); initializationIsCompleted = true; } } }; - this._nodeProcess.BeginOutputReadLine(); - this._nodeProcess.BeginErrorReadLine(); + NodeProcess.BeginOutputReadLine(); + NodeProcess.BeginErrorReadLine(); } - protected virtual void OnBeforeLaunchProcess() { + protected virtual void OnBeforeLaunchProcess() + { } - protected virtual void OnOutputDataReceived(string outputData) { + protected virtual void OnOutputDataReceived(string outputData) + { Console.WriteLine("[Node] " + outputData); } - protected virtual void OnErrorDataReceived(string errorData) { - Console.WriteLine("[Node] " + errorData); - } - - public void Dispose() + protected virtual void OnErrorDataReceived(string errorData) { - Dispose(true); - GC.SuppressFinalize(this); + Console.WriteLine("[Node] " + errorData); } protected virtual void Dispose(bool disposing) { - if (!disposed) { - if (disposing) { - this._entryPointScript.Dispose(); + if (!_disposed) + { + if (disposing) + { + _entryPointScript.Dispose(); } - if (this._nodeProcess != null && !this._nodeProcess.HasExited) { - this._nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? + if (NodeProcess != null && !NodeProcess.HasExited) + { + 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; } } - ~OutOfProcessNodeInstance() { - Dispose (false); + ~OutOfProcessNodeInstance() + { + Dispose(false); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs index fae73db4..0c17ea1a 100644 --- a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs @@ -1,8 +1,10 @@ using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.NodeServices { - public interface INodeServices : IDisposable { +namespace Microsoft.AspNetCore.NodeServices +{ + public interface INodeServices : IDisposable + { Task Invoke(string moduleName, params object[] args); Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); diff --git a/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj b/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj index f03c1129..ee1f0389 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj +++ b/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj @@ -9,11 +9,10 @@ b0fa4175-8b29-4904-9780-28b3c24b0567 Microsoft.AspNetCore.NodeServices ..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs index b363631f..64d91704 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs @@ -1,5 +1,7 @@ -namespace Microsoft.AspNetCore.NodeServices { - public enum NodeHostingModel { +namespace Microsoft.AspNetCore.NodeServices +{ + public enum NodeHostingModel + { Http, InputOutputStream, } diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs new file mode 100644 index 00000000..7452c5f6 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs @@ -0,0 +1,14 @@ +namespace Microsoft.AspNetCore.NodeServices +{ + public class NodeServicesOptions + { + public NodeServicesOptions() + { + HostingModel = NodeHostingModel.Http; + } + + public NodeHostingModel HostingModel { get; set; } + public string ProjectPath { get; set; } + public string[] WatchFileExtensions { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs b/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs index 73fe29ea..a7147c14 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs @@ -2,16 +2,20 @@ using System.IO; using System.Reflection; -namespace Microsoft.AspNetCore.NodeServices { - public static class EmbeddedResourceReader { - public static string Read(Type assemblyContainingType, string path) { +namespace Microsoft.AspNetCore.NodeServices +{ + public static class EmbeddedResourceReader + { + public static string Read(Type assemblyContainingType, string path) + { var asm = assemblyContainingType.GetTypeInfo().Assembly; var embeddedResourceName = asm.GetName().Name + path.Replace("/", "."); using (var stream = asm.GetManifestResourceStream(embeddedResourceName)) - using (var sr = new StreamReader(stream)) { + using (var sr = new StreamReader(stream)) + { return sr.ReadToEnd(); } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs index bfcb333d..a9f04a94 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs @@ -1,39 +1,45 @@ using System; using System.IO; -namespace Microsoft.AspNetCore.NodeServices { +namespace Microsoft.AspNetCore.NodeServices +{ // Makes it easier to pass script files to Node in a way that's sure to clean up after the process exits - public sealed class StringAsTempFile : IDisposable { - public string FileName { get; private set; } - + public sealed class StringAsTempFile : IDisposable + { private bool _disposedValue; - public StringAsTempFile(string content) { - this.FileName = Path.GetTempFileName(); - File.WriteAllText(this.FileName, content); + public StringAsTempFile(string content) + { + FileName = Path.GetTempFileName(); + File.WriteAllText(FileName, content); + } + + public string FileName { get; } + + public void Dispose() + { + DisposeImpl(true); + GC.SuppressFinalize(this); } private void DisposeImpl(bool disposing) { - if (!_disposedValue) { - if (disposing) { + if (!_disposedValue) + { + if (disposing) + { // TODO: dispose managed state (managed objects). } - File.Delete(this.FileName); + File.Delete(FileName); _disposedValue = true; } } - public void Dispose() + ~StringAsTempFile() { - DisposeImpl(true); - GC.SuppressFinalize(this); - } - - ~StringAsTempFile() { - DisposeImpl(false); + DisposeImpl(false); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj b/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj index 0f542765..ae962e27 100644 --- a/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj +++ b/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj @@ -9,11 +9,10 @@ b04381de-991f-4831-a0b5-fe1bd3ef80c4 Microsoft.AspNetCore.ReactServices ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj b/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj index 99a66bae..31f1d769 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj +++ b/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj @@ -9,11 +9,10 @@ 4624f728-6dff-44b6-93b5-3c7d9c94bf3f Microsoft.AspNetCore.SpaServices ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs new file mode 100644 index 00000000..054a6938 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs @@ -0,0 +1,14 @@ +namespace Microsoft.AspNetCore.SpaServices.Prerendering +{ + public class JavaScriptModuleExport + { + public JavaScriptModuleExport(string moduleName) + { + this.ModuleName = moduleName; + } + + public string ModuleName { get; private set; } + public string ExportName { get; set; } + public string WebpackConfig { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index ee07d6f4..ea95821f 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -16,11 +16,31 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] public class PrerenderTagHelper : TagHelper { - static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI + private const string PrerenderModuleAttributeName = "asp-prerender-module"; + private const string PrerenderExportAttributeName = "asp-prerender-export"; + private const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; + private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI - const string PrerenderModuleAttributeName = "asp-prerender-module"; - const string PrerenderExportAttributeName = "asp-prerender-export"; - const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; + private readonly string _applicationBasePath; + private readonly INodeServices _nodeServices; + + public PrerenderTagHelper(IServiceProvider serviceProvider) + { + var hostEnv = (IHostingEnvironment) serviceProvider.GetService(typeof(IHostingEnvironment)); + _nodeServices = (INodeServices) serviceProvider.GetService(typeof(INodeServices)) ?? _fallbackNodeServices; + _applicationBasePath = hostEnv.ContentRootPath; + + // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() + // in your startup file, but then again it might be confusing that you don't need to. + if (_nodeServices == null) + { + _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions + { + HostingModel = NodeHostingModel.Http, + ProjectPath = _applicationBasePath + }); + } + } [HtmlAttributeName(PrerenderModuleAttributeName)] public string ModuleName { get; set; } @@ -35,52 +55,37 @@ public class PrerenderTagHelper : TagHelper [ViewContext] public ViewContext ViewContext { get; set; } - private string applicationBasePath; - private INodeServices nodeServices; - - public PrerenderTagHelper(IServiceProvider serviceProvider) - { - var hostEnv = (IHostingEnvironment)serviceProvider.GetService(typeof (IHostingEnvironment)); - this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices; - this.applicationBasePath = hostEnv.ContentRootPath; - - // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() - // in your startup file, but then again it might be confusing that you don't need to. - if (this.nodeServices == null) { - this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = NodeHostingModel.Http, - ProjectPath = this.applicationBasePath - }); - } - } - public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var request = this.ViewContext.HttpContext.Request; + var request = ViewContext.HttpContext.Request; var result = await Prerenderer.RenderToString( - applicationBasePath: this.applicationBasePath, - nodeServices: this.nodeServices, - bootModule: new JavaScriptModuleExport(this.ModuleName) { - exportName = this.ExportName, - webpackConfig = this.WebpackConfigPath + _applicationBasePath, + _nodeServices, + new JavaScriptModuleExport(ModuleName) + { + ExportName = ExportName, + WebpackConfig = WebpackConfigPath }, - requestAbsoluteUrl: UriHelper.GetEncodedUrl(request), - requestPathAndQuery: request.Path + request.QueryString.Value); + request.GetEncodedUrl(), + request.Path + request.QueryString.Value); output.Content.SetHtmlContent(result.Html); // Also attach any specified globals to the 'window' object. This is useful for transferring // general state between server and client. - if (result.Globals != null) { + if (result.Globals != null) + { var stringBuilder = new StringBuilder(); - foreach (var property in result.Globals.Properties()) { + foreach (var property in result.Globals.Properties()) + { stringBuilder.AppendFormat("window.{0} = {1};", property.Name, property.Value.ToString(Formatting.None)); } - if (stringBuilder.Length > 0) { - output.PostElement.SetHtmlContent($""); + if (stringBuilder.Length > 0) + { + output.PostElement.SetHtmlContent($""); } } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 287d5475..5ed85528 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -1,42 +1,34 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices; -using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.SpaServices.Prerendering { public static class Prerenderer { - private static Lazy nodeScript; + private static readonly Lazy NodeScript; - static Prerenderer() { - nodeScript = new Lazy(() => { + static Prerenderer() + { + NodeScript = new Lazy(() => + { var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); return new StringAsTempFile(script); // Will be cleaned up on process exit }); } - public static async Task RenderToString(string applicationBasePath, INodeServices nodeServices, JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery) { - return await nodeServices.InvokeExport(nodeScript.Value.FileName, "renderToString", + public static async Task RenderToString( + string applicationBasePath, + INodeServices nodeServices, + JavaScriptModuleExport bootModule, + string requestAbsoluteUrl, + string requestPathAndQuery) + => await nodeServices.InvokeExport( + NodeScript.Value.FileName, + "renderToString", applicationBasePath, bootModule, requestAbsoluteUrl, requestPathAndQuery); - } - } - - public class JavaScriptModuleExport { - public string moduleName { get; private set; } - public string exportName { get; set; } - public string webpackConfig { get; set; } - - public JavaScriptModuleExport(string moduleName) { - this.moduleName = moduleName; - } - } - - public class RenderToStringResult { - public string Html; - public JObject Globals; } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs new file mode 100644 index 00000000..1d5b482e --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.SpaServices.Prerendering +{ + public class RenderToStringResult + { + public JObject Globals { get; set; } + public string Html { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs index 00243d66..025d11a1 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -7,26 +6,27 @@ namespace Microsoft.AspNetCore.SpaServices { internal class SpaRouteConstraint : IRouteConstraint { - private readonly string clientRouteTokenName; + private readonly string _clientRouteTokenName; - public SpaRouteConstraint(string clientRouteTokenName) { - if (string.IsNullOrEmpty(clientRouteTokenName)) { - throw new ArgumentException("Value cannot be null or empty", "clientRouteTokenName"); + public SpaRouteConstraint(string clientRouteTokenName) + { + if (string.IsNullOrEmpty(clientRouteTokenName)) + { + throw new ArgumentException("Value cannot be null or empty", nameof(clientRouteTokenName)); } - this.clientRouteTokenName = clientRouteTokenName; + _clientRouteTokenName = clientRouteTokenName; } - public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - { - var clientRouteValue = (values[this.clientRouteTokenName] as string) ?? string.Empty; - return !HasDotInLastSegment(clientRouteValue); - } + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + => !HasDotInLastSegment(values[_clientRouteTokenName] as string ?? string.Empty); private bool HasDotInLastSegment(string uri) - { - var lastSegmentStartPos = uri.LastIndexOf('/'); - return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; - } + => uri.IndexOf('.', uri.LastIndexOf('/') + 1) >= 0; } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs index 192c8394..7bdebc69 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs @@ -4,18 +4,34 @@ using Microsoft.AspNetCore.SpaServices; // Putting in this namespace so it's always available whenever MapRoute is + namespace Microsoft.AspNetCore.Builder { public static class SpaRouteExtensions { private const string ClientRouteTokenName = "clientRoute"; - public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null) - { - MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens); - } + public static void MapSpaFallbackRoute( + this IRouteBuilder routeBuilder, + string name, + object defaults, + object constraints = null, + object dataTokens = null) + => MapSpaFallbackRoute( + routeBuilder, + name, + /* templatePrefix */ null, + defaults, + constraints, + dataTokens); - public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null) + public static void MapSpaFallbackRoute( + this IRouteBuilder routeBuilder, + string name, + string templatePrefix, + object defaults, + object constraints = null, + object dataTokens = null) { var template = CreateRouteTemplate(templatePrefix); @@ -29,25 +45,27 @@ private static string CreateRouteTemplate(string templatePrefix) { templatePrefix = templatePrefix ?? string.Empty; - if (templatePrefix.Contains("?")) { + if (templatePrefix.Contains("?")) + { // TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?' throw new ArgumentException("SPA fallback route templates don't support querystrings"); } - if (templatePrefix.Contains("#")) { - throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); + if (templatePrefix.Contains("#")) + { + throw new ArgumentException( + "SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); } - if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) { + if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) + { templatePrefix += "/"; } - return templatePrefix + $"{{*{ ClientRouteTokenName }}}"; + return templatePrefix + $"{{*{ClientRouteTokenName}}}"; } private static IDictionary ObjectToDictionary(object value) - { - return value as IDictionary ?? new RouteValueDictionary(value); - } + => value as IDictionary ?? new RouteValueDictionary(value); } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs index 8c82a8f7..63c4f3e1 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -8,65 +8,85 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack { - // Based on https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs - // Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain - // This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for - // chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). - internal class ConditionalProxyMiddleware { - private RequestDelegate next; - private ConditionalProxyMiddlewareOptions options; - private HttpClient httpClient; - private string pathPrefix; + /// + /// Based on https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs + /// Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain + /// This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for + /// chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). + /// + internal class ConditionalProxyMiddleware + { + private readonly HttpClient _httpClient; + private readonly RequestDelegate _next; + private readonly ConditionalProxyMiddlewareOptions _options; + private readonly string _pathPrefix; - public ConditionalProxyMiddleware(RequestDelegate next, string pathPrefix, ConditionalProxyMiddlewareOptions options) + public ConditionalProxyMiddleware( + RequestDelegate next, + string pathPrefix, + ConditionalProxyMiddlewareOptions options) { - this.next = next; - this.pathPrefix = pathPrefix; - this.options = options; - this.httpClient = new HttpClient(new HttpClientHandler()); + _next = next; + _pathPrefix = pathPrefix; + _options = options; + _httpClient = new HttpClient(new HttpClientHandler()); } - + public async Task Invoke(HttpContext context) { - if (context.Request.Path.StartsWithSegments(this.pathPrefix)) { + if (context.Request.Path.StartsWithSegments(_pathPrefix)) + { var didProxyRequest = await PerformProxyRequest(context); - if (didProxyRequest) { + if (didProxyRequest) + { return; } } - + // Not a request we can proxy - await this.next.Invoke(context); + await _next.Invoke(context); } - - private async Task PerformProxyRequest(HttpContext context) { + + private async Task PerformProxyRequest(HttpContext context) + { var requestMessage = new HttpRequestMessage(); // Copy the request headers - foreach (var header in context.Request.Headers) { - if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null) { + foreach (var header in context.Request.Headers) + { + if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) + { requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); } } - - requestMessage.Headers.Host = options.Host + ":" + options.Port; - var uriString = $"{options.Scheme}://{options.Host}:{options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"; + + requestMessage.Headers.Host = _options.Host + ":" + _options.Port; + var uriString = + $"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"; requestMessage.RequestUri = new Uri(uriString); requestMessage.Method = new HttpMethod(context.Request.Method); - using (var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted)) { - if (responseMessage.StatusCode == HttpStatusCode.NotFound) { + using ( + var responseMessage = + await + _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, + context.RequestAborted)) + { + if (responseMessage.StatusCode == HttpStatusCode.NotFound) + { // Let some other middleware handle this return false; } - + // We can handle this - context.Response.StatusCode = (int)responseMessage.StatusCode; - foreach (var header in responseMessage.Headers) { + context.Response.StatusCode = (int) responseMessage.StatusCode; + foreach (var header in responseMessage.Headers) + { context.Response.Headers[header.Key] = header.Value.ToArray(); } - foreach (var header in responseMessage.Content.Headers) { + foreach (var header in responseMessage.Content.Headers) + { context.Response.Headers[header.Key] = header.Value.ToArray(); } @@ -77,16 +97,4 @@ private async Task PerformProxyRequest(HttpContext context) { } } } - - internal class ConditionalProxyMiddlewareOptions { - public string Scheme { get; private set; } - public string Host { get; private set; } - public string Port { get; private set; } - - public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) { - this.Scheme = scheme; - this.Host = host; - this.Port = port; - } - } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs new file mode 100644 index 00000000..56540075 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs @@ -0,0 +1,16 @@ +namespace Microsoft.AspNetCore.SpaServices.Webpack +{ + internal class ConditionalProxyMiddlewareOptions + { + public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) + { + Scheme = scheme; + Host = host; + Port = port; + } + + public string Scheme { get; } + public string Host { get; } + public string Port { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index dd8a1887..f998c551 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -9,22 +9,31 @@ using Newtonsoft.Json; // Putting in this namespace so it's always available whenever MapRoute is + namespace Microsoft.AspNetCore.Builder { public static class WebpackDevMiddleware { - const string WebpackDevMiddlewareScheme = "http"; - const string WebpackDevMiddlewareHostname = "localhost"; - const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; - const string DefaultConfigFile = "webpack.config.js"; + private const string WebpackDevMiddlewareScheme = "http"; + private const string WebpackDevMiddlewareHostname = "localhost"; + private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; + private const string DefaultConfigFile = "webpack.config.js"; - public static void UseWebpackDevMiddleware(this IApplicationBuilder appBuilder, WebpackDevMiddlewareOptions options = null) { - // Validate options - if (options == null) { + public static void UseWebpackDevMiddleware( + this IApplicationBuilder appBuilder, + WebpackDevMiddlewareOptions options = null) + { + // Prepare options + if (options == null) + { options = new WebpackDevMiddlewareOptions(); } - if (options.ReactHotModuleReplacement && !options.HotModuleReplacement) { - throw new ArgumentException("To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); + + // Validate options + if (options.ReactHotModuleReplacement && !options.HotModuleReplacement) + { + throw new ArgumentException( + "To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); } // Unlike other consumers of NodeServices, WebpackDevMiddleware dosen't share Node instances, nor does it @@ -32,44 +41,55 @@ public static void UseWebpackDevMiddleware(this IApplicationBuilder appBuilder, // because it must *not* restart when files change (if it did, you'd lose all the benefits of Webpack // middleware). And since this is a dev-time-only feature, it doesn't matter if the default transport isn't // as fast as some theoretical future alternative. - var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof (IHostingEnvironment)); - var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { + var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); + var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions + { HostingModel = NodeHostingModel.Http, ProjectPath = hostEnv.ContentRootPath, - WatchFileExtensions = new string[] {} // Don't watch anything + WatchFileExtensions = new string[] { } // Don't watch anything }); // Get a filename matching the middleware Node script - var script = EmbeddedResourceReader.Read(typeof (WebpackDevMiddleware), "/Content/Node/webpack-dev-middleware.js"); + var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), + "/Content/Node/webpack-dev-middleware.js"); var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit // Tell Node to start the server hosting webpack-dev-middleware - var devServerOptions = new { + var devServerOptions = new + { webpackConfigPath = Path.Combine(hostEnv.ContentRootPath, options.ConfigFile ?? DefaultConfigFile), suppliedOptions = options }; - var devServerInfo = nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; + var devServerInfo = + nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", + JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener - var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, WebpackDevMiddlewareHostname, devServerInfo.Port.ToString()); + var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, + WebpackDevMiddlewareHostname, devServerInfo.Port.ToString()); appBuilder.UseMiddleware(devServerInfo.PublicPath, proxyOptions); // While it would be nice to proxy the /__webpack_hmr requests too, these return an EventStream, // and the Microsoft.AspNetCore.Proxy code doesn't handle that entirely - it throws an exception after // a while. So, just serve a 302 for those. - appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => { - builder.Use(next => async ctx => { - ctx.Response.Redirect($"{ WebpackDevMiddlewareScheme }://{ WebpackDevMiddlewareHostname }:{ devServerInfo.Port.ToString() }{ WebpackHotMiddlewareEndpoint }"); + appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => + { + builder.Use(next => async ctx => + { + ctx.Response.Redirect( + $"{WebpackDevMiddlewareScheme}://{WebpackDevMiddlewareHostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); await Task.Yield(); }); }); } - #pragma warning disable CS0649 - class WebpackDevServerInfo { - public int Port; - public string PublicPath; +#pragma warning disable CS0649 + class WebpackDevServerInfo + { + public int Port { get; set; } + public string PublicPath { get; set; } } - #pragma warning restore CS0649 } +#pragma warning restore CS0649 } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs index 020589a7..08a6dbd9 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs @@ -1,7 +1,9 @@ -namespace Microsoft.AspNetCore.SpaServices.Webpack { - public class WebpackDevMiddlewareOptions { +namespace Microsoft.AspNetCore.SpaServices.Webpack +{ + public class WebpackDevMiddlewareOptions + { public bool HotModuleReplacement { get; set; } public bool ReactHotModuleReplacement { get; set; } public string ConfigFile { get; set; } } -} +} \ No newline at end of file diff --git a/templates/Angular2Spa/Angular2Spa.xproj b/templates/Angular2Spa/Angular2Spa.xproj index 455712bd..d4c5f075 100644 --- a/templates/Angular2Spa/Angular2Spa.xproj +++ b/templates/Angular2Spa/Angular2Spa.xproj @@ -10,11 +10,11 @@ 8f5cb8a9-3086-4b49-a1c2-32a9f89bca11 Angular2Spa ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/templates/ReactReduxSpa/ReactReduxSpa.xproj b/templates/ReactReduxSpa/ReactReduxSpa.xproj index 630731ac..0996bb60 100644 --- a/templates/ReactReduxSpa/ReactReduxSpa.xproj +++ b/templates/ReactReduxSpa/ReactReduxSpa.xproj @@ -10,11 +10,11 @@ dbfc6db0-a6d1-4694-a108-1c604b988da3 ReactReduxSpa ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/templates/ReactSpa/ReactSpa.xproj b/templates/ReactSpa/ReactSpa.xproj index d1b8262e..abe5a589 100644 --- a/templates/ReactSpa/ReactSpa.xproj +++ b/templates/ReactSpa/ReactSpa.xproj @@ -10,11 +10,11 @@ e9d1a695-f0e6-46f2-b5e3-72f4af805387 ReactSpa ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/templates/WebApplicationBasic/WebApplicationBasic.xproj b/templates/WebApplicationBasic/WebApplicationBasic.xproj index 3845bf24..b0c23a00 100644 --- a/templates/WebApplicationBasic/WebApplicationBasic.xproj +++ b/templates/WebApplicationBasic/WebApplicationBasic.xproj @@ -10,11 +10,11 @@ cb4398d6-b7f1-449a-ae02-828769679232 WebApplicationBasic ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file From b141419b147e87eec684de839db17ad9034c25e5 Mon Sep 17 00:00:00 2001 From: Andrei Tserakhau Date: Mon, 23 May 2016 11:51:29 +0300 Subject: [PATCH 002/876] refactor(angular-services): beutify base uri creation bug: remove redundant closing brackets --- .../PrimeCacheHelper.cs | 11 ++++++++--- .../Webpack/WebpackDevMiddleware.cs | 1 - 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs index ca9f6030..649a154b 100644 --- a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs +++ b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs @@ -26,9 +26,14 @@ public static async Task PrimeCache(this IHtmlHelper html, string ur { var request = html.ViewContext.HttpContext.Request; var baseUri = - new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), - request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), - request.QueryString.ToUriComponent())); + new Uri( + string.Concat( + request.Scheme, + "://", + request.Host.ToUriComponent(), + request.PathBase.ToUriComponent(), + request.Path.ToUriComponent(), + request.QueryString.ToUriComponent())); var fullUri = new Uri(baseUri, url); var response = await new HttpClient().GetAsync(fullUri.ToString()); var responseBody = await response.Content.ReadAsStringAsync(); diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index f998c551..88fb3292 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -92,4 +92,3 @@ class WebpackDevServerInfo } #pragma warning restore CS0649 } -} \ No newline at end of file From 5bb92d02dd361d1f6088bf1194e3f9ad7af504bf Mon Sep 17 00:00:00 2001 From: Andrei Tserakhau Date: Wed, 25 May 2016 16:11:53 +0300 Subject: [PATCH 003/876] refactor: prime cache formatter beautify --- .../PrimeCacheHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs index 649a154b..53ce7415 100644 --- a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs +++ b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs @@ -50,9 +50,9 @@ public static async Task PrimeCache(this IHtmlHelper html, string ur private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody) => ""; } } \ No newline at end of file From cb289fd3879d7a5eec67372a0ad7f133fd65666f Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 31 May 2016 22:29:27 +0100 Subject: [PATCH 004/876] Further stylistic tweaks --- .../PrimeCacheHelper.cs | 32 +++++++++---------- .../Configuration.cs | 9 +++--- .../HostingModels/HttpNodeInstance.cs | 2 ++ .../HostingModels/OutOfProcessNodeInstance.cs | 31 ++++++++++++------ .../Prerendering/JavaScriptModuleExport.cs | 2 +- .../Prerendering/Prerenderer.cs | 6 ++-- .../Routing/SpaRouteConstraint.cs | 9 ++++-- .../Routing/SpaRouteExtensions.cs | 5 +-- .../Webpack/ConditionalProxyMiddleware.cs | 8 ++--- .../Webpack/WebpackDevMiddleware.cs | 2 +- 10 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs index 53ce7415..1b016622 100644 --- a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs +++ b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs @@ -25,16 +25,14 @@ public static async Task PrimeCache(this IHtmlHelper html, string ur try { var request = html.ViewContext.HttpContext.Request; - var baseUri = - new Uri( - string.Concat( - request.Scheme, - "://", - request.Host.ToUriComponent(), - request.PathBase.ToUriComponent(), - request.Path.ToUriComponent(), - request.QueryString.ToUriComponent())); - var fullUri = new Uri(baseUri, url); + var baseUriString = string.Concat( + request.Scheme, + "://", + request.Host.ToUriComponent(), + request.PathBase.ToUriComponent(), + request.Path.ToUriComponent(), + request.QueryString.ToUriComponent()); + var fullUri = new Uri(new Uri(baseUriString), url); var response = await new HttpClient().GetAsync(fullUri.ToString()); var responseBody = await response.Content.ReadAsStringAsync(); return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody)); @@ -48,11 +46,13 @@ public static async Task PrimeCache(this IHtmlHelper html, string ur } private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody) - => - ""; + { + var preCachedUrl = JsonConvert.SerializeObject(url); + var preCachedJson = JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody }); + return ""; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs index d40733a7..1a0e4ed0 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs @@ -1,11 +1,9 @@ +using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.PlatformAbstractions; using Microsoft.AspNetCore.Hosting; namespace Microsoft.AspNetCore.NodeServices { - using System; - public static class Configuration { private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"}; @@ -20,15 +18,18 @@ public static void AddNodeServices(this IServiceCollection serviceCollection) => AddNodeServices(serviceCollection, DefaultOptions); public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) - => serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => + { + serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => { var hostEnv = serviceProvider.GetRequiredService(); if (string.IsNullOrEmpty(options.ProjectPath)) { options.ProjectPath = hostEnv.ContentRootPath; } + return CreateNodeServices(options); }); + } public static INodeServices CreateNodeServices(NodeServicesOptions options) { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 8cff8830..26edeb30 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -64,12 +64,14 @@ public override async Task Invoke(NodeInvocationInfo invocationInfo) { return JsonConvert.DeserializeObject(responseString); } + if (typeof(T) != typeof(string)) { throw new ArgumentException( "Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + typeof(T).FullName); } + return (T)(object)responseString; } } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 65e21a39..ef256187 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -15,6 +15,7 @@ public abstract class OutOfProcessNodeInstance : INodeServices private readonly object _childProcessLauncherLock; private readonly string _commandLineArguments; private readonly StringAsTempFile _entryPointScript; + private Process _nodeProcess; private TaskCompletionSource _nodeProcessIsReadySource; private readonly string _projectPath; private bool _disposed; @@ -27,18 +28,28 @@ public OutOfProcessNodeInstance(string entryPointScript, string projectPath, str _commandLineArguments = commandLineArguments ?? string.Empty; } - protected Process NodeProcess { get; private set; } + protected Process NodeProcess + { + get + { + // This is only exposed to support the unreliable InputOutputStreamNodeInstance, which is just to verify that + // other hosting/transport mechanisms are possible. This shouldn't really be exposed, and will be removed. + return this._nodeProcess; + } + } public Task Invoke(string moduleName, params object[] args) => InvokeExport(moduleName, null, args); public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) - => Invoke(new NodeInvocationInfo + { + return Invoke(new NodeInvocationInfo { ModuleName = moduleName, ExportedFunctionName = exportedFunctionName, Args = args }); + } public void Dispose() { @@ -52,7 +63,7 @@ protected async Task EnsureReady() { lock (_childProcessLauncherLock) { - if (NodeProcess == null || NodeProcess.HasExited) + if (_nodeProcess == null || _nodeProcess.HasExited) { var startInfo = new ProcessStartInfo("node") { @@ -79,7 +90,7 @@ protected async Task EnsureReady() #endif OnBeforeLaunchProcess(); - NodeProcess = Process.Start(startInfo); + _nodeProcess = Process.Start(startInfo); ConnectToInputOutputStreams(); } } @@ -98,7 +109,7 @@ private void ConnectToInputOutputStreams() var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) _nodeProcessIsReadySource = new TaskCompletionSource(); - NodeProcess.OutputDataReceived += (sender, evt) => + _nodeProcess.OutputDataReceived += (sender, evt) => { if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted) { @@ -111,7 +122,7 @@ private void ConnectToInputOutputStreams() } }; - NodeProcess.ErrorDataReceived += (sender, evt) => + _nodeProcess.ErrorDataReceived += (sender, evt) => { if (evt.Data != null) { @@ -124,8 +135,8 @@ private void ConnectToInputOutputStreams() } }; - NodeProcess.BeginOutputReadLine(); - NodeProcess.BeginErrorReadLine(); + _nodeProcess.BeginOutputReadLine(); + _nodeProcess.BeginErrorReadLine(); } protected virtual void OnBeforeLaunchProcess() @@ -151,9 +162,9 @@ protected virtual void Dispose(bool disposing) _entryPointScript.Dispose(); } - if (NodeProcess != null && !NodeProcess.HasExited) + if (_nodeProcess != null && !_nodeProcess.HasExited) { - NodeProcess.Kill(); + _nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? } diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs index 054a6938..34038668 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs @@ -4,7 +4,7 @@ public class JavaScriptModuleExport { public JavaScriptModuleExport(string moduleName) { - this.ModuleName = moduleName; + ModuleName = moduleName; } public string ModuleName { get; private set; } diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 5ed85528..3b08eff9 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -17,18 +17,20 @@ static Prerenderer() }); } - public static async Task RenderToString( + public static Task RenderToString( string applicationBasePath, INodeServices nodeServices, JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery) - => await nodeServices.InvokeExport( + { + return nodeServices.InvokeExport( NodeScript.Value.FileName, "renderToString", applicationBasePath, bootModule, requestAbsoluteUrl, requestPathAndQuery); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs index 025d11a1..d6a1d5b2 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs @@ -24,9 +24,14 @@ public bool Match( string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - => !HasDotInLastSegment(values[_clientRouteTokenName] as string ?? string.Empty); + { + return !HasDotInLastSegment(values[_clientRouteTokenName] as string ?? string.Empty); + } private bool HasDotInLastSegment(string uri) - => uri.IndexOf('.', uri.LastIndexOf('/') + 1) >= 0; + { + var lastSegmentStartPos = uri.LastIndexOf('/'); + return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs index 7bdebc69..919390e6 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs @@ -17,13 +17,15 @@ public static void MapSpaFallbackRoute( object defaults, object constraints = null, object dataTokens = null) - => MapSpaFallbackRoute( + { + MapSpaFallbackRoute( routeBuilder, name, /* templatePrefix */ null, defaults, constraints, dataTokens); + } public static void MapSpaFallbackRoute( this IRouteBuilder routeBuilder, @@ -34,7 +36,6 @@ public static void MapSpaFallbackRoute( object dataTokens = null) { var template = CreateRouteTemplate(templatePrefix); - var constraintsDict = ObjectToDictionary(constraints); constraintsDict.Add(ClientRouteTokenName, new SpaRouteConstraint(ClientRouteTokenName)); diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs index 63c4f3e1..72b66c30 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -67,10 +67,10 @@ private async Task PerformProxyRequest(HttpContext context) requestMessage.Method = new HttpMethod(context.Request.Method); using ( - var responseMessage = - await - _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, - context.RequestAborted)) + var responseMessage = await _httpClient.SendAsync( + requestMessage, + HttpCompletionOption.ResponseHeadersRead, + context.RequestAborted)) { if (responseMessage.StatusCode == HttpStatusCode.NotFound) { diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 88fb3292..7c597219 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -91,4 +91,4 @@ class WebpackDevServerInfo } } #pragma warning restore CS0649 -} +} \ No newline at end of file From 32ebaecdd8d064a2d692c1236f624f2f95888aeb Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 1 Jun 2016 16:15:34 +0100 Subject: [PATCH 005/876] Define DefaultNodeHostingModel in one common place so it can be changed easily --- samples/misc/LatencyTest/Program.cs | 6 +++--- src/Microsoft.AspNetCore.NodeServices/Configuration.cs | 5 +++-- .../NodeServicesOptions.cs | 2 +- .../Prerendering/PrerenderTagHelper.cs | 2 +- .../Webpack/WebpackDevMiddleware.cs | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/samples/misc/LatencyTest/Program.cs b/samples/misc/LatencyTest/Program.cs index 8654e2ce..855402b4 100755 --- a/samples/misc/LatencyTest/Program.cs +++ b/samples/misc/LatencyTest/Program.cs @@ -8,11 +8,11 @@ namespace ConsoleApplication { // This project is a micro-benchmark for .NET->Node RPC via NodeServices. It doesn't reflect // real-world usage patterns (you're not likely to make hundreds of sequential calls like this), - // but is a starting point for comparing the overhead of different hosting models and transports. + // but is a starting point for comparing the overhead of different hosting models and transports. public class Program { public static void Main(string[] args) { - using (var nodeServices = CreateNodeServices(NodeHostingModel.Http)) { + using (var nodeServices = CreateNodeServices(Configuration.DefaultNodeHostingModel)) { MeasureLatency(nodeServices).Wait(); } } @@ -21,7 +21,7 @@ private static async Task MeasureLatency(INodeServices nodeServices) { // Ensure the connection is open, so we can measure per-request timings below var response = await nodeServices.Invoke("latencyTest", "C#"); Console.WriteLine(response); - + // Now perform a series of requests, capturing the time taken const int requestCount = 100; var watch = Stopwatch.StartNew(); diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs index 1a0e4ed0..9e983a04 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs @@ -6,11 +6,12 @@ namespace Microsoft.AspNetCore.NodeServices { public static class Configuration { - private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"}; + public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; + private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"}; private static readonly NodeServicesOptions DefaultOptions = new NodeServicesOptions { - HostingModel = NodeHostingModel.Http, + HostingModel = DefaultNodeHostingModel, WatchFileExtensions = DefaultWatchFileExtensions }; diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs index 7452c5f6..dccd85de 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs @@ -4,7 +4,7 @@ public class NodeServicesOptions { public NodeServicesOptions() { - HostingModel = NodeHostingModel.Http; + HostingModel = Configuration.DefaultNodeHostingModel; } public NodeHostingModel HostingModel { get; set; } diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index ea95821f..6e364812 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -36,7 +36,7 @@ public PrerenderTagHelper(IServiceProvider serviceProvider) { _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = NodeHostingModel.Http, + HostingModel = Configuration.DefaultNodeHostingModel, ProjectPath = _applicationBasePath }); } diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 7c597219..8c3dbd2a 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -44,7 +44,7 @@ public static void UseWebpackDevMiddleware( var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = NodeHostingModel.Http, + HostingModel = Configuration.DefaultNodeHostingModel, ProjectPath = hostEnv.ContentRootPath, WatchFileExtensions = new string[] { } // Don't watch anything }); From 341cd4f1cb7172f9f23c69edd758c48638dfedb9 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 1 Jun 2016 16:05:29 +0100 Subject: [PATCH 006/876] Implement SocketNodeInstance --- .../.gitignore | 1 + .../Configuration.cs | 2 + .../Content/Node/entrypoint-socket.js | 408 +++ .../HostingModels/NodeInvocationException.cs | 12 + .../HostingModels/OutOfProcessNodeInstance.cs | 26 +- .../NamedPipeConnection.cs | 32 + .../PhysicalConnections/StreamConnection.cs | 26 + .../UnixDomainSocketConnection.cs | 40 + .../UnixDomainSocketEndPoint.cs | 86 + .../HostingModels/SocketNodeInstance.cs | 191 ++ .../VirtualConnections/VirtualConnection.cs | 149 + .../VirtualConnectionClient.cs | 230 ++ .../NodeHostingModel.cs | 1 + .../SocketNodeInstanceEntryPoint.ts | 91 + .../VirtualConnections/VirtualConnection.ts | 43 + .../VirtualConnectionServer.ts | 198 ++ .../TypeScript/tsconfig.json | 10 + .../TypeScript/typings/node/node.d.ts | 2391 +++++++++++++++++ .../TypeScript/typings/tsd.d.ts | 1 + .../package.json | 17 + .../project.json | 3 +- .../tsd.json | 12 + .../webpack.config.js | 20 + 23 files changed, 3982 insertions(+), 8 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnectionClient.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnection.ts create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnectionServer.ts create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/tsconfig.json create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/node/node.d.ts create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/tsd.d.ts create mode 100644 src/Microsoft.AspNetCore.NodeServices/package.json create mode 100644 src/Microsoft.AspNetCore.NodeServices/tsd.json create mode 100644 src/Microsoft.AspNetCore.NodeServices/webpack.config.js diff --git a/src/Microsoft.AspNetCore.NodeServices/.gitignore b/src/Microsoft.AspNetCore.NodeServices/.gitignore index ae3c1726..c6958891 100644 --- a/src/Microsoft.AspNetCore.NodeServices/.gitignore +++ b/src/Microsoft.AspNetCore.NodeServices/.gitignore @@ -1 +1,2 @@ /bin/ +/node_modules/ diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs index 9e983a04..51325d46 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs @@ -39,6 +39,8 @@ public static INodeServices CreateNodeServices(NodeServicesOptions options) { case NodeHostingModel.Http: return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions); + case NodeHostingModel.Socket: + return new SocketNodeInstance(options.ProjectPath, watchFileExtensions); case NodeHostingModel.InputOutputStream: return new InputOutputStreamNodeInstance(options.ProjectPath); default: diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js new file mode 100644 index 00000000..b18bf5ee --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js @@ -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"); + +/***/ } +/******/ ]))); \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs new file mode 100644 index 00000000..64398c4c --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs @@ -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) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index ef256187..1f65c603 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.NodeServices public abstract class OutOfProcessNodeInstance : INodeServices { private readonly object _childProcessLauncherLock; - private readonly string _commandLineArguments; + private string _commandLineArguments; private readonly StringAsTempFile _entryPointScript; private Process _nodeProcess; private TaskCompletionSource _nodeProcessIsReadySource; @@ -27,6 +27,12 @@ public OutOfProcessNodeInstance(string entryPointScript, string projectPath, str _projectPath = projectPath; _commandLineArguments = commandLineArguments ?? string.Empty; } + + public string CommandLineArguments + { + get { return _commandLineArguments; } + set { _commandLineArguments = value; } + } protected Process NodeProcess { @@ -59,12 +65,23 @@ public void Dispose() public abstract Task Invoke(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, @@ -89,7 +106,6 @@ protected async Task EnsureReady() startInfo.Environment.Add("NODE_PATH", nodePathValue); #endif - OnBeforeLaunchProcess(); _nodeProcess = Process.Start(startInfo); ConnectToInputOutputStreams(); } @@ -162,11 +178,7 @@ protected virtual void Dispose(bool disposing) _entryPointScript.Dispose(); } - if (_nodeProcess != null && !_nodeProcess.HasExited) - { - _nodeProcess.Kill(); - // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? - } + ExitNodeProcess(); _disposed = true; } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs new file mode 100644 index 00000000..ea2159a0 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs @@ -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 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; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs new file mode 100644 index 00000000..6e903242 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs @@ -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 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(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs new file mode 100644 index 00000000..93d3a442 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs @@ -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 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; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs new file mode 100644 index 00000000..8937a0de --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs @@ -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(); + _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; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs new file mode 100644 index 00000000..6bbce164 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -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 Invoke(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>(virtualConnection); + if (response.ErrorMessage != null) + { + throw new NodeInvocationException(response.ErrorMessage, response.ErrorDetails); + } + + return response.Result; + } + } + + private async Task 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 ReadJsonAsync(Stream stream) + { + var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream)); + return JsonConvert.DeserializeObject(json, jsonSerializerSettings); + } + + private static async Task 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 + { + public TResult Result { get; set; } + public string ErrorMessage { get; set; } + public string ErrorDetails { get; set; } + } +#pragma warning restore 649 + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs new file mode 100644 index 00000000..2a2d148b --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs @@ -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 +{ + /// + /// 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). + /// + internal class VirtualConnection : Stream + { + private VirtualConnectionClient _host; + private readonly BufferBlock _receivedDataQueue = new BufferBlock(); + private ArraySegment _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 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(_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(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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnectionClient.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnectionClient.cs new file mode 100644 index 00000000..9f343033 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnectionClient.cs @@ -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); + + /// + /// 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. + /// + internal class VirtualConnectionClient : IDisposable + { + internal const int MaxFrameBodyLength = 16 * 1024; + + public event VirtualConnectionReadErrorHandler OnError; + + private Stream _underlyingTransport; + private Dictionary _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(); + + 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 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 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 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); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs index 64d91704..d5b7b455 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs @@ -4,5 +4,6 @@ public enum NodeHostingModel { Http, InputOutputStream, + Socket, } } diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts new file mode 100644 index 00000000..0f762bb5 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts @@ -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[]; +} diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnection.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnection.ts new file mode 100644 index 00000000..de71f607 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnection.ts @@ -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); + } + } +} diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnectionServer.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnectionServer.ts new file mode 100644 index 00000000..7381060a --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/VirtualConnections/VirtualConnectionServer.ts @@ -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; +} diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/tsconfig.json b/src/Microsoft.AspNetCore.NodeServices/TypeScript/tsconfig.json new file mode 100644 index 00000000..25b9d308 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/node/node.d.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/node/node.d.ts new file mode 100644 index 00000000..42621d50 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/node/node.d.ts @@ -0,0 +1,2391 @@ +// Type definitions for Node.js v6.x +// Project: http://nodejs.org/ +// Definitions by: Microsoft TypeScript , DefinitelyTyped +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/************************************************ +* * +* Node.js v6.x API * +* * +************************************************/ + +interface Error { + stack?: string; +} + + +// compat for TypeScript 1.8 +// if you use with --target es3 or --target es5 and use below definitions, +// use the lib.es6.d.ts that is bundled with TypeScript 1.8. +interface MapConstructor {} +interface WeakMapConstructor {} +interface SetConstructor {} +interface WeakSetConstructor {} + +/************************************************ +* * +* GLOBAL * +* * +************************************************/ +declare var process: NodeJS.Process; +declare var global: NodeJS.Global; + +declare var __filename: string; +declare var __dirname: string; + +declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer; +declare function clearTimeout(timeoutId: NodeJS.Timer): void; +declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer; +declare function clearInterval(intervalId: NodeJS.Timer): void; +declare function setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; +declare function clearImmediate(immediateId: any): void; + +interface NodeRequireFunction { + (id: string): any; +} + +interface NodeRequire extends NodeRequireFunction { + resolve(id:string): string; + cache: any; + extensions: any; + main: any; +} + +declare var require: NodeRequire; + +interface NodeModule { + exports: any; + require: NodeRequireFunction; + id: string; + filename: string; + loaded: boolean; + parent: any; + children: any[]; +} + +declare var module: NodeModule; + +// Same as module.exports +declare var exports: any; +declare var SlowBuffer: { + new (str: string, encoding?: string): Buffer; + new (size: number): Buffer; + new (size: Uint8Array): Buffer; + new (array: any[]): Buffer; + prototype: Buffer; + isBuffer(obj: any): boolean; + byteLength(string: string, encoding?: string): number; + concat(list: Buffer[], totalLength?: number): Buffer; +}; + + +// Buffer class +type BufferEncoding = "ascii" | "utf8" | "utf16le" | "ucs2" | "binary" | "hex"; +interface Buffer extends NodeBuffer {} + +/** + * Raw data is stored in instances of the Buffer class. + * A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized. + * Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex' + */ +declare var Buffer: { + /** + * Allocates a new buffer containing the given {str}. + * + * @param str String to store in buffer. + * @param encoding encoding to use, optional. Default is 'utf8' + */ + new (str: string, encoding?: string): Buffer; + /** + * Allocates a new buffer of {size} octets. + * + * @param size count of octets to allocate. + */ + new (size: number): Buffer; + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + */ + new (array: Uint8Array): Buffer; + /** + * Produces a Buffer backed by the same allocated memory as + * the given {ArrayBuffer}. + * + * + * @param arrayBuffer The ArrayBuffer with which to share memory. + */ + new (arrayBuffer: ArrayBuffer): Buffer; + /** + * Allocates a new buffer containing the given {array} of octets. + * + * @param array The octets to store. + */ + new (array: any[]): Buffer; + /** + * Copies the passed {buffer} data onto a new {Buffer} instance. + * + * @param buffer The buffer to copy. + */ + new (buffer: Buffer): Buffer; + prototype: Buffer; + /** + * Allocates a new Buffer using an {array} of octets. + * + * @param array + */ + from(array: any[]): Buffer; + /** + * When passed a reference to the .buffer property of a TypedArray instance, + * the newly created Buffer will share the same allocated memory as the TypedArray. + * The optional {byteOffset} and {length} arguments specify a memory range + * within the {arrayBuffer} that will be shared by the Buffer. + * + * @param arrayBuffer The .buffer property of a TypedArray or a new ArrayBuffer() + * @param byteOffset + * @param length + */ + from(arrayBuffer: ArrayBuffer, byteOffset?: number, length?:number): Buffer; + /** + * Copies the passed {buffer} data onto a new Buffer instance. + * + * @param buffer + */ + from(buffer: Buffer): Buffer; + /** + * Creates a new Buffer containing the given JavaScript string {str}. + * If provided, the {encoding} parameter identifies the character encoding. + * If not provided, {encoding} defaults to 'utf8'. + * + * @param str + */ + from(str: string, encoding?: string): Buffer; + /** + * Returns true if {obj} is a Buffer + * + * @param obj object to test. + */ + isBuffer(obj: any): obj is Buffer; + /** + * Returns true if {encoding} is a valid encoding argument. + * Valid string encodings in Node 0.12: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex' + * + * @param encoding string to test. + */ + isEncoding(encoding: string): boolean; + /** + * Gives the actual byte length of a string. encoding defaults to 'utf8'. + * This is not the same as String.prototype.length since that returns the number of characters in a string. + * + * @param string string to test. + * @param encoding encoding used to evaluate (defaults to 'utf8') + */ + byteLength(string: string, encoding?: string): number; + /** + * Returns a buffer which is the result of concatenating all the buffers in the list together. + * + * If the list has no items, or if the totalLength is 0, then it returns a zero-length buffer. + * If the list has exactly one item, then the first item of the list is returned. + * If the list has more than one item, then a new Buffer is created. + * + * @param list An array of Buffer objects to concatenate + * @param totalLength Total length of the buffers when concatenated. + * If totalLength is not provided, it is read from the buffers in the list. However, this adds an additional loop to the function, so it is faster to provide the length explicitly. + */ + concat(list: Buffer[], totalLength?: number): Buffer; + /** + * The same as buf1.compare(buf2). + */ + compare(buf1: Buffer, buf2: Buffer): number; +}; + +/************************************************ +* * +* GLOBAL INTERFACES * +* * +************************************************/ +declare namespace NodeJS { + export interface ErrnoException extends Error { + errno?: number; + code?: string; + path?: string; + syscall?: string; + stack?: string; + } + + export interface EventEmitter { + addListener(event: string, listener: Function): this; + on(event: string, listener: Function): this; + once(event: string, listener: Function): this; + removeListener(event: string, listener: Function): this; + removeAllListeners(event?: string): this; + setMaxListeners(n: number): this; + getMaxListeners(): number; + listeners(event: string): Function[]; + emit(event: string, ...args: any[]): boolean; + listenerCount(type: string): number; + } + + export interface ReadableStream extends EventEmitter { + readable: boolean; + read(size?: number): string|Buffer; + setEncoding(encoding: string): void; + pause(): void; + resume(): void; + pipe(destination: T, options?: { end?: boolean; }): T; + unpipe(destination?: T): void; + unshift(chunk: string): void; + unshift(chunk: Buffer): void; + wrap(oldStream: ReadableStream): ReadableStream; + } + + export interface WritableStream extends EventEmitter { + writable: boolean; + write(buffer: Buffer|string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + } + + export interface ReadWriteStream extends ReadableStream, WritableStream {} + + export interface Events extends EventEmitter { } + + export interface Domain extends Events { + run(fn: Function): void; + add(emitter: Events): void; + remove(emitter: Events): void; + bind(cb: (err: Error, data: any) => any): any; + intercept(cb: (data: any) => any): any; + dispose(): void; + + addListener(event: string, listener: Function): this; + on(event: string, listener: Function): this; + once(event: string, listener: Function): this; + removeListener(event: string, listener: Function): this; + removeAllListeners(event?: string): this; + } + + export interface MemoryUsage { + rss: number; + heapTotal: number; + heapUsed: number; + } + + export interface Process extends EventEmitter { + stdout: WritableStream; + stderr: WritableStream; + stdin: ReadableStream; + argv: string[]; + execArgv: string[]; + execPath: string; + abort(): void; + chdir(directory: string): void; + cwd(): string; + env: any; + exit(code?: number): void; + getgid(): number; + setgid(id: number): void; + setgid(id: string): void; + getuid(): number; + setuid(id: number): void; + setuid(id: string): void; + version: string; + versions: { + http_parser: string; + node: string; + v8: string; + ares: string; + uv: string; + zlib: string; + openssl: string; + }; + config: { + target_defaults: { + cflags: any[]; + default_configuration: string; + defines: string[]; + include_dirs: string[]; + libraries: string[]; + }; + variables: { + clang: number; + host_arch: string; + node_install_npm: boolean; + node_install_waf: boolean; + node_prefix: string; + node_shared_openssl: boolean; + node_shared_v8: boolean; + node_shared_zlib: boolean; + node_use_dtrace: boolean; + node_use_etw: boolean; + node_use_openssl: boolean; + target_arch: string; + v8_no_strict_aliasing: number; + v8_use_snapshot: boolean; + visibility: string; + }; + }; + kill(pid:number, signal?: string|number): void; + pid: number; + title: string; + arch: string; + platform: string; + memoryUsage(): MemoryUsage; + nextTick(callback: Function): void; + umask(mask?: number): number; + uptime(): number; + hrtime(time?:number[]): number[]; + domain: Domain; + + // Worker + send?(message: any, sendHandle?: any): void; + disconnect(): void; + connected: boolean; + } + + export interface Global { + Array: typeof Array; + ArrayBuffer: typeof ArrayBuffer; + Boolean: typeof Boolean; + Buffer: typeof Buffer; + DataView: typeof DataView; + Date: typeof Date; + Error: typeof Error; + EvalError: typeof EvalError; + Float32Array: typeof Float32Array; + Float64Array: typeof Float64Array; + Function: typeof Function; + GLOBAL: Global; + Infinity: typeof Infinity; + Int16Array: typeof Int16Array; + Int32Array: typeof Int32Array; + Int8Array: typeof Int8Array; + Intl: typeof Intl; + JSON: typeof JSON; + Map: MapConstructor; + Math: typeof Math; + NaN: typeof NaN; + Number: typeof Number; + Object: typeof Object; + Promise: Function; + RangeError: typeof RangeError; + ReferenceError: typeof ReferenceError; + RegExp: typeof RegExp; + Set: SetConstructor; + String: typeof String; + Symbol: Function; + SyntaxError: typeof SyntaxError; + TypeError: typeof TypeError; + URIError: typeof URIError; + Uint16Array: typeof Uint16Array; + Uint32Array: typeof Uint32Array; + Uint8Array: typeof Uint8Array; + Uint8ClampedArray: Function; + WeakMap: WeakMapConstructor; + WeakSet: WeakSetConstructor; + clearImmediate: (immediateId: any) => void; + clearInterval: (intervalId: NodeJS.Timer) => void; + clearTimeout: (timeoutId: NodeJS.Timer) => void; + console: typeof console; + decodeURI: typeof decodeURI; + decodeURIComponent: typeof decodeURIComponent; + encodeURI: typeof encodeURI; + encodeURIComponent: typeof encodeURIComponent; + escape: (str: string) => string; + eval: typeof eval; + global: Global; + isFinite: typeof isFinite; + isNaN: typeof isNaN; + parseFloat: typeof parseFloat; + parseInt: typeof parseInt; + process: Process; + root: Global; + setImmediate: (callback: (...args: any[]) => void, ...args: any[]) => any; + setInterval: (callback: (...args: any[]) => void, ms: number, ...args: any[]) => NodeJS.Timer; + setTimeout: (callback: (...args: any[]) => void, ms: number, ...args: any[]) => NodeJS.Timer; + undefined: typeof undefined; + unescape: (str: string) => string; + gc: () => void; + v8debug?: any; + } + + export interface Timer { + ref() : void; + unref() : void; + } +} + +/** + * @deprecated + */ +interface NodeBuffer extends Uint8Array { + write(string: string, offset?: number, length?: number, encoding?: string): number; + toString(encoding?: string, start?: number, end?: number): string; + toJSON(): any; + equals(otherBuffer: Buffer): boolean; + compare(otherBuffer: Buffer): number; + copy(targetBuffer: Buffer, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; + slice(start?: number, end?: number): Buffer; + writeUIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + writeUIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + writeIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + writeIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; + readUIntLE(offset: number, byteLength: number, noAssert?: boolean): number; + readUIntBE(offset: number, byteLength: number, noAssert?: boolean): number; + readIntLE(offset: number, byteLength: number, noAssert?: boolean): number; + readIntBE(offset: number, byteLength: number, noAssert?: boolean): number; + readUInt8(offset: number, noAssert?: boolean): number; + readUInt16LE(offset: number, noAssert?: boolean): number; + readUInt16BE(offset: number, noAssert?: boolean): number; + readUInt32LE(offset: number, noAssert?: boolean): number; + readUInt32BE(offset: number, noAssert?: boolean): number; + readInt8(offset: number, noAssert?: boolean): number; + readInt16LE(offset: number, noAssert?: boolean): number; + readInt16BE(offset: number, noAssert?: boolean): number; + readInt32LE(offset: number, noAssert?: boolean): number; + readInt32BE(offset: number, noAssert?: boolean): number; + readFloatLE(offset: number, noAssert?: boolean): number; + readFloatBE(offset: number, noAssert?: boolean): number; + readDoubleLE(offset: number, noAssert?: boolean): number; + readDoubleBE(offset: number, noAssert?: boolean): number; + writeUInt8(value: number, offset: number, noAssert?: boolean): number; + writeUInt16LE(value: number, offset: number, noAssert?: boolean): number; + writeUInt16BE(value: number, offset: number, noAssert?: boolean): number; + writeUInt32LE(value: number, offset: number, noAssert?: boolean): number; + writeUInt32BE(value: number, offset: number, noAssert?: boolean): number; + writeInt8(value: number, offset: number, noAssert?: boolean): number; + writeInt16LE(value: number, offset: number, noAssert?: boolean): number; + writeInt16BE(value: number, offset: number, noAssert?: boolean): number; + writeInt32LE(value: number, offset: number, noAssert?: boolean): number; + writeInt32BE(value: number, offset: number, noAssert?: boolean): number; + writeFloatLE(value: number, offset: number, noAssert?: boolean): number; + writeFloatBE(value: number, offset: number, noAssert?: boolean): number; + writeDoubleLE(value: number, offset: number, noAssert?: boolean): number; + writeDoubleBE(value: number, offset: number, noAssert?: boolean): number; + fill(value: any, offset?: number, end?: number): this; + // TODO: encoding param + indexOf(value: string | number | Buffer, byteOffset?: number): number; + // TODO: entries + // TODO: includes + // TODO: keys + // TODO: values +} + +/************************************************ +* * +* MODULES * +* * +************************************************/ +declare module "buffer" { + export var INSPECT_MAX_BYTES: number; + var BuffType: typeof Buffer; + var SlowBuffType: typeof SlowBuffer; + export { BuffType as Buffer, SlowBuffType as SlowBuffer }; +} + +declare module "querystring" { + export interface StringifyOptions { + encodeURIComponent?: Function; + } + + export interface ParseOptions { + maxKeys?: number; + decodeURIComponent?: Function; + } + + export function stringify(obj: T, sep?: string, eq?: string, options?: StringifyOptions): string; + export function parse(str: string, sep?: string, eq?: string, options?: ParseOptions): any; + export function parse(str: string, sep?: string, eq?: string, options?: ParseOptions): T; + export function escape(str: string): string; + export function unescape(str: string): string; +} + +declare module "events" { + export class EventEmitter implements NodeJS.EventEmitter { + static EventEmitter: EventEmitter; + static listenerCount(emitter: EventEmitter, event: string): number; // deprecated + static defaultMaxListeners: number; + + addListener(event: string, listener: Function): this; + on(event: string, listener: Function): this; + once(event: string, listener: Function): this; + removeListener(event: string, listener: Function): this; + removeAllListeners(event?: string): this; + setMaxListeners(n: number): this; + getMaxListeners(): number; + listeners(event: string): Function[]; + emit(event: string, ...args: any[]): boolean; + listenerCount(type: string): number; + } +} + +declare module "http" { + import * as events from "events"; + import * as net from "net"; + import * as stream from "stream"; + + export interface RequestOptions { + protocol?: string; + host?: string; + hostname?: string; + family?: number; + port?: number; + localAddress?: string; + socketPath?: string; + method?: string; + path?: string; + headers?: { [key: string]: any }; + auth?: string; + agent?: Agent|boolean; + } + + export interface Server extends events.EventEmitter, net.Server { + setTimeout(msecs: number, callback: Function): void; + maxHeadersCount: number; + timeout: number; + } + /** + * @deprecated Use IncomingMessage + */ + export interface ServerRequest extends IncomingMessage { + connection: net.Socket; + } + export interface ServerResponse extends events.EventEmitter, stream.Writable { + // Extended base methods + write(buffer: Buffer): boolean; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + write(str: string, encoding?: string, fd?: string): boolean; + + writeContinue(): void; + writeHead(statusCode: number, reasonPhrase?: string, headers?: any): void; + writeHead(statusCode: number, headers?: any): void; + statusCode: number; + statusMessage: string; + headersSent: boolean; + setHeader(name: string, value: string | string[]): void; + sendDate: boolean; + getHeader(name: string): string; + removeHeader(name: string): void; + write(chunk: any, encoding?: string): any; + addTrailers(headers: any): void; + + // Extended base methods + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + end(data?: any, encoding?: string): void; + } + export interface ClientRequest extends events.EventEmitter, stream.Writable { + // Extended base methods + write(buffer: Buffer): boolean; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + write(str: string, encoding?: string, fd?: string): boolean; + + write(chunk: any, encoding?: string): void; + abort(): void; + setTimeout(timeout: number, callback?: Function): void; + setNoDelay(noDelay?: boolean): void; + setSocketKeepAlive(enable?: boolean, initialDelay?: number): void; + + setHeader(name: string, value: string | string[]): void; + getHeader(name: string): string; + removeHeader(name: string): void; + addTrailers(headers: any): void; + + // Extended base methods + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + end(data?: any, encoding?: string): void; + } + export interface IncomingMessage extends events.EventEmitter, stream.Readable { + httpVersion: string; + headers: any; + rawHeaders: string[]; + trailers: any; + rawTrailers: any; + setTimeout(msecs: number, callback: Function): NodeJS.Timer; + /** + * Only valid for request obtained from http.Server. + */ + method?: string; + /** + * Only valid for request obtained from http.Server. + */ + url?: string; + /** + * Only valid for response obtained from http.ClientRequest. + */ + statusCode?: number; + /** + * Only valid for response obtained from http.ClientRequest. + */ + statusMessage?: string; + socket: net.Socket; + } + /** + * @deprecated Use IncomingMessage + */ + export interface ClientResponse extends IncomingMessage { } + + export interface AgentOptions { + /** + * Keep sockets around in a pool to be used by other requests in the future. Default = false + */ + keepAlive?: boolean; + /** + * When using HTTP KeepAlive, how often to send TCP KeepAlive packets over sockets being kept alive. Default = 1000. + * Only relevant if keepAlive is set to true. + */ + keepAliveMsecs?: number; + /** + * Maximum number of sockets to allow per host. Default for Node 0.10 is 5, default for Node 0.12 is Infinity + */ + maxSockets?: number; + /** + * Maximum number of sockets to leave open in a free state. Only relevant if keepAlive is set to true. Default = 256. + */ + maxFreeSockets?: number; + } + + export class Agent { + maxSockets: number; + sockets: any; + requests: any; + + constructor(opts?: AgentOptions); + + /** + * Destroy any sockets that are currently in use by the agent. + * It is usually not necessary to do this. However, if you are using an agent with KeepAlive enabled, + * then it is best to explicitly shut down the agent when you know that it will no longer be used. Otherwise, + * sockets may hang open for quite a long time before the server terminates them. + */ + destroy(): void; + } + + export var METHODS: string[]; + + export var STATUS_CODES: { + [errorCode: number]: string; + [errorCode: string]: string; + }; + export function createServer(requestListener?: (request: IncomingMessage, response: ServerResponse) =>void ): Server; + export function createClient(port?: number, host?: string): any; + export function request(options: RequestOptions, callback?: (res: IncomingMessage) => void): ClientRequest; + export function get(options: any, callback?: (res: IncomingMessage) => void): ClientRequest; + export var globalAgent: Agent; +} + +declare module "cluster" { + import * as child from "child_process"; + import * as events from "events"; + + export interface ClusterSettings { + exec?: string; + args?: string[]; + silent?: boolean; + } + + export interface Address { + address: string; + port: number; + addressType: string; + } + + export class Worker extends events.EventEmitter { + id: string; + process: child.ChildProcess; + suicide: boolean; + send(message: any, sendHandle?: any): void; + kill(signal?: string): void; + destroy(signal?: string): void; + disconnect(): void; + isConnected(): boolean; + isDead(): boolean; + } + + export var settings: ClusterSettings; + export var isMaster: boolean; + export var isWorker: boolean; + export function setupMaster(settings?: ClusterSettings): void; + export function fork(env?: any): Worker; + export function disconnect(callback?: Function): void; + export var worker: Worker; + export var workers: { + [index: string]: Worker + }; + + // Event emitter + export function addListener(event: string, listener: Function): void; + export function on(event: "disconnect", listener: (worker: Worker) => void): void; + export function on(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): void; + export function on(event: "fork", listener: (worker: Worker) => void): void; + export function on(event: "listening", listener: (worker: Worker, address: any) => void): void; + export function on(event: "message", listener: (worker: Worker, message: any) => void): void; + export function on(event: "online", listener: (worker: Worker) => void): void; + export function on(event: "setup", listener: (settings: any) => void): void; + export function on(event: string, listener: Function): any; + export function once(event: string, listener: Function): void; + export function removeListener(event: string, listener: Function): void; + export function removeAllListeners(event?: string): void; + export function setMaxListeners(n: number): void; + export function listeners(event: string): Function[]; + export function emit(event: string, ...args: any[]): boolean; +} + +declare module "zlib" { + import * as stream from "stream"; + export interface ZlibOptions { chunkSize?: number; windowBits?: number; level?: number; memLevel?: number; strategy?: number; dictionary?: any; } + + export interface Gzip extends stream.Transform { } + export interface Gunzip extends stream.Transform { } + export interface Deflate extends stream.Transform { } + export interface Inflate extends stream.Transform { } + export interface DeflateRaw extends stream.Transform { } + export interface InflateRaw extends stream.Transform { } + export interface Unzip extends stream.Transform { } + + export function createGzip(options?: ZlibOptions): Gzip; + export function createGunzip(options?: ZlibOptions): Gunzip; + export function createDeflate(options?: ZlibOptions): Deflate; + export function createInflate(options?: ZlibOptions): Inflate; + export function createDeflateRaw(options?: ZlibOptions): DeflateRaw; + export function createInflateRaw(options?: ZlibOptions): InflateRaw; + export function createUnzip(options?: ZlibOptions): Unzip; + + export function deflate(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function deflateSync(buf: Buffer, options?: ZlibOptions): any; + export function deflateRaw(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function deflateRawSync(buf: Buffer, options?: ZlibOptions): any; + export function gzip(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function gzipSync(buf: Buffer, options?: ZlibOptions): any; + export function gunzip(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function gunzipSync(buf: Buffer, options?: ZlibOptions): any; + export function inflate(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function inflateSync(buf: Buffer, options?: ZlibOptions): any; + export function inflateRaw(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function inflateRawSync(buf: Buffer, options?: ZlibOptions): any; + export function unzip(buf: Buffer, callback: (error: Error, result: any) =>void ): void; + export function unzipSync(buf: Buffer, options?: ZlibOptions): any; + + // Constants + export var Z_NO_FLUSH: number; + export var Z_PARTIAL_FLUSH: number; + export var Z_SYNC_FLUSH: number; + export var Z_FULL_FLUSH: number; + export var Z_FINISH: number; + export var Z_BLOCK: number; + export var Z_TREES: number; + export var Z_OK: number; + export var Z_STREAM_END: number; + export var Z_NEED_DICT: number; + export var Z_ERRNO: number; + export var Z_STREAM_ERROR: number; + export var Z_DATA_ERROR: number; + export var Z_MEM_ERROR: number; + export var Z_BUF_ERROR: number; + export var Z_VERSION_ERROR: number; + export var Z_NO_COMPRESSION: number; + export var Z_BEST_SPEED: number; + export var Z_BEST_COMPRESSION: number; + export var Z_DEFAULT_COMPRESSION: number; + export var Z_FILTERED: number; + export var Z_HUFFMAN_ONLY: number; + export var Z_RLE: number; + export var Z_FIXED: number; + export var Z_DEFAULT_STRATEGY: number; + export var Z_BINARY: number; + export var Z_TEXT: number; + export var Z_ASCII: number; + export var Z_UNKNOWN: number; + export var Z_DEFLATED: number; + export var Z_NULL: number; +} + +declare module "os" { + export interface CpuInfo { + model: string; + speed: number; + times: { + user: number; + nice: number; + sys: number; + idle: number; + irq: number; + }; + } + + export interface NetworkInterfaceInfo { + address: string; + netmask: string; + family: string; + mac: string; + internal: boolean; + } + + export function tmpdir(): string; + export function homedir(): string; + export function endianness(): string; + export function hostname(): string; + export function type(): string; + export function platform(): string; + export function arch(): string; + export function release(): string; + export function uptime(): number; + export function loadavg(): number[]; + export function totalmem(): number; + export function freemem(): number; + export function cpus(): CpuInfo[]; + export function networkInterfaces(): {[index: string]: NetworkInterfaceInfo[]}; + export var EOL: string; +} + +declare module "https" { + import * as tls from "tls"; + import * as events from "events"; + import * as http from "http"; + + export interface ServerOptions { + pfx?: any; + key?: any; + passphrase?: string; + cert?: any; + ca?: any; + crl?: any; + ciphers?: string; + honorCipherOrder?: boolean; + requestCert?: boolean; + rejectUnauthorized?: boolean; + NPNProtocols?: any; + SNICallback?: (servername: string) => any; + } + + export interface RequestOptions extends http.RequestOptions { + pfx?: any; + key?: any; + passphrase?: string; + cert?: any; + ca?: any; + ciphers?: string; + rejectUnauthorized?: boolean; + secureProtocol?: string; + } + + export interface Agent extends http.Agent { } + + export interface AgentOptions extends http.AgentOptions { + maxCachedSessions?: number; + } + + export var Agent: { + new (options?: AgentOptions): Agent; + }; + export interface Server extends tls.Server { } + export function createServer(options: ServerOptions, requestListener?: Function): Server; + export function request(options: RequestOptions, callback?: (res: http.IncomingMessage) =>void ): http.ClientRequest; + export function get(options: RequestOptions, callback?: (res: http.IncomingMessage) =>void ): http.ClientRequest; + export var globalAgent: Agent; +} + +declare module "punycode" { + export function decode(string: string): string; + export function encode(string: string): string; + export function toUnicode(domain: string): string; + export function toASCII(domain: string): string; + export var ucs2: ucs2; + interface ucs2 { + decode(string: string): number[]; + encode(codePoints: number[]): string; + } + export var version: any; +} + +declare module "repl" { + import * as stream from "stream"; + import * as events from "events"; + + export interface ReplOptions { + prompt?: string; + input?: NodeJS.ReadableStream; + output?: NodeJS.WritableStream; + terminal?: boolean; + eval?: Function; + useColors?: boolean; + useGlobal?: boolean; + ignoreUndefined?: boolean; + writer?: Function; + } + export function start(options: ReplOptions): events.EventEmitter; +} + +declare module "readline" { + import * as events from "events"; + import * as stream from "stream"; + + export interface Key { + sequence?: string; + name?: string; + ctrl?: boolean; + meta?: boolean; + shift?: boolean; + } + + export interface ReadLine extends events.EventEmitter { + setPrompt(prompt: string): void; + prompt(preserveCursor?: boolean): void; + question(query: string, callback: (answer: string) => void): void; + pause(): ReadLine; + resume(): ReadLine; + close(): void; + write(data: string|Buffer, key?: Key): void; + } + + export interface Completer { + (line: string): CompleterResult; + (line: string, callback: (err: any, result: CompleterResult) => void): any; + } + + export interface CompleterResult { + completions: string[]; + line: string; + } + + export interface ReadLineOptions { + input: NodeJS.ReadableStream; + output?: NodeJS.WritableStream; + completer?: Completer; + terminal?: boolean; + historySize?: number; + } + + export function createInterface(input: NodeJS.ReadableStream, output?: NodeJS.WritableStream, completer?: Completer, terminal?: boolean): ReadLine; + export function createInterface(options: ReadLineOptions): ReadLine; + + export function cursorTo(stream: NodeJS.WritableStream, x: number, y: number): void; + export function moveCursor(stream: NodeJS.WritableStream, dx: number|string, dy: number|string): void; + export function clearLine(stream: NodeJS.WritableStream, dir: number): void; + export function clearScreenDown(stream: NodeJS.WritableStream): void; +} + +declare module "vm" { + export interface Context { } + export interface ScriptOptions { + filename?: string; + lineOffset?: number; + columnOffset?: number; + displayErrors?: boolean; + timeout?: number; + cachedData?: Buffer; + produceCachedData?: boolean; + } + export interface RunningScriptOptions { + filename?: string; + lineOffset?: number; + columnOffset?: number; + displayErrors?: boolean; + timeout?: number; + } + export class Script { + constructor(code: string, options?: ScriptOptions); + runInContext(contextifiedSandbox: Context, options?: RunningScriptOptions): any; + runInNewContext(sandbox?: Context, options?: RunningScriptOptions): any; + runInThisContext(options?: RunningScriptOptions): any; + } + export function createContext(sandbox?: Context): Context; + export function isContext(sandbox: Context): boolean; + export function runInContext(code: string, contextifiedSandbox: Context, options?: RunningScriptOptions): any; + export function runInDebugContext(code: string): any; + export function runInNewContext(code: string, sandbox?: Context, options?: RunningScriptOptions): any; + export function runInThisContext(code: string, options?: RunningScriptOptions): any; +} + +declare module "child_process" { + import * as events from "events"; + import * as stream from "stream"; + + export interface ChildProcess extends events.EventEmitter { + stdin: stream.Writable; + stdout: stream.Readable; + stderr: stream.Readable; + stdio: [stream.Writable, stream.Readable, stream.Readable]; + pid: number; + kill(signal?: string): void; + send(message: any, sendHandle?: any): void; + connected: boolean; + disconnect(): void; + unref(): void; + } + + export interface SpawnOptions { + cwd?: string; + env?: any; + stdio?: any; + detached?: boolean; + uid?: number; + gid?: number; + shell?: boolean | string; + } + export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess; + + export interface ExecOptions { + cwd?: string; + env?: any; + shell?: string; + timeout?: number; + maxBuffer?: number; + killSignal?: string; + uid?: number; + gid?: number; + } + export interface ExecOptionsWithStringEncoding extends ExecOptions { + encoding: BufferEncoding; + } + export interface ExecOptionsWithBufferEncoding extends ExecOptions { + encoding: string; // specify `null`. + } + export function exec(command: string, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + export function exec(command: string, options: ExecOptionsWithStringEncoding, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + // usage. child_process.exec("tsc", {encoding: null as string}, (err, stdout, stderr) => {}); + export function exec(command: string, options: ExecOptionsWithBufferEncoding, callback?: (error: Error, stdout: Buffer, stderr: Buffer) =>void ): ChildProcess; + export function exec(command: string, options: ExecOptions, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + + export interface ExecFileOptions { + cwd?: string; + env?: any; + timeout?: number; + maxBuffer?: number; + killSignal?: string; + uid?: number; + gid?: number; + } + export interface ExecFileOptionsWithStringEncoding extends ExecFileOptions { + encoding: BufferEncoding; + } + export interface ExecFileOptionsWithBufferEncoding extends ExecFileOptions { + encoding: string; // specify `null`. + } + export function execFile(file: string, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + export function execFile(file: string, options?: ExecFileOptionsWithStringEncoding, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + // usage. child_process.execFile("file.sh", {encoding: null as string}, (err, stdout, stderr) => {}); + export function execFile(file: string, options?: ExecFileOptionsWithBufferEncoding, callback?: (error: Error, stdout: Buffer, stderr: Buffer) =>void ): ChildProcess; + export function execFile(file: string, options?: ExecFileOptions, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + export function execFile(file: string, args?: string[], callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + export function execFile(file: string, args?: string[], options?: ExecFileOptionsWithStringEncoding, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + // usage. child_process.execFile("file.sh", ["foo"], {encoding: null as string}, (err, stdout, stderr) => {}); + export function execFile(file: string, args?: string[], options?: ExecFileOptionsWithBufferEncoding, callback?: (error: Error, stdout: Buffer, stderr: Buffer) =>void ): ChildProcess; + export function execFile(file: string, args?: string[], options?: ExecFileOptions, callback?: (error: Error, stdout: string, stderr: string) =>void ): ChildProcess; + + export interface ForkOptions { + cwd?: string; + env?: any; + execPath?: string; + execArgv?: string[]; + silent?: boolean; + uid?: number; + gid?: number; + } + export function fork(modulePath: string, args?: string[], options?: ForkOptions): ChildProcess; + + export interface SpawnSyncOptions { + cwd?: string; + input?: string | Buffer; + stdio?: any; + env?: any; + uid?: number; + gid?: number; + timeout?: number; + killSignal?: string; + maxBuffer?: number; + encoding?: string; + shell?: boolean | string; + } + export interface SpawnSyncOptionsWithStringEncoding extends SpawnSyncOptions { + encoding: BufferEncoding; + } + export interface SpawnSyncOptionsWithBufferEncoding extends SpawnSyncOptions { + encoding: string; // specify `null`. + } + export interface SpawnSyncReturns { + pid: number; + output: string[]; + stdout: T; + stderr: T; + status: number; + signal: string; + error: Error; + } + export function spawnSync(command: string): SpawnSyncReturns; + export function spawnSync(command: string, options?: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns; + export function spawnSync(command: string, options?: SpawnSyncOptionsWithBufferEncoding): SpawnSyncReturns; + export function spawnSync(command: string, options?: SpawnSyncOptions): SpawnSyncReturns; + export function spawnSync(command: string, args?: string[], options?: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns; + export function spawnSync(command: string, args?: string[], options?: SpawnSyncOptionsWithBufferEncoding): SpawnSyncReturns; + export function spawnSync(command: string, args?: string[], options?: SpawnSyncOptions): SpawnSyncReturns; + + export interface ExecSyncOptions { + cwd?: string; + input?: string | Buffer; + stdio?: any; + env?: any; + shell?: string; + uid?: number; + gid?: number; + timeout?: number; + killSignal?: string; + maxBuffer?: number; + encoding?: string; + } + export interface ExecSyncOptionsWithStringEncoding extends ExecSyncOptions { + encoding: BufferEncoding; + } + export interface ExecSyncOptionsWithBufferEncoding extends ExecSyncOptions { + encoding: string; // specify `null`. + } + export function execSync(command: string): Buffer; + export function execSync(command: string, options?: ExecSyncOptionsWithStringEncoding): string; + export function execSync(command: string, options?: ExecSyncOptionsWithBufferEncoding): Buffer; + export function execSync(command: string, options?: ExecSyncOptions): Buffer; + + export interface ExecFileSyncOptions { + cwd?: string; + input?: string | Buffer; + stdio?: any; + env?: any; + uid?: number; + gid?: number; + timeout?: number; + killSignal?: string; + maxBuffer?: number; + encoding?: string; + } + export interface ExecFileSyncOptionsWithStringEncoding extends ExecFileSyncOptions { + encoding: BufferEncoding; + } + export interface ExecFileSyncOptionsWithBufferEncoding extends ExecFileSyncOptions { + encoding: string; // specify `null`. + } + export function execFileSync(command: string): Buffer; + export function execFileSync(command: string, options?: ExecFileSyncOptionsWithStringEncoding): string; + export function execFileSync(command: string, options?: ExecFileSyncOptionsWithBufferEncoding): Buffer; + export function execFileSync(command: string, options?: ExecFileSyncOptions): Buffer; + export function execFileSync(command: string, args?: string[], options?: ExecFileSyncOptionsWithStringEncoding): string; + export function execFileSync(command: string, args?: string[], options?: ExecFileSyncOptionsWithBufferEncoding): Buffer; + export function execFileSync(command: string, args?: string[], options?: ExecFileSyncOptions): Buffer; +} + +declare module "url" { + export interface Url { + href?: string; + protocol?: string; + auth?: string; + hostname?: string; + port?: string; + host?: string; + pathname?: string; + search?: string; + query?: string | any; + slashes?: boolean; + hash?: string; + path?: string; + } + + export function parse(urlStr: string, parseQueryString?: boolean , slashesDenoteHost?: boolean ): Url; + export function format(url: Url): string; + export function resolve(from: string, to: string): string; +} + +declare module "dns" { + export function lookup(domain: string, family: number, callback: (err: Error, address: string, family: number) =>void ): string; + export function lookup(domain: string, callback: (err: Error, address: string, family: number) =>void ): string; + export function resolve(domain: string, rrtype: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolve(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolve4(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolve6(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolveMx(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolveTxt(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolveSrv(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolveNs(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function resolveCname(domain: string, callback: (err: Error, addresses: string[]) =>void ): string[]; + export function reverse(ip: string, callback: (err: Error, domains: string[]) =>void ): string[]; +} + +declare module "net" { + import * as stream from "stream"; + + export interface Socket extends stream.Duplex { + // Extended base methods + write(buffer: Buffer): boolean; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + write(str: string, encoding?: string, fd?: string): boolean; + + connect(port: number, host?: string, connectionListener?: Function): void; + connect(path: string, connectionListener?: Function): void; + bufferSize: number; + setEncoding(encoding?: string): void; + write(data: any, encoding?: string, callback?: Function): void; + destroy(): void; + pause(): void; + resume(): void; + setTimeout(timeout: number, callback?: Function): void; + setNoDelay(noDelay?: boolean): void; + setKeepAlive(enable?: boolean, initialDelay?: number): void; + address(): { port: number; family: string; address: string; }; + unref(): void; + ref(): void; + + remoteAddress: string; + remoteFamily: string; + remotePort: number; + localAddress: string; + localPort: number; + bytesRead: number; + bytesWritten: number; + + // Extended base methods + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + end(data?: any, encoding?: string): void; + } + + export var Socket: { + new (options?: { fd?: string; type?: string; allowHalfOpen?: boolean; }): Socket; + }; + + export interface ListenOptions { + port?: number; + host?: string; + backlog?: number; + path?: string; + exclusive?: boolean; + } + + export interface Server extends Socket { + listen(port: number, hostname?: string, backlog?: number, listeningListener?: Function): Server; + listen(port: number, hostname?: string, listeningListener?: Function): Server; + listen(port: number, backlog?: number, listeningListener?: Function): Server; + listen(port: number, listeningListener?: Function): Server; + listen(path: string, backlog?: number, listeningListener?: Function): Server; + listen(path: string, listeningListener?: Function): Server; + listen(handle: any, backlog?: number, listeningListener?: Function): Server; + listen(handle: any, listeningListener?: Function): Server; + listen(options: ListenOptions, listeningListener?: Function): Server; + close(callback?: Function): Server; + address(): { port: number; family: string; address: string; }; + getConnections(cb: (error: Error, count: number) => void): void; + ref(): Server; + unref(): Server; + maxConnections: number; + connections: number; + } + export function createServer(connectionListener?: (socket: Socket) =>void ): Server; + export function createServer(options?: { allowHalfOpen?: boolean; }, connectionListener?: (socket: Socket) =>void ): Server; + export function connect(options: { port: number, host?: string, localAddress? : string, localPort? : string, family? : number, allowHalfOpen?: boolean; }, connectionListener?: Function): Socket; + export function connect(port: number, host?: string, connectionListener?: Function): Socket; + export function connect(path: string, connectionListener?: Function): Socket; + export function createConnection(options: { port: number, host?: string, localAddress? : string, localPort? : string, family? : number, allowHalfOpen?: boolean; }, connectionListener?: Function): Socket; + export function createConnection(port: number, host?: string, connectionListener?: Function): Socket; + export function createConnection(path: string, connectionListener?: Function): Socket; + export function isIP(input: string): number; + export function isIPv4(input: string): boolean; + export function isIPv6(input: string): boolean; +} + +declare module "dgram" { + import * as events from "events"; + + interface RemoteInfo { + address: string; + port: number; + size: number; + } + + interface AddressInfo { + address: string; + family: string; + port: number; + } + + export function createSocket(type: string, callback?: (msg: Buffer, rinfo: RemoteInfo) => void): Socket; + + interface Socket extends events.EventEmitter { + send(buf: Buffer, offset: number, length: number, port: number, address: string, callback?: (error: Error, bytes: number) => void): void; + bind(port: number, address?: string, callback?: () => void): void; + close(): void; + address(): AddressInfo; + setBroadcast(flag: boolean): void; + setMulticastTTL(ttl: number): void; + setMulticastLoopback(flag: boolean): void; + addMembership(multicastAddress: string, multicastInterface?: string): void; + dropMembership(multicastAddress: string, multicastInterface?: string): void; + } +} + +declare module "fs" { + import * as stream from "stream"; + import * as events from "events"; + + interface Stats { + isFile(): boolean; + isDirectory(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isSymbolicLink(): boolean; + isFIFO(): boolean; + isSocket(): boolean; + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atime: Date; + mtime: Date; + ctime: Date; + birthtime: Date; + } + + interface FSWatcher extends events.EventEmitter { + close(): void; + } + + export interface ReadStream extends stream.Readable { + close(): void; + } + export interface WriteStream extends stream.Writable { + close(): void; + bytesWritten: number; + } + + /** + * Asynchronous rename. + * @param oldPath + * @param newPath + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function rename(oldPath: string, newPath: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + /** + * Synchronous rename + * @param oldPath + * @param newPath + */ + export function renameSync(oldPath: string, newPath: string): void; + export function truncate(path: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function truncate(path: string | Buffer, len: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function truncateSync(path: string | Buffer, len?: number): void; + export function ftruncate(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function ftruncate(fd: number, len: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function ftruncateSync(fd: number, len?: number): void; + export function chown(path: string | Buffer, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function chownSync(path: string | Buffer, uid: number, gid: number): void; + export function fchown(fd: number, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fchownSync(fd: number, uid: number, gid: number): void; + export function lchown(path: string | Buffer, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function lchownSync(path: string | Buffer, uid: number, gid: number): void; + export function chmod(path: string | Buffer, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function chmod(path: string | Buffer, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function chmodSync(path: string | Buffer, mode: number): void; + export function chmodSync(path: string | Buffer, mode: string): void; + export function fchmod(fd: number, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fchmod(fd: number, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fchmodSync(fd: number, mode: number): void; + export function fchmodSync(fd: number, mode: string): void; + export function lchmod(path: string | Buffer, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function lchmod(path: string | Buffer, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function lchmodSync(path: string | Buffer, mode: number): void; + export function lchmodSync(path: string | Buffer, mode: string): void; + export function stat(path: string | Buffer, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; + export function lstat(path: string | Buffer, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; + export function fstat(fd: number, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; + export function statSync(path: string | Buffer): Stats; + export function lstatSync(path: string | Buffer): Stats; + export function fstatSync(fd: number): Stats; + export function link(srcpath: string | Buffer, dstpath: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function linkSync(srcpath: string | Buffer, dstpath: string | Buffer): void; + export function symlink(srcpath: string | Buffer, dstpath: string | Buffer, type?: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function symlinkSync(srcpath: string | Buffer, dstpath: string | Buffer, type?: string): void; + export function readlink(path: string | Buffer, callback?: (err: NodeJS.ErrnoException, linkString: string) => any): void; + export function readlinkSync(path: string | Buffer): string; + export function realpath(path: string | Buffer, callback?: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; + export function realpath(path: string | Buffer, cache: {[path: string]: string}, callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; + export function realpathSync(path: string | Buffer, cache?: { [path: string]: string }): string; + /* + * Asynchronous unlink - deletes the file specified in {path} + * + * @param path + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function unlink(path: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; + /* + * Synchronous unlink - deletes the file specified in {path} + * + * @param path + */ + export function unlinkSync(path: string | Buffer): void; + /* + * Asynchronous rmdir - removes the directory specified in {path} + * + * @param path + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function rmdir(path: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; + /* + * Synchronous rmdir - removes the directory specified in {path} + * + * @param path + */ + export function rmdirSync(path: string | Buffer): void; + /* + * Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param path + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function mkdir(path: string | Buffer, callback?: (err?: NodeJS.ErrnoException) => void): void; + /* + * Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param path + * @param mode + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function mkdir(path: string | Buffer, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + /* + * Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param path + * @param mode + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function mkdir(path: string | Buffer, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + /* + * Synchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param path + * @param mode + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function mkdirSync(path: string | Buffer, mode?: number): void; + /* + * Synchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777. + * + * @param path + * @param mode + * @param callback No arguments other than a possible exception are given to the completion callback. + */ + export function mkdirSync(path: string | Buffer, mode?: string): void; + /* + * Asynchronous mkdtemp - Creates a unique temporary directory. Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * + * @param prefix + * @param callback The created folder path is passed as a string to the callback's second parameter. + */ + export function mkdtemp(prefix: string, callback?: (err: NodeJS.ErrnoException, folder: string) => void): void; + /* + * Synchronous mkdtemp - Creates a unique temporary directory. Generates six random characters to be appended behind a required prefix to create a unique temporary directory. + * + * @param prefix + * @returns Returns the created folder path. + */ + export function mkdtempSync(prefix: string): string; + export function readdir(path: string | Buffer, callback?: (err: NodeJS.ErrnoException, files: string[]) => void): void; + export function readdirSync(path: string | Buffer): string[]; + export function close(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function closeSync(fd: number): void; + export function open(path: string | Buffer, flags: string, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; + export function open(path: string | Buffer, flags: string, mode: number, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; + export function open(path: string | Buffer, flags: string, mode: string, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; + export function openSync(path: string | Buffer, flags: string, mode?: number): number; + export function openSync(path: string | Buffer, flags: string, mode?: string): number; + export function utimes(path: string | Buffer, atime: number, mtime: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function utimes(path: string | Buffer, atime: Date, mtime: Date, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function utimesSync(path: string | Buffer, atime: number, mtime: number): void; + export function utimesSync(path: string | Buffer, atime: Date, mtime: Date): void; + export function futimes(fd: number, atime: number, mtime: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function futimes(fd: number, atime: Date, mtime: Date, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function futimesSync(fd: number, atime: number, mtime: number): void; + export function futimesSync(fd: number, atime: Date, mtime: Date): void; + export function fsync(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fsyncSync(fd: number): void; + export function write(fd: number, buffer: Buffer, offset: number, length: number, position: number, callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void; + export function write(fd: number, buffer: Buffer, offset: number, length: number, callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void; + export function write(fd: number, data: any, callback?: (err: NodeJS.ErrnoException, written: number, str: string) => void): void; + export function write(fd: number, data: any, offset: number, callback?: (err: NodeJS.ErrnoException, written: number, str: string) => void): void; + export function write(fd: number, data: any, offset: number, encoding: string, callback?: (err: NodeJS.ErrnoException, written: number, str: string) => void): void; + export function writeSync(fd: number, buffer: Buffer, offset: number, length: number, position?: number): number; + export function writeSync(fd: number, data: any, position?: number, enconding?: string): number; + export function read(fd: number, buffer: Buffer, offset: number, length: number, position: number, callback?: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void): void; + export function readSync(fd: number, buffer: Buffer, offset: number, length: number, position: number): number; + /* + * Asynchronous readFile - Asynchronously reads the entire contents of a file. + * + * @param fileName + * @param encoding + * @param callback - The callback is passed two arguments (err, data), where data is the contents of the file. + */ + export function readFile(filename: string, encoding: string, callback: (err: NodeJS.ErrnoException, data: string) => void): void; + /* + * Asynchronous readFile - Asynchronously reads the entire contents of a file. + * + * @param fileName + * @param options An object with optional {encoding} and {flag} properties. If {encoding} is specified, readFile returns a string; otherwise it returns a Buffer. + * @param callback - The callback is passed two arguments (err, data), where data is the contents of the file. + */ + export function readFile(filename: string, options: { encoding: string; flag?: string; }, callback: (err: NodeJS.ErrnoException, data: string) => void): void; + /* + * Asynchronous readFile - Asynchronously reads the entire contents of a file. + * + * @param fileName + * @param options An object with optional {encoding} and {flag} properties. If {encoding} is specified, readFile returns a string; otherwise it returns a Buffer. + * @param callback - The callback is passed two arguments (err, data), where data is the contents of the file. + */ + export function readFile(filename: string, options: { flag?: string; }, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; + /* + * Asynchronous readFile - Asynchronously reads the entire contents of a file. + * + * @param fileName + * @param callback - The callback is passed two arguments (err, data), where data is the contents of the file. + */ + export function readFile(filename: string, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; + /* + * Synchronous readFile - Synchronously reads the entire contents of a file. + * + * @param fileName + * @param encoding + */ + export function readFileSync(filename: string, encoding: string): string; + /* + * Synchronous readFile - Synchronously reads the entire contents of a file. + * + * @param fileName + * @param options An object with optional {encoding} and {flag} properties. If {encoding} is specified, readFileSync returns a string; otherwise it returns a Buffer. + */ + export function readFileSync(filename: string, options: { encoding: string; flag?: string; }): string; + /* + * Synchronous readFile - Synchronously reads the entire contents of a file. + * + * @param fileName + * @param options An object with optional {encoding} and {flag} properties. If {encoding} is specified, readFileSync returns a string; otherwise it returns a Buffer. + */ + export function readFileSync(filename: string, options?: { flag?: string; }): Buffer; + export function writeFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; + export function writeFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function writeFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void; + export function writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: string; flag?: string; }): void; + export function appendFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function appendFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function appendFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; + export function appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void; + export function appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: string; flag?: string; }): void; + export function watchFile(filename: string, listener: (curr: Stats, prev: Stats) => void): void; + export function watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: (curr: Stats, prev: Stats) => void): void; + export function unwatchFile(filename: string, listener?: (curr: Stats, prev: Stats) => void): void; + export function watch(filename: string, listener?: (event: string, filename: string) => any): FSWatcher; + export function watch(filename: string, options: { persistent?: boolean; }, listener?: (event: string, filename: string) => any): FSWatcher; + export function exists(path: string | Buffer, callback?: (exists: boolean) => void): void; + export function existsSync(path: string | Buffer): boolean; + /** Constant for fs.access(). File is visible to the calling process. */ + export var F_OK: number; + /** Constant for fs.access(). File can be read by the calling process. */ + export var R_OK: number; + /** Constant for fs.access(). File can be written by the calling process. */ + export var W_OK: number; + /** Constant for fs.access(). File can be executed by the calling process. */ + export var X_OK: number; + /** Tests a user's permissions for the file specified by path. */ + export function access(path: string | Buffer, callback: (err: NodeJS.ErrnoException) => void): void; + export function access(path: string | Buffer, mode: number, callback: (err: NodeJS.ErrnoException) => void): void; + /** Synchronous version of fs.access. This throws if any accessibility checks fail, and does nothing otherwise. */ + export function accessSync(path: string | Buffer, mode ?: number): void; + export function createReadStream(path: string | Buffer, options?: { + flags?: string; + encoding?: string; + fd?: number; + mode?: number; + autoClose?: boolean; + }): ReadStream; + export function createWriteStream(path: string | Buffer, options?: { + flags?: string; + encoding?: string; + fd?: number; + mode?: number; + }): WriteStream; +} + +declare module "path" { + + /** + * A parsed path object generated by path.parse() or consumed by path.format(). + */ + export interface ParsedPath { + /** + * The root of the path such as '/' or 'c:\' + */ + root: string; + /** + * The full directory path such as '/home/user/dir' or 'c:\path\dir' + */ + dir: string; + /** + * The file name including extension (if any) such as 'index.html' + */ + base: string; + /** + * The file extension (if any) such as '.html' + */ + ext: string; + /** + * The file name without extension (if any) such as 'index' + */ + name: string; + } + + /** + * Normalize a string path, reducing '..' and '.' parts. + * When multiple slashes are found, they're replaced by a single one; when the path contains a trailing slash, it is preserved. On Windows backslashes are used. + * + * @param p string path to normalize. + */ + export function normalize(p: string): string; + /** + * Join all arguments together and normalize the resulting path. + * Arguments must be strings. In v0.8, non-string arguments were silently ignored. In v0.10 and up, an exception is thrown. + * + * @param paths string paths to join. + */ + export function join(...paths: any[]): string; + /** + * Join all arguments together and normalize the resulting path. + * Arguments must be strings. In v0.8, non-string arguments were silently ignored. In v0.10 and up, an exception is thrown. + * + * @param paths string paths to join. + */ + export function join(...paths: string[]): string; + /** + * The right-most parameter is considered {to}. Other parameters are considered an array of {from}. + * + * Starting from leftmost {from} paramter, resolves {to} to an absolute path. + * + * If {to} isn't already absolute, {from} arguments are prepended in right to left order, until an absolute path is found. If after using all {from} paths still no absolute path is found, the current working directory is used as well. The resulting path is normalized, and trailing slashes are removed unless the path gets resolved to the root directory. + * + * @param pathSegments string paths to join. Non-string arguments are ignored. + */ + export function resolve(...pathSegments: any[]): string; + /** + * Determines whether {path} is an absolute path. An absolute path will always resolve to the same location, regardless of the working directory. + * + * @param path path to test. + */ + export function isAbsolute(path: string): boolean; + /** + * Solve the relative path from {from} to {to}. + * At times we have two absolute paths, and we need to derive the relative path from one to the other. This is actually the reverse transform of path.resolve. + * + * @param from + * @param to + */ + export function relative(from: string, to: string): string; + /** + * Return the directory name of a path. Similar to the Unix dirname command. + * + * @param p the path to evaluate. + */ + export function dirname(p: string): string; + /** + * Return the last portion of a path. Similar to the Unix basename command. + * Often used to extract the file name from a fully qualified path. + * + * @param p the path to evaluate. + * @param ext optionally, an extension to remove from the result. + */ + export function basename(p: string, ext?: string): string; + /** + * Return the extension of the path, from the last '.' to end of string in the last portion of the path. + * If there is no '.' in the last portion of the path or the first character of it is '.', then it returns an empty string + * + * @param p the path to evaluate. + */ + export function extname(p: string): string; + /** + * The platform-specific file separator. '\\' or '/'. + */ + export var sep: string; + /** + * The platform-specific file delimiter. ';' or ':'. + */ + export var delimiter: string; + /** + * Returns an object from a path string - the opposite of format(). + * + * @param pathString path to evaluate. + */ + export function parse(pathString: string): ParsedPath; + /** + * Returns a path string from an object - the opposite of parse(). + * + * @param pathString path to evaluate. + */ + export function format(pathObject: ParsedPath): string; + + export module posix { + export function normalize(p: string): string; + export function join(...paths: any[]): string; + export function resolve(...pathSegments: any[]): string; + export function isAbsolute(p: string): boolean; + export function relative(from: string, to: string): string; + export function dirname(p: string): string; + export function basename(p: string, ext?: string): string; + export function extname(p: string): string; + export var sep: string; + export var delimiter: string; + export function parse(p: string): ParsedPath; + export function format(pP: ParsedPath): string; + } + + export module win32 { + export function normalize(p: string): string; + export function join(...paths: any[]): string; + export function resolve(...pathSegments: any[]): string; + export function isAbsolute(p: string): boolean; + export function relative(from: string, to: string): string; + export function dirname(p: string): string; + export function basename(p: string, ext?: string): string; + export function extname(p: string): string; + export var sep: string; + export var delimiter: string; + export function parse(p: string): ParsedPath; + export function format(pP: ParsedPath): string; + } +} + +declare module "string_decoder" { + export interface NodeStringDecoder { + write(buffer: Buffer): string; + detectIncompleteChar(buffer: Buffer): number; + } + export var StringDecoder: { + new (encoding: string): NodeStringDecoder; + }; +} + +declare module "tls" { + import * as crypto from "crypto"; + import * as net from "net"; + import * as stream from "stream"; + + var CLIENT_RENEG_LIMIT: number; + var CLIENT_RENEG_WINDOW: number; + + export interface TlsOptions { + host?: string; + port?: number; + pfx?: any; //string or buffer + key?: any; //string or buffer + passphrase?: string; + cert?: any; + ca?: any; //string or buffer + crl?: any; //string or string array + ciphers?: string; + honorCipherOrder?: any; + requestCert?: boolean; + rejectUnauthorized?: boolean; + NPNProtocols?: any; //array or Buffer; + SNICallback?: (servername: string) => any; + } + + export interface ConnectionOptions { + host?: string; + port?: number; + socket?: net.Socket; + pfx?: string | Buffer + key?: string | Buffer + passphrase?: string; + cert?: string | Buffer + ca?: (string | Buffer)[]; + rejectUnauthorized?: boolean; + NPNProtocols?: (string | Buffer)[]; + servername?: string; + } + + export interface Server extends net.Server { + close(): Server; + address(): { port: number; family: string; address: string; }; + addContext(hostName: string, credentials: { + key: string; + cert: string; + ca: string; + }): void; + maxConnections: number; + connections: number; + } + + export interface ClearTextStream extends stream.Duplex { + authorized: boolean; + authorizationError: Error; + getPeerCertificate(): any; + getCipher: { + name: string; + version: string; + }; + address: { + port: number; + family: string; + address: string; + }; + remoteAddress: string; + remotePort: number; + } + + export interface SecurePair { + encrypted: any; + cleartext: any; + } + + export interface SecureContextOptions { + pfx?: string | Buffer; + key?: string | Buffer; + passphrase?: string; + cert?: string | Buffer; + ca?: string | Buffer; + crl?: string | string[] + ciphers?: string; + honorCipherOrder?: boolean; + } + + export interface SecureContext { + context: any; + } + + export function createServer(options: TlsOptions, secureConnectionListener?: (cleartextStream: ClearTextStream) =>void ): Server; + export function connect(options: TlsOptions, secureConnectionListener?: () =>void ): ClearTextStream; + export function connect(port: number, host?: string, options?: ConnectionOptions, secureConnectListener?: () =>void ): ClearTextStream; + export function connect(port: number, options?: ConnectionOptions, secureConnectListener?: () =>void ): ClearTextStream; + export function createSecurePair(credentials?: crypto.Credentials, isServer?: boolean, requestCert?: boolean, rejectUnauthorized?: boolean): SecurePair; + export function createSecureContext(details: SecureContextOptions): SecureContext; +} + +declare module "crypto" { + export interface CredentialDetails { + pfx: string; + key: string; + passphrase: string; + cert: string; + ca: string | string[]; + crl: string | string[]; + ciphers: string; + } + export interface Credentials { context?: any; } + export function createCredentials(details: CredentialDetails): Credentials; + export function createHash(algorithm: string): Hash; + export function createHmac(algorithm: string, key: string): Hmac; + export function createHmac(algorithm: string, key: Buffer): Hmac; + export interface Hash { + update(data: any, input_encoding?: string): Hash; + digest(encoding: 'buffer'): Buffer; + digest(encoding: string): any; + digest(): Buffer; + } + export interface Hmac extends NodeJS.ReadWriteStream { + update(data: any, input_encoding?: string): Hmac; + digest(encoding: 'buffer'): Buffer; + digest(encoding: string): any; + digest(): Buffer; + } + export function createCipher(algorithm: string, password: any): Cipher; + export function createCipheriv(algorithm: string, key: any, iv: any): Cipher; + export interface Cipher extends NodeJS.ReadWriteStream { + update(data: Buffer): Buffer; + update(data: string, input_encoding: "utf8"|"ascii"|"binary"): Buffer; + update(data: Buffer, input_encoding: any, output_encoding: "binary"|"base64"|"hex"): string; + update(data: string, input_encoding: "utf8"|"ascii"|"binary", output_encoding: "binary"|"base64"|"hex"): string; + final(): Buffer; + final(output_encoding: string): string; + setAutoPadding(auto_padding: boolean): void; + getAuthTag(): Buffer; + } + export function createDecipher(algorithm: string, password: any): Decipher; + export function createDecipheriv(algorithm: string, key: any, iv: any): Decipher; + export interface Decipher extends NodeJS.ReadWriteStream { + update(data: Buffer): Buffer; + update(data: string, input_encoding: "binary"|"base64"|"hex"): Buffer; + update(data: Buffer, input_encoding: any, output_encoding: "utf8"|"ascii"|"binary"): string; + update(data: string, input_encoding: "binary"|"base64"|"hex", output_encoding: "utf8"|"ascii"|"binary"): string; + final(): Buffer; + final(output_encoding: string): string; + setAutoPadding(auto_padding: boolean): void; + setAuthTag(tag: Buffer): void; + } + export function createSign(algorithm: string): Signer; + export interface Signer extends NodeJS.WritableStream { + update(data: any): void; + sign(private_key: string, output_format: string): string; + } + export function createVerify(algorith: string): Verify; + export interface Verify extends NodeJS.WritableStream { + update(data: any): void; + verify(object: string, signature: string, signature_format?: string): boolean; + } + export function createDiffieHellman(prime_length: number): DiffieHellman; + export function createDiffieHellman(prime: number, encoding?: string): DiffieHellman; + export interface DiffieHellman { + generateKeys(encoding?: string): string; + computeSecret(other_public_key: string, input_encoding?: string, output_encoding?: string): string; + getPrime(encoding?: string): string; + getGenerator(encoding: string): string; + getPublicKey(encoding?: string): string; + getPrivateKey(encoding?: string): string; + setPublicKey(public_key: string, encoding?: string): void; + setPrivateKey(public_key: string, encoding?: string): void; + } + export function getDiffieHellman(group_name: string): DiffieHellman; + export function pbkdf2(password: string|Buffer, salt: string|Buffer, iterations: number, keylen: number, callback: (err: Error, derivedKey: Buffer) => any): void; + export function pbkdf2(password: string|Buffer, salt: string|Buffer, iterations: number, keylen: number, digest: string, callback: (err: Error, derivedKey: Buffer) => any): void; + export function pbkdf2Sync(password: string|Buffer, salt: string|Buffer, iterations: number, keylen: number) : Buffer; + export function pbkdf2Sync(password: string|Buffer, salt: string|Buffer, iterations: number, keylen: number, digest: string) : Buffer; + export function randomBytes(size: number): Buffer; + export function randomBytes(size: number, callback: (err: Error, buf: Buffer) =>void ): void; + export function pseudoRandomBytes(size: number): Buffer; + export function pseudoRandomBytes(size: number, callback: (err: Error, buf: Buffer) =>void ): void; + export interface RsaPublicKey { + key: string; + padding?: any; + } + export interface RsaPrivateKey { + key: string; + passphrase?: string, + padding?: any; + } + export function publicEncrypt(public_key: string|RsaPublicKey, buffer: Buffer): Buffer + export function privateDecrypt(private_key: string|RsaPrivateKey, buffer: Buffer): Buffer +} + +declare module "stream" { + import * as events from "events"; + + export class Stream extends events.EventEmitter { + pipe(destination: T, options?: { end?: boolean; }): T; + } + + export interface ReadableOptions { + highWaterMark?: number; + encoding?: string; + objectMode?: boolean; + } + + export class Readable extends events.EventEmitter implements NodeJS.ReadableStream { + readable: boolean; + constructor(opts?: ReadableOptions); + _read(size: number): void; + read(size?: number): any; + setEncoding(encoding: string): void; + pause(): void; + resume(): void; + pipe(destination: T, options?: { end?: boolean; }): T; + unpipe(destination?: T): void; + unshift(chunk: any): void; + wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; + push(chunk: any, encoding?: string): boolean; + } + + export interface WritableOptions { + highWaterMark?: number; + decodeStrings?: boolean; + objectMode?: boolean; + } + + export class Writable extends events.EventEmitter implements NodeJS.WritableStream { + writable: boolean; + constructor(opts?: WritableOptions); + _write(chunk: any, encoding: string, callback: Function): void; + write(chunk: any, cb?: Function): boolean; + write(chunk: any, encoding?: string, cb?: Function): boolean; + end(): void; + end(chunk: any, cb?: Function): void; + end(chunk: any, encoding?: string, cb?: Function): void; + } + + export interface DuplexOptions extends ReadableOptions, WritableOptions { + allowHalfOpen?: boolean; + } + + // Note: Duplex extends both Readable and Writable. + export class Duplex extends Readable implements NodeJS.ReadWriteStream { + writable: boolean; + constructor(opts?: DuplexOptions); + _write(chunk: any, encoding: string, callback: Function): void; + write(chunk: any, cb?: Function): boolean; + write(chunk: any, encoding?: string, cb?: Function): boolean; + end(): void; + end(chunk: any, cb?: Function): void; + end(chunk: any, encoding?: string, cb?: Function): void; + } + + export interface TransformOptions extends ReadableOptions, WritableOptions {} + + // Note: Transform lacks the _read and _write methods of Readable/Writable. + export class Transform extends events.EventEmitter implements NodeJS.ReadWriteStream { + readable: boolean; + writable: boolean; + constructor(opts?: TransformOptions); + _transform(chunk: any, encoding: string, callback: Function): void; + _flush(callback: Function): void; + read(size?: number): any; + setEncoding(encoding: string): void; + pause(): void; + resume(): void; + pipe(destination: T, options?: { end?: boolean; }): T; + unpipe(destination?: T): void; + unshift(chunk: any): void; + wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; + push(chunk: any, encoding?: string): boolean; + write(chunk: any, cb?: Function): boolean; + write(chunk: any, encoding?: string, cb?: Function): boolean; + end(): void; + end(chunk: any, cb?: Function): void; + end(chunk: any, encoding?: string, cb?: Function): void; + } + + export class PassThrough extends Transform {} +} + +declare module "util" { + export interface InspectOptions { + showHidden?: boolean; + depth?: number; + colors?: boolean; + customInspect?: boolean; + } + + export function format(format: any, ...param: any[]): string; + export function debug(string: string): void; + export function error(...param: any[]): void; + export function puts(...param: any[]): void; + export function print(...param: any[]): void; + export function log(string: string): void; + export function inspect(object: any, showHidden?: boolean, depth?: number, color?: boolean): string; + export function inspect(object: any, options: InspectOptions): string; + export function isArray(object: any): boolean; + export function isRegExp(object: any): boolean; + export function isDate(object: any): boolean; + export function isError(object: any): boolean; + export function inherits(constructor: any, superConstructor: any): void; + export function debuglog(key:string): (msg:string,...param: any[])=>void; +} + +declare module "assert" { + function internal (value: any, message?: string): void; + namespace internal { + export class AssertionError implements Error { + name: string; + message: string; + actual: any; + expected: any; + operator: string; + generatedMessage: boolean; + + constructor(options?: {message?: string; actual?: any; expected?: any; + operator?: string; stackStartFunction?: Function}); + } + + export function fail(actual?: any, expected?: any, message?: string, operator?: string): void; + export function ok(value: any, message?: string): void; + export function equal(actual: any, expected: any, message?: string): void; + export function notEqual(actual: any, expected: any, message?: string): void; + export function deepEqual(actual: any, expected: any, message?: string): void; + export function notDeepEqual(acutal: any, expected: any, message?: string): void; + export function strictEqual(actual: any, expected: any, message?: string): void; + export function notStrictEqual(actual: any, expected: any, message?: string): void; + export function deepStrictEqual(actual: any, expected: any, message?: string): void; + export function notDeepStrictEqual(actual: any, expected: any, message?: string): void; + export var throws: { + (block: Function, message?: string): void; + (block: Function, error: Function, message?: string): void; + (block: Function, error: RegExp, message?: string): void; + (block: Function, error: (err: any) => boolean, message?: string): void; + }; + + export var doesNotThrow: { + (block: Function, message?: string): void; + (block: Function, error: Function, message?: string): void; + (block: Function, error: RegExp, message?: string): void; + (block: Function, error: (err: any) => boolean, message?: string): void; + }; + + export function ifError(value: any): void; + } + + export = internal; +} + +declare module "tty" { + import * as net from "net"; + + export function isatty(fd: number): boolean; + export interface ReadStream extends net.Socket { + isRaw: boolean; + setRawMode(mode: boolean): void; + isTTY: boolean; + } + export interface WriteStream extends net.Socket { + columns: number; + rows: number; + isTTY: boolean; + } +} + +declare module "domain" { + import * as events from "events"; + + export class Domain extends events.EventEmitter implements NodeJS.Domain { + run(fn: Function): void; + add(emitter: events.EventEmitter): void; + remove(emitter: events.EventEmitter): void; + bind(cb: (err: Error, data: any) => any): any; + intercept(cb: (data: any) => any): any; + dispose(): void; + } + + export function create(): Domain; +} + +declare module "constants" { + export var E2BIG: number; + export var EACCES: number; + export var EADDRINUSE: number; + export var EADDRNOTAVAIL: number; + export var EAFNOSUPPORT: number; + export var EAGAIN: number; + export var EALREADY: number; + export var EBADF: number; + export var EBADMSG: number; + export var EBUSY: number; + export var ECANCELED: number; + export var ECHILD: number; + export var ECONNABORTED: number; + export var ECONNREFUSED: number; + export var ECONNRESET: number; + export var EDEADLK: number; + export var EDESTADDRREQ: number; + export var EDOM: number; + export var EEXIST: number; + export var EFAULT: number; + export var EFBIG: number; + export var EHOSTUNREACH: number; + export var EIDRM: number; + export var EILSEQ: number; + export var EINPROGRESS: number; + export var EINTR: number; + export var EINVAL: number; + export var EIO: number; + export var EISCONN: number; + export var EISDIR: number; + export var ELOOP: number; + export var EMFILE: number; + export var EMLINK: number; + export var EMSGSIZE: number; + export var ENAMETOOLONG: number; + export var ENETDOWN: number; + export var ENETRESET: number; + export var ENETUNREACH: number; + export var ENFILE: number; + export var ENOBUFS: number; + export var ENODATA: number; + export var ENODEV: number; + export var ENOENT: number; + export var ENOEXEC: number; + export var ENOLCK: number; + export var ENOLINK: number; + export var ENOMEM: number; + export var ENOMSG: number; + export var ENOPROTOOPT: number; + export var ENOSPC: number; + export var ENOSR: number; + export var ENOSTR: number; + export var ENOSYS: number; + export var ENOTCONN: number; + export var ENOTDIR: number; + export var ENOTEMPTY: number; + export var ENOTSOCK: number; + export var ENOTSUP: number; + export var ENOTTY: number; + export var ENXIO: number; + export var EOPNOTSUPP: number; + export var EOVERFLOW: number; + export var EPERM: number; + export var EPIPE: number; + export var EPROTO: number; + export var EPROTONOSUPPORT: number; + export var EPROTOTYPE: number; + export var ERANGE: number; + export var EROFS: number; + export var ESPIPE: number; + export var ESRCH: number; + export var ETIME: number; + export var ETIMEDOUT: number; + export var ETXTBSY: number; + export var EWOULDBLOCK: number; + export var EXDEV: number; + export var WSAEINTR: number; + export var WSAEBADF: number; + export var WSAEACCES: number; + export var WSAEFAULT: number; + export var WSAEINVAL: number; + export var WSAEMFILE: number; + export var WSAEWOULDBLOCK: number; + export var WSAEINPROGRESS: number; + export var WSAEALREADY: number; + export var WSAENOTSOCK: number; + export var WSAEDESTADDRREQ: number; + export var WSAEMSGSIZE: number; + export var WSAEPROTOTYPE: number; + export var WSAENOPROTOOPT: number; + export var WSAEPROTONOSUPPORT: number; + export var WSAESOCKTNOSUPPORT: number; + export var WSAEOPNOTSUPP: number; + export var WSAEPFNOSUPPORT: number; + export var WSAEAFNOSUPPORT: number; + export var WSAEADDRINUSE: number; + export var WSAEADDRNOTAVAIL: number; + export var WSAENETDOWN: number; + export var WSAENETUNREACH: number; + export var WSAENETRESET: number; + export var WSAECONNABORTED: number; + export var WSAECONNRESET: number; + export var WSAENOBUFS: number; + export var WSAEISCONN: number; + export var WSAENOTCONN: number; + export var WSAESHUTDOWN: number; + export var WSAETOOMANYREFS: number; + export var WSAETIMEDOUT: number; + export var WSAECONNREFUSED: number; + export var WSAELOOP: number; + export var WSAENAMETOOLONG: number; + export var WSAEHOSTDOWN: number; + export var WSAEHOSTUNREACH: number; + export var WSAENOTEMPTY: number; + export var WSAEPROCLIM: number; + export var WSAEUSERS: number; + export var WSAEDQUOT: number; + export var WSAESTALE: number; + export var WSAEREMOTE: number; + export var WSASYSNOTREADY: number; + export var WSAVERNOTSUPPORTED: number; + export var WSANOTINITIALISED: number; + export var WSAEDISCON: number; + export var WSAENOMORE: number; + export var WSAECANCELLED: number; + export var WSAEINVALIDPROCTABLE: number; + export var WSAEINVALIDPROVIDER: number; + export var WSAEPROVIDERFAILEDINIT: number; + export var WSASYSCALLFAILURE: number; + export var WSASERVICE_NOT_FOUND: number; + export var WSATYPE_NOT_FOUND: number; + export var WSA_E_NO_MORE: number; + export var WSA_E_CANCELLED: number; + export var WSAEREFUSED: number; + export var SIGHUP: number; + export var SIGINT: number; + export var SIGILL: number; + export var SIGABRT: number; + export var SIGFPE: number; + export var SIGKILL: number; + export var SIGSEGV: number; + export var SIGTERM: number; + export var SIGBREAK: number; + export var SIGWINCH: number; + export var SSL_OP_ALL: number; + export var SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION: number; + export var SSL_OP_CIPHER_SERVER_PREFERENCE: number; + export var SSL_OP_CISCO_ANYCONNECT: number; + export var SSL_OP_COOKIE_EXCHANGE: number; + export var SSL_OP_CRYPTOPRO_TLSEXT_BUG: number; + export var SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS: number; + export var SSL_OP_EPHEMERAL_RSA: number; + export var SSL_OP_LEGACY_SERVER_CONNECT: number; + export var SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER: number; + export var SSL_OP_MICROSOFT_SESS_ID_BUG: number; + export var SSL_OP_MSIE_SSLV2_RSA_PADDING: number; + export var SSL_OP_NETSCAPE_CA_DN_BUG: number; + export var SSL_OP_NETSCAPE_CHALLENGE_BUG: number; + export var SSL_OP_NETSCAPE_DEMO_CIPHER_CHANGE_BUG: number; + export var SSL_OP_NETSCAPE_REUSE_CIPHER_CHANGE_BUG: number; + export var SSL_OP_NO_COMPRESSION: number; + export var SSL_OP_NO_QUERY_MTU: number; + export var SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION: number; + export var SSL_OP_NO_SSLv2: number; + export var SSL_OP_NO_SSLv3: number; + export var SSL_OP_NO_TICKET: number; + export var SSL_OP_NO_TLSv1: number; + export var SSL_OP_NO_TLSv1_1: number; + export var SSL_OP_NO_TLSv1_2: number; + export var SSL_OP_PKCS1_CHECK_1: number; + export var SSL_OP_PKCS1_CHECK_2: number; + export var SSL_OP_SINGLE_DH_USE: number; + export var SSL_OP_SINGLE_ECDH_USE: number; + export var SSL_OP_SSLEAY_080_CLIENT_DH_BUG: number; + export var SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG: number; + export var SSL_OP_TLS_BLOCK_PADDING_BUG: number; + export var SSL_OP_TLS_D5_BUG: number; + export var SSL_OP_TLS_ROLLBACK_BUG: number; + export var ENGINE_METHOD_DSA: number; + export var ENGINE_METHOD_DH: number; + export var ENGINE_METHOD_RAND: number; + export var ENGINE_METHOD_ECDH: number; + export var ENGINE_METHOD_ECDSA: number; + export var ENGINE_METHOD_CIPHERS: number; + export var ENGINE_METHOD_DIGESTS: number; + export var ENGINE_METHOD_STORE: number; + export var ENGINE_METHOD_PKEY_METHS: number; + export var ENGINE_METHOD_PKEY_ASN1_METHS: number; + export var ENGINE_METHOD_ALL: number; + export var ENGINE_METHOD_NONE: number; + export var DH_CHECK_P_NOT_SAFE_PRIME: number; + export var DH_CHECK_P_NOT_PRIME: number; + export var DH_UNABLE_TO_CHECK_GENERATOR: number; + export var DH_NOT_SUITABLE_GENERATOR: number; + export var NPN_ENABLED: number; + export var RSA_PKCS1_PADDING: number; + export var RSA_SSLV23_PADDING: number; + export var RSA_NO_PADDING: number; + export var RSA_PKCS1_OAEP_PADDING: number; + export var RSA_X931_PADDING: number; + export var RSA_PKCS1_PSS_PADDING: number; + export var POINT_CONVERSION_COMPRESSED: number; + export var POINT_CONVERSION_UNCOMPRESSED: number; + export var POINT_CONVERSION_HYBRID: number; + export var O_RDONLY: number; + export var O_WRONLY: number; + export var O_RDWR: number; + export var S_IFMT: number; + export var S_IFREG: number; + export var S_IFDIR: number; + export var S_IFCHR: number; + export var S_IFLNK: number; + export var O_CREAT: number; + export var O_EXCL: number; + export var O_TRUNC: number; + export var O_APPEND: number; + export var F_OK: number; + export var R_OK: number; + export var W_OK: number; + export var X_OK: number; + export var UV_UDP_REUSEADDR: number; +} diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/tsd.d.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/tsd.d.ts new file mode 100644 index 00000000..4bd49f30 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/typings/tsd.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/Microsoft.AspNetCore.NodeServices/package.json b/src/Microsoft.AspNetCore.NodeServices/package.json new file mode 100644 index 00000000..684ad246 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/package.json @@ -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" + } +} diff --git a/src/Microsoft.AspNetCore.NodeServices/project.json b/src/Microsoft.AspNetCore.NodeServices/project.json index 3223b3e6..e408c67d 100644 --- a/src/Microsoft.AspNetCore.NodeServices/project.json +++ b/src/Microsoft.AspNetCore.NodeServices/project.json @@ -10,6 +10,7 @@ "version": "1.0.0-rc2-*", "type": "platform" }, + "System.IO.Pipes": "4.0.0-*", "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.0-*", "Microsoft.Extensions.Configuration.Json": "1.0.0-*", "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0-*", @@ -17,7 +18,7 @@ "Newtonsoft.Json": "8.0.3" }, "frameworks": { - "netcoreapp1.0": { + "netstandard1.3": { "imports": [ "dotnet5.6", "dnxcore50", diff --git a/src/Microsoft.AspNetCore.NodeServices/tsd.json b/src/Microsoft.AspNetCore.NodeServices/tsd.json new file mode 100644 index 00000000..3443664c --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/tsd.json @@ -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" + } + } +} diff --git a/src/Microsoft.AspNetCore.NodeServices/webpack.config.js b/src/Microsoft.AspNetCore.NodeServices/webpack.config.js new file mode 100644 index 00000000..c11ba647 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/webpack.config.js @@ -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' + } +}; From 50ee405656ed6138ff72f0611a9b57b974355ac0 Mon Sep 17 00:00:00 2001 From: pauldotknopf Date: Wed, 1 Jun 2016 16:47:28 +0100 Subject: [PATCH 007/876] Workaround for a bug in .NET Core. This issue is referenced by #92. It is has been reported to the dotnet corefx team here: dotnet/corefx#8809 The issue won't be resolved in 1.0.0, so @stephentoub recommended that we reuse the HttpClient. --- .../HostingModels/HttpNodeInstance.cs | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 26edeb30..eb4f1f0c 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -19,6 +18,8 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance ContractResolver = new CamelCasePropertyNamesContractResolver() }; + private HttpClient _client; + private bool _disposed; private int _portNumber; public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null) @@ -29,7 +30,8 @@ public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExte projectPath, MakeCommandLineOptions(port, watchFileExtensions)) { - } + _client = new HttpClient(); + } private static string MakeCommandLineOptions(int port, string[] watchFileExtensions) { @@ -46,34 +48,31 @@ public override async Task Invoke(NodeInvocationInfo invocationInfo) { await EnsureReady(); - using (var client = new HttpClient()) - { - // TODO: Use System.Net.Http.Formatting (PostAsJsonAsync etc.) - var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); - var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); - var response = await client.PostAsync("http://localhost:" + _portNumber, payload); - var responseString = await response.Content.ReadAsStringAsync(); + // TODO: Use System.Net.Http.Formatting (PostAsJsonAsync etc.) + var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); + var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); + var response = await _client.PostAsync("http://localhost:" + _portNumber, payload); + var responseString = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) - { - throw new Exception("Call to Node module failed with error: " + responseString); - } - - var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; - if (responseIsJson) - { - return JsonConvert.DeserializeObject(responseString); - } + if (!response.IsSuccessStatusCode) + { + throw new Exception("Call to Node module failed with error: " + responseString); + } - if (typeof(T) != typeof(string)) - { - throw new ArgumentException( - "Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + - typeof(T).FullName); - } + var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; + if (responseIsJson) + { + return JsonConvert.DeserializeObject(responseString); + } - return (T)(object)responseString; + if (typeof(T) != typeof(string)) + { + throw new ArgumentException( + "Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + + typeof(T).FullName); } + + return (T)(object)responseString; } protected override void OnOutputDataReceived(string outputData) @@ -94,5 +93,19 @@ protected override void OnBeforeLaunchProcess() // Prepare to receive a new port number _portNumber = 0; } - } + + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + + if (!_disposed) + { + if (disposing) + { + _client.Dispose(); + } + + _disposed = true; + } + } + } } \ No newline at end of file From f2e89fd3bcfee38de3950d908888f8d4e53747cb Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 1 Jun 2016 17:03:05 +0100 Subject: [PATCH 008/876] Remove obsolete InputOutputStream transport, now that the Stream transport is implemented --- .../Configuration.cs | 2 - .../Content/Node/entrypoint-stream.js | 23 ------ .../InputOutputStreamNodeInstance.cs | 75 ------------------- .../HostingModels/OutOfProcessNodeInstance.cs | 12 +-- .../NodeHostingModel.cs | 1 - 5 files changed, 1 insertion(+), 112 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-stream.js delete mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs index 51325d46..d24a7e33 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs @@ -41,8 +41,6 @@ public static INodeServices CreateNodeServices(NodeServicesOptions options) return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions); case NodeHostingModel.Socket: return new SocketNodeInstance(options.ProjectPath, watchFileExtensions); - case NodeHostingModel.InputOutputStream: - return new InputOutputStreamNodeInstance(options.ProjectPath); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-stream.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-stream.js deleted file mode 100644 index 9d9e792d..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-stream.js +++ /dev/null @@ -1,23 +0,0 @@ -var path = require('path'); -var readline = require('readline'); -var invocationPrefix = 'invoke:'; - -function invocationCallback(errorValue, successValue) { - if (errorValue) { - throw new Error('InputOutputStreamHost doesn\'t support errors. Got error: ' + errorValue.toString()); - } else { - var serializedResult = JSON.stringify(successValue); - console.log(serializedResult); - } -} - -readline.createInterface({ input: process.stdin }).on('line', function (message) { - if (message && message.substring(0, invocationPrefix.length) === invocationPrefix) { - var invocation = JSON.parse(message.substring(invocationPrefix.length)); - var invokedModule = require(path.resolve(process.cwd(), invocation.moduleName)); - var func = invocation.exportedFunctionName ? invokedModule[invocation.exportedFunctionName] : invokedModule; - func.apply(null, [invocationCallback].concat(invocation.args)); - } -}); - -console.log('[Microsoft.AspNetCore.NodeServices:Listening]'); // The .NET app waits for this signal before sending any invocations diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs deleted file mode 100644 index ffa2cdca..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Microsoft.AspNetCore.NodeServices -{ - /// - /// This is just to demonstrate that other transports are possible. This implementation is extremely - /// dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an - /// error), then it will just hang forever. So don't use this. - /// - /// But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine), - /// versus 2-3ms for the HTTP transport. - /// - /// Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes - /// on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the - /// requests, associating them with responses, and scheduling use of the comms channel. - /// - /// - internal class InputOutputStreamNodeInstance : OutOfProcessNodeInstance - { - private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - - private TaskCompletionSource _currentInvocationResult; - private readonly SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); - - public InputOutputStreamNodeInstance(string projectPath) - : base( - EmbeddedResourceReader.Read( - typeof(InputOutputStreamNodeInstance), - "/Content/Node/entrypoint-stream.js"), - projectPath) - { - } - - public override async Task Invoke(NodeInvocationInfo invocationInfo) - { - await _invocationSemaphore.WaitAsync(); - try - { - await EnsureReady(); - - var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); - var nodeProcess = NodeProcess; - _currentInvocationResult = new TaskCompletionSource(); - nodeProcess.StandardInput.Write("\ninvoke:"); - nodeProcess.StandardInput.WriteLine(payloadJson); // WriteLineAsync isn't supported cross-platform - var resultString = await _currentInvocationResult.Task; - return JsonConvert.DeserializeObject(resultString); - } - finally - { - _invocationSemaphore.Release(); - _currentInvocationResult = null; - } - } - - protected override void OnOutputDataReceived(string outputData) - { - if (_currentInvocationResult != null) - { - _currentInvocationResult.SetResult(outputData); - } - else - { - base.OnOutputDataReceived(outputData); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 1f65c603..1a8a5e10 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -27,23 +27,13 @@ public OutOfProcessNodeInstance(string entryPointScript, string projectPath, str _projectPath = projectPath; _commandLineArguments = commandLineArguments ?? string.Empty; } - + public string CommandLineArguments { get { return _commandLineArguments; } set { _commandLineArguments = value; } } - protected Process NodeProcess - { - get - { - // This is only exposed to support the unreliable InputOutputStreamNodeInstance, which is just to verify that - // other hosting/transport mechanisms are possible. This shouldn't really be exposed, and will be removed. - return this._nodeProcess; - } - } - public Task Invoke(string moduleName, params object[] args) => InvokeExport(moduleName, null, args); diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs index d5b7b455..eacca817 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs @@ -3,7 +3,6 @@ namespace Microsoft.AspNetCore.NodeServices public enum NodeHostingModel { Http, - InputOutputStream, Socket, } } From 931ba118e1ff25f91f455991a3a22d2a6e8aa876 Mon Sep 17 00:00:00 2001 From: Rasmus Melchior Jacobsen Date: Mon, 23 May 2016 11:52:47 +0200 Subject: [PATCH 009/876] net451 support and changed netcoreapp1.0->netstandard1.5 for libraries --- .../project.json | 4 +++- .../project.json | 20 ++++++++++++------- .../project.json | 4 +++- .../project.json | 4 +++- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.AngularServices/project.json b/src/Microsoft.AspNetCore.AngularServices/project.json index 2de17928..493962aa 100644 --- a/src/Microsoft.AspNetCore.AngularServices/project.json +++ b/src/Microsoft.AspNetCore.AngularServices/project.json @@ -13,7 +13,9 @@ "Microsoft.AspNetCore.SpaServices": "1.0.0-*" }, "frameworks": { - "netcoreapp1.0": { + "net451": { + }, + "netstandard1.5": { "imports": [ "dotnet5.6", "dnxcore50", diff --git a/src/Microsoft.AspNetCore.NodeServices/project.json b/src/Microsoft.AspNetCore.NodeServices/project.json index e408c67d..5f8cbe76 100644 --- a/src/Microsoft.AspNetCore.NodeServices/project.json +++ b/src/Microsoft.AspNetCore.NodeServices/project.json @@ -6,11 +6,6 @@ }, "authors": [ "Microsoft" ], "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-rc2-*", - "type": "platform" - }, - "System.IO.Pipes": "4.0.0-*", "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.0-*", "Microsoft.Extensions.Configuration.Json": "1.0.0-*", "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0-*", @@ -18,17 +13,28 @@ "Newtonsoft.Json": "8.0.3" }, "frameworks": { + "net451": { + "frameworkAssemblies": { + "System.Net.Http": "4.0.0.0" + } + }, "netstandard1.3": { "imports": [ "dotnet5.6", "dnxcore50", "portable-net45+win8" - ] + ], + "dependencies": { + "System.Console": "4.0.0-*", + "System.Diagnostics.Process": "4.1.0-*", + "System.Net.Http": "4.0.1-*", + "System.Text.RegularExpressions": "4.0.12-*" + } } }, "buildOptions": { "embed": [ "Content/**/*" ] - } + } } diff --git a/src/Microsoft.AspNetCore.ReactServices/project.json b/src/Microsoft.AspNetCore.ReactServices/project.json index 18e475af..aed28551 100644 --- a/src/Microsoft.AspNetCore.ReactServices/project.json +++ b/src/Microsoft.AspNetCore.ReactServices/project.json @@ -13,7 +13,9 @@ "Microsoft.AspNetCore.SpaServices": "1.0.0-*" }, "frameworks": { - "netcoreapp1.0": { + "net451": { + }, + "netstandard1.5": { "imports": [ "dotnet5.6", "dnxcore50", diff --git a/src/Microsoft.AspNetCore.SpaServices/project.json b/src/Microsoft.AspNetCore.SpaServices/project.json index 1132fa3b..8b4f2531 100644 --- a/src/Microsoft.AspNetCore.SpaServices/project.json +++ b/src/Microsoft.AspNetCore.SpaServices/project.json @@ -12,7 +12,9 @@ "Microsoft.AspNetCore.NodeServices": "1.0.0-*" }, "frameworks": { - "netcoreapp1.0": { + "net451": { + }, + "netstandard1.5": { "imports": [ "dotnet5.6", "dnxcore50", From 311733b11383492ffe1372b28430cf655623dc4a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 1 Jun 2016 18:04:40 +0100 Subject: [PATCH 010/876] Fix references to socket APIs, and target the lowest net standard versions possible --- src/Microsoft.AspNetCore.AngularServices/project.json | 2 +- .../PhysicalConnections/NamedPipeConnection.cs | 6 ++++++ .../PhysicalConnections/StreamConnection.cs | 8 ++++++-- .../PhysicalConnections/UnixDomainSocketConnection.cs | 11 +++++++++++ .../PhysicalConnections/UnixDomainSocketEndPoint.cs | 4 ++++ .../VirtualConnections/VirtualConnection.cs | 7 ++++++- src/Microsoft.AspNetCore.NodeServices/project.json | 10 ++++++++-- src/Microsoft.AspNetCore.ReactServices/project.json | 2 +- src/Microsoft.AspNetCore.SpaServices/project.json | 4 ++-- 9 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNetCore.AngularServices/project.json b/src/Microsoft.AspNetCore.AngularServices/project.json index 493962aa..60be12cb 100644 --- a/src/Microsoft.AspNetCore.AngularServices/project.json +++ b/src/Microsoft.AspNetCore.AngularServices/project.json @@ -15,7 +15,7 @@ "frameworks": { "net451": { }, - "netstandard1.5": { + "netstandard1.0": { "imports": [ "dotnet5.6", "dnxcore50", diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs index ea2159a0..0d0d79bf 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs @@ -9,12 +9,18 @@ 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 Open(string address) { _namedPipeClientStream = new NamedPipeClientStream(".", address, PipeDirection.InOut); +#if NET451 + _namedPipeClientStream.Connect(); +#else await _namedPipeClientStream.ConnectAsync().ConfigureAwait(false); +#endif return _namedPipeClientStream; } +#pragma warning restore 1998 public override void Dispose() { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs index 6e903242..45a53532 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/StreamConnection.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Runtime.InteropServices; using System.Threading.Tasks; namespace Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections @@ -12,7 +11,11 @@ internal abstract class StreamConnection : IDisposable public static StreamConnection Create() { - var useNamedPipes = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#if NET451 + return new NamedPipeConnection(); +#else + var useNamedPipes = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( + System.Runtime.InteropServices.OSPlatform.Windows); if (useNamedPipes) { return new NamedPipeConnection(); @@ -21,6 +24,7 @@ public static StreamConnection Create() { return new UnixDomainSocketConnection(); } +#endif } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs index 93d3a442..bd0240bf 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketConnection.cs @@ -10,6 +10,16 @@ internal class UnixDomainSocketConnection : StreamConnection private NetworkStream _networkStream; private Socket _socket; +#if NET451 + public override Task 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 Open(string address) { var endPoint = new UnixDomainSocketEndPoint("/tmp/" + address); @@ -18,6 +28,7 @@ public override async Task Open(string address) _networkStream = new NetworkStream(_socket); return _networkStream; } +#endif public override void Dispose() { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs index 8937a0de..6276a08d 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/UnixDomainSocketEndPoint.cs @@ -59,7 +59,11 @@ internal UnixDomainSocketEndPoint(SocketAddress socketAddress) } else { +#if NET451 + _encodedPath = new byte[0]; +#else _encodedPath = Array.Empty(); +#endif _path = string.Empty; } } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs index 2a2d148b..afc543ca 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/VirtualConnections/VirtualConnection.cs @@ -12,6 +12,11 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections /// 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 _receivedDataQueue = new BufferBlock(); private ArraySegment _receivedDataNotYetUsed; @@ -109,7 +114,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati throw new InvalidOperationException("The connection was already closed by the remote party"); } - return count > 0 ? _host.WriteAsync(Id, buffer, offset, count, cancellationToken) : Task.CompletedTask; + return count > 0 ? _host.WriteAsync(Id, buffer, offset, count, cancellationToken) : CompletedTask; } public override int Read(byte[] buffer, int offset, int count) diff --git a/src/Microsoft.AspNetCore.NodeServices/project.json b/src/Microsoft.AspNetCore.NodeServices/project.json index 5f8cbe76..b5b8897c 100644 --- a/src/Microsoft.AspNetCore.NodeServices/project.json +++ b/src/Microsoft.AspNetCore.NodeServices/project.json @@ -15,7 +15,10 @@ "frameworks": { "net451": { "frameworkAssemblies": { - "System.Net.Http": "4.0.0.0" + "System.Net.Http": "4.0.0-*" + }, + "dependencies": { + "Microsoft.Tpl.Dataflow": "4.5.24" } }, "netstandard1.3": { @@ -27,8 +30,11 @@ "dependencies": { "System.Console": "4.0.0-*", "System.Diagnostics.Process": "4.1.0-*", + "System.IO.Pipes": "4.0.0-*", "System.Net.Http": "4.0.1-*", - "System.Text.RegularExpressions": "4.0.12-*" + "System.Net.Sockets": "4.1.0-*", + "System.Text.RegularExpressions": "4.0.12-*", + "System.Threading.Tasks.Dataflow": "4.5.25-*" } } }, diff --git a/src/Microsoft.AspNetCore.ReactServices/project.json b/src/Microsoft.AspNetCore.ReactServices/project.json index aed28551..79b2eb72 100644 --- a/src/Microsoft.AspNetCore.ReactServices/project.json +++ b/src/Microsoft.AspNetCore.ReactServices/project.json @@ -15,7 +15,7 @@ "frameworks": { "net451": { }, - "netstandard1.5": { + "netstandard1.0": { "imports": [ "dotnet5.6", "dnxcore50", diff --git a/src/Microsoft.AspNetCore.SpaServices/project.json b/src/Microsoft.AspNetCore.SpaServices/project.json index 8b4f2531..64011be6 100644 --- a/src/Microsoft.AspNetCore.SpaServices/project.json +++ b/src/Microsoft.AspNetCore.SpaServices/project.json @@ -14,7 +14,7 @@ "frameworks": { "net451": { }, - "netstandard1.5": { + "netstandard1.0": { "imports": [ "dotnet5.6", "dnxcore50", @@ -26,5 +26,5 @@ "embed": [ "Content/**/*" ] - } + } } From c8859abeb73d98ebc1a51d77b5c9a5809f390045 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 7 Jun 2016 11:36:38 +0100 Subject: [PATCH 011/876] Fix #110 - enable asynchronous mode for Stream transport on Windows --- .../PhysicalConnections/NamedPipeConnection.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs index 0d0d79bf..7a980cf6 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/PhysicalConnections/NamedPipeConnection.cs @@ -12,12 +12,18 @@ internal class NamedPipeConnection : StreamConnection #pragma warning disable 1998 // Because in the NET451 code path, there's nothing to await public override async Task Open(string address) { - _namedPipeClientStream = new NamedPipeClientStream(".", address, PipeDirection.InOut); + _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 From facc2c6d08460f97c7ea2c2c9a22cfefb6bd6fc8 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 7 Jun 2016 14:20:43 +0100 Subject: [PATCH 012/876] Rename ES2015Transpilation sample to NodeServicesExamples (because will be adding some more examples here) --- JavaScriptServices.sln | 2 +- .../.gitignore | 0 .../Controllers/HomeController.cs | 2 +- .../NodeServicesExamples.xproj} | 2 +- .../Startup.cs | 4 ++-- .../Views/Home/Index.cshtml | 0 .../Views/Shared/Error.cshtml | 0 .../Views/Shared/_Layout.cshtml | 2 +- .../Views/_ViewImports.cshtml | 2 +- .../Views/_ViewStart.cshtml | 0 .../appsettings.json | 0 .../jsconfig.json | 0 .../package.json | 2 +- .../project.json | 2 +- .../transpilation.js | 0 .../wwwroot/favicon.ico | Bin .../wwwroot/js/main.js | 0 .../wwwroot/web.config | 0 18 files changed, 9 insertions(+), 9 deletions(-) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/.gitignore (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Controllers/HomeController.cs (88%) rename samples/misc/{ES2015Transpilation/ES2015Transpilation.xproj => NodeServicesExamples/NodeServicesExamples.xproj} (95%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Startup.cs (98%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Views/Home/Index.cshtml (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Views/Shared/Error.cshtml (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Views/Shared/_Layout.cshtml (80%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Views/_ViewImports.cshtml (66%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/Views/_ViewStart.cshtml (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/appsettings.json (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/jsconfig.json (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/package.json (77%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/project.json (96%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/transpilation.js (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/wwwroot/favicon.ico (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/wwwroot/js/main.js (100%) rename samples/misc/{ES2015Transpilation => NodeServicesExamples}/wwwroot/web.config (100%) diff --git a/JavaScriptServices.sln b/JavaScriptServices.sln index e8d88be6..9b150601 100644 --- a/JavaScriptServices.sln +++ b/JavaScriptServices.sln @@ -7,7 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{E6E8 EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.NodeServices", "src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.xproj", "{B0FA4175-8B29-4904-9780-28B3C24B0567}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ES2015Transpilation", "samples\misc\ES2015Transpilation\ES2015Transpilation.xproj", "{6D4BCDD6-7951-449B-BE55-CB7F014B7430}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "NodeServicesExamples", "samples\misc\NodeServicesExamples\NodeServicesExamples.xproj", "{6D4BCDD6-7951-449B-BE55-CB7F014B7430}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78DAC76C-1092-45AB-BF0D-594B8C7B6569}" ProjectSection(SolutionItems) = preProject diff --git a/samples/misc/ES2015Transpilation/.gitignore b/samples/misc/NodeServicesExamples/.gitignore similarity index 100% rename from samples/misc/ES2015Transpilation/.gitignore rename to samples/misc/NodeServicesExamples/.gitignore diff --git a/samples/misc/ES2015Transpilation/Controllers/HomeController.cs b/samples/misc/NodeServicesExamples/Controllers/HomeController.cs similarity index 88% rename from samples/misc/ES2015Transpilation/Controllers/HomeController.cs rename to samples/misc/NodeServicesExamples/Controllers/HomeController.cs index 2d6f80ed..8a52e7de 100755 --- a/samples/misc/ES2015Transpilation/Controllers/HomeController.cs +++ b/samples/misc/NodeServicesExamples/Controllers/HomeController.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -namespace ES2015Example.Controllers +namespace NodeServicesExamples.Controllers { public class HomeController : Controller { diff --git a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj b/samples/misc/NodeServicesExamples/NodeServicesExamples.xproj similarity index 95% rename from samples/misc/ES2015Transpilation/ES2015Transpilation.xproj rename to samples/misc/NodeServicesExamples/NodeServicesExamples.xproj index ba3c0634..2cd1742b 100644 --- a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj +++ b/samples/misc/NodeServicesExamples/NodeServicesExamples.xproj @@ -8,7 +8,7 @@ 6d4bcdd6-7951-449b-be55-cb7f014b7430 - ES2015Transpilation + NodeServicesExamples ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) .\bin\ diff --git a/samples/misc/ES2015Transpilation/Startup.cs b/samples/misc/NodeServicesExamples/Startup.cs similarity index 98% rename from samples/misc/ES2015Transpilation/Startup.cs rename to samples/misc/NodeServicesExamples/Startup.cs index 2d24aef6..2f157a64 100755 --- a/samples/misc/ES2015Transpilation/Startup.cs +++ b/samples/misc/NodeServicesExamples/Startup.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using System.IO; -namespace ES2015Example +namespace NodeServicesExamples { public class Startup { @@ -14,7 +14,7 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddMvc(); - + // Enable Node Services services.AddNodeServices(); } diff --git a/samples/misc/ES2015Transpilation/Views/Home/Index.cshtml b/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml similarity index 100% rename from samples/misc/ES2015Transpilation/Views/Home/Index.cshtml rename to samples/misc/NodeServicesExamples/Views/Home/Index.cshtml diff --git a/samples/misc/ES2015Transpilation/Views/Shared/Error.cshtml b/samples/misc/NodeServicesExamples/Views/Shared/Error.cshtml similarity index 100% rename from samples/misc/ES2015Transpilation/Views/Shared/Error.cshtml rename to samples/misc/NodeServicesExamples/Views/Shared/Error.cshtml diff --git a/samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml b/samples/misc/NodeServicesExamples/Views/Shared/_Layout.cshtml similarity index 80% rename from samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml rename to samples/misc/NodeServicesExamples/Views/Shared/_Layout.cshtml index e0d98023..23ebe30d 100755 --- a/samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml +++ b/samples/misc/NodeServicesExamples/Views/Shared/_Layout.cshtml @@ -2,7 +2,7 @@ - ES2015 Example + NodeServices Examples @RenderBody() diff --git a/samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml b/samples/misc/NodeServicesExamples/Views/_ViewImports.cshtml similarity index 66% rename from samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml rename to samples/misc/NodeServicesExamples/Views/_ViewImports.cshtml index 08b930c8..340f37c6 100755 --- a/samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml +++ b/samples/misc/NodeServicesExamples/Views/_ViewImports.cshtml @@ -1,2 +1,2 @@ -@using ES2015Example +@using NodeServicesExamples @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" diff --git a/samples/misc/ES2015Transpilation/Views/_ViewStart.cshtml b/samples/misc/NodeServicesExamples/Views/_ViewStart.cshtml similarity index 100% rename from samples/misc/ES2015Transpilation/Views/_ViewStart.cshtml rename to samples/misc/NodeServicesExamples/Views/_ViewStart.cshtml diff --git a/samples/misc/ES2015Transpilation/appsettings.json b/samples/misc/NodeServicesExamples/appsettings.json similarity index 100% rename from samples/misc/ES2015Transpilation/appsettings.json rename to samples/misc/NodeServicesExamples/appsettings.json diff --git a/samples/misc/ES2015Transpilation/jsconfig.json b/samples/misc/NodeServicesExamples/jsconfig.json similarity index 100% rename from samples/misc/ES2015Transpilation/jsconfig.json rename to samples/misc/NodeServicesExamples/jsconfig.json diff --git a/samples/misc/ES2015Transpilation/package.json b/samples/misc/NodeServicesExamples/package.json similarity index 77% rename from samples/misc/ES2015Transpilation/package.json rename to samples/misc/NodeServicesExamples/package.json index 1e0df597..7db4c892 100644 --- a/samples/misc/ES2015Transpilation/package.json +++ b/samples/misc/NodeServicesExamples/package.json @@ -1,5 +1,5 @@ { - "name": "ES2015Example", + "name": "nodeservicesexamples", "version": "0.0.0", "dependencies": { "babel-core": "^6.7.4", diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/NodeServicesExamples/project.json similarity index 96% rename from samples/misc/ES2015Transpilation/project.json rename to samples/misc/NodeServicesExamples/project.json index 9188c665..de6a8d5f 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/NodeServicesExamples/project.json @@ -1,7 +1,7 @@ { "version": "1.0.0-*", "tooling": { - "defaultNamespace": "ES2015Example" + "defaultNamespace": "NodeServicesExamples" }, "buildOptions": { "emitEntryPoint": true, diff --git a/samples/misc/ES2015Transpilation/transpilation.js b/samples/misc/NodeServicesExamples/transpilation.js similarity index 100% rename from samples/misc/ES2015Transpilation/transpilation.js rename to samples/misc/NodeServicesExamples/transpilation.js diff --git a/samples/misc/ES2015Transpilation/wwwroot/favicon.ico b/samples/misc/NodeServicesExamples/wwwroot/favicon.ico similarity index 100% rename from samples/misc/ES2015Transpilation/wwwroot/favicon.ico rename to samples/misc/NodeServicesExamples/wwwroot/favicon.ico diff --git a/samples/misc/ES2015Transpilation/wwwroot/js/main.js b/samples/misc/NodeServicesExamples/wwwroot/js/main.js similarity index 100% rename from samples/misc/ES2015Transpilation/wwwroot/js/main.js rename to samples/misc/NodeServicesExamples/wwwroot/js/main.js diff --git a/samples/misc/ES2015Transpilation/wwwroot/web.config b/samples/misc/NodeServicesExamples/wwwroot/web.config similarity index 100% rename from samples/misc/ES2015Transpilation/wwwroot/web.config rename to samples/misc/NodeServicesExamples/wwwroot/web.config From 8dbd1438572b8b3682e879900fd0a9c44ac94227 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 7 Jun 2016 14:31:23 +0100 Subject: [PATCH 013/876] Move ES2015 example into a subpage of the NodeServicesExamples sample --- .../Controllers/HomeController.cs | 5 +++++ .../{ => Node}/transpilation.js | 0 samples/misc/NodeServicesExamples/Startup.cs | 2 +- .../Views/Home/ES2015Transpilation.cshtml | 16 ++++++++++++++++ .../NodeServicesExamples/Views/Home/Index.cshtml | 15 +++++++++++---- 5 files changed, 33 insertions(+), 5 deletions(-) rename samples/misc/NodeServicesExamples/{ => Node}/transpilation.js (100%) create mode 100755 samples/misc/NodeServicesExamples/Views/Home/ES2015Transpilation.cshtml mode change 100755 => 100644 samples/misc/NodeServicesExamples/Views/Home/Index.cshtml diff --git a/samples/misc/NodeServicesExamples/Controllers/HomeController.cs b/samples/misc/NodeServicesExamples/Controllers/HomeController.cs index 8a52e7de..be21da69 100755 --- a/samples/misc/NodeServicesExamples/Controllers/HomeController.cs +++ b/samples/misc/NodeServicesExamples/Controllers/HomeController.cs @@ -10,6 +10,11 @@ public IActionResult Index(int pageIndex) return View(); } + public IActionResult ES2015Transpilation() + { + return View(); + } + public IActionResult Error() { return View("~/Views/Shared/Error.cshtml"); diff --git a/samples/misc/NodeServicesExamples/transpilation.js b/samples/misc/NodeServicesExamples/Node/transpilation.js similarity index 100% rename from samples/misc/NodeServicesExamples/transpilation.js rename to samples/misc/NodeServicesExamples/Node/transpilation.js diff --git a/samples/misc/NodeServicesExamples/Startup.cs b/samples/misc/NodeServicesExamples/Startup.cs index 2f157a64..cec3093a 100755 --- a/samples/misc/NodeServicesExamples/Startup.cs +++ b/samples/misc/NodeServicesExamples/Startup.cs @@ -30,7 +30,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IHo if (requestPath.StartsWith("/js/") && requestPath.EndsWith(".js")) { var fileInfo = env.WebRootFileProvider.GetFileInfo(requestPath); if (fileInfo.Exists) { - var transpiled = await nodeServices.Invoke("transpilation.js", fileInfo.PhysicalPath, requestPath); + var transpiled = await nodeServices.Invoke("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath); await context.Response.WriteAsync(transpiled); return; } diff --git a/samples/misc/NodeServicesExamples/Views/Home/ES2015Transpilation.cshtml b/samples/misc/NodeServicesExamples/Views/Home/ES2015Transpilation.cshtml new file mode 100755 index 00000000..bfb35ec5 --- /dev/null +++ b/samples/misc/NodeServicesExamples/Views/Home/ES2015Transpilation.cshtml @@ -0,0 +1,16 @@ +

ES2015 Transpilation

+ +

+ This sample demonstrates a way of intercepting requests for .js files and dynamically transpiling them + from ES2015 code to browser-compatible ES5 code using the Babel library. +

+ +

+ To see that it's working, open your browser's 'Debug' console and look for the log message. This is + produced by the file /js/main.js, which is transpiled from ES2015 dynamically + when requested. +

+ +@section scripts { + +} diff --git a/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml b/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml old mode 100755 new mode 100644 index 0df9cfed..21dcd3ba --- a/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml +++ b/samples/misc/NodeServicesExamples/Views/Home/Index.cshtml @@ -1,5 +1,12 @@ -Hello +

NodeServices examples

+ +

+ These examples demonstrate the direct use of the NodeServices package, independently of the usual SPA scenarios. + In general, NodeServices offers an efficient way to use Node-provided functionality (e.g., NPM modules) from inside + a .NET application. +

+ +