Cleanup processes started by UseReactDevelopmentServer and UseAngularCliServer
Fixes #11597
This commit is contained in:
parent
b8d867509f
commit
db3d23b3af
|
|
@ -1,15 +1,18 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
||||
{
|
||||
|
|
@ -48,15 +51,20 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||
throw new InvalidOperationException($"To use {nameof(AngularCliBuilder)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
||||
}
|
||||
|
||||
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||
var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;
|
||||
var logger = LoggerFinder.GetOrCreateLogger(
|
||||
spaBuilder.ApplicationBuilder,
|
||||
appBuilder,
|
||||
nameof(AngularCliBuilder));
|
||||
var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService<DiagnosticSource>();
|
||||
var scriptRunner = new NodeScriptRunner(
|
||||
sourcePath,
|
||||
_scriptName,
|
||||
"--watch",
|
||||
null,
|
||||
pkgManagerCommand);
|
||||
pkgManagerCommand,
|
||||
diagnosticSource,
|
||||
applicationStoppingToken);
|
||||
scriptRunner.AttachToLogger(logger);
|
||||
|
||||
using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut))
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.SpaServices.Extensions.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
||||
{
|
||||
|
|
@ -40,8 +43,10 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||
|
||||
// Start Angular CLI and attach to middleware pipeline
|
||||
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||
var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;
|
||||
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
|
||||
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger);
|
||||
var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService<DiagnosticSource>();
|
||||
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger, diagnosticSource, applicationStoppingToken);
|
||||
|
||||
// Everything we proxy is hardcoded to target http://localhost because:
|
||||
// - the requests are always from the local machine (we're not accepting remote
|
||||
|
|
@ -64,7 +69,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||
}
|
||||
|
||||
private static async Task<AngularCliServerInfo> StartAngularCliServerAsync(
|
||||
string sourcePath, string scriptName, string pkgManagerCommand, int portNumber, ILogger logger)
|
||||
string sourcePath, string scriptName, string pkgManagerCommand, int portNumber, ILogger logger, DiagnosticSource diagnosticSource, CancellationToken applicationStoppingToken)
|
||||
{
|
||||
if (portNumber == default(int))
|
||||
{
|
||||
|
|
@ -73,7 +78,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||
logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
|
||||
|
||||
var scriptRunner = new NodeScriptRunner(
|
||||
sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand);
|
||||
sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand, diagnosticSource, applicationStoppingToken);
|
||||
scriptRunner.AttachToLogger(logger);
|
||||
|
||||
Match openBrowserLine;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// This is under the NodeServices namespace because post 2.1 it will be moved to that package
|
||||
namespace Microsoft.AspNetCore.NodeServices.Npm
|
||||
|
|
@ -16,14 +17,15 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
|
|||
/// Executes the <c>script</c> entries defined in a <c>package.json</c> file,
|
||||
/// capturing any output written to stdio.
|
||||
/// </summary>
|
||||
internal class NodeScriptRunner
|
||||
internal class NodeScriptRunner : IDisposable
|
||||
{
|
||||
private Process _npmProcess;
|
||||
public EventedStreamReader StdOut { get; }
|
||||
public EventedStreamReader StdErr { get; }
|
||||
|
||||
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
|
||||
|
||||
public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, string pkgManagerCommand)
|
||||
public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars, string pkgManagerCommand, DiagnosticSource diagnosticSource, CancellationToken applicationStoppingToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(workingDirectory))
|
||||
{
|
||||
|
|
@ -69,9 +71,22 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
|
|||
}
|
||||
}
|
||||
|
||||
var process = LaunchNodeProcess(processStartInfo, pkgManagerCommand);
|
||||
StdOut = new EventedStreamReader(process.StandardOutput);
|
||||
StdErr = new EventedStreamReader(process.StandardError);
|
||||
_npmProcess = LaunchNodeProcess(processStartInfo, pkgManagerCommand);
|
||||
StdOut = new EventedStreamReader(_npmProcess.StandardOutput);
|
||||
StdErr = new EventedStreamReader(_npmProcess.StandardError);
|
||||
|
||||
applicationStoppingToken.Register(((IDisposable)this).Dispose);
|
||||
|
||||
if (diagnosticSource.IsEnabled("Microsoft.AspNetCore.NodeServices.Npm.NpmStarted"))
|
||||
{
|
||||
diagnosticSource.Write(
|
||||
"Microsoft.AspNetCore.NodeServices.Npm.NpmStarted",
|
||||
new
|
||||
{
|
||||
processStartInfo = processStartInfo,
|
||||
process = _npmProcess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachToLogger(ILogger logger)
|
||||
|
|
@ -132,5 +147,14 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
|
|||
throw new InvalidOperationException(message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
if (_npmProcess != null && !_npmProcess.HasExited)
|
||||
{
|
||||
_npmProcess.Kill(entireProcessTree: true);
|
||||
_npmProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SpaServices.Extensions.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
||||
{
|
||||
|
|
@ -39,8 +43,10 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
|||
|
||||
// Start create-react-app and attach to middleware pipeline
|
||||
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||
var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;
|
||||
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
|
||||
var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger);
|
||||
var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService<DiagnosticSource>();
|
||||
var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger, diagnosticSource, applicationStoppingToken);
|
||||
|
||||
// Everything we proxy is hardcoded to target http://localhost because:
|
||||
// - the requests are always from the local machine (we're not accepting remote
|
||||
|
|
@ -63,7 +69,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
|||
}
|
||||
|
||||
private static async Task<int> StartCreateReactAppServerAsync(
|
||||
string sourcePath, string scriptName, string pkgManagerCommand, int portNumber, ILogger logger)
|
||||
string sourcePath, string scriptName, string pkgManagerCommand, int portNumber, ILogger logger, DiagnosticSource diagnosticSource, CancellationToken applicationStoppingToken)
|
||||
{
|
||||
if (portNumber == default(int))
|
||||
{
|
||||
|
|
@ -77,7 +83,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
|||
{ "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port
|
||||
};
|
||||
var scriptRunner = new NodeScriptRunner(
|
||||
sourcePath, scriptName, null, envVars, pkgManagerCommand);
|
||||
sourcePath, scriptName, null, envVars, pkgManagerCommand, diagnosticSource, applicationStoppingToken);
|
||||
scriptRunner.AttachToLogger(logger);
|
||||
|
||||
using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Extensions.Tests
|
||||
{
|
||||
public class ListLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly Func<string, bool> _shouldLogCategory;
|
||||
private bool _disposed;
|
||||
|
||||
public ListLoggerFactory()
|
||||
: this(_ => true)
|
||||
{
|
||||
}
|
||||
|
||||
public ListLoggerFactory(Func<string, bool> shouldLogCategory)
|
||||
{
|
||||
_shouldLogCategory = shouldLogCategory;
|
||||
Logger = new ListLogger();
|
||||
}
|
||||
|
||||
public List<(LogLevel Level, EventId Id, string Message, object State, Exception Exception)> Log => Logger.LoggedEvents;
|
||||
protected ListLogger Logger { get; set; }
|
||||
|
||||
public virtual void Clear() => Logger.Clear();
|
||||
|
||||
public void SetTestOutputHelper(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
Logger.TestOutputHelper = testOutputHelper;
|
||||
}
|
||||
|
||||
public virtual ILogger CreateLogger(string name)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
return !_shouldLogCategory(name)
|
||||
? (ILogger)NullLogger.Instance
|
||||
: Logger;
|
||||
}
|
||||
|
||||
private void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(ListLoggerFactory));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
CheckDisposed();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
protected class ListLogger : ILogger
|
||||
{
|
||||
private readonly object _sync = new object();
|
||||
|
||||
public ITestOutputHelper TestOutputHelper { get; set; }
|
||||
|
||||
public List<(LogLevel, EventId, string, object, Exception)> LoggedEvents { get; }
|
||||
= new List<(LogLevel, EventId, string, object, Exception)>();
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_sync) // Guard against tests with explicit concurrency
|
||||
{
|
||||
LoggedEvents.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
lock (_sync) // Guard against tests with explicit concurrency
|
||||
{
|
||||
var message = formatter(state, exception)?.Trim();
|
||||
if (message != null)
|
||||
{
|
||||
TestOutputHelper?.WriteLine(message + Environment.NewLine);
|
||||
}
|
||||
|
||||
LoggedEvents.Add((logLevel, eventId, message, state, exception));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public IDisposable BeginScope(object state) => null;
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<TestDependsOnNode>true</TestDependsOnNode>
|
||||
<!-- Depends on npm which is not picked up on helix -->
|
||||
<!-- https://github.com/dotnet/aspnetcore/issues/18672 -->
|
||||
<BuildHelixPayload>false</BuildHelixPayload>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.SpaServices.Extensions" />
|
||||
<Reference Include="Microsoft.AspNetCore.Hosting" />
|
||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||
<Reference Include="Microsoft.Extensions.DiagnosticAdapter" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
<Content Include="js\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="package.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,22 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.SpaServices.AngularCli;
|
||||
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
|
||||
using Microsoft.AspNetCore.SpaServices.StaticFiles;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DiagnosticAdapter;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -24,9 +38,90 @@ namespace Microsoft.AspNetCore.SpaServices.Extensions.Tests
|
|||
Assert.Equal("No RootPath was set on the SpaStaticFilesOptions.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseSpa_KillsRds_WhenAppIsStopped()
|
||||
{
|
||||
var serviceProvider = GetServiceProvider(s => s.RootPath = "/");
|
||||
var applicationbuilder = new ApplicationBuilder(serviceProvider);
|
||||
var applicationLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
|
||||
var diagnosticListener = serviceProvider.GetRequiredService<DiagnosticListener>();
|
||||
var listener = new NpmStartedDiagnosticListener();
|
||||
diagnosticListener.SubscribeWithAdapter(listener);
|
||||
|
||||
applicationbuilder.UseSpa(b =>
|
||||
{
|
||||
b.Options.SourcePath = Directory.GetCurrentDirectory();
|
||||
b.UseReactDevelopmentServer(GetPlatformSpecificWaitCommand());
|
||||
});
|
||||
|
||||
await Assert_NpmKilled_WhenAppIsStopped(applicationLifetime, listener);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseSpa_KillsAngularCli_WhenAppIsStopped()
|
||||
{
|
||||
var serviceProvider = GetServiceProvider(s => s.RootPath = "/");
|
||||
var applicationbuilder = new ApplicationBuilder(serviceProvider);
|
||||
var applicationLifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
|
||||
var diagnosticListener = serviceProvider.GetRequiredService<DiagnosticListener>();
|
||||
var listener = new NpmStartedDiagnosticListener();
|
||||
diagnosticListener.SubscribeWithAdapter(listener);
|
||||
|
||||
applicationbuilder.UseSpa(b =>
|
||||
{
|
||||
b.Options.SourcePath = Directory.GetCurrentDirectory();
|
||||
b.UseAngularCliServer(GetPlatformSpecificWaitCommand());
|
||||
});
|
||||
|
||||
await Assert_NpmKilled_WhenAppIsStopped(applicationLifetime, listener);
|
||||
}
|
||||
|
||||
private async Task Assert_NpmKilled_WhenAppIsStopped(IHostApplicationLifetime applicationLifetime, NpmStartedDiagnosticListener listener)
|
||||
{
|
||||
// Give node a moment to start up
|
||||
await Task.WhenAny(listener.NpmStarted, Task.Delay(TimeSpan.FromSeconds(30)));
|
||||
|
||||
Process npmProcess = null;
|
||||
var npmExitEvent = new ManualResetEventSlim();
|
||||
if (listener.NpmStarted.IsCompleted)
|
||||
{
|
||||
npmProcess = listener.NpmStarted.Result.Process;
|
||||
Assert.False(npmProcess.HasExited);
|
||||
npmProcess.Exited += (_, __) => npmExitEvent.Set();
|
||||
}
|
||||
|
||||
// Act
|
||||
applicationLifetime.StopApplication();
|
||||
|
||||
// Assert
|
||||
AssertNoErrors();
|
||||
Assert.True(listener.NpmStarted.IsCompleted, "npm wasn't launched");
|
||||
|
||||
npmExitEvent.Wait(TimeSpan.FromSeconds(30));
|
||||
Assert.True(npmProcess.HasExited, "npm wasn't killed");
|
||||
}
|
||||
|
||||
private class NpmStartedDiagnosticListener
|
||||
{
|
||||
private readonly TaskCompletionSource<(ProcessStartInfo ProcessStartInfo, Process Process)> _npmStartedTaskCompletionSource
|
||||
= new TaskCompletionSource<(ProcessStartInfo ProcessStartInfo, Process Process)>();
|
||||
|
||||
public Task<(ProcessStartInfo ProcessStartInfo, Process Process)> NpmStarted
|
||||
=> _npmStartedTaskCompletionSource.Task;
|
||||
|
||||
[DiagnosticName("Microsoft.AspNetCore.NodeServices.Npm.NpmStarted")]
|
||||
public virtual void OnNpmStarted(ProcessStartInfo processStartInfo, Process process)
|
||||
{
|
||||
_npmStartedTaskCompletionSource.TrySetResult((processStartInfo, process));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetPlatformSpecificWaitCommand()
|
||||
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "waitWindows" : "wait";
|
||||
|
||||
private IApplicationBuilder GetApplicationBuilder(IServiceProvider serviceProvider = null)
|
||||
{
|
||||
if(serviceProvider == null)
|
||||
if (serviceProvider == null)
|
||||
{
|
||||
serviceProvider = new Mock<IServiceProvider>(MockBehavior.Strict).Object;
|
||||
}
|
||||
|
|
@ -39,13 +134,74 @@ namespace Microsoft.AspNetCore.SpaServices.Extensions.Tests
|
|||
return applicationbuilderMock.Object;
|
||||
}
|
||||
|
||||
private IServiceProvider GetServiceProvider()
|
||||
private IServiceProvider GetServiceProvider(Action<SpaStaticFilesOptions> configuration = null)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSpaStaticFiles();
|
||||
services.AddSpaStaticFiles(configuration);
|
||||
services.AddSingleton<ILoggerFactory>(ListLoggerFactory);
|
||||
services.AddSingleton(typeof(IHostApplicationLifetime), new TestHostApplicationLifetime());
|
||||
services.AddSingleton(typeof(IWebHostEnvironment), new TestWebHostEnvironment());
|
||||
|
||||
var listener = new DiagnosticListener("Microsoft.AspNetCore");
|
||||
services.AddSingleton(listener);
|
||||
services.AddSingleton<DiagnosticSource>(listener);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void AssertNoErrors()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var line in ListLoggerFactory.Log)
|
||||
{
|
||||
if (line.Level < LogLevel.Error)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
builder.AppendLine(line.Message);
|
||||
}
|
||||
|
||||
Assert.True(builder.Length == 0, builder.ToString());
|
||||
}
|
||||
|
||||
private ListLoggerFactory ListLoggerFactory { get; } = new ListLoggerFactory(c => c == "Microsoft.AspNetCore.SpaServices");
|
||||
|
||||
private class TestHostApplicationLifetime : IHostApplicationLifetime
|
||||
{
|
||||
CancellationTokenSource _applicationStoppingSource;
|
||||
CancellationTokenSource _applicationStoppedSource;
|
||||
|
||||
public TestHostApplicationLifetime()
|
||||
{
|
||||
_applicationStoppingSource = new CancellationTokenSource();
|
||||
ApplicationStopping = _applicationStoppingSource.Token;
|
||||
|
||||
_applicationStoppedSource = new CancellationTokenSource();
|
||||
ApplicationStopped = _applicationStoppedSource.Token;
|
||||
}
|
||||
|
||||
public CancellationToken ApplicationStarted => CancellationToken.None;
|
||||
|
||||
public CancellationToken ApplicationStopping { get; }
|
||||
|
||||
public CancellationToken ApplicationStopped { get; }
|
||||
|
||||
public void StopApplication()
|
||||
{
|
||||
_applicationStoppingSource.Cancel();
|
||||
_applicationStoppedSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestWebHostEnvironment : IWebHostEnvironment
|
||||
{
|
||||
public string EnvironmentName { get; set; }
|
||||
public string ApplicationName { get; set; }
|
||||
public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory();
|
||||
public string WebRootPath { get; set; } = Directory.GetCurrentDirectory();
|
||||
public IFileProvider ContentRootFileProvider { get; set; }
|
||||
public IFileProvider WebRootFileProvider { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"scripts": {
|
||||
"wait": "sleep 30 1>/dev/null && echo",
|
||||
"waitWindows": "ping 127.0.0.1 -n 30 >nul && echo"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue