From eb3e0f73dfbdc88609ed6e76b82c5c4b94cb390e Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 17 Jan 2025 12:27:20 -0800 Subject: [PATCH 1/4] Implement workaround for Aspire dashboard launching --- .../dotnet-watch/Browser/BrowserConnector.cs | 28 ++++++++++++++----- .../Browser/BrowserConnectorTests.cs | 23 +++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 test/dotnet-watch.Tests/Browser/BrowserConnectorTests.cs diff --git a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs index 9126b8ec54b6..7aaffa5717df 100644 --- a/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs +++ b/src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs @@ -14,10 +14,14 @@ internal sealed partial class BrowserConnector(DotNetWatchContext context) : IAs // This needs to be in sync with the version BrowserRefreshMiddleware is compiled against. private static readonly Version s_minimumSupportedVersion = Versions.Version6_0; - private static readonly Regex s_nowListeningRegex = s_nowListeningOnRegex(); + private static readonly Regex s_nowListeningRegex = GetNowListeningOnRegex(); + private static readonly Regex s_aspireDashboardUrlRegex = GetAspireDashboardUrlRegex(); [GeneratedRegex(@"Now listening on: (?.*)\s*$", RegexOptions.Compiled)] - private static partial Regex s_nowListeningOnRegex(); + private static partial Regex GetNowListeningOnRegex(); + + [GeneratedRegex(@"Login to the dashboard at (?.*)\s*$", RegexOptions.Compiled)] + private static partial Regex GetAspireDashboardUrlRegex(); private readonly object _serversGuard = new(); private readonly Dictionary _servers = []; @@ -117,6 +121,10 @@ public bool TryGetRefreshServer(ProjectGraphNode projectNode, [NotNullWhen(true) bool matchFound = false; + // Workaround for Aspire dashboard launching: scan for "Login to the dashboard at " prefix in the output and use the URL. + // TODO: Share launch profile processing logic as implemented in VS with dotnet-run and implement browser launching there. + var isAspireHost = projectNode.GetCapabilities().Contains(AspireServiceFactory.AppHostProjectCapability); + return handler; void handler(OutputLine line) @@ -129,7 +137,7 @@ void handler(OutputLine line) return; } - var match = s_nowListeningRegex.Match(line.Content); + var match = (isAspireHost ? s_aspireDashboardUrlRegex : s_nowListeningRegex).Match(line.Content); if (!match.Success) { return; @@ -141,7 +149,8 @@ void handler(OutputLine line) if (projectAddedToAttemptedSet) { // first iteration: - LaunchBrowser(launchProfile, match.Groups["url"].Value, server); + var launchUrl = GetLaunchUrl(launchProfile.LaunchUrl, match.Groups["url"].Value); + LaunchBrowser(launchUrl, server); } else if (server != null) { @@ -153,10 +162,15 @@ void handler(OutputLine line) } } - private void LaunchBrowser(LaunchSettingsProfile launchProfile, string launchUrl, BrowserRefreshServer? server) + public static string GetLaunchUrl(string? profileLaunchUrl, string outputLaunchUrl) + => string.IsNullOrWhiteSpace(profileLaunchUrl) ? outputLaunchUrl : + Uri.TryCreate(profileLaunchUrl, UriKind.Absolute, out _) ? profileLaunchUrl : + Uri.TryCreate(outputLaunchUrl, UriKind.Absolute, out var launchUri) ? new Uri(launchUri, profileLaunchUrl).ToString() : + outputLaunchUrl; + + private void LaunchBrowser(string launchUrl, BrowserRefreshServer? server) { - var launchPath = launchProfile.LaunchUrl; - var fileName = Uri.TryCreate(launchPath, UriKind.Absolute, out _) ? launchPath : launchUrl + "/" + launchPath; + var fileName = launchUrl; var args = string.Empty; if (EnvironmentVariables.BrowserPath is { } browserPath) diff --git a/test/dotnet-watch.Tests/Browser/BrowserConnectorTests.cs b/test/dotnet-watch.Tests/Browser/BrowserConnectorTests.cs new file mode 100644 index 000000000000..add6710a3010 --- /dev/null +++ b/test/dotnet-watch.Tests/Browser/BrowserConnectorTests.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class BrowserConnectorTests +{ + [Theory] + [InlineData(null, "https://localhost:1234", "https://localhost:1234")] + [InlineData("", "https://localhost:1234", "https://localhost:1234")] + [InlineData(" ", "https://localhost:1234", "https://localhost:1234")] + [InlineData("", "a/b", "a/b")] + [InlineData("x/y", "a/b", "a/b")] + [InlineData("a/b?X=1", "https://localhost:1234", "https://localhost:1234/a/b?X=1")] + [InlineData("https://localhost:1000/a/b", "https://localhost:1234", "https://localhost:1000/a/b")] + [InlineData("https://localhost:1000/x/y?z=u", "https://localhost:1234/a?b=c", "https://localhost:1000/x/y?z=u")] + public void GetLaunchUrl(string? profileLaunchUrl, string outputLaunchUrl, string expected) + { + Assert.Equal(expected, BrowserConnector.GetLaunchUrl(profileLaunchUrl, outputLaunchUrl)); + } +} From 965832676a101ab273fe8d83193f37f009a96e0a Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Sun, 19 Jan 2025 12:03:01 -0800 Subject: [PATCH 2/4] Fix --- test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs b/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs index fc55382456ac..b7f24c464250 100644 --- a/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs +++ b/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs @@ -43,7 +43,7 @@ public async Task UsesBrowserSpecifiedInEnvironment() await App.AssertOutputLineStartsWith(MessageDescriptor.ConfiguredToLaunchBrowser); // Verify we launched the browser. - await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: mycustombrowser.bat https://localhost:5001/"); + await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: mycustombrowser.bat https://localhost:5001"); } } } From 030695e97a4da08c8eee98d348a1f38bdd966966 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Sun, 19 Jan 2025 13:47:04 -0800 Subject: [PATCH 3/4] Fix --- test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs b/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs index b7f24c464250..569c2b8d07ac 100644 --- a/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs +++ b/test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs @@ -27,7 +27,7 @@ public async Task LaunchesBrowserOnStart() Assert.Contains(App.Process.Output, line => line.Contains("Hosting environment: Development")); // Verify we launched the browser. - Assert.Contains(App.Process.Output, line => line.Contains("dotnet watch ⌚ Launching browser: https://localhost:5001/")); + Assert.Contains(App.Process.Output, line => line.Contains("dotnet watch ⌚ Launching browser: https://localhost:5001")); } [Fact] From 8a8df711dc855efa04156e23f7d655de458502eb Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Tue, 21 Jan 2025 11:38:56 -0800 Subject: [PATCH 4/4] Fix --- test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index 8ca084b74b48..52e3c8bac40b 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -328,7 +328,7 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities) App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); // Browser is launched based on blazor-devserver output "Now listening on: ...". - await App.WaitUntilOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}/"); + await App.WaitUntilOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}"); // Middleware should have been loaded to blazor-devserver before the browser is launched: App.AssertOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorWasmHotReloadMiddleware[0]"); @@ -395,7 +395,7 @@ public async Task Razor_Component_ScopedCssAndStaticAssets() App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh); App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser); - App.AssertOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}/"); + App.AssertOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}"); App.Process.ClearOutput(); var scopedCssPath = Path.Combine(testAsset.Path, "RazorClassLibrary", "Components", "Example.razor.css");