Add support for launching a browser on file change (#24220)

This commit is contained in:
Pranav K 2020-07-23 17:04:31 -07:00 committed by GitHub
parent b33f8ddbe2
commit 11835cf768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 606 additions and 31 deletions

View File

@ -0,0 +1,108 @@
// 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.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools
{
public class BrowserRefreshServer : IAsyncDisposable
{
private readonly IReporter _reporter;
private readonly TaskCompletionSource _taskCompletionSource;
private IHost _refreshServer;
private WebSocket _webSocket;
public BrowserRefreshServer(IReporter reporter)
{
_reporter = reporter;
_taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
}
public async ValueTask<string> StartAsync(CancellationToken cancellationToken)
{
_refreshServer = new HostBuilder()
.ConfigureWebHost(builder =>
{
builder.UseKestrel();
builder.UseUrls("http://127.0.0.1:0");
builder.Configure(app =>
{
app.UseWebSockets();
app.Run(WebSocketRequest);
});
})
.Build();
await _refreshServer.StartAsync(cancellationToken);
var serverUrl = _refreshServer.Services
.GetRequiredService<IServer>()
.Features
.Get<IServerAddressesFeature>()
.Addresses
.First();
return serverUrl.Replace("http://", "ws://");
}
private async Task WebSocketRequest(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
_webSocket = await context.WebSockets.AcceptWebSocketAsync();
await _taskCompletionSource.Task;
}
public async Task SendMessage(byte[] messageBytes, CancellationToken cancellationToken = default)
{
if (_webSocket == null || _webSocket.CloseStatus.HasValue)
{
return;
}
try
{
await _webSocket.SendAsync(messageBytes, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
}
catch (Exception ex)
{
_reporter.Output($"Refresh server error: {ex}");
}
}
public async ValueTask DisposeAsync()
{
if (_webSocket != null)
{
await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default);
_webSocket.Dispose();
}
if (_refreshServer != null)
{
await _refreshServer.StopAsync();
_refreshServer.Dispose();
}
_taskCompletionSource.TrySetResult();
}
}
}

View File

@ -14,7 +14,7 @@ using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher
{
public class DotNetWatcher
public class DotNetWatcher : IAsyncDisposable
{
private readonly IReporter _reporter;
private readonly ProcessRunner _processRunner;
@ -31,6 +31,7 @@ namespace Microsoft.DotNet.Watcher
{
new MSBuildEvaluationFilter(fileSetFactory),
new NoRestoreFilter(),
new LaunchBrowserFilter(),
};
}
@ -140,5 +141,21 @@ namespace Microsoft.DotNet.Watcher
}
}
}
public async ValueTask DisposeAsync()
{
foreach (var filter in _filters)
{
if (filter is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else if (filter is IDisposable diposable)
{
diposable.Dispose();
}
}
}
}
}

View File

@ -1,4 +1,4 @@
// 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.
using System.Collections.Generic;
@ -7,6 +7,8 @@ namespace Microsoft.DotNet.Watcher
{
public interface IFileSet : IEnumerable<string>
{
bool IsNetCoreApp31OrNewer { get; }
bool Contains(string filePath);
}
}

View File

