From b444831c8dc246076d12b35e23a483b9ff1b68b3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 7 Jun 2017 11:45:14 +0100 Subject: [PATCH] Remove System.Runtime.Loader dependency to enable net461 support. Use IApplicationLifetime instead of AssemblyLoadContext.Default.Unloading. --- .../SocketNodeInstance.cs | 1 + .../Configuration/NodeServicesOptions.cs | 12 ++++++++ .../HostingModels/HttpNodeInstance.cs | 1 + .../HostingModels/OutOfProcessNodeInstance.cs | 4 ++- .../Microsoft.AspNetCore.NodeServices.csproj | 1 - .../Util/StringAsTempFile.cs | 17 ++++------- .../Prerendering/PrerenderTagHelper.cs | 6 ++++ .../Prerendering/Prerenderer.cs | 30 ++++++++++++------- .../Webpack/WebpackDevMiddleware.cs | 2 +- 9 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices.Sockets/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices.Sockets/SocketNodeInstance.cs index b656978..1e6afd1 100644 --- a/src/Microsoft.AspNetCore.NodeServices.Sockets/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices.Sockets/SocketNodeInstance.cs @@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets options.ProjectPath, options.WatchFileExtensions, MakeNewCommandLineOptions(socketAddress), + options.ApplicationStoppingToken, options.NodeInstanceOutputLogger, options.EnvironmentVariables, options.InvocationTimeoutMilliseconds, diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index 43d703b..73dd228 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using Microsoft.AspNetCore.NodeServices.HostingModels; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; @@ -42,6 +43,12 @@ namespace Microsoft.AspNetCore.NodeServices EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node } + var applicationLifetime = serviceProvider.GetService(); + if (applicationLifetime != null) + { + ApplicationStoppingToken = applicationLifetime.ApplicationStopping; + } + // If the DI system gives us a logger, use it. Otherwise, set up a default one. var loggerFactory = serviceProvider.GetService(); NodeInstanceOutputLogger = loggerFactory != null @@ -93,5 +100,10 @@ namespace Microsoft.AspNetCore.NodeServices /// Specifies the maximum duration, in milliseconds, that your .NET code should wait for Node.js RPC calls to return. /// public int InvocationTimeoutMilliseconds { get; set; } + + /// + /// A token that indicates when the host application is stopping. + /// + public CancellationToken ApplicationStoppingToken { 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 8beea51..b1b0fe9 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels options.ProjectPath, options.WatchFileExtensions, MakeCommandLineOptions(port), + options.ApplicationStoppingToken, options.NodeInstanceOutputLogger, options.EnvironmentVariables, options.InvocationTimeoutMilliseconds, diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 6bfa177..e353292 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels /// The root path of the current project. This is used when resolving Node.js module paths relative to the project root. /// The filename extensions that should be watched within the project root. The Node instance will automatically shut itself down if any matching file changes. /// Additional command-line arguments to be passed to the Node.js instance. + /// A token that indicates when the host application is stopping. /// The to which the Node.js instance's stdout/stderr (and other log information) should be written. /// Environment variables to be set on the Node.js process. /// The maximum duration, in milliseconds, to wait for RPC calls to complete. @@ -55,6 +56,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels string projectPath, string[] watchFileExtensions, string commandLineArguments, + CancellationToken applicationStoppingToken, ILogger nodeOutputLogger, IDictionary environmentVars, int invocationTimeoutMilliseconds, @@ -67,7 +69,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels } OutputLogger = nodeOutputLogger; - _entryPointScript = new StringAsTempFile(entryPointScript); + _entryPointScript = new StringAsTempFile(entryPointScript, applicationStoppingToken); _invocationTimeoutMilliseconds = invocationTimeoutMilliseconds; _launchWithDebugging = launchWithDebugging; diff --git a/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.csproj b/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.csproj index 27d8c62..cbf7828 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.csproj +++ b/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.csproj @@ -18,7 +18,6 @@ - diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs index a798b31..c15bca3 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading; namespace Microsoft.AspNetCore.NodeServices { @@ -11,22 +12,21 @@ namespace Microsoft.AspNetCore.NodeServices private bool _disposedValue; private bool _hasDeletedTempFile; private object _fileDeletionLock = new object(); + private IDisposable _applicationLifetimeRegistration; /// /// Create a new instance of . /// /// The contents of the temporary file to be created. - public StringAsTempFile(string content) + /// A token that indicates when the host application is stopping. + public StringAsTempFile(string content, CancellationToken applicationStoppingToken) { FileName = Path.GetTempFileName(); File.WriteAllText(FileName, content); // Because .NET finalizers don't reliably run when the process is terminating, also // add event handlers for other shutdown scenarios. - // Note that this still doesn't capture SIGKILL (at least on macOS) - there doesn't - // appear to be a way of doing that. So in that case, the temporary file will be - // left behind. - System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += HandleAssemblyUnloading; + _applicationLifetimeRegistration = applicationStoppingToken.Register(EnsureTempFileDeleted); } /// @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.NodeServices if (disposing) { // Dispose managed state - System.Runtime.Loader.AssemblyLoadContext.Default.Unloading -= HandleAssemblyUnloading; + _applicationLifetimeRegistration.Dispose(); } EnsureTempFileDeleted(); @@ -71,11 +71,6 @@ namespace Microsoft.AspNetCore.NodeServices } } - private void HandleAssemblyUnloading(System.Runtime.Loader.AssemblyLoadContext context) - { - EnsureTempFileDeleted(); - } - /// /// Implements the finalization part of the IDisposable pattern by calling Dispose(false). /// diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index bad274f..c2c5308 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; @@ -24,6 +25,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI private readonly string _applicationBasePath; + private readonly CancellationToken _applicationStoppingToken; private readonly INodeServices _nodeServices; /// @@ -35,6 +37,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering var hostEnv = (IHostingEnvironment) serviceProvider.GetService(typeof(IHostingEnvironment)); _nodeServices = (INodeServices) serviceProvider.GetService(typeof(INodeServices)) ?? _fallbackNodeServices; _applicationBasePath = hostEnv.ContentRootPath; + + var applicationLifetime = (IApplicationLifetime) serviceProvider.GetService(typeof(IApplicationLifetime)); + _applicationStoppingToken = applicationLifetime.ApplicationStopping; // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() // in your startup file, but then again it might be confusing that you don't need to. @@ -101,6 +106,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering var result = await Prerenderer.RenderToString( _applicationBasePath, _nodeServices, + _applicationStoppingToken, new JavaScriptModuleExport(ModuleName) { ExportName = ExportName diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 43c2ed0..e7ce841 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices; @@ -9,22 +10,16 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// public static class Prerenderer { - private static readonly Lazy NodeScript; + private static readonly object CreateNodeScriptLock = new object(); - static Prerenderer() - { - NodeScript = new Lazy(() => - { - var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); - return new StringAsTempFile(script); // Will be cleaned up on process exit - }); - } + private static StringAsTempFile NodeScript; /// /// Performs server-side prerendering by invoking code in Node.js. /// /// The root path to your application. This is used when resolving project-relative paths. /// The instance of that will be used to invoke JavaScript code. + /// A token that indicates when the host application is stopping. /// The path to the JavaScript file containing the prerendering logic. /// The URL of the currently-executing HTTP request. This is supplied to the prerendering code. /// The path and query part of the URL of the currently-executing HTTP request. This is supplied to the prerendering code. @@ -35,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering public static Task RenderToString( string applicationBasePath, INodeServices nodeServices, + CancellationToken applicationStoppingToken, JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery, @@ -43,7 +39,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering string requestPathBase) { return nodeServices.InvokeExportAsync( - NodeScript.Value.FileName, + GetNodeScriptFilename(applicationStoppingToken), "renderToString", applicationBasePath, bootModule, @@ -53,5 +49,19 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering timeoutMilliseconds, requestPathBase); } + + private static string GetNodeScriptFilename(CancellationToken applicationStoppingToken) + { + lock(CreateNodeScriptLock) + { + if (NodeScript == null) + { + var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); + NodeScript = new StringAsTempFile(script, applicationStoppingToken); // Will be cleaned up on process exit + } + } + + return NodeScript.FileName; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 8afd7b6..9f02223 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Builder // Get a filename matching the middleware Node script var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), "/Content/Node/webpack-dev-middleware.js"); - var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + var nodeScript = new StringAsTempFile(script, nodeServicesOptions.ApplicationStoppingToken); // Will be cleaned up on process exit // Ideally, this would be relative to the application's PathBase (so it could work in virtual directories) // but it's not clear that such information exists during application startup, as opposed to within the context