Cleanup processes started by UseReactDevelopmentServer and UseAngularCliServer

Fixes #11597
This commit is contained in:
AndriySvyryd 2019-12-14 12:53:35 -08:00 committed by Andriy Svyryd
parent b8d867509f
commit db3d23b3af
9 changed files with 356 additions and 40 deletions

View File

@ -1,15 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Builder;
using Microsoft.AspNetCore.NodeServices.Npm; using Microsoft.AspNetCore.NodeServices.Npm;
using Microsoft.AspNetCore.NodeServices.Util; using Microsoft.AspNetCore.NodeServices.Util;
using Microsoft.AspNetCore.SpaServices.Prerendering; using Microsoft.AspNetCore.SpaServices.Prerendering;
using Microsoft.AspNetCore.SpaServices.Util; using Microsoft.AspNetCore.SpaServices.Util;
using System; using Microsoft.Extensions.DependencyInjection;
using System.IO; using Microsoft.Extensions.Hosting;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.SpaServices.AngularCli 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)}."); 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( var logger = LoggerFinder.GetOrCreateLogger(
spaBuilder.ApplicationBuilder, appBuilder,
nameof(AngularCliBuilder)); nameof(AngularCliBuilder));
var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService<DiagnosticSource>();
var scriptRunner = new NodeScriptRunner( var scriptRunner = new NodeScriptRunner(
sourcePath, sourcePath,
_scriptName, _scriptName,
"--watch", "--watch",
null, null,
pkgManagerCommand); pkgManagerCommand,
diagnosticSource,
applicationStoppingToken);
scriptRunner.AttachToLogger(logger); scriptRunner.AttachToLogger(logger);
using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut)) using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut))

View File

@ -1,18 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.NodeServices.Npm; using Microsoft.AspNetCore.NodeServices.Npm;
using Microsoft.AspNetCore.NodeServices.Util; 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.Extensions.Util;
using Microsoft.AspNetCore.SpaServices.Util;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.SpaServices.AngularCli namespace Microsoft.AspNetCore.SpaServices.AngularCli
{ {
@ -40,8 +43,10 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
// Start Angular CLI and attach to middleware pipeline // Start Angular CLI and attach to middleware pipeline
var appBuilder = spaBuilder.ApplicationBuilder; var appBuilder = spaBuilder.ApplicationBuilder;
var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); 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: // Everything we proxy is hardcoded to target http://localhost because:
// - the requests are always from the local machine (we're not accepting remote // - 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( 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)) if (portNumber == default(int))
{ {
@ -73,7 +78,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
logger.LogInformation($"Starting @angular/cli on port {portNumber}..."); logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
var scriptRunner = new NodeScriptRunner( var scriptRunner = new NodeScriptRunner(
sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand); sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand, diagnosticSource, applicationStoppingToken);
scriptRunner.AttachToLogger(logger); scriptRunner.AttachToLogger(logger);
Match openBrowserLine; Match openBrowserLine;

View File

@ -1,13 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; 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 // This is under the NodeServices namespace because post 2.1 it will be moved to that package
namespace Microsoft.AspNetCore.NodeServices.Npm 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, /// Executes the <c>script</c> entries defined in a <c>package.json</c> file,
/// capturing any output written to stdio. /// capturing any output written to stdio.
/// </summary> /// </summary>
internal class NodeScriptRunner internal class NodeScriptRunner : IDisposable
{ {
private Process _npmProcess;
public EventedStreamReader StdOut { get; } public EventedStreamReader StdOut { get; }
public EventedStreamReader StdErr { get; } public EventedStreamReader StdErr { get; }
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1)); 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)) if (string.IsNullOrEmpty(workingDirectory))
{ {
@ -69,9 +71,22 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
} }
} }
var process = LaunchNodeProcess(processStartInfo, pkgManagerCommand); _npmProcess = LaunchNodeProcess(processStartInfo, pkgManagerCommand);
StdOut = new EventedStreamReader(process.StandardOutput); StdOut = new EventedStreamReader(_npmProcess.StandardOutput);
StdErr = new EventedStreamReader(process.StandardError); 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) public void AttachToLogger(ILogger logger)
@ -132,5 +147,14 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
throw new InvalidOperationException(message, ex); throw new InvalidOperationException(message, ex);
} }
} }
void IDisposable.Dispose()
{
if (_npmProcess != null && !_npmProcess.HasExited)
{
_npmProcess.Kill(entireProcessTree: true);
_npmProcess = null;
}
}
} }
} }