@ -11,8 +11,9 @@ namespace Microsoft.DotNet.Watcher.Internal
{
private readonly HashSet<string> _files;
public FileSet(IEnumerable<string> files)
public FileSet(bool isNetCoreApp31OrNewer, IEnumerable<string> files)
{
IsNetCoreApp31OrNewer = isNetCoreApp31OrNewer;
_files = new HashSet<string>(files, StringComparer.OrdinalIgnoreCase);
}
@ -20,7 +21,9 @@ namespace Microsoft.DotNet.Watcher.Internal
public int Count => _files.Count;
public static IFileSet Empty = new FileSet(Array.Empty<string>());
public bool IsNetCoreApp31OrNewer { get; }
public static IFileSet Empty = new FileSet(false, Array.Empty<string>());
public IEnumerator<string> GetEnumerator() => _files.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _files.GetEnumerator();

View File

@ -72,6 +72,7 @@ namespace Microsoft.DotNet.Watcher.Internal
Arguments = new[]
{
"msbuild",
"/nologo",
_projectFile,
$"/p:_DotNetWatchListFile={watchList}"
}.Concat(_buildFlags),
@ -84,8 +85,12 @@ namespace Microsoft.DotNet.Watcher.Internal
if (exitCode == 0 && File.Exists(watchList))
{
var lines = File.ReadAllLines(watchList);
var isNetCoreApp31OrNewer = lines.FirstOrDefault() == "true";
var fileset = new FileSet(
File.ReadAllLines(watchList)
isNetCoreApp31OrNewer,
lines.Skip(1)
.Select(l => l?.Trim())
.Where(l => !string.IsNullOrEmpty(l)));
@ -123,7 +128,7 @@ namespace Microsoft.DotNet.Watcher.Internal
{
_reporter.Warn("Fix the error to continue or press Ctrl+C to exit.");
var fileSet = new FileSet(new[] { _projectFile });
var fileSet = new FileSet(false, new[] { _projectFile });
using (var watcher = new FileSetWatcher(fileSet, _reporter))
{

View File

@ -37,37 +37,49 @@ namespace Microsoft.DotNet.Watcher.Internal
{
cancellationToken.Register(() => processState.TryKill());
process.OutputDataReceived += (_, a) =>
var readOutput = false;
var readError = false;
if (processSpec.IsOutputCaptured)
{
if (!string.IsNullOrEmpty(a.Data))
readOutput = true;
readError = true;
process.OutputDataReceived += (_, a) =>
{
processSpec.OutputCapture.AddLine(a.Data);
}
};
process.ErrorDataReceived += (_, a) =>
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.OutputCapture.AddLine(a.Data);
}
};
process.ErrorDataReceived += (_, a) =>
{
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.OutputCapture.AddLine(a.Data);
}
};
}
else if (processSpec.OnOutput != null)
{
if (!string.IsNullOrEmpty(a.Data))
{
processSpec.OutputCapture.AddLine(a.Data);
}
};
readOutput = true;
process.OutputDataReceived += processSpec.OnOutput;
}
stopwatch.Start();
process.Start();
_reporter.Verbose($"Started '{processSpec.Executable}' with process id {process.Id}");
if (processSpec.IsOutputCaptured)
if (readOutput)
{
process.BeginOutputReadLine();
}
if (readError)
{
process.BeginErrorReadLine();
process.BeginOutputReadLine();
await processState.Task;
}
else
{
await processState.Task;
}
await processState.Task;
exitCode = process.ExitCode;
stopwatch.Stop();
_reporter.Verbose($"Process id {process.Id} ran for {stopwatch.ElapsedMilliseconds}ms");
@ -87,7 +99,7 @@ namespace Microsoft.DotNet.Watcher.Internal
Arguments = ArgumentEscaper.EscapeAndConcatenate(processSpec.Arguments),
UseShellExecute = false,
WorkingDirectory = processSpec.WorkingDirectory,
RedirectStandardOutput = processSpec.IsOutputCaptured,
RedirectStandardOutput = processSpec.IsOutputCaptured || (processSpec.OnOutput != null),
RedirectStandardError = processSpec.IsOutputCaptured,
}
};

View File

