Skip to content

Commit 0e713ce

Browse files
committed
Add browser emulator for testing browser refresh
Improve auto-restart reporting in the browser
1 parent 389f3bb commit 0e713ce

File tree

25 files changed

+453
-152
lines changed

25 files changed

+453
-152
lines changed

Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
<ItemGroup Condition="$(MicrosoftAspNetCoreAppRefPackageVersion.StartsWith('$(_TargetFrameworkVersionWithoutV)'))">
7474
<KnownFrameworkReference Update="Microsoft.AspNetCore.App">
7575
<LatestRuntimeFrameworkVersion>$(MicrosoftAspNetCoreAppRefPackageVersion)</LatestRuntimeFrameworkVersion>
76-
<RuntimePackRuntimeIdentifiers>${SupportedRuntimeIdentifiers}</RuntimePackRuntimeIdentifiers>
76+
<RuntimePackRuntimeIdentifiers>$(SupportedRuntimeIdentifiers)</RuntimePackRuntimeIdentifiers>
7777
<TargetingPackVersion>$(MicrosoftAspNetCoreAppRefPackageVersion)</TargetingPackVersion>
7878
<DefaultRuntimeFrameworkVersion>$(MicrosoftAspNetCoreAppRefPackageVersion)</DefaultRuntimeFrameworkVersion>
7979
</KnownFrameworkReference>

sdk.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@
312312
<Project Path="test/dotnet-format.UnitTests/dotnet-format.UnitTests.csproj" />
313313
<Project Path="test/dotnet-MsiInstallation.Tests/dotnet-MsiInstallation.Tests.csproj" />
314314
<Project Path="test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj" />
315+
<Project Path="test/dotnet-watch-test-browser/dotnet-watch-test-browser.csproj" />
315316
<Project Path="test/dotnet-watch.Tests/dotnet-watch.Tests.csproj" />
316317
<Project Path="test/dotnet.Tests/dotnet.Tests.csproj" />
317318
<Project Path="test/EndToEnd.Tests/EndToEnd.Tests.csproj" />

src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ public static void Log(this ILogger logger, LogEvent logEvent, params object[] a
3030
public static readonly LogEvent UpdatingDiagnostics = Create(LogLevel.Debug, "Updating diagnostics.");
3131
public static readonly LogEvent SendingStaticAssetUpdateRequest = Create(LogLevel.Debug, "Sending static asset update request to connected browsers: '{0}'.");
3232
public static readonly LogEvent RefreshServerRunningAt = Create(LogLevel.Debug, "Refresh server running at {0}.");
33+
public static readonly LogEvent ConnectedToRefreshServer = Create(LogLevel.Debug, "Connected to refresh server.");
3334
}

src/BuiltInTools/HotReloadClient/Web/BrowserConnection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public BrowserConnection(WebSocket clientSocket, string? sharedSecret, ILoggerFa
3737
ServerLogger = loggerFactory.CreateLogger(ServerLogComponentName, displayName);
3838
AgentLogger = loggerFactory.CreateLogger(AgentLogComponentName, displayName);
3939

40-
ServerLogger.LogDebug("Connected to referesh server.");
40+
ServerLogger.Log(LogEvents.ConnectedToRefreshServer);
4141
}
4242

4343
public void Dispose()

src/BuiltInTools/dotnet-watch.slnf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
"src\\BuiltInTools\\HotReloadClient\\Microsoft.DotNet.HotReload.Client.shproj",
1919
"src\\BuiltInTools\\dotnet-watch\\dotnet-watch.csproj",
2020
"test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj",
21+
"test\\Microsoft.DotNet.HotReload.Client.Tests\\Microsoft.DotNet.HotReload.Client.Tests.csproj",
2122
"test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj",
2223
"test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj",
2324
"test\\Microsoft.WebTools.AspireService.Tests\\Microsoft.WebTools.AspireService.Tests.csproj",
24-
"test\\Microsoft.DotNet.HotReload.Client.Tests\\Microsoft.DotNet.HotReload.Client.Tests.csproj",
25+
"test\\dotnet-watch-test-browser\\dotnet-watch-test-browser.csproj",
2526
"test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj"
2627
]
2728
}