View File

@ -1,17 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.NodeServices.Npm; using Microsoft.AspNetCore.NodeServices.Npm;
using Microsoft.AspNetCore.NodeServices.Util; 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.Extensions.Util;
using Microsoft.AspNetCore.SpaServices.Util;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
{ {
@ -39,8 +43,10 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
// Start create-react-app and attach to middleware pipeline // Start create-react-app and attach to middleware pipeline
var appBuilder = spaBuilder.ApplicationBuilder; var appBuilder = spaBuilder.ApplicationBuilder;
var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); 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: // Everything we proxy is hardcoded to target http://localhost because:
// - the requests are always from the local machine (we're not accepting remote // - 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( 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)) 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 { "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( var scriptRunner = new NodeScriptRunner(
sourcePath, scriptName, null, envVars, pkgManagerCommand); sourcePath, scriptName, null, envVars, pkgManagerCommand, diagnosticSource, applicationStoppingToken);
scriptRunner.AttachToLogger(logger); scriptRunner.AttachToLogger(logger);
using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr))

View File

@ -1,8 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 System;
using Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
{ {

View File

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

View File

@ -1,17 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TestDependsOnNode>true</TestDependsOnNode> <TestDependsOnNode>true</TestDependsOnNode>
<!-- Depends on npm which is not picked up on helix -->
<!-- https://github.com/dotnet/aspnetcore/issues/18672 -->
<BuildHelixPayload>false</BuildHelixPayload>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.SpaServices.Extensions" /> <Reference Include="Microsoft.AspNetCore.SpaServices.Extensions" />
<Reference Include="Microsoft.AspNetCore.Hosting" /> <Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.TestHost" /> <Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Extensions.DiagnosticAdapter" />
<Reference Include="Microsoft.Extensions.Hosting" /> <Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.Logging.Testing" /> <Reference Include="Microsoft.Extensions.Logging.Testing" />
<Content Include="js\**\*" /> <Content Include="js\**\*" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="package.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@ -2,8 +2,22 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; 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.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.DependencyInjection;
using Microsoft.Extensions.DiagnosticAdapter;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Xunit; using Xunit;
@ -24,9 +38,90 @@ namespace Microsoft.AspNetCore.SpaServices.Extensions.Tests
Assert.Equal("No RootPath was set on the SpaStaticFilesOptions.", exception.Message); 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) private IApplicationBuilder GetApplicationBuilder(IServiceProvider serviceProvider = null)
{ {
if(serviceProvider == null) if (serviceProvider == null)
{ {
serviceProvider = new Mock<IServiceProvider>(MockBehavior.Strict).Object; serviceProvider = new Mock<IServiceProvider>(MockBehavior.Strict).Object;
} }
@ -39,13 +134,74 @@ namespace Microsoft.AspNetCore.SpaServices.Extensions.Tests
return applicationbuilderMock.Object; return applicationbuilderMock.Object;
} }
private IServiceProvider GetServiceProvider() private IServiceProvider GetServiceProvider(Action<SpaStaticFilesOptions> configuration = null)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddLogging(); 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(); 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; }
}
} }
} }

View File

@ -0,0 +1,6 @@
{
"scripts": {
"wait": "sleep 30 1>/dev/null && echo",
"waitWindows": "ping 127.0.0.1 -n 30 >nul && echo"
}
}