@ -0,0 +1,219 @@
// 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.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.DotNet.Watcher.Tools
{
public sealed class LaunchBrowserFilter : IWatchFilter, IAsyncDisposable
{
private readonly byte[] ReloadMessage = Encoding.UTF8.GetBytes("Reload");
private readonly byte[] WaitMessage = Encoding.UTF8.GetBytes("Wait");
private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$", RegexOptions.None | RegexOptions.Compiled, TimeSpan.FromSeconds(10));
private readonly bool _runningInTest;
private readonly bool _suppressLaunchBrowser;
private readonly string _browserPath;
private bool _canLaunchBrowser;
private Process _browserProcess;
private bool _browserLaunched;
private BrowserRefreshServer _refreshServer;
private IReporter _reporter;
private string _launchPath;
private CancellationToken _cancellationToken;
public LaunchBrowserFilter()
{
var suppressLaunchBrowser = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER");
_suppressLaunchBrowser = (suppressLaunchBrowser == "1" || suppressLaunchBrowser == "true");
_runningInTest = Environment.GetEnvironmentVariable("__DOTNET_WATCH_RUNNING_AS_TEST") == "true";
_browserPath = Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH");
}
public async ValueTask ProcessAsync(DotNetWatchContext context, CancellationToken cancellationToken)
{
if (_suppressLaunchBrowser)
{
return;
}
if (context.Iteration == 0)
{
_reporter = context.Reporter;
if (CanLaunchBrowser(context, out var launchPath))
{
context.Reporter.Verbose("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.");
_canLaunchBrowser = true;
_launchPath = launchPath;
_cancellationToken = cancellationToken;
_refreshServer = new BrowserRefreshServer(context.Reporter);
var serverUrl = await _refreshServer.StartAsync(cancellationToken);
context.Reporter.Verbose($"Refresh server running at {serverUrl}.");
context.ProcessSpec.EnvironmentVariables["DOTNET_WATCH_REFRESH_URL"] = serverUrl;
context.ProcessSpec.OnOutput += OnOutput;
}
}
if (_canLaunchBrowser)
{
if (context.Iteration > 0)
{
// We've detected a change. Notify the browser.
await _refreshServer.SendMessage(WaitMessage, cancellationToken);
}
}
}
private void OnOutput(object sender, DataReceivedEventArgs eventArgs)
{
// We've redirected the output, but want to ensure that it continues to appear in the user's console.
Console.WriteLine(eventArgs.Data);
if (string.IsNullOrEmpty(eventArgs.Data))
{
return;
}
var match = NowListeningRegex.Match(eventArgs.Data);
if (match.Success)
{
var launchUrl = match.Groups["url"].Value;
var process = (Process)sender;
process.OutputDataReceived -= OnOutput;
process.CancelOutputRead();
if (!_browserLaunched)
{
_reporter.Verbose("Launching browser.");
try
{
LaunchBrowser(launchUrl);
_browserLaunched = true;
}
catch (Exception ex)
{
_reporter.Output($"Unable to launch browser: {ex}");
_canLaunchBrowser = false;
}
}
else
{
_reporter.Verbose("Reloading browser.");
_ = _refreshServer.SendMessage(ReloadMessage, _cancellationToken);
}
}
}
private void LaunchBrowser(string launchUrl)
{
var fileName = launchUrl + "/" + _launchPath;
var args = string.Empty;
if (!string.IsNullOrEmpty(_browserPath))
{
args = fileName;
fileName = _browserPath;
}
if (_runningInTest)
{
_reporter.Output($"Launching browser: {fileName} {args}");
return;
}
_browserProcess = Process.Start(new ProcessStartInfo
{
FileName = fileName,
Arguments = args,
UseShellExecute = true,
});
}
private static bool CanLaunchBrowser(DotNetWatchContext context, out string launchUrl)
{
launchUrl = null;
var reporter = context.Reporter;
if (!context.FileSet.IsNetCoreApp31OrNewer)
{
// Browser refresh middleware supports 3.1 or newer
reporter.Verbose("Browser refresh is only supported in .NET Core 3.1 or newer projects.");
return false;
}
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Launching a browser requires file associations that are not available in all operating systems.
reporter.Verbose("Browser refresh is only supported in Windows and MacOS.");
return false;
}
var dotnetCommand = context.ProcessSpec.Arguments.FirstOrDefault();
if (!string.Equals(dotnetCommand, "run", StringComparison.Ordinal))
{
reporter.Verbose("Browser refresh is only supported for run commands.");
return false;
}
// We're executing the run-command. Determine if the launchSettings allows it
var launchSettingsPath = Path.Combine(context.ProcessSpec.WorkingDirectory, "Properties", "launchSettings.json");
if (!File.Exists(launchSettingsPath))
{
reporter.Verbose($"No launchSettings.json file found at {launchSettingsPath}. Unable to determine if browser refresh is allowed.");
return false;
}
LaunchSettingsJson launchSettings;
try
{
launchSettings = JsonSerializer.Deserialize<LaunchSettingsJson>(
File.ReadAllText(launchSettingsPath),
new JsonSerializerOptions(JsonSerializerDefaults.Web));
}
catch (Exception ex)
{
reporter.Verbose($"Error reading launchSettings.json: {ex}.");
return false;
}
var defaultProfile = launchSettings.Profiles.FirstOrDefault(f => f.Value.CommandName == "Project").Value;
if (defaultProfile is null)
{
reporter.Verbose("Unable to find default launchSettings profile.");
return false;
}
if (!defaultProfile.LaunchBrowser)
{
reporter.Verbose("launchSettings does not allow launching browsers.");
return false;
}
launchUrl = defaultProfile.LaunchUrl;
return true;
}
public async ValueTask DisposeAsync()
{
_browserProcess?.Dispose();
if (_refreshServer != null)
{
await _refreshServer.DisposeAsync();
}
}
}
}