src/BuiltInTools/dotnet-watch/Browser/BrowserLauncher.cs

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
using System.Collections.Immutable;
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
7+
using System.Runtime.CompilerServices;
78
using Microsoft.Build.Graph;
89
using Microsoft.DotNet.HotReload;
910
using Microsoft.Extensions.Logging;
1011

1112
namespace Microsoft.DotNet.Watch;
1213

13-
internal sealed class BrowserLauncher(ILogger logger, EnvironmentOptions environmentOptions)
14+
internal sealed class BrowserLauncher(ILogger logger, IProcessOutputReporter processOutputReporter, EnvironmentOptions environmentOptions)
1415
{
1516
// interlocked
1617
private ImmutableHashSet<ProjectInstanceId> _browserLaunchAttempted = [];
@@ -61,18 +62,13 @@ public static string GetLaunchUrl(string? profileLaunchUrl, string outputLaunchU
6162

6263
private void LaunchBrowser(string launchUrl, BrowserRefreshServer? server)
6364
{
64-
var fileName = launchUrl;
65+
var (fileName, arg, useShellExecute) = environmentOptions.BrowserPath is { } browserPath
66+
? (browserPath, launchUrl, false)
67+
: (launchUrl, null, true);
6568

66-
var args = string.Empty;
67-
if (environmentOptions.BrowserPath is { } browserPath)
68-
{
69-
args = fileName;
70-
fileName = browserPath;
71-
}
69+
logger.Log(MessageDescriptor.LaunchingBrowser, fileName, arg);
7270

73-
logger.Log(MessageDescriptor.LaunchingBrowser, fileName, args);
74-
75-
if (environmentOptions.TestFlags != TestFlags.None)
71+
if (environmentOptions.TestFlags != TestFlags.None && environmentOptions.BrowserPath == null)
7672
{
7773
if (environmentOptions.TestFlags.HasFlag(TestFlags.MockBrowser))
7874
{
@@ -83,29 +79,23 @@ private void LaunchBrowser(string launchUrl, BrowserRefreshServer? server)
8379
return;
8480
}
8581

86-
var info = new ProcessStartInfo
82+
// dotnet-watch, by default, relies on URL file association to launch browsers. On Windows and MacOS, this works fairly well
83+
// where URLs are associated with the default browser. On Linux, this is a bit murky.
84+
// From emperical observation, it's noted that failing to launch a browser results in either Process.Start returning a null-value
85+
// or for the process to have immediately exited.
86+
// We can use this to provide a helpful message.
87+
var processSpec = new ProcessSpec()
8788
{
88-
FileName = fileName,
89-
Arguments = args,
90-
UseShellExecute = true,
89+
Executable = fileName,
90+
Arguments = arg != null ? [arg] : [],
91+
UseShellExecute = useShellExecute,
92+
OnOutput = environmentOptions.TestFlags.HasFlag(TestFlags.RedirectBrowserOutput) ? processOutputReporter.ReportOutput : null,
9193
};
9294

93-
try
94-
{
95-
using var browserProcess = Process.Start(info);
96-
if (browserProcess is null or { HasExited: true })
97-
{
98-
// dotnet-watch, by default, relies on URL file association to launch browsers. On Windows and MacOS, this works fairly well
99-
// where URLs are associated with the default browser. On Linux, this is a bit murky.
100-
// From emperical observation, it's noted that failing to launch a browser results in either Process.Start returning a null-value
101-
// or for the process to have immediately exited.
102-
// We can use this to provide a helpful message.
103-
logger.LogInformation("Unable to launch the browser. Url '{Url}'.", launchUrl);
104-
}
105-
}
106-
catch (Exception e)
95+
using var browserProcess = ProcessRunner.TryStartProcess(processSpec, logger);
96+
if (browserProcess is null or { HasExited: true })
10797
{
108-
logger.LogDebug("Failed to launch a browser: {Message}", e.Message);
98+
logger.LogWarning("Unable to launch the browser. Url '{Url}'.", launchUrl);
10999
}
110100
}
111101

src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ internal static class ProjectGraphUtilities
4343
}
4444
catch (Exception e) when (e is not OperationCanceledException)
4545
{
46+
// ProejctGraph aggregates OperationCanceledException exception,
47+
// throw here to propagate the cancellation.
48+
cancellationToken.ThrowIfCancellationRequested();
49+
4650
logger.LogDebug("Failed to load project graph.");
4751

4852
if (e is AggregateException { InnerExceptions: var innerExceptions })

src/BuiltInTools/dotnet-watch/CommandLine/EnvironmentOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ internal enum TestFlags
2222
/// This allows tests to trigger key based events.
2323
/// </summary>
2424
ReadKeyFromStdin = 1 << 3,
25+
26+
/// <summary>
27+
/// Redirects the output of the launched browser process to watch output.
28+
/// </summary>
29+
RedirectBrowserOutput = 1 << 4,
2530
}
2631

2732
internal sealed record EnvironmentOptions(

src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates2 updat
363363
_logger.Log(MessageDescriptor.RestartNeededToApplyChanges);
364364
}
365365

366-
var diagnosticsToDisplayInApp = new List<string>();
366+
var errorsToDisplayInApp = new List<string>();
367367

368368
// Display errors first, then warnings:
369369
ReportCompilationDiagnostics(DiagnosticSeverity.Error);
@@ -373,7 +373,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates2 updat
373373
// report or clear diagnostics in the browser UI
374374
await ForEachProjectAsync(
375375
_runningProjects,
376-
(project, cancellationToken) => project.Clients.ReportCompilationErrorsInApplicationAsync([.. diagnosticsToDisplayInApp], cancellationToken).AsTask() ?? Task.CompletedTask,
376+
(project, cancellationToken) => project.Clients.ReportCompilationErrorsInApplicationAsync([.. errorsToDisplayInApp], cancellationToken).AsTask() ?? Task.CompletedTask,
377377
cancellationToken);
378378

379379
void ReportCompilationDiagnostics(DiagnosticSeverity severity)
@@ -437,16 +437,20 @@ void ReportRudeEdits()
437437
bool IsAutoRestartEnabled(ProjectId id)
438438
=> runningProjectInfos.TryGetValue(id, out var info) && info.RestartWhenChangesHaveNoEffect;
439439

440-
void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, string prefix = "")
440+
void ReportDiagnostic(Diagnostic diagnostic, MessageDescriptor descriptor, string autoPrefix = "")
441441
{
442442
var display = CSharpDiagnosticFormatter.Instance.Format(diagnostic);
443-
var args = new[] { prefix, display };
443+
var args = new[] { autoPrefix, display };
444444

445445
_logger.Log(descriptor, args);
446446

447-
if (descriptor.Severity != MessageSeverity.None)
447+
if (autoPrefix != "")
448448
{
449-
diagnosticsToDisplayInApp.Add(descriptor.GetMessage(args));
449+
errorsToDisplayInApp.Add(MessageDescriptor.RestartingApplicationToApplyChanges.GetMessage());
450+
}
451+
else if (descriptor.Severity != MessageSeverity.None)
452+
{
453+
errorsToDisplayInApp.Add(descriptor.GetMessage(args));
450454
}
451455
}
452456

src/BuiltInTools/dotnet-watch/HotReload/HotReloadClients.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public async Task ApplyStaticAssetUpdatesAsync(IEnumerable<(string filePath, str
160160
{
161161
content = ImmutableCollectionsMarshal.AsImmutableArray(await File.ReadAllBytesAsync(filePath, cancellationToken));
162162
}
163-
catch (Exception e)
163+
catch (Exception e) when (e is not OperationCanceledException)
164164
{
165165
ClientLogger.LogError("Failed to read file {FilePath}: {Message}", filePath, e.Message);
166166
continue;

0 commit comments

Comments
 (0)