Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement workaround for Aspire dashboard launching #46100

Merged
merged 5 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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: (?<url>.*)\s*$", RegexOptions.Compiled)]
private static partial Regex s_nowListeningOnRegex();
private static partial Regex GetNowListeningOnRegex();

[GeneratedRegex(@"Login to the dashboard at (?<url>.*)\s*$", RegexOptions.Compiled)]
private static partial Regex GetAspireDashboardUrlRegex();

private readonly object _serversGuard = new();
private readonly Dictionary<ProjectGraphNode, BrowserRefreshServer?> _servers = [];
Expand Down Expand Up @@ -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.
tmat marked this conversation as resolved.
Show resolved Hide resolved
// 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)
Expand All @@ -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;
Expand All @@ -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)
{
Expand All @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions test/dotnet-watch.Tests/Browser/BrowserConnectorTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
4 changes: 2 additions & 2 deletions test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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");
}
}
}
Loading