Skip to content

Commit b6240ba

Browse files
authored
Implement workaround for Aspire dashboard launching (#46100)
2 parents 4f810fb + f32eca6 commit b6240ba

File tree

4 files changed

+48
-11
lines changed

4 files changed

+48
-11
lines changed

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ internal sealed partial class BrowserConnector(DotNetWatchContext context) : IAs
1414
// This needs to be in sync with the version BrowserRefreshMiddleware is compiled against.
1515
private static readonly Version s_minimumSupportedVersion = Versions.Version6_0;
1616

17-
private static readonly Regex s_nowListeningRegex = s_nowListeningOnRegex();
17+
private static readonly Regex s_nowListeningRegex = GetNowListeningOnRegex();
18+
private static readonly Regex s_aspireDashboardUrlRegex = GetAspireDashboardUrlRegex();
1819

1920
[GeneratedRegex(@"Now listening on: (?<url>.*)\s*$", RegexOptions.Compiled)]
20-
private static partial Regex s_nowListeningOnRegex();
21+
private static partial Regex GetNowListeningOnRegex();
22+
23+
[GeneratedRegex(@"Login to the dashboard at (?<url>.*)\s*$", RegexOptions.Compiled)]
24+
private static partial Regex GetAspireDashboardUrlRegex();
2125

2226
private readonly object _serversGuard = new();
2327
private readonly Dictionary<ProjectGraphNode, BrowserRefreshServer?> _servers = [];
@@ -115,6 +119,10 @@ public bool TryGetRefreshServer(ProjectGraphNode projectNode, [NotNullWhen(true)
115119

116120
bool matchFound = false;
117121

122+
// Workaround for Aspire dashboard launching: scan for "Login to the dashboard at " prefix in the output and use the URL.
123+
// TODO: Share launch profile processing logic as implemented in VS with dotnet-run and implement browser launching there.
124+
var isAspireHost = projectNode.GetCapabilities().Contains(AspireServiceFactory.AppHostProjectCapability);
125+
118126
return handler;
119127

120128
void handler(OutputLine line)
@@ -127,7 +135,7 @@ void handler(OutputLine line)
127135
return;
128136
}
129137

130-
var match = s_nowListeningRegex.Match(line.Content);
138+
var match = (isAspireHost ? s_aspireDashboardUrlRegex : s_nowListeningRegex).Match(line.Content);
131139
if (!match.Success)
132140
{
133141
return;
@@ -139,7 +147,8 @@ void handler(OutputLine line)
139147
ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, projectNode) => set.Add(projectNode), projectNode))
140148
{
141149
// first build iteration of a root project:
142-
LaunchBrowser(launchProfile, match.Groups["url"].Value, server);
150+
var launchUrl = GetLaunchUrl(launchProfile.LaunchUrl, match.Groups["url"].Value);
151+
LaunchBrowser(launchUrl, server);
143152
}
144153
else if (server != null)
145154
{
@@ -151,10 +160,15 @@ void handler(OutputLine line)
151160
}
152161
}
153162

154-
private void LaunchBrowser(LaunchSettingsProfile launchProfile, string launchUrl, BrowserRefreshServer? server)
163+
public static string GetLaunchUrl(string? profileLaunchUrl, string outputLaunchUrl)
164+
=> string.IsNullOrWhiteSpace(profileLaunchUrl) ? outputLaunchUrl :
165+
Uri.TryCreate(profileLaunchUrl, UriKind.Absolute, out _) ? profileLaunchUrl :
166+
Uri.TryCreate(outputLaunchUrl, UriKind.Absolute, out var launchUri) ? new Uri(launchUri, profileLaunchUrl).ToString() :
167+
outputLaunchUrl;
168+
169+
private void LaunchBrowser(string launchUrl, BrowserRefreshServer? server)
155170
{
156-
var launchPath = launchProfile.LaunchUrl;
157-
var fileName = Uri.TryCreate(launchPath, UriKind.Absolute, out _) ? launchPath : launchUrl + "/" + launchPath;
171+
var fileName = launchUrl;
158172

159173
var args = string.Empty;
160174
if (EnvironmentVariables.BrowserPath is { } browserPath)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
namespace Microsoft.DotNet.Watch.UnitTests;
7+
8+
public class BrowserConnectorTests
9+
{
10+
[Theory]
11+
[InlineData(null, "https://localhost:1234", "https://localhost:1234")]
12+
[InlineData("", "https://localhost:1234", "https://localhost:1234")]
13+
[InlineData(" ", "https://localhost:1234", "https://localhost:1234")]
14+
[InlineData("", "a/b", "a/b")]
15+
[InlineData("x/y", "a/b", "a/b")]
16+
[InlineData("a/b?X=1", "https://localhost:1234", "https://localhost:1234/a/b?X=1")]
17+
[InlineData("https://localhost:1000/a/b", "https://localhost:1234", "https://localhost:1000/a/b")]
18+
[InlineData("https://localhost:1000/x/y?z=u", "https://localhost:1234/a?b=c", "https://localhost:1000/x/y?z=u")]
19+
public void GetLaunchUrl(string? profileLaunchUrl, string outputLaunchUrl, string expected)
20+
{
21+
Assert.Equal(expected, BrowserConnector.GetLaunchUrl(profileLaunchUrl, outputLaunchUrl));
22+
}
23+
}

test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ public async Task BlazorWasm(bool projectSpecifiesCapabilities)
328328
App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser);
329329

330330
// Browser is launched based on blazor-devserver output "Now listening on: ...".
331-
await App.WaitUntilOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}/");
331+
await App.WaitUntilOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}");
332332

333333
// Middleware should have been loaded to blazor-devserver before the browser is launched:
334334
App.AssertOutputContains("dbug: Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorWasmHotReloadMiddleware[0]");
@@ -395,7 +395,7 @@ public async Task Razor_Component_ScopedCssAndStaticAssets()
395395

396396
App.AssertOutputContains(MessageDescriptor.ConfiguredToUseBrowserRefresh);
397397
App.AssertOutputContains(MessageDescriptor.ConfiguredToLaunchBrowser);
398-
App.AssertOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}/");
398+
App.AssertOutputContains($"dotnet watch ⌚ Launching browser: http://localhost:{port}");
399399
App.Process.ClearOutput();
400400

401401
var scopedCssPath = Path.Combine(testAsset.Path, "RazorClassLibrary", "Components", "Example.razor.css");

test/dotnet-watch.Tests/Watch/BrowserLaunchTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public async Task LaunchesBrowserOnStart()
2727
Assert.Contains(App.Process.Output, line => line.Contains("Hosting environment: Development"));
2828

2929
// Verify we launched the browser.
30-
Assert.Contains(App.Process.Output, line => line.Contains("dotnet watch ⌚ Launching browser: https://localhost:5001/"));
30+
Assert.Contains(App.Process.Output, line => line.Contains("dotnet watch ⌚ Launching browser: https://localhost:5001"));
3131
}
3232

3333
[Fact]
@@ -43,7 +43,7 @@ public async Task UsesBrowserSpecifiedInEnvironment()
4343
await App.AssertOutputLineStartsWith(MessageDescriptor.ConfiguredToLaunchBrowser);
4444

4545
// Verify we launched the browser.
46-
await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: mycustombrowser.bat https://localhost:5001/");
46+
await App.AssertOutputLineStartsWith("dotnet watch ⌚ Launching browser: mycustombrowser.bat https://localhost:5001");
4747
}
4848
}
4949
}

0 commit comments

Comments
 (0)