View File

@ -0,0 +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.Collections.Generic;
namespace Microsoft.DotNet.Watcher.Tools
{
public class LaunchSettingsJson
{
public Dictionary<string, LaunchSettingsProfile> Profiles { get; set; }
}
public class LaunchSettingsProfile
{
public string CommandName { get; set; }
public bool LaunchBrowser { get; set; }
public string LaunchUrl { get; set; }
}
}

View File

@ -1,8 +1,10 @@
// 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.
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.DotNet.Watcher.Internal;
namespace Microsoft.DotNet.Watcher
@ -19,5 +21,9 @@ namespace Microsoft.DotNet.Watcher
=> Path.GetFileNameWithoutExtension(Executable);
public bool IsOutputCaptured => OutputCapture != null;
public DataReceivedEventHandler OnOutput { get; set; }
public CancellationToken CancelOutputCapture { get; set; }
}
}

View File

@ -162,8 +162,8 @@ namespace Microsoft.DotNet.Watcher
_reporter.Output("Polling file watcher is enabled");
}
await new DotNetWatcher(reporter, fileSetFactory)
.WatchAsync(processInfo, cancellationToken);
await using var watcher = new DotNetWatcher(reporter, fileSetFactory);
await watcher.WatchAsync(processInfo, cancellationToken);
return 0;
}

View File

@ -9,9 +9,22 @@ them to a file.
-->
<Target Name="GenerateWatchList"
DependsOnTargets="_CollectWatchItems">
<PropertyGroup>
<_IsMicrosoftNETCoreApp31OrNewer
Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '3.1'))">true</_IsMicrosoftNETCoreApp31OrNewer>
<_IsMicrosoftNETCoreApp31OrNewer Condition="'$(_IsMicrosoftNETCoreApp31OrNewer)' == ''">false</_IsMicrosoftNETCoreApp31OrNewer>
</PropertyGroup>
<ItemGroup>
<_WatchListLine Include="$(_IsMicrosoftNETCoreApp31OrNewer)" />
<_WatchListLine Include="%(Watch.FullPath)" />
</ItemGroup>
<WriteLinesToFile Overwrite="true"
File="$(_DotNetWatchListFile)"
Lines="@(Watch -> '%(FullPath)')" />
Lines="@(_WatchListLine)" />
</Target>
<!--

View File

@ -19,4 +19,24 @@
<None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.WebSockets" />
</ItemGroup>
<Target Name="_FixupRuntimeConfig" BeforeTargets="_GenerateRuntimeConfigurationFilesInputCache">
<ItemGroup>
<_RuntimeFramework Include="@(RuntimeFramework)" />
<RuntimeFramework Remove="@(RuntimeFramework)" />
<RuntimeFramework Include="Microsoft.AspNetCore.App" FrameworkName="Microsoft.AspNetCore.App" Version="5.0.0-preview" />
</ItemGroup>
</Target>
<Target Name="_UndoRuntimeConfigWorkarounds" AfterTargets="GenerateBuildRuntimeConfigurationFiles">
<ItemGroup>
<RuntimeFramework Remove="@(RuntimeFramework)" />
<RuntimeFramework Include="@(_RuntimeFramework)" />
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,3 @@
{
"rollForwardOnNoCandidateFx": 2
}

View File

