Remove System.Runtime.Loader dependency to enable net461 support. Use IApplicationLifetime instead of AssemblyLoadContext.Default.Unloading.

This commit is contained in:
Steve Sanderson
2017-06-07 11:45:14 +01:00
parent 2aaceaa9f8
commit b444831c8d
9 changed files with 50 additions and 24 deletions

View File

@@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets
options.ProjectPath,
options.WatchFileExtensions,
MakeNewCommandLineOptions(socketAddress),
options.ApplicationStoppingToken,
options.NodeInstanceOutputLogger,
options.EnvironmentVariables,
options.InvocationTimeoutMilliseconds,

View File

@@ -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<IApplicationLifetime>();
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<ILoggerFactory>();
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.
/// </summary>
public int InvocationTimeoutMilliseconds { get; set; }
/// <summary>
/// A token that indicates when the host application is stopping.
/// </summary>
public CancellationToken ApplicationStoppingToken { get; set; }
}
}

View File

@@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
options.ProjectPath,
options.WatchFileExtensions,
MakeCommandLineOptions(port),
options.ApplicationStoppingToken,
options.NodeInstanceOutputLogger,
options.EnvironmentVariables,
options.InvocationTimeoutMilliseconds,

View File

@@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
/// <param name="projectPath">The root path of the current project. This is used when resolving Node.js module paths relative to the project root.</param>
/// <param name="watchFileExtensions">The filename extensions that should be watched within the project root. The Node instance will automatically shut itself down if any matching file changes.</param>
/// <param name="commandLineArguments">Additional command-line arguments to be passed to the Node.js instance.</param>
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
/// <param name="nodeOutputLogger">The <see cref="ILogger"/> to which the Node.js instance's stdout/stderr (and other log information) should be written.</param>
/// <param name="environmentVars">Environment variables to be set on the Node.js process.</param>
/// <param name="invocationTimeoutMilliseconds">The maximum duration, in milliseconds, to wait for RPC calls to complete.</param>
@@ -55,6 +56,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
string projectPath,
string[] watchFileExtensions,
string commandLineArguments,
CancellationToken applicationStoppingToken,
ILogger nodeOutputLogger,
IDictionary<string, string> 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;

View File

@@ -18,7 +18,6 @@
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
<PackageReference Include="System.Runtime.Loader" Version="$(CoreFxVersion)" />
</ItemGroup>
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">

View File

@@ -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;
/// <summary>
/// Create a new instance of <see cref="StringAsTempFile"/>.
/// </summary>
/// <param name="content">The contents of the temporary file to be created.</param>
public StringAsTempFile(string content)
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
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);
}
/// <summary>
@@ -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();
}
/// <summary>
/// Implements the finalization part of the IDisposable pattern by calling Dispose(false).
/// </summary>

View File

@@ -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;
/// <summary>
@@ -36,6 +38,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
_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.
if (_nodeServices == null)
@@ -101,6 +106,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
var result = await Prerenderer.RenderToString(
_applicationBasePath,
_nodeServices,
_applicationStoppingToken,
new JavaScriptModuleExport(ModuleName)
{
ExportName = ExportName

View File

@@ -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
/// </summary>
public static class Prerenderer
{
private static readonly Lazy<StringAsTempFile> NodeScript;
private static readonly object CreateNodeScriptLock = new object();
static Prerenderer()
{
NodeScript = new Lazy<StringAsTempFile>(() =>
{
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;
/// <summary>
/// Performs server-side prerendering by invoking code in Node.js.
/// </summary>
/// <param name="applicationBasePath">The root path to your application. This is used when resolving project-relative paths.</param>
/// <param name="nodeServices">The instance of <see cref="INodeServices"/> that will be used to invoke JavaScript code.</param>
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
/// <param name="bootModule">The path to the JavaScript file containing the prerendering logic.</param>
/// <param name="requestAbsoluteUrl">The URL of the currently-executing HTTP request. This is supplied to the prerendering code.</param>
/// <param name="requestPathAndQuery">The path and query part of the URL of the currently-executing HTTP request. This is supplied to the prerendering code.</param>
@@ -35,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
public static Task<RenderToStringResult> 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<RenderToStringResult>(
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;
}
}
}

View File

@@ -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