From 00b598176cfbbbe563ab2a79c9c31e6adaa17fc8 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 6 Jul 2016 10:44:21 +0100 Subject: [PATCH 01/28] Minor style tweaks --- .../npm/aspnet-webpack/src/WebpackDevMiddleware.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts index cef40e05..e33433fc 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts @@ -1,6 +1,6 @@ import * as connect from 'connect'; import * as webpack from 'webpack'; -import * as url from 'url' +import * as url from 'url'; import { requireNewCopy } from './RequireNewCopy'; export interface CreateDevServerCallback { @@ -98,6 +98,6 @@ function removeTrailingSlash(str: string) { return str; } -function getPath(publicPath: string){ - return url.parse(publicPath).path; -} \ No newline at end of file +function getPath(publicPath: string) { + return url.parse(publicPath).path; +} From 7ce5f8d4ad58b644a9bf544aedd3508b6add8a2f Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 6 Jul 2016 11:18:22 +0100 Subject: [PATCH 02/28] Remove trailing whitespace in KO template --- .../KnockoutSpa/ClientApp/components/app-root/app-root.ts | 8 ++++---- .../components/counter-example/counter-example.ts | 2 +- .../ClientApp/components/fetch-data/fetch-data.ts | 2 +- .../KnockoutSpa/ClientApp/components/nav-menu/nav-menu.ts | 2 +- templates/KnockoutSpa/ClientApp/router.ts | 8 ++++---- .../KnockoutSpa/ClientApp/webpack-component-loader.ts | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/templates/KnockoutSpa/ClientApp/components/app-root/app-root.ts b/templates/KnockoutSpa/ClientApp/components/app-root/app-root.ts index 2d05cb11..7583faff 100644 --- a/templates/KnockoutSpa/ClientApp/components/app-root/app-root.ts +++ b/templates/KnockoutSpa/ClientApp/components/app-root/app-root.ts @@ -12,12 +12,12 @@ const routes: Route[] = [ class AppRootViewModel { public route: KnockoutObservable; private _router: Router; - + constructor(params: { history: HistoryModule.History }) { // Activate the client-side router this._router = new Router(params.history, routes) this.route = this._router.currentRoute; - + // Load and register all the KO components needed to handle the routes // The optional 'bundle?lazy!' prefix is a Webpack feature that causes the referenced modules // to be split into separate files that are then loaded on demand. @@ -27,12 +27,12 @@ class AppRootViewModel { ko.components.register('counter-example', require('bundle?lazy!../counter-example/counter-example')); ko.components.register('fetch-data', require('bundle?lazy!../fetch-data/fetch-data')); } - + // To support hot module replacement, this method unregisters the router and KO components. // In production scenarios where hot module replacement is disabled, this would not be invoked. public dispose() { this._router.dispose(); - + // TODO: Need a better API for this Object.getOwnPropertyNames((ko).components._allRegisteredComponents).forEach(componentName => { ko.components.unregister(componentName); diff --git a/templates/KnockoutSpa/ClientApp/components/counter-example/counter-example.ts b/templates/KnockoutSpa/ClientApp/components/counter-example/counter-example.ts index b056ed27..39b7f046 100644 --- a/templates/KnockoutSpa/ClientApp/components/counter-example/counter-example.ts +++ b/templates/KnockoutSpa/ClientApp/components/counter-example/counter-example.ts @@ -2,7 +2,7 @@ import * as ko from 'knockout'; class CounterExampleViewModel { public currentCount = ko.observable(0); - + public incrementCounter() { let prevCount = this.currentCount(); this.currentCount(prevCount + 1); diff --git a/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts b/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts index cfa71e4d..deab0682 100644 --- a/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts +++ b/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts @@ -9,7 +9,7 @@ interface WeatherForecast { class FetchDataViewModel { public forecasts = ko.observableArray(); - + constructor() { fetch('/api/SampleData/WeatherForecasts') .then(response => response.json()) diff --git a/templates/KnockoutSpa/ClientApp/components/nav-menu/nav-menu.ts b/templates/KnockoutSpa/ClientApp/components/nav-menu/nav-menu.ts index ffd476ae..186c60e1 100644 --- a/templates/KnockoutSpa/ClientApp/components/nav-menu/nav-menu.ts +++ b/templates/KnockoutSpa/ClientApp/components/nav-menu/nav-menu.ts @@ -7,7 +7,7 @@ interface NavMenuParams { class NavMenuViewModel { public route: KnockoutObservable; - + constructor(params: NavMenuParams) { // This viewmodel doesn't do anything except pass through the 'route' parameter to the view. // You could remove this viewmodel entirely, and define 'nav-menu' as a template-only component. diff --git a/templates/KnockoutSpa/ClientApp/router.ts b/templates/KnockoutSpa/ClientApp/router.ts index 6162eddb..526394e6 100644 --- a/templates/KnockoutSpa/ClientApp/router.ts +++ b/templates/KnockoutSpa/ClientApp/router.ts @@ -13,7 +13,7 @@ export class Router { public currentRoute = ko.observable({}); private disposeHistory: () => void; private clickEventListener: EventListener; - + constructor(history: HistoryModule.History, routes: Route[]) { // Reset and configure Crossroads so it matches routes and updates this.currentRoute crossroads.removeAllRoutes(); @@ -25,7 +25,7 @@ export class Router { }); }); - // Make history.js watch for navigation and notify Crossroads + // Make history.js watch for navigation and notify Crossroads this.disposeHistory = history.listen(location => crossroads.parse(location.pathname)); this.clickEventListener = evt => { let target: any = evt.target; @@ -37,10 +37,10 @@ export class Router { } } }; - + document.addEventListener('click', this.clickEventListener); } - + public dispose() { this.disposeHistory(); document.removeEventListener('click', this.clickEventListener); diff --git a/templates/KnockoutSpa/ClientApp/webpack-component-loader.ts b/templates/KnockoutSpa/ClientApp/webpack-component-loader.ts index d46ec827..10f13e8f 100644 --- a/templates/KnockoutSpa/ClientApp/webpack-component-loader.ts +++ b/templates/KnockoutSpa/ClientApp/webpack-component-loader.ts @@ -9,11 +9,11 @@ ko.components.loaders.unshift({ if (typeof componentConfig === 'function') { // It's a lazy-loaded Webpack bundle (componentConfig as any)(loadedModule => { - // Handle TypeScript-style default exports + // Handle TypeScript-style default exports if (loadedModule.__esModule && loadedModule.default) { loadedModule = loadedModule.default; } - + // Pass the loaded module to KO's default loader ko.components.defaultLoader.loadComponent(name, loadedModule, callback); }); From 4ee09cbe827ae1378d3173e08ab059736c8e2c6d Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 6 Jul 2016 15:47:06 +0100 Subject: [PATCH 03/28] Make Http hosting model able to report exceptions that happened while locating the function to invoke --- .../Content/Node/entrypoint-http.js | 12 ++++++------ .../TypeScript/HttpNodeInstanceEntryPoint.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index 795317e0..edf5949d 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -67,12 +67,6 @@ } var server = http.createServer(function (req, res) { readRequestBodyAsJson(req, function (bodyJson) { - var resolvedPath = path.resolve(process.cwd(), bodyJson.moduleName); - var invokedModule = dynamicRequire(resolvedPath); - var func = bodyJson.exportedFunctionName ? invokedModule[bodyJson.exportedFunctionName] : invokedModule; - if (!func) { - throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"'); - } var hasSentResult = false; var callback = function (errorValue, successValue) { if (!hasSentResult) { @@ -110,6 +104,12 @@ } }); try { + var resolvedPath = path.resolve(process.cwd(), bodyJson.moduleName); + var invokedModule = dynamicRequire(resolvedPath); + var func = bodyJson.exportedFunctionName ? invokedModule[bodyJson.exportedFunctionName] : invokedModule; + if (!func) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"'); + } func.apply(null, [callback].concat(bodyJson.args)); } catch (synchronousException) { diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index 7ce84e3d..b28f38b9 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -16,13 +16,6 @@ if (parsedArgs.watch) { const server = http.createServer((req, res) => { readRequestBodyAsJson(req, bodyJson => { - const resolvedPath = path.resolve(process.cwd(), bodyJson.moduleName); - const invokedModule = dynamicRequire(resolvedPath); - const func = bodyJson.exportedFunctionName ? invokedModule[bodyJson.exportedFunctionName] : invokedModule; - if (!func) { - throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"'); - } - let hasSentResult = false; const callback = (errorValue, successValue) => { if (!hasSentResult) { @@ -31,9 +24,9 @@ const server = http.createServer((req, res) => { res.statusCode = 500; if (errorValue.stack) { - res.end(errorValue.stack); + res.end(errorValue.stack); } else { - res.end(errorValue.toString()); + res.end(errorValue.toString()); } } else if (typeof successValue !== 'string') { // Arbitrary object/number/etc - JSON-serialize it @@ -61,6 +54,13 @@ const server = http.createServer((req, res) => { }); try { + const resolvedPath = path.resolve(process.cwd(), bodyJson.moduleName); + const invokedModule = dynamicRequire(resolvedPath); + const func = bodyJson.exportedFunctionName ? invokedModule[bodyJson.exportedFunctionName] : invokedModule; + if (!func) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"'); + } + func.apply(null, [callback].concat(bodyJson.args)); } catch (synchronousException) { callback(synchronousException, null); From 4fb3b18868fb6b05f6c40c9206fdfbef4a457e3c Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 6 Jul 2016 18:23:25 +0100 Subject: [PATCH 04/28] Create new top-level DefaultNodeInstance concept that will soon hold the "connection draining" logic --- samples/misc/LatencyTest/Program.cs | 6 +- .../Controllers/ResizeImage.cs | 2 +- samples/misc/NodeServicesExamples/Startup.cs | 2 +- .../Configuration.cs | 49 ---------- .../Configuration/Configuration.cs | 58 ++++++++++++ .../{ => Configuration}/NodeHostingModel.cs | 0 .../Configuration/NodeServicesOptions.cs | 23 +++++ .../HostingModels/HttpNodeInstance.cs | 4 +- .../HostingModels/INodeInstance.cs | 10 ++ .../HostingModels/NodeInvocationException.cs | 2 +- .../HostingModels/NodeInvocationInfo.cs | 2 +- .../HostingModels/OutOfProcessNodeInstance.cs | 17 ++-- .../HostingModels/SocketNodeInstance.cs | 4 +- .../{INodeInstance.cs => INodeServices.cs} | 8 +- .../NodeServicesImpl.cs | 92 +++++++++++++++++++ .../NodeServicesOptions.cs | 14 --- .../Prerendering/PrerenderTagHelper.cs | 1 - .../Prerendering/Prerenderer.cs | 2 +- .../Webpack/WebpackDevMiddleware.cs | 3 +- 19 files changed, 210 insertions(+), 89 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs rename src/Microsoft.AspNetCore.NodeServices/{ => Configuration}/NodeHostingModel.cs (100%) create mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs rename src/Microsoft.AspNetCore.NodeServices/{INodeInstance.cs => INodeServices.cs} (53%) create mode 100644 src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs delete mode 100644 src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs diff --git a/samples/misc/LatencyTest/Program.cs b/samples/misc/LatencyTest/Program.cs index 855402b4..2087d29e 100755 --- a/samples/misc/LatencyTest/Program.cs +++ b/samples/misc/LatencyTest/Program.cs @@ -12,21 +12,21 @@ namespace ConsoleApplication public class Program { public static void Main(string[] args) { - using (var nodeServices = CreateNodeServices(Configuration.DefaultNodeHostingModel)) { + using (var nodeServices = CreateNodeServices(NodeServicesOptions.DefaultNodeHostingModel)) { MeasureLatency(nodeServices).Wait(); } } 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#"); + var response = await nodeServices.InvokeAsync("latencyTest", "C#"); Console.WriteLine(response); // Now perform a series of requests, capturing the time taken const int requestCount = 100; var watch = Stopwatch.StartNew(); for (var i = 0; i < requestCount; i++) { - await nodeServices.Invoke("latencyTest", "C#"); + await nodeServices.InvokeAsync("latencyTest", "C#"); } // Display results diff --git a/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs b/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs index e0f498e2..43f45df3 100644 --- a/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs +++ b/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs @@ -46,7 +46,7 @@ public async Task Index(string imagePath, int maxWidth, int maxHe } // Invoke Node and pipe the result to the response - var imageStream = await _nodeServices.Invoke( + var imageStream = await _nodeServices.InvokeAsync( "./Node/resizeImage", fileInfo.PhysicalPath, mimeType, diff --git a/samples/misc/NodeServicesExamples/Startup.cs b/samples/misc/NodeServicesExamples/Startup.cs index cec3093a..1c38c60a 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("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath); + var transpiled = await nodeServices.InvokeAsync("./Node/transpilation.js", fileInfo.PhysicalPath, requestPath); await context.Response.WriteAsync(transpiled); return; } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs deleted file mode 100644 index d24a7e33..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Hosting; - -namespace Microsoft.AspNetCore.NodeServices -{ - public static class Configuration - { - 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 = DefaultNodeHostingModel, - WatchFileExtensions = DefaultWatchFileExtensions - }; - - public static void AddNodeServices(this IServiceCollection serviceCollection) - => AddNodeServices(serviceCollection, DefaultOptions); - - public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) - { - 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) - { - var watchFileExtensions = options.WatchFileExtensions ?? DefaultWatchFileExtensions; - switch (options.HostingModel) - { - case NodeHostingModel.Http: - return new HttpNodeInstance(options.ProjectPath, /* port */ 0, watchFileExtensions); - case NodeHostingModel.Socket: - return new SocketNodeInstance(options.ProjectPath, watchFileExtensions); - default: - throw new ArgumentException("Unknown hosting model: " + options.HostingModel); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs new file mode 100644 index 00000000..b4872518 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + public static class Configuration + { + public static void AddNodeServices(this IServiceCollection serviceCollection) + => AddNodeServices(serviceCollection, new NodeServicesOptions()); + + public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) + { + serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => + { + // Since this instance is being created through DI, we can access the IHostingEnvironment + // to populate options.ProjectPath if it wasn't explicitly specified. + var hostEnv = serviceProvider.GetRequiredService(); + if (string.IsNullOrEmpty(options.ProjectPath)) + { + options.ProjectPath = hostEnv.ContentRootPath; + } + + return new NodeServicesImpl(options, () => CreateNodeInstance(options)); + }); + } + + public static INodeServices CreateNodeServices(NodeServicesOptions options) + { + return new NodeServicesImpl(options, () => CreateNodeInstance(options)); + } + + private static INodeInstance CreateNodeInstance(NodeServicesOptions options) + { + if (options.NodeInstanceFactory != null) + { + // If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for + // custom INodeInstance implementations. + return options.NodeInstanceFactory(); + } + else + { + // Otherwise we'll construct the type of INodeInstance specified by the HostingModel property, + // which itself has a useful default value. + switch (options.HostingModel) + { + case NodeHostingModel.Http: + return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions); + case NodeHostingModel.Socket: + return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions); + default: + throw new ArgumentException("Unknown hosting model: " + options.HostingModel); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeHostingModel.cs similarity index 100% rename from src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs rename to src/Microsoft.AspNetCore.NodeServices/Configuration/NodeHostingModel.cs diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs new file mode 100644 index 00000000..fd8b3642 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + public class NodeServicesOptions + { + public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; + + private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; + + public NodeServicesOptions() + { + HostingModel = DefaultNodeHostingModel; + WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); + } + + public NodeHostingModel HostingModel { get; set; } + public Func NodeInstanceFactory { 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/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index a2aeaf19..13d7527b 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { internal class HttpNodeInstance : OutOfProcessNodeInstance { @@ -45,7 +45,7 @@ private static string MakeCommandLineOptions(int port, string[] watchFileExtensi return result; } - public override async Task Invoke(NodeInvocationInfo invocationInfo) + protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { await EnsureReady(); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs new file mode 100644 index 00000000..cac69f2a --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.NodeServices.HostingModels +{ + public interface INodeInstance : IDisposable + { + Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs index 64398c4c..ee056ab6 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs @@ -1,6 +1,6 @@ using System; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { public class NodeInvocationException : Exception { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs index 2e196f6e..92d96f8c 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs @@ -1,4 +1,4 @@ -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { public class NodeInvocationInfo { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 296bb488..8018d7f5 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,14 +3,14 @@ using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { /// /// 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 + /// + public abstract class OutOfProcessNodeInstance : INodeInstance { private readonly object _childProcessLauncherLock; private string _commandLineArguments; @@ -34,15 +34,12 @@ public string CommandLineArguments set { _commandLineArguments = value; } } - public Task Invoke(string moduleName, params object[] args) - => InvokeExport(moduleName, null, args); - - public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) + public Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args) { - return Invoke(new NodeInvocationInfo + return InvokeExportAsync(new NodeInvocationInfo { ModuleName = moduleName, - ExportedFunctionName = exportedFunctionName, + ExportedFunctionName = exportNameOrNull, Args = args }); } @@ -53,7 +50,7 @@ public void Dispose() GC.SuppressFinalize(this); } - public abstract Task Invoke(NodeInvocationInfo invocationInfo); + protected abstract Task InvokeExportAsync(NodeInvocationInfo invocationInfo); protected void ExitNodeProcess() { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index abeeac5e..f35fca26 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices +namespace Microsoft.AspNetCore.NodeServices.HostingModels { internal class SocketNodeInstance : OutOfProcessNodeInstance { @@ -32,7 +32,7 @@ public SocketNodeInstance(string projectPath, string[] watchFileExtensions = nul _watchFileExtensions = watchFileExtensions; } - public override async Task Invoke(NodeInvocationInfo invocationInfo) + protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { await EnsureReady(); var virtualConnectionClient = await GetOrCreateVirtualConnectionClientAsync(); diff --git a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs similarity index 53% rename from src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs rename to src/Microsoft.AspNetCore.NodeServices/INodeServices.cs index 0c17ea1a..3aa09e3b 100644 --- a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs @@ -5,8 +5,14 @@ namespace Microsoft.AspNetCore.NodeServices { public interface INodeServices : IDisposable { + Task InvokeAsync(string moduleName, params object[] args); + + Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args); + + [Obsolete("Use InvokeAsync instead")] Task Invoke(string moduleName, params object[] args); + [Obsolete("Use InvokeExportAsync instead")] Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs new file mode 100644 index 00000000..29649fda --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + /// + /// Default implementation of INodeServices. This is the primary API surface through which developers + /// make use of this package. It provides simple "InvokeAsync" methods that dispatch calls to the + /// correct Node instance, creating and destroying those instances as needed. + /// + /// If a Node instance dies (or none was yet created), this class takes care of creating a new one. + /// If a Node instance signals that it needs to be restarted (e.g., because a file changed), then this + /// class will create a new instance and dispatch future calls to it, while keeping the old instance + /// alive for a defined period so that any in-flight RPC calls can complete. This latter feature is + /// analogous to the "connection draining" feature implemented by HTTP load balancers. + /// + /// TODO: Implement everything in the preceding paragraph. + /// + /// + internal class NodeServicesImpl : INodeServices + { + private NodeServicesOptions _options; + private Func _nodeInstanceFactory; + private INodeInstance _currentNodeInstance; + private object _currentNodeInstanceAccessLock = new object(); + + internal NodeServicesImpl(NodeServicesOptions options, Func nodeInstanceFactory) + { + _options = options; + _nodeInstanceFactory = nodeInstanceFactory; + } + + public Task InvokeAsync(string moduleName, params object[] args) + { + return InvokeExportAsync(moduleName, null, args); + } + + public Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args) + { + var nodeInstance = GetOrCreateCurrentNodeInstance(); + return nodeInstance.InvokeExportAsync(moduleName, exportedFunctionName, args); + } + + public void Dispose() + { + lock (_currentNodeInstanceAccessLock) + { + if (_currentNodeInstance != null) + { + _currentNodeInstance.Dispose(); + _currentNodeInstance = null; + } + } + } + + private INodeInstance GetOrCreateCurrentNodeInstance() + { + var instance = _currentNodeInstance; + if (instance == null) + { + lock (_currentNodeInstanceAccessLock) + { + instance = _currentNodeInstance; + if (instance == null) + { + instance = _currentNodeInstance = CreateNewNodeInstance(); + } + } + } + + return instance; + } + + private INodeInstance CreateNewNodeInstance() + { + return _nodeInstanceFactory(); + } + + // Obsolete method - will be removed soon + public Task Invoke(string moduleName, params object[] args) + { + return InvokeAsync(moduleName, args); + } + + // Obsolete method - will be removed soon + public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) + { + return InvokeExportAsync(moduleName, exportedFunctionName, args); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs deleted file mode 100644 index dccd85de..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.AspNetCore.NodeServices -{ - public class NodeServicesOptions - { - public NodeServicesOptions() - { - HostingModel = Configuration.DefaultNodeHostingModel; - } - - 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.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index b09eaae7..e0dcd293 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -37,7 +37,6 @@ public PrerenderTagHelper(IServiceProvider serviceProvider) { _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = Configuration.DefaultNodeHostingModel, ProjectPath = _applicationBasePath }); } diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 5e103226..40286d14 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -25,7 +25,7 @@ public static Task RenderToString( string requestPathAndQuery, object customDataParameter) { - return nodeServices.InvokeExport( + return nodeServices.InvokeExportAsync( NodeScript.Value.FileName, "renderToString", applicationBasePath, diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 8c3dbd2a..959ee46e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -44,7 +44,6 @@ public static void UseWebpackDevMiddleware( var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = Configuration.DefaultNodeHostingModel, ProjectPath = hostEnv.ContentRootPath, WatchFileExtensions = new string[] { } // Don't watch anything }); @@ -61,7 +60,7 @@ public static void UseWebpackDevMiddleware( suppliedOptions = options }; var devServerInfo = - nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", + nodeServices.InvokeExportAsync(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener From a19e37f3c0118c0194ffa44965c7741994a55d96 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 11:35:25 +0100 Subject: [PATCH 05/28] Move logic for restarting Node child process into NodeServicesImpl. Tidy up lots. --- .../Configuration/Configuration.cs | 3 +- .../Content/Node/entrypoint-socket.js | 2 +- .../HostingModels/HttpNodeInstance.cs | 26 +-- .../HostingModels/NodeInvocationException.cs | 8 + .../HostingModels/OutOfProcessNodeInstance.cs | 170 ++++++++---------- .../HostingModels/SocketNodeInstance.cs | 154 ++++++++-------- .../NodeServicesImpl.cs | 46 ++++- .../SocketNodeInstanceEntryPoint.ts | 2 +- 8 files changed, 225 insertions(+), 186 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs index b4872518..4bda4d64 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -48,7 +48,8 @@ private static INodeInstance CreateNodeInstance(NodeServicesOptions options) case NodeHostingModel.Http: return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions); case NodeHostingModel.Socket: - return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions); + var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string + return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js index 2c1cd9d4..355bbbb3 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js @@ -176,7 +176,7 @@ // 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; + var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; server.listen(listenAddress); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 13d7527b..c1fdbafc 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -9,6 +9,18 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels { + /// + /// A specialisation of the OutOfProcessNodeInstance base class that uses HTTP to perform RPC invocations. + /// + /// The Node child process starts an HTTP listener on an arbitrary available port (except where a nonzero + /// port number is specified as a constructor parameter), and signals which port was selected using the same + /// input/output-based mechanism that the base class uses to determine when the child process is ready to + /// accept RPC invocations. + /// + /// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing + /// the actual watching in .NET code (not Node), for consistency across platforms. + /// + /// internal class HttpNodeInstance : OutOfProcessNodeInstance { private static readonly Regex PortMessageRegex = @@ -19,7 +31,7 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance ContractResolver = new CamelCasePropertyNamesContractResolver() }; - private HttpClient _client; + private readonly HttpClient _client; private bool _disposed; private int _portNumber; @@ -47,9 +59,6 @@ private static string MakeCommandLineOptions(int port, string[] watchFileExtensi protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { - await EnsureReady(); - - // 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); @@ -97,6 +106,9 @@ protected override async Task InvokeExportAsync(NodeInvocationInfo invocat protected override void OnOutputDataReceived(string outputData) { + // Watch for "port selected" messages, and when observed, store the port number + // so we can use it when making HTTP requests. The child process will always send + // one of these messages before it sends a "ready for connections" message. var match = _portNumber != 0 ? null : PortMessageRegex.Match(outputData); if (match != null && match.Success) { @@ -108,12 +120,6 @@ protected override void OnOutputDataReceived(string outputData) } } - protected override void OnBeforeLaunchProcess() - { - // Prepare to receive a new port number - _portNumber = 0; - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs index ee056ab6..733af444 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationException.cs @@ -4,9 +4,17 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels { public class NodeInvocationException : Exception { + public bool NodeInstanceUnavailable { get; private set; } + public NodeInvocationException(string message, string details) : base(message + Environment.NewLine + details) { } + + public NodeInvocationException(string message, string details, bool nodeInstanceUnavailable) + : this(message, details) + { + NodeInstanceUnavailable = nodeInstanceUnavailable; + } } } \ 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 8018d7f5..498c3163 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -6,37 +6,43 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels { /// - /// 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. + /// Class responsible for launching a Node child process on the local machine, determining when it is ready to + /// accept invocations, detecting if it dies on its own, and finally terminating it on disposal. + /// + /// This abstract base class uses the input/output streams of the child process to perform a simple handshake + /// to determine when the child process is ready to accept invocations. This is agnostic to the mechanism that + /// derived classes use to actually perform the invocations (e.g., they could use HTTP-RPC, or a binary TCP + /// protocol, or any other RPC-type mechanism). /// - /// + /// public abstract class OutOfProcessNodeInstance : INodeInstance { - private readonly object _childProcessLauncherLock; - private string _commandLineArguments; - private readonly StringAsTempFile _entryPointScript; - private Process _nodeProcess; - private TaskCompletionSource _nodeProcessIsReadySource; - private readonly string _projectPath; + private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]"; + private readonly TaskCompletionSource _connectionIsReadySource = new TaskCompletionSource(); private bool _disposed; + private readonly StringAsTempFile _entryPointScript; + private readonly Process _nodeProcess; public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null) { - _childProcessLauncherLock = new object(); _entryPointScript = new StringAsTempFile(entryPointScript); - _projectPath = projectPath; - _commandLineArguments = commandLineArguments ?? string.Empty; + _nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments); + ConnectToInputOutputStreams(); } - public string CommandLineArguments + public async Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args) { - get { return _commandLineArguments; } - set { _commandLineArguments = value; } - } + // Wait until the connection is established. This will throw if the connection fails to initialize. + await _connectionIsReadySource.Task; - public Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args) - { - return InvokeExportAsync(new NodeInvocationInfo + if (_nodeProcess.HasExited) + { + // This special kind of exception triggers a transparent retry - NodeServicesImpl will launch + // a new Node instance and pass the invocation to that one instead. + throw new NodeInvocationException("The Node process has exited", null, nodeInstanceUnavailable: true); + } + + return await InvokeExportAsync(new NodeInvocationInfo { ModuleName = moduleName, ExportedFunctionName = exportNameOrNull, @@ -52,71 +58,74 @@ public void Dispose() protected abstract Task InvokeExportAsync(NodeInvocationInfo invocationInfo); - protected void ExitNodeProcess() + protected virtual void OnOutputDataReceived(string outputData) + { + Console.WriteLine("[Node] " + outputData); + } + + protected virtual void OnErrorDataReceived(string errorData) + { + Console.WriteLine("[Node] " + errorData); + } + + protected virtual void Dispose(bool disposing) { - if (_nodeProcess != null && !_nodeProcess.HasExited) + if (!_disposed) { + if (disposing) + { + _entryPointScript.Dispose(); + } + + // Make sure the Node process is finished // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? - _nodeProcess.Kill(); + if (!_nodeProcess.HasExited) + { + _nodeProcess.Kill(); + } + + _disposed = true; } } - protected async Task EnsureReady() + private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments) { - lock (_childProcessLauncherLock) + var startInfo = new ProcessStartInfo("node") { - if (_nodeProcess == null || _nodeProcess.HasExited) - { - this.OnBeforeLaunchProcess(); + Arguments = "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty), + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = projectPath + }; - var startInfo = new ProcessStartInfo("node") - { - Arguments = "\"" + _entryPointScript.FileName + "\" " + _commandLineArguments, - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - WorkingDirectory = _projectPath - }; - - // Append projectPath to NODE_PATH so it can locate node_modules - var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; - if (existingNodePath != string.Empty) - { - existingNodePath += ":"; - } + // Append projectPath to NODE_PATH so it can locate node_modules + var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; + if (existingNodePath != string.Empty) + { + existingNodePath += ":"; + } - var nodePathValue = existingNodePath + Path.Combine(_projectPath, "node_modules"); + var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules"); #if NET451 - startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue; + startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue; #else - startInfo.Environment["NODE_PATH"] = nodePathValue; + startInfo.Environment["NODE_PATH"] = nodePathValue; #endif - _nodeProcess = Process.Start(startInfo); - ConnectToInputOutputStreams(); - } - } - - var task = _nodeProcessIsReadySource.Task; - var initializationSucceeded = await task; - - if (!initializationSucceeded) - { - throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception); - } + return Process.Start(startInfo); } private void ConnectToInputOutputStreams() { - var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) - _nodeProcessIsReadySource = new TaskCompletionSource(); + var initializationIsCompleted = false; _nodeProcess.OutputDataReceived += (sender, evt) => { - if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted) + if (evt.Data == ConnectionEstablishedMessage && !initializationIsCompleted) { - _nodeProcessIsReadySource.SetResult(true); + _connectionIsReadySource.SetResult(null); initializationIsCompleted = true; } else if (evt.Data != null) @@ -129,12 +138,16 @@ private void ConnectToInputOutputStreams() { if (evt.Data != null) { - OnErrorDataReceived(evt.Data); if (!initializationIsCompleted) { - _nodeProcessIsReadySource.SetResult(false); + _connectionIsReadySource.SetException( + new InvalidOperationException("The Node.js process failed to initialize: " + evt.Data)); initializationIsCompleted = true; } + else + { + OnErrorDataReceived(evt.Data); + } } }; @@ -142,35 +155,6 @@ private void ConnectToInputOutputStreams() _nodeProcess.BeginErrorReadLine(); } - protected virtual void OnBeforeLaunchProcess() - { - } - - protected virtual void OnOutputDataReceived(string outputData) - { - Console.WriteLine("[Node] " + outputData); - } - - protected virtual void OnErrorDataReceived(string errorData) - { - Console.WriteLine("[Node] " + errorData); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _entryPointScript.Dispose(); - } - - ExitNodeProcess(); - - _disposed = true; - } - } - ~OutOfProcessNodeInstance() { Dispose(false); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index f35fca26..7a411a82 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -10,6 +10,22 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels { + /// + /// A specialisation of the OutOfProcessNodeInstance base class that uses a lightweight binary streaming protocol + /// to perform RPC invocations. The physical transport is Named Pipes on Windows, or Domain Sockets on Linux/Mac. + /// For details on the binary streaming protocol, see + /// Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections.VirtualConnectionClient. + /// The advantage versus using HTTP for RPC is that this is faster (not surprisingly - there's much less overhead + /// because we don't need most of the functionality of HTTP. + /// + /// The address of the pipe/socket is selected randomly here on the .NET side and sent to the child process as a + /// command-line argument (the address space is wide enough that there's no real risk of a clash, unlike when + /// selecting TCP port numbers). + /// + /// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing + /// the actual watching in .NET code (not Node), for consistency across platforms. + /// + /// internal class SocketNodeInstance : OutOfProcessNodeInstance { private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings @@ -17,31 +33,48 @@ internal class SocketNodeInstance : OutOfProcessNodeInstance ContractResolver = new CamelCasePropertyNamesContractResolver() }; - private string _addressForNextConnection; - private readonly SemaphoreSlim _clientModificationSemaphore = new SemaphoreSlim(1); - private StreamConnection _currentPhysicalConnection; - private VirtualConnectionClient _currentVirtualConnectionClient; + private readonly SemaphoreSlim _connectionCreationSemaphore = new SemaphoreSlim(1); + private bool _connectionHasFailed; + private StreamConnection _physicalConnection; + private string _socketAddress; + private VirtualConnectionClient _virtualConnectionClient; private readonly string[] _watchFileExtensions; - public SocketNodeInstance(string projectPath, string[] watchFileExtensions = null): base( + public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress): base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), "/Content/Node/entrypoint-socket.js"), - projectPath) + projectPath, + MakeNewCommandLineOptions(socketAddress, watchFileExtensions)) { _watchFileExtensions = watchFileExtensions; + _socketAddress = socketAddress; } protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { - await EnsureReady(); - var virtualConnectionClient = await GetOrCreateVirtualConnectionClientAsync(); + if (_connectionHasFailed) + { + // This special exception type forces NodeServicesImpl to restart the Node instance + throw new NodeInvocationException( + "The SocketNodeInstance socket connection failed. See logs to identify the reason.", + null, + nodeInstanceUnavailable: true); + } + + if (_virtualConnectionClient == null) + { + await EnsureVirtualConnectionClientCreated(); + } + // For each invocation, we open a new virtual connection. This gives an API equivalent to opening a new + // physical connection to the child process, but without the overhead of doing so, because it's really + // just multiplexed into the existing physical connection stream. bool shouldDisposeVirtualConnection = true; Stream virtualConnection = null; try { - virtualConnection = _currentVirtualConnectionClient.OpenVirtualConnection(); + virtualConnection = _virtualConnectionClient.OpenVirtualConnection(); // Send request await WriteJsonLineAsync(virtualConnection, invocationInfo); @@ -75,46 +108,34 @@ protected override async Task InvokeExportAsync(NodeInvocationInfo invocat } } - private async Task GetOrCreateVirtualConnectionClientAsync() + private async Task EnsureVirtualConnectionClientCreated() { - var client = _currentVirtualConnectionClient; - if (client == null) + // Asynchronous equivalent to a 'lock(...) { ... }' + await _connectionCreationSemaphore.WaitAsync(); + try { - await _clientModificationSemaphore.WaitAsync(); - try + if (_virtualConnectionClient == null) { - 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 - }; - } + _physicalConnection = StreamConnection.Create(); - return _currentVirtualConnectionClient; - } - finally - { - _clientModificationSemaphore.Release(); + var connection = await _physicalConnection.Open(_socketAddress); + _virtualConnectionClient = new VirtualConnectionClient(connection); + _virtualConnectionClient.OnError += (ex) => + { + // This callback is fired only if there's a protocol-level failure (e.g., child process disconnected + // unexpectedly). It does *not* fire when RPC calls return errors. Since there's been a protocol-level + // failure, this Node instance is no longer usable and should be discarded. + _connectionHasFailed = true; + + // 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); + }; } } - else + finally { - return client; + _connectionCreationSemaphore.Release(); } } @@ -122,21 +143,22 @@ protected override void Dispose(bool disposing) { if (disposing) { - EnsurePipeRpcClientDisposed(); + if (_virtualConnectionClient != null) + { + _virtualConnectionClient.Dispose(); + _virtualConnectionClient = null; + } + + if (_physicalConnection != null) + { + _physicalConnection.Dispose(); + _physicalConnection = null; + } } 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); @@ -166,9 +188,9 @@ private static async Task ReadAllBytesAsync(Stream input) } } - private static string MakeNewCommandLineOptions(string pipeName, string[] watchFileExtensions) + private static string MakeNewCommandLineOptions(string listenAddress, string[] watchFileExtensions) { - var result = "--pipename " + pipeName; + var result = "--listenAddress " + listenAddress; if (watchFileExtensions != null && watchFileExtensions.Length > 0) { result += " --watch " + string.Join(",", watchFileExtensions); @@ -177,30 +199,6 @@ private static string MakeNewCommandLineOptions(string pipeName, string[] watchF 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 RpcJsonResponse { diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs index 29649fda..a5245e50 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs @@ -37,9 +37,44 @@ public Task InvokeAsync(string moduleName, params object[] args) } public Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args) + { + return InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, allowRetry: true); + } + + public async Task InvokeExportWithPossibleRetryAsync(string moduleName, string exportedFunctionName, object[] args, bool allowRetry) { var nodeInstance = GetOrCreateCurrentNodeInstance(); - return nodeInstance.InvokeExportAsync(moduleName, exportedFunctionName, args); + + try + { + return await nodeInstance.InvokeExportAsync(moduleName, exportedFunctionName, args); + } + catch (NodeInvocationException ex) + { + // If the Node instance can't complete the invocation because it needs to restart (e.g., because the underlying + // Node process has exited, or a file it depends on has changed), then we make one attempt to restart transparently. + if (allowRetry && ex.NodeInstanceUnavailable) + { + // Perform the retry after clearing away the old instance + lock (_currentNodeInstanceAccessLock) + { + if (_currentNodeInstance == nodeInstance) + { + DisposeNodeInstance(_currentNodeInstance); + _currentNodeInstance = null; + } + } + + // One the next call, don't allow retries, because we could get into an infinite retry loop, or a long retry + // loop that masks an underlying problem. A newly-created Node instance should be able to accept invocations, + // or something more serious must be wrong. + return await InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, allowRetry: false); + } + else + { + throw; + } + } } public void Dispose() @@ -48,12 +83,19 @@ public void Dispose() { if (_currentNodeInstance != null) { - _currentNodeInstance.Dispose(); + DisposeNodeInstance(_currentNodeInstance); _currentNodeInstance = null; } } } + private static void DisposeNodeInstance(INodeInstance nodeInstance) + { + // TODO: Implement delayed disposal for connection draining + // Or consider having the delayedness of it being a responsibility of the INodeInstance + nodeInstance.Dispose(); + } + private INodeInstance GetOrCreateCurrentNodeInstance() { var instance = _currentNodeInstance; diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts index ce168873..78e0058c 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts @@ -69,7 +69,7 @@ virtualConnectionServer.createInterface(server).on('connection', (connection: Du // 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; +const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; server.listen(listenAddress); interface RpcInvocation { From 26e8bd823cd26401372264ea5dfeb3f6ac55ed70 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 12:01:28 +0100 Subject: [PATCH 06/28] Instead of the Node process exiting instantly on file change, send a signal to .NET that it should restart. This is working towards the connection-draining feature. --- .../Content/Node/entrypoint-http.js | 5 ++++- .../Content/Node/entrypoint-socket.js | 5 ++++- .../HostingModels/OutOfProcessNodeInstance.cs | 8 ++++++++ .../TypeScript/Util/AutoQuit.ts | 6 +++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index edf5949d..9e665ab9 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -184,7 +184,10 @@ var ext = path.extname(filename); if (extensions.indexOf(ext) >= 0) { console.log('Restarting due to file change: ' + filename); - process.exit(0); + // Temporarily, the file-watching logic is in Node, so we signal it's time to restart by + // sending the following message back to .NET. Soon the file-watching logic will move over + // to the .NET side, and this whole file can be removed. + console.log('[Microsoft.AspNetCore.NodeServices:Restart]'); } }); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js index 355bbbb3..3035b343 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js @@ -95,7 +95,10 @@ var ext = path.extname(filename); if (extensions.indexOf(ext) >= 0) { console.log('Restarting due to file change: ' + filename); - process.exit(0); + // Temporarily, the file-watching logic is in Node, so we signal it's time to restart by + // sending the following message back to .NET. Soon the file-watching logic will move over + // to the .NET side, and this whole file can be removed. + console.log('[Microsoft.AspNetCore.NodeServices:Restart]'); } }); } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 498c3163..b81651d8 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels public abstract class OutOfProcessNodeInstance : INodeInstance { private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]"; + private const string NeedsRestartMessage = "[Microsoft.AspNetCore.NodeServices:Restart]"; private readonly TaskCompletionSource _connectionIsReadySource = new TaskCompletionSource(); private bool _disposed; private readonly StringAsTempFile _entryPointScript; @@ -128,6 +129,13 @@ private void ConnectToInputOutputStreams() _connectionIsReadySource.SetResult(null); initializationIsCompleted = true; } + else if (evt.Data == NeedsRestartMessage) + { + // Temporarily, the file-watching logic is in Node, so look out for the + // signal that we need to restart. This can be removed once the file-watching + // logic is moved over to the .NET side. + Dispose(); + } else if (evt.Data != null) { OnOutputDataReceived(evt.Data); diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts index e65567c5..5c75c1cb 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts @@ -8,7 +8,11 @@ export function autoQuitOnFileChange(rootDir: string, extensions: string[]) { var ext = path.extname(filename); if (extensions.indexOf(ext) >= 0) { console.log('Restarting due to file change: ' + filename); - process.exit(0); + + // Temporarily, the file-watching logic is in Node, so we signal it's time to restart by + // sending the following message back to .NET. Soon the file-watching logic will move over + // to the .NET side, and this whole file can be removed. + console.log('[Microsoft.AspNetCore.NodeServices:Restart]'); } }); } From be13f0b7bf3b74213e1bebfb371ca161016d93c4 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 12:52:15 +0100 Subject: [PATCH 07/28] Centralise the child-process-terminating logic in NodeServicesImpl - don't also do it in OutOfProcessNodeInstance. This works towards connection draining. --- .../HostingModels/OutOfProcessNodeInstance.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index b81651d8..ac1b6919 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -23,6 +23,7 @@ public abstract class OutOfProcessNodeInstance : INodeInstance private bool _disposed; private readonly StringAsTempFile _entryPointScript; private readonly Process _nodeProcess; + private bool _nodeProcessNeedsRestart; public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null) { @@ -33,16 +34,19 @@ public OutOfProcessNodeInstance(string entryPointScript, string projectPath, str public async Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args) { - // Wait until the connection is established. This will throw if the connection fails to initialize. - await _connectionIsReadySource.Task; - - if (_nodeProcess.HasExited) + if (_nodeProcess.HasExited || _nodeProcessNeedsRestart) { // This special kind of exception triggers a transparent retry - NodeServicesImpl will launch // a new Node instance and pass the invocation to that one instead. - throw new NodeInvocationException("The Node process has exited", null, nodeInstanceUnavailable: true); + var message = _nodeProcess.HasExited + ? "The Node process has exited" + : "The Node process needs to restart"; + throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true); } + // Wait until the connection is established. This will throw if the connection fails to initialize. + await _connectionIsReadySource.Task; + return await InvokeExportAsync(new NodeInvocationInfo { ModuleName = moduleName, @@ -115,7 +119,17 @@ private static Process LaunchNodeProcess(string entryPointFilename, string proje startInfo.Environment["NODE_PATH"] = nodePathValue; #endif - return Process.Start(startInfo); + var process = Process.Start(startInfo); + + // On Mac at least, a killed child process is left open as a zombie until the parent + // captures its exit code. We don't need the exit code for this process, and don't want + // to use process.WaitForExit() explicitly (we'd have to block the thread until it really + // has exited), but we don't want to leave zombies lying around either. It's sufficient + // to use process.EnableRaisingEvents so that .NET will grab the exit code and let the + // zombie be cleaned away without having to block our thread. + process.EnableRaisingEvents = true; + + return process; } private void ConnectToInputOutputStreams() @@ -134,7 +148,7 @@ private void ConnectToInputOutputStreams() // Temporarily, the file-watching logic is in Node, so look out for the // signal that we need to restart. This can be removed once the file-watching // logic is moved over to the .NET side. - Dispose(); + _nodeProcessNeedsRestart = true; } else if (evt.Data != null) { From ce127f0d70e5358de29647e7074d3d88a74f6be1 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 13:18:48 +0100 Subject: [PATCH 08/28] Implement connection draining feature --- .../NodeServicesImpl.cs | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs index a5245e50..82232150 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs @@ -14,16 +14,16 @@ namespace Microsoft.AspNetCore.NodeServices /// class will create a new instance and dispatch future calls to it, while keeping the old instance /// alive for a defined period so that any in-flight RPC calls can complete. This latter feature is /// analogous to the "connection draining" feature implemented by HTTP load balancers. - /// - /// TODO: Implement everything in the preceding paragraph. /// /// internal class NodeServicesImpl : INodeServices { + private static TimeSpan ConnectionDrainingTimespan = TimeSpan.FromSeconds(15); private NodeServicesOptions _options; private Func _nodeInstanceFactory; private INodeInstance _currentNodeInstance; private object _currentNodeInstanceAccessLock = new object(); + private Exception _instanceDelayedDisposalException; internal NodeServicesImpl(NodeServicesOptions options, Func nodeInstanceFactory) { @@ -43,6 +43,7 @@ public Task InvokeExportAsync(string moduleName, string exportedFunctionNa public async Task InvokeExportWithPossibleRetryAsync(string moduleName, string exportedFunctionName, object[] args, bool allowRetry) { + ThrowAnyOutstandingDelayedDisposalException(); var nodeInstance = GetOrCreateCurrentNodeInstance(); try @@ -56,11 +57,13 @@ public async Task InvokeExportWithPossibleRetryAsync(string moduleName, st if (allowRetry && ex.NodeInstanceUnavailable) { // Perform the retry after clearing away the old instance + // Since we disposal is delayed even though the node instance is replaced immediately, this produces the + // "connection draining" feature whereby in-flight RPC calls are given a certain period to complete. lock (_currentNodeInstanceAccessLock) { if (_currentNodeInstance == nodeInstance) { - DisposeNodeInstance(_currentNodeInstance); + DisposeNodeInstance(_currentNodeInstance, delay: ConnectionDrainingTimespan); _currentNodeInstance = null; } } @@ -83,17 +86,47 @@ public void Dispose() { if (_currentNodeInstance != null) { - DisposeNodeInstance(_currentNodeInstance); + DisposeNodeInstance(_currentNodeInstance, delay: TimeSpan.Zero); _currentNodeInstance = null; } } } - private static void DisposeNodeInstance(INodeInstance nodeInstance) + private void DisposeNodeInstance(INodeInstance nodeInstance, TimeSpan delay) + { + if (delay == TimeSpan.Zero) + { + nodeInstance.Dispose(); + } + else + { + Task.Run(async () => { + try + { + await Task.Delay(delay); + nodeInstance.Dispose(); + } + catch(Exception ex) + { + // Nothing's waiting for the delayed disposal task, so any exceptions in it would + // by default just get ignored. To make these discoverable, capture them here so + // they can be rethrown to the next caller to InvokeExportAsync. + _instanceDelayedDisposalException = ex; + } + }); + } + } + + private void ThrowAnyOutstandingDelayedDisposalException() { - // TODO: Implement delayed disposal for connection draining - // Or consider having the delayedness of it being a responsibility of the INodeInstance - nodeInstance.Dispose(); + if (_instanceDelayedDisposalException != null) + { + var ex = _instanceDelayedDisposalException; + _instanceDelayedDisposalException = null; + throw new AggregateException( + "A previous attempt to dispose a Node instance failed. See InnerException for details.", + ex); + } } private INodeInstance GetOrCreateCurrentNodeInstance() From eec370e9389abc6ef32fc3b5109f398f75d023e6 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 14:25:54 +0100 Subject: [PATCH 09/28] Move file-watching logic into .NET to avoid Node's fs.watch issues on Windows (#128) --- .../Configuration/Configuration.cs | 2 +- .../Content/Node/entrypoint-http.js | 36 +------- .../Content/Node/entrypoint-socket.js | 62 +++---------- .../HostingModels/HttpNodeInstance.cs | 18 +--- .../HostingModels/OutOfProcessNodeInstance.cs | 92 +++++++++++++++++-- .../HostingModels/SocketNodeInstance.cs | 18 +--- .../TypeScript/HttpNodeInstanceEntryPoint.ts | 7 +- .../SocketNodeInstanceEntryPoint.ts | 6 +- .../TypeScript/Util/AutoQuit.ts | 18 ---- .../Util/StringAsTempFile.cs | 2 +- 10 files changed, 111 insertions(+), 150 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs index 4bda4d64..a497c641 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -46,7 +46,7 @@ private static INodeInstance CreateNodeInstance(NodeServicesOptions options) switch (options.HostingModel) { case NodeHostingModel.Http: - return new HttpNodeInstance(options.ProjectPath, /* port */ 0, options.WatchFileExtensions); + return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, /* port */ 0); case NodeHostingModel.Socket: var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName); diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index 9e665ab9..ab728aeb 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -57,14 +57,9 @@ var http = __webpack_require__(2); var path = __webpack_require__(3); var ArgsUtil_1 = __webpack_require__(4); - var AutoQuit_1 = __webpack_require__(5); // 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 = ArgsUtil_1.parseArgs(process.argv); - if (parsedArgs.watch) { - AutoQuit_1.autoQuitOnFileChange(process.cwd(), parsedArgs.watch.split(',')); - } var server = http.createServer(function (req, res) { readRequestBodyAsJson(req, function (bodyJson) { var hasSentResult = false; @@ -117,6 +112,7 @@ } }); }); + var parsedArgs = ArgsUtil_1.parseArgs(process.argv); var requestedPortOrZero = parsedArgs.port || 0; // 0 means 'let the OS decide' server.listen(requestedPortOrZero, 'localhost', function () { // Signal to HttpNodeHost which port it should make its HTTP connections on @@ -170,35 +166,5 @@ exports.parseArgs = parseArgs; -/***/ }, -/* 5 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - var fs = __webpack_require__(6); - var path = __webpack_require__(3); - 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); - // Temporarily, the file-watching logic is in Node, so we signal it's time to restart by - // sending the following message back to .NET. Soon the file-watching logic will move over - // to the .NET side, and this whole file can be removed. - console.log('[Microsoft.AspNetCore.NodeServices:Restart]'); - } - }); - } - exports.autoQuitOnFileChange = autoQuitOnFileChange; - - -/***/ }, -/* 6 */ -/***/ function(module, exports) { - - module.exports = require("fs"); - /***/ } /******/ ]))); \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js index 3035b343..15856132 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js @@ -44,7 +44,7 @@ /* 0 */ /***/ function(module, exports, __webpack_require__) { - module.exports = __webpack_require__(7); + module.exports = __webpack_require__(5); /***/ }, @@ -83,54 +83,19 @@ /***/ }, /* 5 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - var fs = __webpack_require__(6); - var path = __webpack_require__(3); - 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); - // Temporarily, the file-watching logic is in Node, so we signal it's time to restart by - // sending the following message back to .NET. Soon the file-watching logic will move over - // to the .NET side, and this whole file can be removed. - console.log('[Microsoft.AspNetCore.NodeServices:Restart]'); - } - }); - } - exports.autoQuitOnFileChange = autoQuitOnFileChange; - - -/***/ }, -/* 6 */ -/***/ function(module, exports) { - - module.exports = require("fs"); - -/***/ }, -/* 7 */ /***/ 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 net = __webpack_require__(8); + var net = __webpack_require__(6); var path = __webpack_require__(3); - var readline = __webpack_require__(9); + var readline = __webpack_require__(7); var ArgsUtil_1 = __webpack_require__(4); - var AutoQuit_1 = __webpack_require__(5); - var virtualConnectionServer = __webpack_require__(10); + var virtualConnectionServer = __webpack_require__(8); // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct // reference to Node's runtime 'require' function. var dynamicRequire = eval('require'); - var parsedArgs = ArgsUtil_1.parseArgs(process.argv); - if (parsedArgs.watch) { - AutoQuit_1.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]'); @@ -179,29 +144,30 @@ // Begin listening now. The underlying transport varies according to the runtime platform. // On Windows it's Named Pipes; on Linux/OSX it's Domain Sockets. var useWindowsNamedPipes = /^win/.test(process.platform); + var parsedArgs = ArgsUtil_1.parseArgs(process.argv); var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; server.listen(listenAddress); /***/ }, -/* 8 */ +/* 6 */ /***/ function(module, exports) { module.exports = require("net"); /***/ }, -/* 9 */ +/* 7 */ /***/ function(module, exports) { module.exports = require("readline"); /***/ }, -/* 10 */ +/* 8 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var events_1 = __webpack_require__(11); - var VirtualConnection_1 = __webpack_require__(12); + var events_1 = __webpack_require__(9); + var VirtualConnection_1 = __webpack_require__(10); // Keep this in sync with the equivalent constant in the .NET code. Both sides split up their transmissions into frames with this max length, // and both will reject longer frames. var MaxFrameBodyLength = 16 * 1024; @@ -382,13 +348,13 @@ /***/ }, -/* 11 */ +/* 9 */ /***/ function(module, exports) { module.exports = require("events"); /***/ }, -/* 12 */ +/* 10 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -397,7 +363,7 @@ function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; - var stream_1 = __webpack_require__(13); + var stream_1 = __webpack_require__(11); /** * Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection. */ @@ -438,7 +404,7 @@ /***/ }, -/* 13 */ +/* 11 */ /***/ function(module, exports) { module.exports = require("stream"); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index c1fdbafc..1b59e873 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -16,9 +16,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels /// port number is specified as a constructor parameter), and signals which port was selected using the same /// input/output-based mechanism that the base class uses to determine when the child process is ready to /// accept RPC invocations. - /// - /// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing - /// the actual watching in .NET code (not Node), for consistency across platforms. /// /// internal class HttpNodeInstance : OutOfProcessNodeInstance @@ -35,26 +32,21 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance private bool _disposed; private int _portNumber; - public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null) + public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, - MakeCommandLineOptions(port, watchFileExtensions)) + watchFileExtensions, + MakeCommandLineOptions(port)) { _client = new HttpClient(); } - private static string MakeCommandLineOptions(int port, string[] watchFileExtensions) + private static string MakeCommandLineOptions(int port) { - var result = "--port " + port; - if (watchFileExtensions != null && watchFileExtensions.Length > 0) - { - result += " --watch " + string.Join(",", watchFileExtensions); - } - - return result; + return $"--port {port}"; } protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index ac1b6919..c45c1004 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace Microsoft.AspNetCore.NodeServices.HostingModels @@ -18,17 +19,24 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels public abstract class OutOfProcessNodeInstance : INodeInstance { private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]"; - private const string NeedsRestartMessage = "[Microsoft.AspNetCore.NodeServices:Restart]"; private readonly TaskCompletionSource _connectionIsReadySource = new TaskCompletionSource(); private bool _disposed; private readonly StringAsTempFile _entryPointScript; + private FileSystemWatcher _fileSystemWatcher; private readonly Process _nodeProcess; private bool _nodeProcessNeedsRestart; + private readonly string[] _watchFileExtensions; - public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null) + public OutOfProcessNodeInstance( + string entryPointScript, + string projectPath, + string[] watchFileExtensions, + string commandLineArguments) { _entryPointScript = new StringAsTempFile(entryPointScript); _nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments); + _watchFileExtensions = watchFileExtensions; + _fileSystemWatcher = BeginFileWatcher(projectPath); ConnectToInputOutputStreams(); } @@ -80,6 +88,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { _entryPointScript.Dispose(); + EnsureFileSystemWatcherIsDisposed(); } // Make sure the Node process is finished @@ -93,6 +102,15 @@ protected virtual void Dispose(bool disposing) } } + private void EnsureFileSystemWatcherIsDisposed() + { + if (_fileSystemWatcher != null) + { + _fileSystemWatcher.Dispose(); + _fileSystemWatcher = null; + } + } + private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments) { var startInfo = new ProcessStartInfo("node") @@ -143,13 +161,6 @@ private void ConnectToInputOutputStreams() _connectionIsReadySource.SetResult(null); initializationIsCompleted = true; } - else if (evt.Data == NeedsRestartMessage) - { - // Temporarily, the file-watching logic is in Node, so look out for the - // signal that we need to restart. This can be removed once the file-watching - // logic is moved over to the .NET side. - _nodeProcessNeedsRestart = true; - } else if (evt.Data != null) { OnOutputDataReceived(evt.Data); @@ -177,6 +188,69 @@ private void ConnectToInputOutputStreams() _nodeProcess.BeginErrorReadLine(); } + private FileSystemWatcher BeginFileWatcher(string rootDir) + { + if (_watchFileExtensions == null || _watchFileExtensions.Length == 0) + { + // Nothing to watch + return null; + } + + var watcher = new FileSystemWatcher(rootDir) + { + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + }; + watcher.Changed += OnFileChanged; + watcher.Created += OnFileChanged; + watcher.Deleted += OnFileChanged; + watcher.Renamed += OnFileRenamed; + watcher.EnableRaisingEvents = true; + return watcher; + } + + private void OnFileChanged(object source, FileSystemEventArgs e) + { + if (IsFilenameBeingWatched(e.FullPath)) + { + RestartDueToFileChange(e.FullPath); + } + } + + private void OnFileRenamed(object source, RenamedEventArgs e) + { + if (IsFilenameBeingWatched(e.OldFullPath) || IsFilenameBeingWatched(e.FullPath)) + { + RestartDueToFileChange(e.OldFullPath); + } + } + + private bool IsFilenameBeingWatched(string fullPath) + { + if (string.IsNullOrEmpty(fullPath)) + { + return false; + } + else + { + var actualExtension = Path.GetExtension(fullPath) ?? string.Empty; + return _watchFileExtensions.Any(actualExtension.Equals); + } + } + + private void RestartDueToFileChange(string fullPath) + { + // TODO: Use proper logger + Console.WriteLine($"Node will restart because file changed: {fullPath}"); + + _nodeProcessNeedsRestart = true; + + // There's no need to watch for any more changes, since we're already restarting, and if the + // restart takes some time (e.g., due to connection draining), we could end up getting duplicate + // notifications. + EnsureFileSystemWatcherIsDisposed(); + } + ~OutOfProcessNodeInstance() { Dispose(false); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 7a411a82..3fb25f06 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -21,9 +21,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels /// The address of the pipe/socket is selected randomly here on the .NET side and sent to the child process as a /// command-line argument (the address space is wide enough that there's no real risk of a clash, unlike when /// selecting TCP port numbers). - /// - /// TODO: Remove the file-watching logic from here and centralise it in OutOfProcessNodeInstance, implementing - /// the actual watching in .NET code (not Node), for consistency across platforms. /// /// internal class SocketNodeInstance : OutOfProcessNodeInstance @@ -38,16 +35,15 @@ internal class SocketNodeInstance : OutOfProcessNodeInstance private StreamConnection _physicalConnection; private string _socketAddress; private VirtualConnectionClient _virtualConnectionClient; - private readonly string[] _watchFileExtensions; public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress): base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), "/Content/Node/entrypoint-socket.js"), projectPath, - MakeNewCommandLineOptions(socketAddress, watchFileExtensions)) + watchFileExtensions, + MakeNewCommandLineOptions(socketAddress)) { - _watchFileExtensions = watchFileExtensions; _socketAddress = socketAddress; } @@ -188,15 +184,9 @@ private static async Task ReadAllBytesAsync(Stream input) } } - private static string MakeNewCommandLineOptions(string listenAddress, string[] watchFileExtensions) + private static string MakeNewCommandLineOptions(string listenAddress) { - var result = "--listenAddress " + listenAddress; - if (watchFileExtensions != null && watchFileExtensions.Length > 0) - { - result += " --watch " + string.Join(",", watchFileExtensions); - } - - return result; + return $"--listenAddress {listenAddress}"; } #pragma warning disable 649 // These properties are populated via JSON deserialization diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index b28f38b9..3be759a2 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -3,17 +3,11 @@ import * as http from 'http'; import * as path from 'path'; import { parseArgs } from './Util/ArgsUtil'; -import { autoQuitOnFileChange } from './Util/AutoQuit'; // 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(',')); -} - const server = http.createServer((req, res) => { readRequestBodyAsJson(req, bodyJson => { let hasSentResult = false; @@ -68,6 +62,7 @@ const server = http.createServer((req, res) => { }); }); +const parsedArgs = parseArgs(process.argv); const requestedPortOrZero = parsedArgs.port || 0; // 0 means 'let the OS decide' server.listen(requestedPortOrZero, 'localhost', function () { // Signal to HttpNodeHost which port it should make its HTTP connections on diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts index 78e0058c..0aec5417 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts @@ -5,16 +5,11 @@ import * as path from 'path'; import * as readline from 'readline'; import { Duplex } from 'stream'; import { parseArgs } from './Util/ArgsUtil'; -import { autoQuitOnFileChange } from './Util/AutoQuit'; 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', () => { @@ -69,6 +64,7 @@ virtualConnectionServer.createInterface(server).on('connection', (connection: Du // Begin listening now. The underlying transport varies according to the runtime platform. // On Windows it's Named Pipes; on Linux/OSX it's Domain Sockets. const useWindowsNamedPipes = /^win/.test(process.platform); +const parsedArgs = parseArgs(process.argv); const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; server.listen(listenAddress); diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts deleted file mode 100644 index 5c75c1cb..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/AutoQuit.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -export 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); - - // Temporarily, the file-watching logic is in Node, so we signal it's time to restart by - // sending the following message back to .NET. Soon the file-watching logic will move over - // to the .NET side, and this whole file can be removed. - console.log('[Microsoft.AspNetCore.NodeServices:Restart]'); - } - }); -} diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs index a9f04a94..4458b779 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs @@ -28,7 +28,7 @@ private void DisposeImpl(bool disposing) { if (disposing) { - // TODO: dispose managed state (managed objects). + // Would dispose managed state here, if there was any } File.Delete(FileName); From 4b3851900141ab431978aefdba4e77a664f15aa2 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 14:43:14 +0100 Subject: [PATCH 10/28] Change all links in docs to point to new main branch ('dev') --- README.md | 20 +++++++++---------- .../README.md | 10 +++++----- .../README.md | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index efe6e5a1..ed2f172c 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ This project is part of ASP.NET Core. You can find samples, documentation and ge This repo contains: * A set of NuGet/NPM packages that implement functionality for: - * Invoking arbitrary NPM packages at runtime from .NET code ([docs](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.NodeServices#simple-usage-example)) - * Server-side prerendering of SPA components ([docs](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices#server-side-prerendering)) - * Webpack dev middleware ([docs](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices#webpack-dev-middleware)) - * Hot module replacement (HMR) ([docs](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices#webpack-hot-module-replacement)) - * Server-side and client-side routing integration ([docs](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices#routing-helper-mapspafallbackroute)) + * Invoking arbitrary NPM packages at runtime from .NET code ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.NodeServices#simple-usage-example)) + * Server-side prerendering of SPA components ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#server-side-prerendering)) + * Webpack dev middleware ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#webpack-dev-middleware)) + * Hot module replacement (HMR) ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#webpack-hot-module-replacement)) + * Server-side and client-side routing integration ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#routing-helper-mapspafallbackroute)) * Server-side and client-side validation integration * "Cache priming" for Angular 2 apps * "Lazy loading" for Knockout apps @@ -36,13 +36,13 @@ If you have an existing ASP.NET Core application, or if you just want to use the * `Microsoft.AspNetCore.NodeServices` * This provides a fast and robust way for .NET code to run JavaScript on the server inside a Node.js environment. You can use this to consume arbitrary functionality from NPM packages at runtime in your ASP.NET Core app. * Most applications developers don't need to use this directly, but you can do so if you want to implement your own functionality that involves calling Node.js code from .NET at runtime. - * Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.NodeServices#microsoftaspnetcorenodeservices). + * Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.NodeServices#microsoftaspnetcorenodeservices). * `Microsoft.AspNetCore.SpaServices` * This provides infrastructure that's generally useful when building Single Page Applications (SPAs) with technologies such as Angular 2 or React (for example, server-side prerendering and webpack middleware). Internally, it uses the `NodeServices` package to implement its features. - * Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices#microsoftaspnetcorespaservices). + * Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#microsoftaspnetcorespaservices). * `Microsoft.AspNetCore.AngularServices` * This builds on the `SpaServices` package and includes features specific to Angular 2. Currently, this includes validation helpers and a "cache priming" feature, which let you pre-evaluate ajax requests on the server so that client-side code doesn't need to make network calls once it's loaded. - * The code is [here](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.AngularServices), and you'll find a usage example for [the validation helper here](https://github.com/aspnet/JavaScriptServices/blob/master/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts), and for the [cache priming here](https://github.com/aspnet/JavaScriptServices/blob/master/samples/angular/MusicStore/Views/Home/Index.cshtml#L7-8). Full docs are to be written. + * The code is [here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.AngularServices), and you'll find a usage example for [the validation helper here](https://github.com/aspnet/JavaScriptServices/blob/dev/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts), and for the [cache priming here](https://github.com/aspnet/JavaScriptServices/blob/dev/samples/angular/MusicStore/Views/Home/Index.cshtml#L7-8). Full docs are to be written. There was previously a `Microsoft.AspNetCore.ReactServices` but this is not currently needed - all applicable functionality is in `Microsoft.AspNetCore.SpaServices`, because it's sufficiently general. We might add a new `Microsoft.AspNetCore.ReactServices` package in the future if new React-specific requirements emerge. @@ -50,9 +50,9 @@ If you want to build a helper library for some other SPA framework, you can do s ## Samples and templates -Inside this repo, [the `templates` directory](https://github.com/aspnet/JavaScriptServices/tree/master/templates) contains the application starting points that the `aspnetcore-spa` generator emits. If you want, you can clone this repo and run those applications directly. But it's easier to [use the Yeoman tool to run the generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/). +Inside this repo, [the `templates` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/templates) contains the application starting points that the `aspnetcore-spa` generator emits. If you want, you can clone this repo and run those applications directly. But it's easier to [use the Yeoman tool to run the generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/). -Also in this repo, [the `samples` directory](https://github.com/aspnet/JavaScriptServices/tree/master/samples) contains examples of using the JavaScript services family of packages with Angular 2 and React, plus examples of standalone `NodeServices` usage for runtime code transpilation and image processing. +Also in this repo, [the `samples` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/samples) contains examples of using the JavaScript services family of packages with Angular 2 and React, plus examples of standalone `NodeServices` usage for runtime code transpilation and image processing. **To run the samples:** diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index f0a231be..3a1b7db5 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -9,8 +9,8 @@ This NuGet package provides a fast and robust way to invoke Node.js code from a It is the underlying mechanism supporting the following packages: - * [`Microsoft.AspNetCore.SpaServices`](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices) - builds on NodeServices, adding functionality commonly used in Single Page Applications, such as server-side prerendering, webpack middleware, and integration between server-side and client-side routing. - * [`Microsoft.AspNetCore.AngularServices`](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.AngularServices) and [`Microsoft.AspNetCore.ReactServices`](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.ReactServices) - these build on `SpaServices`, adding helpers specific to Angular 2 and React, such as cache priming and integrating server-side and client-side validation + * [`Microsoft.AspNetCore.SpaServices`](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices) - builds on NodeServices, adding functionality commonly used in Single Page Applications, such as server-side prerendering, webpack middleware, and integration between server-side and client-side routing. + * [`Microsoft.AspNetCore.AngularServices`](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.AngularServices) and [`Microsoft.AspNetCore.ReactServices`](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.ReactServices) - these build on `SpaServices`, adding helpers specific to Angular 2 and React, such as cache priming and integrating server-side and client-side validation ### Requirements @@ -20,7 +20,7 @@ It is the underlying mechanism supporting the following packages: * [.NET](https://dot.net) * For .NET Core (e.g., ASP.NET Core apps), you need at least 1.0 RC2 * For .NET Framework, you need at least version 4.5.1. - + ### Installation For .NET Core apps: @@ -37,7 +37,7 @@ For .NET Framework apps: In that case, you don't need to use NodeServices directly (or install it manually). You can either: * **Recommended:** Use the `aspnetcore-spa` Yeoman generator to get a ready-to-go starting point using your choice of client-side framework. [Instructions here.](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/) -* Or set up your ASP.NET Core and client-side Angular/React/KO/etc. app manually, and then use the [`Microsoft.AspNetCore.SpaServices`](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.SpaServices) package to add features like server-side prerendering or Webpack middleware. But really, at least try using the `aspnetcore-spa` generator first. +* Or set up your ASP.NET Core and client-side Angular/React/KO/etc. app manually, and then use the [`Microsoft.AspNetCore.SpaServices`](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices) package to add features like server-side prerendering or Webpack middleware. But really, at least try using the `aspnetcore-spa` generator first. # Simple usage example @@ -268,7 +268,7 @@ module.exports = function(result, physicalPath, maxWidth, maxHeight) { } ``` -There's a working image resizing example following this approach [here](https://github.com/aspnet/JavaScriptServices/tree/master/samples/misc/NodeServicesExamples) - see the [C# code](https://github.com/aspnet/JavaScriptServices/blob/master/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs) and the [JavaScript code](https://github.com/aspnet/JavaScriptServices/blob/master/samples/misc/NodeServicesExamples/Node/resizeImage.js). +There's a working image resizing example following this approach [here](https://github.com/aspnet/JavaScriptServices/tree/dev/samples/misc/NodeServicesExamples) - see the [C# code](https://github.com/aspnet/JavaScriptServices/blob/dev/samples/misc/NodeServicesExamples/Controllers/ResizeImage.cs) and the [JavaScript code](https://github.com/aspnet/JavaScriptServices/blob/dev/samples/misc/NodeServicesExamples/Node/resizeImage.js). **Parameters** diff --git a/src/Microsoft.AspNetCore.SpaServices/README.md b/src/Microsoft.AspNetCore.SpaServices/README.md index 4b019c18..59e05fdb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/README.md +++ b/src/Microsoft.AspNetCore.SpaServices/README.md @@ -9,7 +9,7 @@ This package enables: * [**Hot module replacement**](#webpack-hot-module-replacement) so that, during development, your code and markup changes will be pushed to your browser and updated in the running application automatically, without even needing to reload the page * [**Routing helpers**](#routing-helper-mapspafallbackroute) for integrating server-side routing with client-side routing -Behind the scenes, it uses the [`Microsoft.AspNetCore.NodeServices`](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.NodeServices) package as a fast and robust way to invoke Node.js-hosted code from ASP.NET Core at runtime. +Behind the scenes, it uses the [`Microsoft.AspNetCore.NodeServices`](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.NodeServices) package as a fast and robust way to invoke Node.js-hosted code from ASP.NET Core at runtime. ### Requirements @@ -206,7 +206,7 @@ npm install --save angular2-universal Now you can use the [`angular2-universal` APIs](https://github.com/angular/universal) from your `boot-server.ts` TypeScript module to execute your Angular 2 component on the server. The code needed for this is fairly complex, but that's unavoidable because Angular 2 supports so many different ways of being configured, and you need to provide wiring for whatever combination of DI modules you're using. -You can find an example `boot-server.ts` that renders arbitrary Angular 2 components [here](https://github.com/aspnet/JavaScriptServices/blob/master/templates/Angular2Spa/ClientApp/boot-server.ts). If you use this with your own application, you might need to edit the `serverBindings` array to reference any other DI services that your Angular 2 component depends on. +You can find an example `boot-server.ts` that renders arbitrary Angular 2 components [here](https://github.com/aspnet/JavaScriptServices/blob/dev/templates/Angular2Spa/ClientApp/boot-server.ts). If you use this with your own application, you might need to edit the `serverBindings` array to reference any other DI services that your Angular 2 component depends on. The easiest way to get started with Angular 2 server-side rendering on ASP.NET Core is to use the [aspnetcore-spa generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/), which creates a ready-made working starting point. @@ -305,7 +305,7 @@ Now you should find that your React app is rendered in the page even before any The above example is extremely simple - it doesn't use `react-router`, and it doesn't load any data asynchronously. Real applications are likely to do both of these. -For an example server-side boot module that knows how to evaluate `react-router` routes and render the correct React component, see [this example](https://github.com/aspnet/JavaScriptServices/blob/master/templates/ReactReduxSpa/ClientApp/boot-server.tsx). +For an example server-side boot module that knows how to evaluate `react-router` routes and render the correct React component, see [this example](https://github.com/aspnet/JavaScriptServices/blob/dev/templates/ReactReduxSpa/ClientApp/boot-server.tsx). Supporting asynchronous data loading involves more considerations. Unlike Angular 2 applications that run asynchronously on the server and freely overwrite server-generated markup with client-generated markup, React strictly wants to run synchronously on the server and always produce the same markup on the server as it does on the client. From 920f1c8bf329fe6cd778c85c86cf14c7fbe8ff8b Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 14:47:36 +0100 Subject: [PATCH 11/28] Replace references to Invoke and InvokeExport with InvokeAsync and InvokeExportAsync throughout docs --- .../README.md | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index 3a1b7db5..8a63dc20 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -76,7 +76,7 @@ public class SomeController : Controller { _nodeServices = nodeServices; } - + // ... your action methods are here ... } ``` @@ -86,7 +86,7 @@ Then you can use this instance to make calls into Node.js code, e.g.: ```csharp public async Task MyAction() { - var result = await _nodeServices.Invoke("./addNumbers", 1, 2); + var result = await _nodeServices.InvokeAsync("./addNumbers", 1, 2); return Content("1 + 2 = " + result); } ``` @@ -102,7 +102,7 @@ module.exports = function (callback, first, second) { As you can see, the exported JavaScript function will receive the arguments you pass from .NET (as long as they are JSON-serializable), along with a Node-style callback you can use to send back a result or error when you are ready. -When the `Invoke` method receives the result back from Node, the result will be JSON-deserialized to whatever generic type you specified when calling `Invoke` (e.g., above, that type is `int`). If `Invoke` receives an error from your Node code, it will throw an exception describing that error. +When the `InvokeAsync` method receives the result back from Node, the result will be JSON-deserialized to whatever generic type you specified when calling `InvokeAsync` (e.g., above, that type is `int`). If `InvokeAsync` receives an error from your Node code, it will throw an exception describing that error. If you want to put `addNumber.js` inside a subfolder rather than the root of your app, then also amend the path in the `_nodeServices.Invoke` call to match that path. @@ -116,9 +116,9 @@ In other types of .NET app where you don't have ASP.NET Core's DI system, you ca var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions()); ``` -Besides this, the usage is the same as described for ASP.NET above, so you can now call `nodeServices.Invoke(...)` etc. +Besides this, the usage is the same as described for ASP.NET above, so you can now call `nodeServices.InvokeAsync(...)` etc. -You can dispose the `nodeServices` object whenever you are done with it (and it will shut down the associated Node.js instance), but because these instances are expensive to create, you should whenever possible retain and reuse instances. They are thread-safe - you can call `Invoke` simultaneously from multiple threads. Also, `NodeServices` instances are smart enough to detect if the associated Node instance has died and will automatically start a new Node instance if needed. +You can dispose the `nodeServices` object whenever you are done with it (and it will shut down the associated Node.js instance), but because these instances are expensive to create, you should whenever possible retain and reuse instances. They are thread-safe - you can call `InvokeAsync` simultaneously from multiple threads. Also, `NodeServices` instances are smart enough to detect if the associated Node instance has died and will automatically start a new Node instance if needed. # API Reference @@ -197,17 +197,17 @@ var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { * `HostingModel` - an `NodeHostingModel` enum value. See: [hosting models](#hosting-models) * `ProjectPath` - if specified, controls the working directory used when launching Node instances. This affects, for example, the location that `require` statements resolve relative paths against. If not specified, your application root directory is used. * `WatchFileExtensions` - if specified, the launched Node instance will watch for changes to any files with these extension, and auto-restarts when any are changed. - + **Return type:** `NodeServices` -If you create a `NodeServices` instance this way, you can also dispose it (call `nodeServiceInstance.Dispose();`) and it will shut down the associated Node instance. But because these instances are expensive to create, you should whenever possible retain and reuse your `NodeServices` object. They are thread-safe - you can call `nodeServiceInstance.Invoke(...)` simultaneously from multiple threads. +If you create a `NodeServices` instance this way, you can also dispose it (call `nodeServiceInstance.Dispose();`) and it will shut down the associated Node instance. But because these instances are expensive to create, you should whenever possible retain and reuse your `NodeServices` object. They are thread-safe - you can call `nodeServiceInstance.InvokeAsync(...)` simultaneously from multiple threads. ### Invoke<T> **Signature:** ```csharp -Invoke(string moduleName, params object[] args) +InvokeAsync(string moduleName, params object[] args) ``` Asynchronously calls a JavaScript function and returns the result, or throws an exception if the result was an error. @@ -215,7 +215,7 @@ Asynchronously calls a JavaScript function and returns the result, or throws an **Example 1: Getting a JSON-serializable object from Node (the most common use case)** ```csharp -var result = await myNodeServicesInstance.Invoke( +var result = await myNodeServicesInstance.InvokeAsync( "./Node/transpile", pathOfSomeFileToBeTranspiled); ``` @@ -226,7 +226,7 @@ var result = await myNodeServicesInstance.Invoke( public class TranspilerResult { public string Code { get; set; } - public string[] Warnings { get; set; + public string[] Warnings { get; set; } } ``` @@ -245,7 +245,7 @@ module.exports = function (callback, filePath) { **Example 2: Getting a stream of binary data from Node** ```csharp -var imageStream = await myNodeServicesInstance.Invoke( +var imageStream = await myNodeServicesInstance.InvokeAsync( "./Node/resizeImage", fullImagePath, width, @@ -287,19 +287,19 @@ There's a working image resizing example following this approach [here](https:// **Signature** ```csharp -InvokeExport(string moduleName, string exportName, params object[] args) +InvokeExportAsync(string moduleName, string exportName, params object[] args) ``` -This is exactly the same as `Invoke`, except that it also takes an `exportName` parameter. You can use this if you want your JavaScript module to export more than one function. +This is exactly the same as `InvokeAsync`, except that it also takes an `exportName` parameter. You can use this if you want your JavaScript module to export more than one function. **Example** ```csharp -var someString = await myNodeServicesInstance.Invoke( +var someString = await myNodeServicesInstance.InvokeExportAsync( "./Node/myNodeApis", "getMeAString"); -var someStringInFrench = await myNodeServicesInstance.Invoke( +var someStringInFrench = await myNodeServicesInstance.InvokeExportAsync( "./Node/myNodeApis", "convertLanguage" someString, @@ -325,7 +325,7 @@ module.exports = { }; ``` -**Parameters, return type, etc.** For all other details, see the docs for [`Invoke`](#invoket) +**Parameters, return type, etc.** For all other details, see the docs for [`InvokeAsync`](#invokeasynct) ## Hosting models From 3bc35aea2170146ff967bfcd54063f6069d3c0c2 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 14:50:24 +0100 Subject: [PATCH 12/28] Simplify docs around receiving an INodeServices instance from DI --- .../README.md | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index 8a63dc20..c771cfd0 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -63,30 +63,12 @@ public void ConfigureServices(IServiceCollection services) } ``` -Now you can receive an instance of `NodeServices` as a constructor parameter to any MVC controller, e.g.: +Now you can receive an instance of `NodeServices` as an action method parameter to any MVC action, and then use it to make calls into Node.js code, e.g.: ```csharp -using Microsoft.AspNetCore.NodeServices; - -public class SomeController : Controller -{ - private INodeServices _nodeServices; - - public SomeController(INodeServices nodeServices) - { - _nodeServices = nodeServices; - } - - // ... your action methods are here ... -} -``` - -Then you can use this instance to make calls into Node.js code, e.g.: - -```csharp -public async Task MyAction() +public async Task MyAction([FromServices] INodeServices nodeServices) { - var result = await _nodeServices.InvokeAsync("./addNumbers", 1, 2); + var result = await nodeServices.InvokeAsync("./addNumbers", 1, 2); return Content("1 + 2 = " + result); } ``` From b0bc80b4d6e6c6bd0ed5f24ecb8ec9fe004413a2 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 14:58:25 +0100 Subject: [PATCH 13/28] Update docs around custom node instances to match latest API changes --- src/Microsoft.AspNetCore.NodeServices/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index c771cfd0..8fdb02af 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -349,11 +349,12 @@ The default transport may change from `Http` to `Socket` in the near future, bec ### Custom hosting models -If you implement a custom hosting model (by implementing `INodeServices`), then you can get instances of that just by using your type's constructor. Or if you want to designate it as the default hosting model that higher-level services (such as those in the `SpaServices` package) should use, register it with ASP.NET Core's DI system: +If you implement a custom hosting model (by implementing `INodeInstance`), then you can cause it to be used by populating `NodeInstanceFactory` on a `NodeServicesOptions`: ```csharp -services.AddSingleton(typeof(INodeServices), serviceProvider => -{ - return new YourCustomHostingModel(); -}); +var options = new NodeServicesOptions { + NodeInstanceFactory = () => new MyCustomNodeInstance() +}; ``` + +Now you can pass this `options` object to [`AddNodeServices`](#addnodeservices) or [`CreateNodeServices`](#createnodeservices). From 8b5136825c9a14d0669240d5ef331b64798a5080 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 14:59:59 +0100 Subject: [PATCH 14/28] Update remaining doc references to Invoke and InvokeExport --- src/Microsoft.AspNetCore.NodeServices/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index 8fdb02af..3fb99282 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -184,7 +184,7 @@ var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { If you create a `NodeServices` instance this way, you can also dispose it (call `nodeServiceInstance.Dispose();`) and it will shut down the associated Node instance. But because these instances are expensive to create, you should whenever possible retain and reuse your `NodeServices` object. They are thread-safe - you can call `nodeServiceInstance.InvokeAsync(...)` simultaneously from multiple threads. -### Invoke<T> +### InvokeAsync<T> **Signature:** @@ -264,7 +264,7 @@ There's a working image resizing example following this approach [here](https:// * A JSON-serializable .NET type, if your JavaScript code uses the `callback(error, result)` pattern to return an object, as in example 1 above * Or, the type `System.IO.Stream`, if your JavaScript code writes data to the `result.stream` object (which is a [Node `Duplex` stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex)), as in example 2 above -### InvokeExport<T> +### InvokeExportAsync<T> **Signature** From 01d5c90e2332f884abf2f5dfb5b4cdae026d4d4d Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 15:47:48 +0100 Subject: [PATCH 15/28] Include Microsoft.DotNet.Watcher.Tools in templates. Fixes #157 --- templates/Angular2Spa/project.json | 3 ++- templates/KnockoutSpa/project.json | 3 ++- templates/ReactReduxSpa/project.json | 3 ++- templates/ReactSpa/project.json | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/templates/Angular2Spa/project.json b/templates/Angular2Spa/project.json index 1e09cd25..1ce5f413 100755 --- a/templates/Angular2Spa/project.json +++ b/templates/Angular2Spa/project.json @@ -56,7 +56,8 @@ "portable-net45+win8+dnxcore50", "portable-net45+win8" ] - } + }, + "Microsoft.DotNet.Watcher.Tools": "1.0.0-preview2-final" }, "frameworks": { diff --git a/templates/KnockoutSpa/project.json b/templates/KnockoutSpa/project.json index 608da4b5..66ff1470 100755 --- a/templates/KnockoutSpa/project.json +++ b/templates/KnockoutSpa/project.json @@ -56,7 +56,8 @@ "portable-net45+win8+dnxcore50", "portable-net45+win8" ] - } + }, + "Microsoft.DotNet.Watcher.Tools": "1.0.0-preview2-final" }, "frameworks": { diff --git a/templates/ReactReduxSpa/project.json b/templates/ReactReduxSpa/project.json index 467e718e..f768ecaf 100755 --- a/templates/ReactReduxSpa/project.json +++ b/templates/ReactReduxSpa/project.json @@ -56,7 +56,8 @@ "portable-net45+win8+dnxcore50", "portable-net45+win8" ] - } + }, + "Microsoft.DotNet.Watcher.Tools": "1.0.0-preview2-final" }, "frameworks": { diff --git a/templates/ReactSpa/project.json b/templates/ReactSpa/project.json index 467e718e..f768ecaf 100755 --- a/templates/ReactSpa/project.json +++ b/templates/ReactSpa/project.json @@ -56,7 +56,8 @@ "portable-net45+win8+dnxcore50", "portable-net45+win8" ] - } + }, + "Microsoft.DotNet.Watcher.Tools": "1.0.0-preview2-final" }, "frameworks": { From c1a1bdf373da625cfff56c54f2b276bc257962f4 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jul 2016 15:50:37 +0100 Subject: [PATCH 16/28] Update React template homepage as per #158 --- templates/ReactReduxSpa/ClientApp/components/Home.tsx | 2 +- templates/ReactSpa/ClientApp/components/Home.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/ReactReduxSpa/ClientApp/components/Home.tsx b/templates/ReactReduxSpa/ClientApp/components/Home.tsx index f681e2e5..50995624 100644 --- a/templates/ReactReduxSpa/ClientApp/components/Home.tsx +++ b/templates/ReactReduxSpa/ClientApp/components/Home.tsx @@ -15,7 +15,7 @@ export default class Home extends React.Component {
  • Client-side navigation. For example, click Counter then Back to return here.
  • Webpack dev middleware. In development mode, there's no need to run the webpack build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.
  • -
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, rebuilt CSS and React components will be injected directly into your running application, preserving its live state.
  • +
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, rebuilt React components will be injected directly into your running application, preserving its live state.
  • Efficient production builds. In production mode, development-time features are disabled, and the webpack build tool produces minified static CSS and JavaScript files.
  • Server-side prerendering. To optimize startup time, your React application is first rendered on the server. The initial HTML and state is then transferred to the browser, where client-side code picks up where the server left off.
diff --git a/templates/ReactSpa/ClientApp/components/Home.tsx b/templates/ReactSpa/ClientApp/components/Home.tsx index d60da060..9f68e05c 100644 --- a/templates/ReactSpa/ClientApp/components/Home.tsx +++ b/templates/ReactSpa/ClientApp/components/Home.tsx @@ -15,7 +15,7 @@ export class Home extends React.Component {
  • Client-side navigation. For example, click Counter then Back to return here.
  • Webpack dev middleware. In development mode, there's no need to run the webpack build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.
  • -
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, rebuilt CSS and React components will be injected directly into your running application, preserving its live state.
  • +
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, rebuilt React components will be injected directly into your running application, preserving its live state.
  • Efficient production builds. In production mode, development-time features are disabled, and the webpack build tool produces minified static CSS and JavaScript files.

Going further

From fc897475f31cc6e60e18b438eb1e7c25ecfe0edf Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 11 Jul 2016 11:42:24 +0100 Subject: [PATCH 17/28] Update domain-task package to version 2.0.1 (major bump because breaking change) and modify 'fetch' behaviour so it no longer tries to register the task with domain-task automatically. See code comments for reasons. --- .../npm/aspnet-prerendering/package.json | 4 +-- .../npm/domain-task/package.json | 4 +-- .../npm/domain-task/src/fetch.ts | 31 ++++++++++++++++--- .../npm/domain-task/src/index.ts | 3 ++ 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/index.ts diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json index 2c0651b7..11bd4ecb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-prerendering", - "version": "1.0.2", + "version": "1.0.4", "description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { @@ -10,7 +10,7 @@ "author": "Microsoft", "license": "Apache-2.0", "dependencies": { - "domain-task": "^1.0.1", + "domain-task": "^2.0.1", "es6-promise": "^3.1.2" }, "devDependencies": { diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json index 02174e89..0bceb713 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json @@ -1,8 +1,8 @@ { "name": "domain-task", - "version": "1.0.1", + "version": "2.0.1", "description": "Tracks outstanding operations for a logical thread of execution", - "main": "main.js", + "main": "index.js", "scripts": { "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"domain-task\"'", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/fetch.ts b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/fetch.ts index 9763a007..be081f04 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/fetch.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/fetch.ts @@ -1,7 +1,6 @@ import * as url from 'url'; import * as domain from 'domain'; import * as domainContext from 'domain-context'; -import { addTask } from './main'; const isomorphicFetch = require('isomorphic-fetch'); const isBrowser: boolean = (new Function('try { return this === window; } catch (e) { return false; }'))(); @@ -42,9 +41,33 @@ function issueRequest(baseUrl: string, req: string | Request, init?: RequestInit } export function fetch(url: string | Request, init?: RequestInit): Promise { - const promise = issueRequest(baseUrl(), url, init); - addTask(promise); - return promise; + // As of domain-task 2.0.0, we no longer auto-add the 'fetch' promise to the current domain task list. + // This is because it's misleading to do so, and can result in race-condition bugs, e.g., + // https://github.com/aspnet/JavaScriptServices/issues/166 + // + // Consider this usage: + // + // import { fetch } from 'domain-task/fetch'; + // fetch(something).then(callback1).then(callback2) ...etc... .then(data => updateCriticalAppState); + // + // If we auto-add the very first 'fetch' promise to the domain task list, then the domain task completion + // callback might fire at any point among all the chained callbacks. If there are enough chained callbacks, + // it's likely to occur before the final 'updateCriticalAppState' one. Previously we thought it was enough + // for domain-task to use setTimeout(..., 0) so that its action occurred after all synchronously-scheduled + // chained promise callbacks, but this turns out not to be the case. Current versions of Node will run + // setTimeout-scheduled callbacks *before* setImmediate ones, if their timeout has elapsed. So even if you + // use setTimeout(..., 10), then this callback will run before setImmediate(...) if there were 10ms or more + // of CPU-blocking activity. In other words, a race condition. + // + // The correct design is for the final chained promise to be the thing added to the domain task list, but + // this can only be done by the developer and not baked into the 'fetch' API. The developer needs to write + // something like: + // + // var myTask = fetch(something).then(callback1).then(callback2) ...etc... .then(data => updateCriticalAppState); + // addDomainTask(myTask); + // + // ... so that the domain-tasks-completed callback never fires until after 'updateCriticalAppState'. + return issueRequest(baseUrl(), url, init); } export function baseUrl(url?: string): string { diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/index.ts b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/index.ts new file mode 100644 index 00000000..78f0c17c --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/src/index.ts @@ -0,0 +1,3 @@ +// This file determines the top-level package exports +export { addTask, run } from './main'; +export { fetch } from './fetch'; From 58bf117442cb10daadc81938fe3e6ffda5007efe Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 11 Jul 2016 11:55:01 +0100 Subject: [PATCH 18/28] Update templates to domain-task 2.0.0. Fixes #166. --- samples/react/MusicStore/ReactApp/store/AlbumDetails.ts | 5 +++-- samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts | 5 +++-- samples/react/MusicStore/ReactApp/store/GenreDetails.ts | 5 +++-- samples/react/MusicStore/ReactApp/store/GenreList.ts | 5 +++-- samples/react/MusicStore/package.json | 2 +- samples/react/ReactGrid/package.json | 2 +- templates/ReactReduxSpa/ClientApp/store/WeatherForecasts.ts | 5 +++-- templates/ReactReduxSpa/package.json | 2 +- templates/yeoman/src/generator/package.json | 2 +- 9 files changed, 19 insertions(+), 14 deletions(-) diff --git a/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts b/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts index 247c947a..ad3f458e 100644 --- a/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts +++ b/samples/react/MusicStore/ReactApp/store/AlbumDetails.ts @@ -1,4 +1,4 @@ -import { fetch } from 'domain-task/fetch'; +import { fetch, addTask } from 'domain-task'; import { typeName, isActionType, Action, Reducer } from 'redux-typed'; import { ActionCreator } from './'; import { Genre } from './GenreList'; @@ -51,7 +51,7 @@ export const actionCreators = { requestAlbumDetails: (albumId: number): ActionCreator => (dispatch, getState) => { // Only load if it's not already loaded (or currently being loaded) if (albumId !== getState().albumDetails.requestedAlbumId) { - fetch(`/api/albums/${ albumId }`) + let fetchTask = fetch(`/api/albums/${ albumId }`) .then(results => results.json()) .then(album => { // Only replace state if it's still the most recent request @@ -60,6 +60,7 @@ export const actionCreators = { } }); + addTask(fetchTask); // Ensure server-side prerendering waits for this to complete dispatch(new RequestAlbumDetails(albumId)); } } diff --git a/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts b/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts index a3d13b15..f9e78282 100644 --- a/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts +++ b/samples/react/MusicStore/ReactApp/store/FeaturedAlbums.ts @@ -1,4 +1,4 @@ -import { fetch } from 'domain-task/fetch'; +import { fetch, addTask } from 'domain-task'; import { typeName, isActionType, Action, Reducer } from 'redux-typed'; import { ActionCreator } from './'; @@ -39,10 +39,11 @@ class ReceiveFeaturedAlbums extends Action { export const actionCreators = { requestFeaturedAlbums: (): ActionCreator => (dispatch, getState) => { if (!getState().featuredAlbums.isLoaded) { - fetch('/api/albums/mostPopular') + let fetchTask = fetch('/api/albums/mostPopular') .then(results => results.json()) .then(albums => dispatch(new ReceiveFeaturedAlbums(albums))); + addTask(fetchTask); // Ensure server-side prerendering waits for this to complete return dispatch(new RequestFeaturedAlbums()); } } diff --git a/samples/react/MusicStore/ReactApp/store/GenreDetails.ts b/samples/react/MusicStore/ReactApp/store/GenreDetails.ts index 2a5f8ff0..24848a10 100644 --- a/samples/react/MusicStore/ReactApp/store/GenreDetails.ts +++ b/samples/react/MusicStore/ReactApp/store/GenreDetails.ts @@ -1,4 +1,4 @@ -import { fetch } from 'domain-task/fetch'; +import { fetch, addTask } from 'domain-task'; import { typeName, isActionType, Action, Reducer } from 'redux-typed'; import { ActionCreator } from './'; import { Album } from './FeaturedAlbums'; @@ -39,7 +39,7 @@ export const actionCreators = { requestGenreDetails: (genreId: number): ActionCreator => (dispatch, getState) => { // Only load if it's not already loaded (or currently being loaded) if (genreId !== getState().genreDetails.requestedGenreId) { - fetch(`/api/genres/${ genreId }/albums`) + let fetchTask = fetch(`/api/genres/${ genreId }/albums`) .then(results => results.json()) .then(albums => { // Only replace state if it's still the most recent request @@ -48,6 +48,7 @@ export const actionCreators = { } }); + addTask(fetchTask); // Ensure server-side prerendering waits for this to complete dispatch(new RequestGenreDetails(genreId)); } } diff --git a/samples/react/MusicStore/ReactApp/store/GenreList.ts b/samples/react/MusicStore/ReactApp/store/GenreList.ts index e24e32cd..8843f22e 100644 --- a/samples/react/MusicStore/ReactApp/store/GenreList.ts +++ b/samples/react/MusicStore/ReactApp/store/GenreList.ts @@ -1,4 +1,4 @@ -import { fetch } from 'domain-task/fetch'; +import { fetch, addTask } from 'domain-task'; import { typeName, isActionType, Action, Reducer } from 'redux-typed'; import { ActionCreator } from './'; @@ -34,9 +34,10 @@ class ReceiveGenresList extends Action { export const actionCreators = { requestGenresList: (): ActionCreator => (dispatch, getState) => { if (!getState().genreList.isLoaded) { - fetch('/api/genres') + let fetchTask = fetch('/api/genres') .then(results => results.json()) .then(genres => dispatch(new ReceiveGenresList(genres))); + addTask(fetchTask); // Ensure server-side prerendering waits for this to complete } } }; diff --git a/samples/react/MusicStore/package.json b/samples/react/MusicStore/package.json index 448f1c9f..084f8ee9 100644 --- a/samples/react/MusicStore/package.json +++ b/samples/react/MusicStore/package.json @@ -25,7 +25,7 @@ "aspnet-webpack-react": "^1.0.1", "bootstrap": "^3.3.6", "domain-context": "^0.5.1", - "domain-task": "^1.0.0", + "domain-task": "^2.0.0", "history": "^2.0.0", "isomorphic-fetch": "^2.2.1", "memory-fs": "^0.3.0", diff --git a/samples/react/ReactGrid/package.json b/samples/react/ReactGrid/package.json index a2de71cd..123cc473 100644 --- a/samples/react/ReactGrid/package.json +++ b/samples/react/ReactGrid/package.json @@ -4,7 +4,7 @@ "dependencies": { "babel-core": "^6.4.5", "bootstrap": "^3.3.5", - "domain-task": "^1.0.0", + "domain-task": "^2.0.0", "formsy-react": "^0.17.0", "formsy-react-components": "^0.6.3", "griddle-react": "^0.3.1", diff --git a/templates/ReactReduxSpa/ClientApp/store/WeatherForecasts.ts b/templates/ReactReduxSpa/ClientApp/store/WeatherForecasts.ts index b3e6b32a..3101a7dd 100644 --- a/templates/ReactReduxSpa/ClientApp/store/WeatherForecasts.ts +++ b/templates/ReactReduxSpa/ClientApp/store/WeatherForecasts.ts @@ -1,4 +1,4 @@ -import { fetch } from 'domain-task/fetch'; +import { fetch, addTask } from 'domain-task'; import { typeName, isActionType, Action, Reducer } from 'redux-typed'; import { ActionCreator } from './'; @@ -45,12 +45,13 @@ export const actionCreators = { requestWeatherForecasts: (startDateIndex: number): ActionCreator => (dispatch, getState) => { // Only load data if it's something we don't already have (and are not already loading) if (startDateIndex !== getState().weatherForecasts.startDateIndex) { - fetch(`/api/SampleData/WeatherForecasts?startDateIndex=${ startDateIndex }`) + let fetchTask = fetch(`/api/SampleData/WeatherForecasts?startDateIndex=${ startDateIndex }`) .then(response => response.json()) .then((data: WeatherForecast[]) => { dispatch(new ReceiveWeatherForecasts(startDateIndex, data)); }); + addTask(fetchTask); // Ensure server-side prerendering waits for this to complete dispatch(new RequestWeatherForecasts(startDateIndex)); } } diff --git a/templates/ReactReduxSpa/package.json b/templates/ReactReduxSpa/package.json index 1e7c3e4a..4070c8af 100644 --- a/templates/ReactReduxSpa/package.json +++ b/templates/ReactReduxSpa/package.json @@ -23,7 +23,7 @@ "aspnet-prerendering": "^1.0.2", "aspnet-webpack": "^1.0.2", "babel-core": "^6.5.2", - "domain-task": "^1.0.0", + "domain-task": "^2.0.0", "react": "^15.0.1", "react-dom": "^15.0.1", "react-redux": "^4.4.4", diff --git a/templates/yeoman/src/generator/package.json b/templates/yeoman/src/generator/package.json index 2a340a9d..ad60454d 100644 --- a/templates/yeoman/src/generator/package.json +++ b/templates/yeoman/src/generator/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.1", + "version": "0.2.2", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", From 057efb43c8faa619b1f1759584c8e2b56f126f1e Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 18 Jul 2016 13:55:26 +0100 Subject: [PATCH 19/28] aspnet-webpack module now preserves 'path' and 'publicPath' config settings when invoking Webpack compiler. Fixes #176. --- .../npm/aspnet-webpack/package.json | 2 +- .../npm/aspnet-webpack/src/LoadViaWebpack.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index db18530e..6ff72aa8 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-webpack", - "version": "1.0.5", + "version": "1.0.6", "description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts index 38cb948c..a6f6fa72 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/LoadViaWebpack.ts @@ -4,6 +4,7 @@ // that your loader plugins (e.g., require('./mystyles.less')) work in exactly the same way on the server as // on the client. import 'es6-promise'; +import * as path from 'path'; import * as webpack from 'webpack'; import { requireNewCopy } from './RequireNewCopy'; @@ -40,7 +41,15 @@ function loadViaWebpackNoCache(webpackConfigPath: string, modulePath: string) const webpackConfig: webpack.Configuration = requireNewCopy(webpackConfigPath); webpackConfig.entry = modulePath; webpackConfig.target = 'node'; - webpackConfig.output = { path: '/', filename: 'webpack-output.js', libraryTarget: 'commonjs' }; + + // Make sure we preserve the 'path' and 'publicPath' config values if specified, as these + // can affect the build output (e.g., when using 'file' loader, the publicPath value gets + // set as a prefix on output paths). + webpackConfig.output = webpackConfig.output || {}; + webpackConfig.output.path = webpackConfig.output.path || '/'; + webpackConfig.output.filename = 'webpack-output.js'; + webpackConfig.output.libraryTarget = 'commonjs'; + const outputVirtualPath = path.join(webpackConfig.output.path, webpackConfig.output.filename); // In Node, we want anything under /node_modules/ to be loaded natively and not bundled into the output // (partly because it's faster, but also because otherwise there'd be different instances of modules @@ -85,7 +94,7 @@ function loadViaWebpackNoCache(webpackConfigPath: string, modulePath: string) + stats.toString({ chunks: false })); } - const fileContent = compiler.outputFileSystem.readFileSync('/webpack-output.js', 'utf8'); + const fileContent = compiler.outputFileSystem.readFileSync(outputVirtualPath, 'utf8'); const moduleInstance = requireFromString(fileContent); resolve(moduleInstance); } catch(ex) { From 7119815d04b46da2565d17a0827524fc44f44fb4 Mon Sep 17 00:00:00 2001 From: thunder7553 Date: Wed, 13 Jul 2016 12:49:09 +0200 Subject: [PATCH 20/28] Added OnBeforeStartExternalProcess callback which to NodeServicesOptions (and OutOfProcessNodeInstance, SocketNodeInstance and HttpNodeInstance) to configure environment of the node.exe process to be started, and the path to the node executable itself. Fixes #20 --- .../Configuration/NodeServicesOptions.cs | 2 +- .../HostingModels/HttpNodeInstance.cs | 30 ++++++++++--------- .../HostingModels/OutOfProcessNodeInstance.cs | 11 +++++-- .../HostingModels/SocketNodeInstance.cs | 11 +++---- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index fd8b3642..dcbfce59 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -14,7 +14,7 @@ public NodeServicesOptions() HostingModel = DefaultNodeHostingModel; WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); } - + public Action OnBeforeStartExternalProcess { get; set; } public NodeHostingModel HostingModel { get; set; } public Func NodeInstanceFactory { get; set; } public string ProjectPath { get; set; } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 1b59e873..749118f6 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -28,21 +28,22 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance ContractResolver = new CamelCasePropertyNamesContractResolver() }; - private readonly HttpClient _client; + private readonly HttpClient _client; private bool _disposed; private int _portNumber; - public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0) + public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0, Action onBeforeStartExternalProcess = null) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, watchFileExtensions, - MakeCommandLineOptions(port)) + MakeCommandLineOptions(port), + onBeforeStartExternalProcess) { _client = new HttpClient(); - } + } private static string MakeCommandLineOptions(int port) { @@ -112,18 +113,19 @@ protected override void OnOutputDataReceived(string outputData) } } - protected override void Dispose(bool disposing) { - base.Dispose(disposing); + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); - if (!_disposed) + if (!_disposed) { - if (disposing) + if (disposing) { - _client.Dispose(); - } + _client.Dispose(); + } - _disposed = true; - } - } - } + _disposed = true; + } + } + } } \ 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 c45c1004..3690a47b 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -31,10 +31,11 @@ public OutOfProcessNodeInstance( string entryPointScript, string projectPath, string[] watchFileExtensions, - string commandLineArguments) + string commandLineArguments, + Action onBeforeStartExternalProcess = null) { _entryPointScript = new StringAsTempFile(entryPointScript); - _nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments); + _nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments, onBeforeStartExternalProcess); _watchFileExtensions = watchFileExtensions; _fileSystemWatcher = BeginFileWatcher(projectPath); ConnectToInputOutputStreams(); @@ -111,7 +112,7 @@ private void EnsureFileSystemWatcherIsDisposed() } } - private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments) + private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments, Action onBeforeStartExternalProcess) { var startInfo = new ProcessStartInfo("node") { @@ -136,6 +137,10 @@ private static Process LaunchNodeProcess(string entryPointFilename, string proje #else startInfo.Environment["NODE_PATH"] = nodePathValue; #endif + if (onBeforeStartExternalProcess != null) + { + onBeforeStartExternalProcess(startInfo); + } var process = Process.Start(startInfo); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 3fb25f06..bb2611e4 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels /// internal class SocketNodeInstance : OutOfProcessNodeInstance { - private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings + private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; @@ -36,16 +36,17 @@ internal class SocketNodeInstance : OutOfProcessNodeInstance private string _socketAddress; private VirtualConnectionClient _virtualConnectionClient; - public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress): base( + public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, Action onBeforeStartExternalProcess = null) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), "/Content/Node/entrypoint-socket.js"), projectPath, watchFileExtensions, - MakeNewCommandLineOptions(socketAddress)) + MakeNewCommandLineOptions(socketAddress), + onBeforeStartExternalProcess) { _socketAddress = socketAddress; - } + } protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { @@ -170,7 +171,7 @@ private static async Task ReadJsonAsync(Stream stream) private static async Task ReadAllBytesAsync(Stream input) { - byte[] buffer = new byte[16*1024]; + byte[] buffer = new byte[16 * 1024]; using (var ms = new MemoryStream()) { From a14d9ba2df56d5e00f53355fc452e0f57b9d8069 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 18 Jul 2016 14:39:36 +0100 Subject: [PATCH 21/28] Change onBeforeStartExternalProcess to a virtual method, so as to avoid expanding the set of constructor params in all hosting models --- .../HostingModels/HttpNodeInstance.cs | 5 +- .../HostingModels/OutOfProcessNodeInstance.cs | 68 ++++++++++--------- .../HostingModels/SocketNodeInstance.cs | 5 +- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 749118f6..55efaeb7 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -32,15 +32,14 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance private bool _disposed; private int _portNumber; - public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0, Action onBeforeStartExternalProcess = null) + public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, watchFileExtensions, - MakeCommandLineOptions(port), - onBeforeStartExternalProcess) + MakeCommandLineOptions(port)) { _client = new HttpClient(); } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 3690a47b..a7cb161b 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -31,11 +31,12 @@ public OutOfProcessNodeInstance( string entryPointScript, string projectPath, string[] watchFileExtensions, - string commandLineArguments, - Action onBeforeStartExternalProcess = null) + string commandLineArguments) { _entryPointScript = new StringAsTempFile(entryPointScript); - _nodeProcess = LaunchNodeProcess(_entryPointScript.FileName, projectPath, commandLineArguments, onBeforeStartExternalProcess); + + var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments); + _nodeProcess = LaunchNodeProcess(startInfo); _watchFileExtensions = watchFileExtensions; _fileSystemWatcher = BeginFileWatcher(projectPath); ConnectToInputOutputStreams(); @@ -72,6 +73,37 @@ public void Dispose() protected abstract Task InvokeExportAsync(NodeInvocationInfo invocationInfo); + // This method is virtual, as it provides a way to override the NODE_PATH or the path to node.exe + protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( + string entryPointFilename, string projectPath, string commandLineArguments) + { + var startInfo = new ProcessStartInfo("node") + { + Arguments = "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty), + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = projectPath + }; + + // Append projectPath to NODE_PATH so it can locate node_modules + var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; + if (existingNodePath != string.Empty) + { + existingNodePath += ":"; + } + + var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules"); +#if NET451 + startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue; +#else + startInfo.Environment["NODE_PATH"] = nodePathValue; +#endif + + return startInfo; + } + protected virtual void OnOutputDataReceived(string outputData) { Console.WriteLine("[Node] " + outputData); @@ -112,36 +144,8 @@ private void EnsureFileSystemWatcherIsDisposed() } } - private static Process LaunchNodeProcess(string entryPointFilename, string projectPath, string commandLineArguments, Action onBeforeStartExternalProcess) + private static Process LaunchNodeProcess(ProcessStartInfo startInfo) { - var startInfo = new ProcessStartInfo("node") - { - Arguments = "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty), - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - WorkingDirectory = projectPath - }; - - // Append projectPath to NODE_PATH so it can locate node_modules - var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; - if (existingNodePath != string.Empty) - { - existingNodePath += ":"; - } - - var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules"); -#if NET451 - startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue; -#else - startInfo.Environment["NODE_PATH"] = nodePathValue; -#endif - if (onBeforeStartExternalProcess != null) - { - onBeforeStartExternalProcess(startInfo); - } - var process = Process.Start(startInfo); // On Mac at least, a killed child process is left open as a zombie until the parent diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index bb2611e4..114e0eab 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -36,14 +36,13 @@ internal class SocketNodeInstance : OutOfProcessNodeInstance private string _socketAddress; private VirtualConnectionClient _virtualConnectionClient; - public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, Action onBeforeStartExternalProcess = null) : base( + public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), "/Content/Node/entrypoint-socket.js"), projectPath, watchFileExtensions, - MakeNewCommandLineOptions(socketAddress), - onBeforeStartExternalProcess) + MakeNewCommandLineOptions(socketAddress)) { _socketAddress = socketAddress; } From 27ffa72e0d18c85624af98661e58e9e76e75065b Mon Sep 17 00:00:00 2001 From: Paul Knopf Date: Fri, 15 Jul 2016 00:42:17 -0400 Subject: [PATCH 22/28] Adding support for capturing the output of a node instance for custom logging implementations. --- .../Configuration/Configuration.cs | 4 ++-- .../Configuration/NodeServicesOptions.cs | 2 ++ .../HostingModels/HttpNodeInstance.cs | 6 ++++-- .../HostingModels/OutOfProcessNodeInstance.cs | 12 +++++++++--- .../HostingModels/SocketNodeInstance.cs | 6 ++++-- .../Util/ConsoleNodeInstanceOutputLogger.cs | 17 +++++++++++++++++ .../Util/INodeInstanceOutputLogger.cs | 14 ++++++++++++++ 7 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs index a497c641..8d41762e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -46,10 +46,10 @@ private static INodeInstance CreateNodeInstance(NodeServicesOptions options) switch (options.HostingModel) { case NodeHostingModel.Http: - return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, /* port */ 0); + return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, /* port */ 0, options.NodeInstanceOutputLogger); case NodeHostingModel.Socket: var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string - return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName); + return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, options.NodeInstanceOutputLogger); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index dcbfce59..d20419f5 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.NodeServices.HostingModels; +using Microsoft.AspNetCore.NodeServices.Util; namespace Microsoft.AspNetCore.NodeServices { @@ -19,5 +20,6 @@ public NodeServicesOptions() public Func NodeInstanceFactory { get; set; } public string ProjectPath { get; set; } public string[] WatchFileExtensions { get; set; } + public INodeInstanceOutputLogger NodeInstanceOutputLogger { get; set; } } } \ 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 55efaeb7..53c50b5f 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.NodeServices.Util; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -32,14 +33,15 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance private bool _disposed; private int _portNumber; - public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0) + public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0, INodeInstanceOutputLogger nodeInstanceOutputLogger = null) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, watchFileExtensions, - MakeCommandLineOptions(port)) + MakeCommandLineOptions(port), + nodeInstanceOutputLogger) { _client = new HttpClient(); } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index a7cb161b..558ca07c 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.NodeServices.Util; namespace Microsoft.AspNetCore.NodeServices.HostingModels { @@ -26,13 +27,18 @@ public abstract class OutOfProcessNodeInstance : INodeInstance private readonly Process _nodeProcess; private bool _nodeProcessNeedsRestart; private readonly string[] _watchFileExtensions; + private INodeInstanceOutputLogger _nodeInstanceOutputLogger; public OutOfProcessNodeInstance( string entryPointScript, string projectPath, string[] watchFileExtensions, - string commandLineArguments) + string commandLineArguments, + INodeInstanceOutputLogger nodeOutputLogger) { + _nodeInstanceOutputLogger = nodeOutputLogger; + if(_nodeInstanceOutputLogger == null) + _nodeInstanceOutputLogger = new ConsoleNodeInstanceOutputLogger(); _entryPointScript = new StringAsTempFile(entryPointScript); var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments); @@ -106,12 +112,12 @@ protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( protected virtual void OnOutputDataReceived(string outputData) { - Console.WriteLine("[Node] " + outputData); + _nodeInstanceOutputLogger.LogOutputData(outputData); } protected virtual void OnErrorDataReceived(string errorData) { - Console.WriteLine("[Node] " + errorData); + _nodeInstanceOutputLogger.LogErrorData(errorData); } protected virtual void Dispose(bool disposing) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 114e0eab..b6ba1063 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections; using Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections; +using Microsoft.AspNetCore.NodeServices.Util; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -36,13 +37,14 @@ internal class SocketNodeInstance : OutOfProcessNodeInstance private string _socketAddress; private VirtualConnectionClient _virtualConnectionClient; - public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress) : base( + public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, INodeInstanceOutputLogger nodeInstanceOutputLogger = null) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), "/Content/Node/entrypoint-socket.js"), projectPath, watchFileExtensions, - MakeNewCommandLineOptions(socketAddress)) + MakeNewCommandLineOptions(socketAddress), + nodeInstanceOutputLogger) { _socketAddress = socketAddress; } diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs b/src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs new file mode 100644 index 00000000..6c02d5e4 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs @@ -0,0 +1,17 @@ +using System; + +namespace Microsoft.AspNetCore.NodeServices.Util +{ + public class ConsoleNodeInstanceOutputLogger : INodeInstanceOutputLogger + { + public void LogOutputData(string outputData) + { + Console.WriteLine("[Node] " + outputData); + } + + public void LogErrorData(string errorData) + { + Console.WriteLine("[Node] " + errorData); + } + } +} diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs b/src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs new file mode 100644 index 00000000..a41b3499 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.NodeServices.Util +{ + public interface INodeInstanceOutputLogger + { + void LogOutputData(string outputData); + + void LogErrorData(string errorData); + } +} From f4efcacd40ce67483ee6a7f4d31b32cd9a408012 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 18 Jul 2016 15:56:45 +0100 Subject: [PATCH 23/28] Switch to native .NET logging APIs --- .../Configuration/Configuration.cs | 30 ++++++++++++++++--- .../Configuration/NodeServicesOptions.cs | 4 +-- .../HostingModels/HttpNodeInstance.cs | 4 +-- .../HostingModels/OutOfProcessNodeInstance.cs | 22 +++++++------- .../HostingModels/SocketNodeInstance.cs | 8 ++--- .../Util/ConsoleNodeInstanceOutputLogger.cs | 17 ----------- .../Util/INodeInstanceOutputLogger.cs | 14 --------- .../project.json | 1 + 8 files changed, 46 insertions(+), 54 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs delete mode 100644 src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs index 8d41762e..bbae3d29 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -2,11 +2,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.NodeServices.HostingModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; namespace Microsoft.AspNetCore.NodeServices { public static class Configuration { + const string LogCategoryName = "Microsoft.AspNetCore.NodeServices"; + public static void AddNodeServices(this IServiceCollection serviceCollection) => AddNodeServices(serviceCollection, new NodeServicesOptions()); @@ -15,13 +19,24 @@ public static void AddNodeServices(this IServiceCollection serviceCollection, No serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => { // Since this instance is being created through DI, we can access the IHostingEnvironment - // to populate options.ProjectPath if it wasn't explicitly specified. - var hostEnv = serviceProvider.GetRequiredService(); + // to populate options.ProjectPath if it wasn't explicitly specified. if (string.IsNullOrEmpty(options.ProjectPath)) { + var hostEnv = serviceProvider.GetRequiredService(); options.ProjectPath = hostEnv.ContentRootPath; } + // Likewise, if no logger was specified explicitly, we should use the one from DI. + // If it doesn't provide one, CreateNodeInstance will set up a default. + if (options.NodeInstanceOutputLogger == null) + { + var loggerFactory = serviceProvider.GetService(); + if (loggerFactory != null) + { + options.NodeInstanceOutputLogger = loggerFactory.CreateLogger(LogCategoryName); + } + } + return new NodeServicesImpl(options, () => CreateNodeInstance(options)); }); } @@ -33,6 +48,13 @@ public static INodeServices CreateNodeServices(NodeServicesOptions options) private static INodeInstance CreateNodeInstance(NodeServicesOptions options) { + // If you've specified no logger, fall back on a default console logger + var logger = options.NodeInstanceOutputLogger; + if (logger == null) + { + logger = new ConsoleLogger(LogCategoryName, null, false); + } + if (options.NodeInstanceFactory != null) { // If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for @@ -46,10 +68,10 @@ private static INodeInstance CreateNodeInstance(NodeServicesOptions options) switch (options.HostingModel) { case NodeHostingModel.Http: - return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, /* port */ 0, options.NodeInstanceOutputLogger); + return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, logger, /* port */ 0); case NodeHostingModel.Socket: var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string - return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, options.NodeInstanceOutputLogger); + return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, logger); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index d20419f5..98c50ecb 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -1,6 +1,6 @@ using System; using Microsoft.AspNetCore.NodeServices.HostingModels; -using Microsoft.AspNetCore.NodeServices.Util; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.NodeServices { @@ -20,6 +20,6 @@ public NodeServicesOptions() public Func NodeInstanceFactory { get; set; } public string ProjectPath { get; set; } public string[] WatchFileExtensions { get; set; } - public INodeInstanceOutputLogger NodeInstanceOutputLogger { get; set; } + public ILogger NodeInstanceOutputLogger { get; set; } } } \ 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 53c50b5f..53dabf92 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -4,7 +4,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.AspNetCore.NodeServices.Util; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -33,7 +33,7 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance private bool _disposed; private int _portNumber; - public HttpNodeInstance(string projectPath, string[] watchFileExtensions, int port = 0, INodeInstanceOutputLogger nodeInstanceOutputLogger = null) + public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger, int port = 0) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 558ca07c..0cba1cd2 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.NodeServices.Util; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.NodeServices.HostingModels { @@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels /// public abstract class OutOfProcessNodeInstance : INodeInstance { + protected readonly ILogger OutputLogger; private const string ConnectionEstablishedMessage = "[Microsoft.AspNetCore.NodeServices:Listening]"; private readonly TaskCompletionSource _connectionIsReadySource = new TaskCompletionSource(); private bool _disposed; @@ -27,18 +28,20 @@ public abstract class OutOfProcessNodeInstance : INodeInstance private readonly Process _nodeProcess; private bool _nodeProcessNeedsRestart; private readonly string[] _watchFileExtensions; - private INodeInstanceOutputLogger _nodeInstanceOutputLogger; public OutOfProcessNodeInstance( string entryPointScript, string projectPath, string[] watchFileExtensions, string commandLineArguments, - INodeInstanceOutputLogger nodeOutputLogger) + ILogger nodeOutputLogger) { - _nodeInstanceOutputLogger = nodeOutputLogger; - if(_nodeInstanceOutputLogger == null) - _nodeInstanceOutputLogger = new ConsoleNodeInstanceOutputLogger(); + if (nodeOutputLogger == null) + { + throw new ArgumentNullException(nameof(nodeOutputLogger)); + } + + OutputLogger = nodeOutputLogger; _entryPointScript = new StringAsTempFile(entryPointScript); var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments); @@ -112,12 +115,12 @@ protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( protected virtual void OnOutputDataReceived(string outputData) { - _nodeInstanceOutputLogger.LogOutputData(outputData); + OutputLogger.LogInformation(outputData); } protected virtual void OnErrorDataReceived(string errorData) { - _nodeInstanceOutputLogger.LogErrorData(errorData); + OutputLogger.LogError(errorData); } protected virtual void Dispose(bool disposing) @@ -255,8 +258,7 @@ private bool IsFilenameBeingWatched(string fullPath) private void RestartDueToFileChange(string fullPath) { - // TODO: Use proper logger - Console.WriteLine($"Node will restart because file changed: {fullPath}"); + OutputLogger.LogInformation($"Node will restart because file changed: {fullPath}"); _nodeProcessNeedsRestart = true; diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index b6ba1063..f5bbce1a 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices.HostingModels.PhysicalConnections; using Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections; -using Microsoft.AspNetCore.NodeServices.Util; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -37,7 +37,7 @@ internal class SocketNodeInstance : OutOfProcessNodeInstance private string _socketAddress; private VirtualConnectionClient _virtualConnectionClient; - public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, INodeInstanceOutputLogger nodeInstanceOutputLogger = null) : base( + public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, ILogger nodeInstanceOutputLogger) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), "/Content/Node/entrypoint-socket.js"), @@ -125,9 +125,7 @@ private async Task EnsureVirtualConnectionClientCreated() // failure, this Node instance is no longer usable and should be discarded. _connectionHasFailed = true; - // 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); + OutputLogger.LogError(0, ex, ex.Message); }; } } diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs b/src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs deleted file mode 100644 index 6c02d5e4..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/Util/ConsoleNodeInstanceOutputLogger.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Microsoft.AspNetCore.NodeServices.Util -{ - public class ConsoleNodeInstanceOutputLogger : INodeInstanceOutputLogger - { - public void LogOutputData(string outputData) - { - Console.WriteLine("[Node] " + outputData); - } - - public void LogErrorData(string errorData) - { - Console.WriteLine("[Node] " + errorData); - } - } -} diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs b/src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs deleted file mode 100644 index a41b3499..00000000 --- a/src/Microsoft.AspNetCore.NodeServices/Util/INodeInstanceOutputLogger.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.NodeServices.Util -{ - public interface INodeInstanceOutputLogger - { - void LogOutputData(string outputData); - - void LogErrorData(string errorData); - } -} diff --git a/src/Microsoft.AspNetCore.NodeServices/project.json b/src/Microsoft.AspNetCore.NodeServices/project.json index 0da37feb..415aff7c 100644 --- a/src/Microsoft.AspNetCore.NodeServices/project.json +++ b/src/Microsoft.AspNetCore.NodeServices/project.json @@ -9,6 +9,7 @@ "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0", + "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.PlatformAbstractions": "1.0.0", "Newtonsoft.Json": "9.0.1" }, From fae0a886af70d26eac6939fe4ec3dcf50b5460ce Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 18 Jul 2016 16:34:36 +0100 Subject: [PATCH 24/28] Transfer multiline log messages from Node to .NET without treating each line as a separate log entry --- .../Content/Node/entrypoint-http.js | 52 ++++++++++-- .../Content/Node/entrypoint-socket.js | 80 ++++++++++++++----- .../HostingModels/OutOfProcessNodeInstance.cs | 16 +++- .../TypeScript/HttpNodeInstanceEntryPoint.ts | 1 + .../SocketNodeInstanceEntryPoint.ts | 1 + .../TypeScript/Util/OverrideStdOutputs.ts | 37 +++++++++ 6 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/OverrideStdOutputs.ts diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index ab728aeb..fef873e7 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -54,9 +54,10 @@ "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 http = __webpack_require__(2); - var path = __webpack_require__(3); - var ArgsUtil_1 = __webpack_require__(4); + __webpack_require__(2); + var http = __webpack_require__(3); + var path = __webpack_require__(4); + var ArgsUtil_1 = __webpack_require__(5); // 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'); @@ -132,16 +133,57 @@ /* 2 */ /***/ function(module, exports) { - module.exports = require("http"); + // When Node writes to stdout/strerr, we capture that and convert the lines into calls on the + // active .NET ILogger. But by default, stdout/stderr don't have any way of distinguishing + // linebreaks inside log messages from the linebreaks that delimit separate log messages, + // so multiline strings will end up being written to the ILogger as multiple independent + // log messages. This makes them very hard to make sense of, especially when they represent + // something like stack traces. + // + // To fix this, we intercept stdout/stderr writes, and replace internal linebreaks with a + // marker token. When .NET receives the lines, it converts the marker tokens back to regular + // linebreaks within the logged messages. + // + // Note that it's better to do the interception at the stdout/stderr level, rather than at + // the console.log/console.error (etc.) level, because this takes place after any native + // message formatting has taken place (e.g., inserting values for % placeholders). + var findInternalNewlinesRegex = /\n(?!$)/g; + var encodedNewline = '__ns_newline__'; + encodeNewlinesWrittenToStream(process.stdout); + encodeNewlinesWrittenToStream(process.stderr); + function encodeNewlinesWrittenToStream(outputStream) { + var origWriteFunction = outputStream.write; + outputStream.write = function (value) { + // Only interfere with the write if it's definitely a string + if (typeof value === 'string') { + var argsClone = Array.prototype.slice.call(arguments, 0); + argsClone[0] = encodeNewlinesInString(value); + origWriteFunction.apply(this, argsClone); + } + else { + origWriteFunction.apply(this, arguments); + } + }; + } + function encodeNewlinesInString(str) { + return str.replace(findInternalNewlinesRegex, encodedNewline); + } + /***/ }, /* 3 */ /***/ function(module, exports) { - module.exports = require("path"); + module.exports = require("http"); /***/ }, /* 4 */ +/***/ function(module, exports) { + + module.exports = require("path"); + +/***/ }, +/* 5 */ /***/ function(module, exports) { "use strict"; diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js index 15856132..e59b6edf 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js @@ -44,19 +44,60 @@ /* 0 */ /***/ function(module, exports, __webpack_require__) { - module.exports = __webpack_require__(5); + module.exports = __webpack_require__(6); /***/ }, /* 1 */, -/* 2 */, -/* 3 */ +/* 2 */ /***/ function(module, exports) { - module.exports = require("path"); + // When Node writes to stdout/strerr, we capture that and convert the lines into calls on the + // active .NET ILogger. But by default, stdout/stderr don't have any way of distinguishing + // linebreaks inside log messages from the linebreaks that delimit separate log messages, + // so multiline strings will end up being written to the ILogger as multiple independent + // log messages. This makes them very hard to make sense of, especially when they represent + // something like stack traces. + // + // To fix this, we intercept stdout/stderr writes, and replace internal linebreaks with a + // marker token. When .NET receives the lines, it converts the marker tokens back to regular + // linebreaks within the logged messages. + // + // Note that it's better to do the interception at the stdout/stderr level, rather than at + // the console.log/console.error (etc.) level, because this takes place after any native + // message formatting has taken place (e.g., inserting values for % placeholders). + var findInternalNewlinesRegex = /\n(?!$)/g; + var encodedNewline = '__ns_newline__'; + encodeNewlinesWrittenToStream(process.stdout); + encodeNewlinesWrittenToStream(process.stderr); + function encodeNewlinesWrittenToStream(outputStream) { + var origWriteFunction = outputStream.write; + outputStream.write = function (value) { + // Only interfere with the write if it's definitely a string + if (typeof value === 'string') { + var argsClone = Array.prototype.slice.call(arguments, 0); + argsClone[0] = encodeNewlinesInString(value); + origWriteFunction.apply(this, argsClone); + } + else { + origWriteFunction.apply(this, arguments); + } + }; + } + function encodeNewlinesInString(str) { + return str.replace(findInternalNewlinesRegex, encodedNewline); + } + /***/ }, +/* 3 */, /* 4 */ +/***/ function(module, exports) { + + module.exports = require("path"); + +/***/ }, +/* 5 */ /***/ function(module, exports) { "use strict"; @@ -82,17 +123,18 @@ /***/ }, -/* 5 */ +/* 6 */ /***/ 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 net = __webpack_require__(6); - var path = __webpack_require__(3); - var readline = __webpack_require__(7); - var ArgsUtil_1 = __webpack_require__(4); - var virtualConnectionServer = __webpack_require__(8); + __webpack_require__(2); + var net = __webpack_require__(7); + var path = __webpack_require__(4); + var readline = __webpack_require__(8); + var ArgsUtil_1 = __webpack_require__(5); + var virtualConnectionServer = __webpack_require__(9); // 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'); @@ -150,24 +192,24 @@ /***/ }, -/* 6 */ +/* 7 */ /***/ function(module, exports) { module.exports = require("net"); /***/ }, -/* 7 */ +/* 8 */ /***/ function(module, exports) { module.exports = require("readline"); /***/ }, -/* 8 */ +/* 9 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var events_1 = __webpack_require__(9); - var VirtualConnection_1 = __webpack_require__(10); + var events_1 = __webpack_require__(10); + var VirtualConnection_1 = __webpack_require__(11); // 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; @@ -348,13 +390,13 @@ /***/ }, -/* 9 */ +/* 10 */ /***/ function(module, exports) { module.exports = require("events"); /***/ }, -/* 10 */ +/* 11 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -363,7 +405,7 @@ function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; - var stream_1 = __webpack_require__(11); + var stream_1 = __webpack_require__(12); /** * Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection. */ @@ -404,7 +446,7 @@ /***/ }, -/* 11 */ +/* 12 */ /***/ function(module, exports) { module.exports = require("stream"); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 0cba1cd2..06649994 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -168,6 +168,18 @@ private static Process LaunchNodeProcess(ProcessStartInfo startInfo) return process; } + private static string UnencodeNewlines(string str) + { + if (str != null) + { + // The token here needs to match the const in OverrideStdOutputs.ts. + // See the comment there for why we're doing this. + str = str.Replace("__ns_newline__", Environment.NewLine); + } + + return str; + } + private void ConnectToInputOutputStreams() { var initializationIsCompleted = false; @@ -181,7 +193,7 @@ private void ConnectToInputOutputStreams() } else if (evt.Data != null) { - OnOutputDataReceived(evt.Data); + OnOutputDataReceived(UnencodeNewlines(evt.Data)); } }; @@ -197,7 +209,7 @@ private void ConnectToInputOutputStreams() } else { - OnErrorDataReceived(evt.Data); + OnErrorDataReceived(UnencodeNewlines(evt.Data)); } } }; diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index 3be759a2..0e10d993 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -1,5 +1,6 @@ // 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 './Util/OverrideStdOutputs'; import * as http from 'http'; import * as path from 'path'; import { parseArgs } from './Util/ArgsUtil'; diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts index 0aec5417..0a6f713d 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts @@ -1,5 +1,6 @@ // 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 './Util/OverrideStdOutputs'; import * as net from 'net'; import * as path from 'path'; import * as readline from 'readline'; diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/OverrideStdOutputs.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/OverrideStdOutputs.ts new file mode 100644 index 00000000..01e4bc6d --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/OverrideStdOutputs.ts @@ -0,0 +1,37 @@ +// When Node writes to stdout/strerr, we capture that and convert the lines into calls on the +// active .NET ILogger. But by default, stdout/stderr don't have any way of distinguishing +// linebreaks inside log messages from the linebreaks that delimit separate log messages, +// so multiline strings will end up being written to the ILogger as multiple independent +// log messages. This makes them very hard to make sense of, especially when they represent +// something like stack traces. +// +// To fix this, we intercept stdout/stderr writes, and replace internal linebreaks with a +// marker token. When .NET receives the lines, it converts the marker tokens back to regular +// linebreaks within the logged messages. +// +// Note that it's better to do the interception at the stdout/stderr level, rather than at +// the console.log/console.error (etc.) level, because this takes place after any native +// message formatting has taken place (e.g., inserting values for % placeholders). +const findInternalNewlinesRegex = /\n(?!$)/g; +const encodedNewline = '__ns_newline__'; + +encodeNewlinesWrittenToStream(process.stdout); +encodeNewlinesWrittenToStream(process.stderr); + +function encodeNewlinesWrittenToStream(outputStream: NodeJS.WritableStream) { + const origWriteFunction = outputStream.write; + outputStream.write = function (value: any) { + // Only interfere with the write if it's definitely a string + if (typeof value === 'string') { + const argsClone = Array.prototype.slice.call(arguments, 0); + argsClone[0] = encodeNewlinesInString(value); + origWriteFunction.apply(this, argsClone); + } else { + origWriteFunction.apply(this, arguments); + } + }; +} + +function encodeNewlinesInString(str: string): string { + return str.replace(findInternalNewlinesRegex, encodedNewline); +} From f4afb25a2d37af9e369e430f507873f99494a10d Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 18 Jul 2016 16:54:52 +0100 Subject: [PATCH 25/28] Set ts-loader to "silent" mode until there's a fix for https://github.com/TypeStrong/ts-loader/issues/249 --- templates/Angular2Spa/webpack.config.js | 2 +- templates/KnockoutSpa/webpack.config.js | 2 +- templates/ReactReduxSpa/webpack.config.js | 2 +- templates/ReactSpa/webpack.config.js | 2 +- templates/WebApplicationBasic/webpack.config.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/Angular2Spa/webpack.config.js b/templates/Angular2Spa/webpack.config.js index bcd7d80a..877b8530 100644 --- a/templates/Angular2Spa/webpack.config.js +++ b/templates/Angular2Spa/webpack.config.js @@ -13,7 +13,7 @@ module.exports = merge({ }, module: { loaders: [ - { test: /\.ts$/, include: /ClientApp/, loader: 'ts-loader' }, + { test: /\.ts$/, include: /ClientApp/, loader: 'ts-loader?silent=true' }, { test: /\.html$/, loader: 'raw-loader' }, { test: /\.css/, loader: extractCSS.extract(['css']) } ] diff --git a/templates/KnockoutSpa/webpack.config.js b/templates/KnockoutSpa/webpack.config.js index 5f8d2909..9762ae89 100644 --- a/templates/KnockoutSpa/webpack.config.js +++ b/templates/KnockoutSpa/webpack.config.js @@ -11,7 +11,7 @@ module.exports = merge({ }, module: { loaders: [ - { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader' }, + { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader?silent=true' }, { test: /\.html$/, loader: 'raw-loader' } ] }, diff --git a/templates/ReactReduxSpa/webpack.config.js b/templates/ReactReduxSpa/webpack.config.js index b3657570..44a2ec42 100644 --- a/templates/ReactReduxSpa/webpack.config.js +++ b/templates/ReactReduxSpa/webpack.config.js @@ -14,7 +14,7 @@ module.exports = merge({ module: { loaders: [ { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'babel-loader' }, - { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader' }, + { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader?silent=true' }, { test: /\.css/, loader: extractCSS.extract(['css']) } ] }, diff --git a/templates/ReactSpa/webpack.config.js b/templates/ReactSpa/webpack.config.js index bb02f421..3cda8d89 100644 --- a/templates/ReactSpa/webpack.config.js +++ b/templates/ReactSpa/webpack.config.js @@ -12,7 +12,7 @@ module.exports = merge({ module: { loaders: [ { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'babel-loader' }, - { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader' } + { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader?silent=true' } ] }, entry: { diff --git a/templates/WebApplicationBasic/webpack.config.js b/templates/WebApplicationBasic/webpack.config.js index 030fe54b..129d22ce 100644 --- a/templates/WebApplicationBasic/webpack.config.js +++ b/templates/WebApplicationBasic/webpack.config.js @@ -13,7 +13,7 @@ module.exports = merge({ }, module: { loaders: [ - { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader' }, + { test: /\.ts(x?)$/, include: /ClientApp/, loader: 'ts-loader?silent=true' }, { test: /\.css/, loader: extractCSS.extract(['css']) } ] }, From edf1f88398d23209b92b22a5f9087e3420b594f2 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Mon, 18 Jul 2016 09:52:17 -0500 Subject: [PATCH 26/28] Updating to AutoMapper 5.0 --- samples/angular/MusicStore/Startup.cs | 19 +++++++++++-------- samples/angular/MusicStore/project.json | 2 +- samples/react/MusicStore/Startup.cs | 19 +++++++++++-------- samples/react/MusicStore/project.json | 2 +- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index 3d5e2636..a367d28f 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -45,14 +45,17 @@ public void ConfigureServices(IServiceCollection services) options.AddPolicy("app-ManageStore", new AuthorizationPolicyBuilder().RequireClaim("app-ManageStore", "Allowed").Build()); }); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); + Mapper.Initialize(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index d341cf13..ac5a9387 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -29,7 +29,7 @@ "Microsoft.Extensions.Logging.Debug": "1.0.0", "Microsoft.EntityFrameworkCore.SQLite": "1.0.0", "Microsoft.AspNetCore.AngularServices": "1.0.0-*", - "AutoMapper": "4.1.1" + "AutoMapper": "5.0.2" }, "frameworks": { "netcoreapp1.0": { diff --git a/samples/react/MusicStore/Startup.cs b/samples/react/MusicStore/Startup.cs index 4ea3fec3..1bc6003d 100755 --- a/samples/react/MusicStore/Startup.cs +++ b/samples/react/MusicStore/Startup.cs @@ -47,14 +47,17 @@ public void ConfigureServices(IServiceCollection services) options.AddPolicy("app-ManageStore", new AuthorizationPolicyBuilder().RequireClaim("app-ManageStore", "Allowed").Build()); }); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); - Mapper.CreateMap(); + Mapper.Initialize(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/samples/react/MusicStore/project.json b/samples/react/MusicStore/project.json index faebf7ac..6f5c801b 100755 --- a/samples/react/MusicStore/project.json +++ b/samples/react/MusicStore/project.json @@ -29,7 +29,7 @@ "Microsoft.Extensions.Logging.Debug": "1.0.0", "Microsoft.EntityFrameworkCore.SQLite": "1.0.0", "Microsoft.AspNetCore.ReactServices": "1.0.0-*", - "AutoMapper": "4.1.1" + "AutoMapper": "5.0.2" }, "frameworks": { "netcoreapp1.0": { From 749c7cb3ce751725d38e23edc5be3e6cf8873c3b Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 19 Jul 2016 15:50:54 +0100 Subject: [PATCH 27/28] Add example of full-page prerendering via a custom action result --- .../Webpack/ActionResults/PrerenderResult.cs | 47 +++++++++++++++++++ .../PrerenderResultExtensions.cs | 13 +++++ .../Webpack/Clientside/PrerenderingSample.ts | 17 +++++++ .../FullPagePrerenderingController.cs | 25 ++++++++++ samples/misc/Webpack/Startup.cs | 2 + samples/misc/Webpack/Views/Home/Index.cshtml | 3 ++ samples/misc/Webpack/package.json | 9 ++-- samples/misc/Webpack/tsconfig.json | 3 +- 8 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 samples/misc/Webpack/ActionResults/PrerenderResult.cs create mode 100644 samples/misc/Webpack/ActionResults/PrerenderResultExtensions.cs create mode 100644 samples/misc/Webpack/Clientside/PrerenderingSample.ts create mode 100644 samples/misc/Webpack/Controllers/FullPagePrerenderingController.cs diff --git a/samples/misc/Webpack/ActionResults/PrerenderResult.cs b/samples/misc/Webpack/ActionResults/PrerenderResult.cs new file mode 100644 index 00000000..b8e7ec15 --- /dev/null +++ b/samples/misc/Webpack/ActionResults/PrerenderResult.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.NodeServices; +using Microsoft.AspNetCore.SpaServices.Prerendering; +using Microsoft.Extensions.DependencyInjection; + +namespace Webpack.ActionResults +{ + // This is an example of how you could invoke the prerendering API from an ActionResult, so as to + // prerender a SPA component as the entire response page (instead of injecting the SPA component + // into a Razor view's output) + public class PrerenderResult : ActionResult + { + private JavaScriptModuleExport _moduleExport; + private object _dataToSupply; + + public PrerenderResult(JavaScriptModuleExport moduleExport, object dataToSupply = null) + { + _moduleExport = moduleExport; + _dataToSupply = dataToSupply; + } + + public override async Task ExecuteResultAsync(ActionContext context) + { + var nodeServices = context.HttpContext.RequestServices.GetRequiredService(); + var hostEnv = context.HttpContext.RequestServices.GetRequiredService(); + var applicationBasePath = hostEnv.ContentRootPath; + var request = context.HttpContext.Request; + var response = context.HttpContext.Response; + + var prerenderedHtml = await Prerenderer.RenderToString( + applicationBasePath, + nodeServices, + _moduleExport, + request.GetEncodedUrl(), + request.Path + request.QueryString.Value, + _dataToSupply + ); + + response.ContentType = "text/html"; + await response.WriteAsync(prerenderedHtml.Html); + } + } +} \ No newline at end of file diff --git a/samples/misc/Webpack/ActionResults/PrerenderResultExtensions.cs b/samples/misc/Webpack/ActionResults/PrerenderResultExtensions.cs new file mode 100644 index 00000000..926e1149 --- /dev/null +++ b/samples/misc/Webpack/ActionResults/PrerenderResultExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SpaServices.Prerendering; + +namespace Webpack.ActionResults +{ + public static class PrerenderResultExtensions + { + public static PrerenderResult Prerender(this ControllerBase controller, JavaScriptModuleExport exportToPrerender, object dataToSupply = null) + { + return new PrerenderResult(exportToPrerender, dataToSupply); + } + } +} diff --git a/samples/misc/Webpack/Clientside/PrerenderingSample.ts b/samples/misc/Webpack/Clientside/PrerenderingSample.ts new file mode 100644 index 00000000..143ddd04 --- /dev/null +++ b/samples/misc/Webpack/Clientside/PrerenderingSample.ts @@ -0,0 +1,17 @@ +export default function (params: any): Promise<{ html: string, globals?: any }> { + return new Promise((resolve, reject) => { + + // Here, you could put any logic that synchronously or asynchronously prerenders + // your SPA components. For example, see the boot-server.ts files in the Angular2Spa + // and ReactReduxSpa templates for ways to prerender Angular 2 and React components. + // + // If you wanted, you could use a property on the 'params.data' object to specify + // which SPA component or template to render. + + const html = ` +

Hello

+ It works! You passed ${ JSON.stringify(params.data) } + and are currently requesting ${ params.location.path }`; + resolve({ html }); + }); +}; diff --git a/samples/misc/Webpack/Controllers/FullPagePrerenderingController.cs b/samples/misc/Webpack/Controllers/FullPagePrerenderingController.cs new file mode 100644 index 00000000..48935459 --- /dev/null +++ b/samples/misc/Webpack/Controllers/FullPagePrerenderingController.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SpaServices.Prerendering; +using Webpack.ActionResults; + +namespace Webpack.Controllers +{ + // This sample shows how you could invoke the prerendering APIs directly from an MVC + // action result. + public class FullPagePrerenderingController : Controller + { + private static JavaScriptModuleExport BootModule = new JavaScriptModuleExport("Clientside/PrerenderingSample") + { + // Because the boot module is written in TypeScript, we need to specify a webpack + // config so it can be built. If it was written in JavaScript, this would not be needed. + WebpackConfig = "webpack.config.js" + }; + + public IActionResult Index() + { + var dataToSupply = new { nowTime = DateTime.Now.Ticks }; + return this.Prerender(BootModule, dataToSupply); + } + } +} diff --git a/samples/misc/Webpack/Startup.cs b/samples/misc/Webpack/Startup.cs index a19fcc1a..c81e5c0d 100755 --- a/samples/misc/Webpack/Startup.cs +++ b/samples/misc/Webpack/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.IO; +using Microsoft.AspNetCore.NodeServices; namespace Webpack { @@ -14,6 +15,7 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddMvc(); + services.AddNodeServices(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/samples/misc/Webpack/Views/Home/Index.cshtml b/samples/misc/Webpack/Views/Home/Index.cshtml index 7828ec19..365dfaad 100755 --- a/samples/misc/Webpack/Views/Home/Index.cshtml +++ b/samples/misc/Webpack/Views/Home/Index.cshtml @@ -5,6 +5,9 @@

Hello

Hi there. Enter some text: +
+See also: Full-page prerendering example + @section scripts { } diff --git a/samples/misc/Webpack/package.json b/samples/misc/Webpack/package.json index 39bacb81..e894a076 100644 --- a/samples/misc/Webpack/package.json +++ b/samples/misc/Webpack/package.json @@ -2,15 +2,18 @@ "name": "Webpack", "version": "0.0.0", "devDependencies": { - "aspnet-webpack": "^1.0.3", "css-loader": "^0.23.1", "extendify": "^1.0.0", "extract-text-webpack-plugin": "^1.0.1", "less": "^2.6.0", "less-loader": "^2.2.2", "style-loader": "^0.13.0", - "ts-loader": "^0.8.1", - "typescript": "^1.7.5", "webpack-hot-middleware": "^2.7.1" + }, + "dependencies": { + "aspnet-webpack": "^1.0.3", + "aspnet-prerendering": "^1.0.4", + "ts-loader": "^0.8.1", + "typescript": "^1.7.5" } } diff --git a/samples/misc/Webpack/tsconfig.json b/samples/misc/Webpack/tsconfig.json index 5cbeb866..bb577c55 100644 --- a/samples/misc/Webpack/tsconfig.json +++ b/samples/misc/Webpack/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "moduleResolution": "node", - "target": "es5", + "module": "commonjs", + "target": "es6", "jsx": "preserve", "sourceMap": true }, From 6b52b3f62acdfed76b45ac4b531aba9d26eed606 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 21 Jul 2016 11:38:15 -0400 Subject: [PATCH 28/28] chore(package): Update to latest ng RC & universal Updated to rc4, Universal 104.5 (bug fixes) Also updated to router beta2 which required pathMatch for Home route. Tested & works. JS disabled working as well. --- templates/Angular2Spa/ClientApp/routes.ts | 2 +- templates/Angular2Spa/package.json | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/Angular2Spa/ClientApp/routes.ts b/templates/Angular2Spa/ClientApp/routes.ts index d693c44a..c0a45b73 100644 --- a/templates/Angular2Spa/ClientApp/routes.ts +++ b/templates/Angular2Spa/ClientApp/routes.ts @@ -4,7 +4,7 @@ import { FetchData } from './components/fetch-data/fetch-data'; import { Counter } from './components/counter/counter'; export const routes: RouterConfig = [ - { path: '', redirectTo: 'home' }, + { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: Home }, { path: 'counter', component: Counter }, { path: 'fetch-data', component: FetchData }, diff --git a/templates/Angular2Spa/package.json b/templates/Angular2Spa/package.json index 047a6ead..a9884c15 100644 --- a/templates/Angular2Spa/package.json +++ b/templates/Angular2Spa/package.json @@ -18,15 +18,15 @@ "webpack-hot-middleware": "^2.10.0" }, "dependencies": { - "@angular/common": "2.0.0-rc.3", - "@angular/compiler": "2.0.0-rc.3", - "@angular/core": "2.0.0-rc.3", - "@angular/http": "2.0.0-rc.3", - "@angular/platform-browser": "2.0.0-rc.3", - "@angular/platform-browser-dynamic": "2.0.0-rc.3", - "@angular/platform-server": "2.0.0-rc.3", - "@angular/router": "3.0.0-alpha.8", - "angular2-universal": "^0.104.1", + "@angular/common": "2.0.0-rc.4", + "@angular/compiler": "2.0.0-rc.4", + "@angular/core": "2.0.0-rc.4", + "@angular/http": "2.0.0-rc.4", + "@angular/platform-browser": "2.0.0-rc.4", + "@angular/platform-browser-dynamic": "2.0.0-rc.4", + "@angular/platform-server": "2.0.0-rc.4", + "@angular/router": "3.0.0-beta.2", + "angular2-universal": "^0.104.5", "aspnet-prerendering": "^1.0.2", "aspnet-webpack": "^1.0.1", "css": "^2.2.1",