@ -0,0 +1,68 @@
// 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.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing;
using Xunit.Abstractions;
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{
public class BrowserLaunchTests
{
private readonly WatchableApp _app;
public BrowserLaunchTests(ITestOutputHelper logger)
{
_app = new WatchableApp("AppWithLaunchSettings", logger);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Linux)]
public async Task LaunchesBrowserOnStart()
{
var expected = "watch : Launching browser: https://localhost:5001/";
_app.DotnetWatchArgs.Add("--verbose");
await _app.StartWatcherAsync();
// Verify we launched the browser.
await _app.Process.GetOutputLineStartsWithAsync(expected, TimeSpan.FromMinutes(2));
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Linux)]
public async Task RefreshesBrowserOnChange()
{
var launchBrowserMessage = "watch : Launching browser: https://localhost:5001/";
var refreshBrowserMessage = "watch : Reloading browser";
_app.DotnetWatchArgs.Add("--verbose");
var source = Path.Combine(_app.SourceDirectory, "Program.cs");
await _app.StartWatcherAsync();
// Verify we launched the browser.
await _app.Process.GetOutputLineStartsWithAsync(launchBrowserMessage, TimeSpan.FromMinutes(2));
// Make a file change and verify we reloaded the browser.
File.SetLastWriteTime(source, DateTime.Now);
await _app.Process.GetOutputLineStartsWithAsync(refreshBrowserMessage, TimeSpan.FromMinutes(2));
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Linux)]
public async Task UsesBrowserSpecifiedInEnvironment()
{
var launchBrowserMessage = "watch : Launching browser: mycustombrowser.bat https://localhost:5001/";
_app.EnvironmentVariables.Add("DOTNET_WATCH_BROWSER_PATH", "mycustombrowser.bat");
_app.DotnetWatchArgs.Add("--verbose");
await _app.StartWatcherAsync();
// Verify we launched the browser.
await _app.Process.GetOutputLineStartsWithAsync(launchBrowserMessage, TimeSpan.FromMinutes(2));
}
}
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.DotNet.Watcher.Tools
public class MSBuildEvaluationFilterTest
{
private readonly IFileSetFactory _fileSetFactory = Mock.Of<IFileSetFactory>(
f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(new FileSet(Enumerable.Empty<string>())));
f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(FileSet.Empty));
[Fact]
public async Task ProcessAsync_EvaluatesFileSetIfProjFileChanges()
@ -98,7 +98,7 @@ namespace Microsoft.DotNet.Watcher.Tools
// concurrent edits. MSBuildEvaluationFilter uses timestamps to additionally track changes to these files.
// Arrange
var fileSet = new FileSet(new[] { "Controlller.cs", "Proj.csproj" });
var fileSet = new FileSet(false, new[] { "Controlller.cs", "Proj.csproj" });
var fileSetFactory = Mock.Of<IFileSetFactory>(f => f.CreateAsync(It.IsAny<CancellationToken>()) == Task.FromResult<IFileSet>(fileSet));
var filter = new TestableMSBuildEvaluationFilter(fileSetFactory)

View File

@ -41,6 +41,8 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
public List<string> DotnetWatchArgs { get; } = new List<string>();
public Dictionary<string, string> EnvironmentVariables { get; } = new Dictionary<string, string>();
public string SourceDirectory { get; }
public Task HasRestarted()
@ -108,9 +110,15 @@ namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
EnvironmentVariables =
{
["DOTNET_USE_POLLING_FILE_WATCHER"] = UsePollingWatcher.ToString(),
["__DOTNET_WATCH_RUNNING_AS_TEST"] = "true",
},
};
foreach (var env in EnvironmentVariables)
{
spec.EnvironmentVariables.Add(env.Key, env.Value);
}
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("helix")))
{
spec.EnvironmentVariables["DOTNET_ROOT"] = Directory.GetParent(dotnetPath).FullName;

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,32 @@
// 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.Threading;
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Started");
// Simulate an ASP.NET Core app. We want to avoid referencing asp.net core from a test asset
Console.WriteLine("info: Microsoft.Hosting.Lifetime[0]");
Console.WriteLine(" Now listening on: https://localhost:5001");
Console.WriteLine("info: Microsoft.Hosting.Lifetime[0]");
Console.WriteLine(" Now listening on: http://localhost:5000");
Console.WriteLine("info: Microsoft.Hosting.Lifetime[0]");
Console.WriteLine(" Application started. Press Ctrl+C to shut down.");
Console.WriteLine("info: Microsoft.Hosting.Lifetime[0]");
Console.WriteLine(" Hosting environment: Development");
Console.WriteLine("info: Microsoft.Hosting.Lifetime[0]");
Console.WriteLine($" Content root path: {Directory.GetCurrentDirectory()}");
Thread.Sleep(Timeout.Infinite);
}
}
}

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:38420",
"sslPort": 44393
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"b3": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}