Add support for launching a browser on file change (#24220)
This commit is contained in:
parent
b33f8ddbe2
commit
11835cf768
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<!--
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rollForwardOnNoCandidateFx": 2
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue