Skip to content

Commit

Permalink
Rewrite dashboard URL in the console. (#6591)
Browse files Browse the repository at this point in the history
* Rewrite dashboard URL in the console.

* Include launch.json (and add Copilot extension).

* Can't get serverReadyAction working.

* Fix compiler error.

* PR feedback.
  • Loading branch information
mitchdenny authored Nov 2, 2024
1 parent 9d06327 commit bcfa2df
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 70 deletions.
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
"ms-dotnettools.vscodeintellicode-csharp",
"ms-azuretools.vscode-bicep",
"EditorConfig.EditorConfig",
"ms-azuretools.azure-dev"
"ms-azuretools.azure-dev",
"GitHub.copilot",
"GitHub.copilot-chat"
],
"settings": {
"remote.autoForwardPorts": false,
Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,6 @@ Session.vim
.netrwhist
*~

# Visual Studio Code
.vscode/

# Private test configuration and binaries.
config.ps1
**/IISApplications
Expand Down
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"request": "launch",
"name": "C#: WaitFor (Debug)",
"type": "dotnet",
"projectPath": "${workspaceFolder}/playground/waitfor/WaitForSandbox.AppHost/WaitForSandbox.AppHost.csproj"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Codespaces;

internal sealed class CodespacesResourceUrlRewriterService(ILogger<CodespacesResourceUrlRewriterService> logger, IOptions<CodespacesOptions> options, CodespacesUrlRewriter codespaceUrlRewriter, ResourceNotificationService resourceNotificationService) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!options.Value.IsCodespace)
{
logger.LogTrace("Not running in Codespaces, skipping URL rewriting.");
return;
}

do
{
try
{
var resourceEvents = resourceNotificationService.WatchAsync(stoppingToken);

await foreach (var resourceEvent in resourceEvents.ConfigureAwait(false))
{
Dictionary<UrlSnapshot, UrlSnapshot>? remappedUrls = null;

foreach (var originalUrlSnapshot in resourceEvent.Snapshot.Urls)
{
var uri = new Uri(originalUrlSnapshot.Url);

if (!originalUrlSnapshot.IsInternal && (uri.Scheme == "http" || uri.Scheme == "https") && uri.Host == "localhost")
{
remappedUrls ??= new();

var newUrlSnapshot = originalUrlSnapshot with
{
// The format of GitHub Codespaces URLs comprises the codespace
// name (from the CODESPACE_NAME environment variable, the port,
// and the port forwarding domain (via GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
// which is typically ".app.github.dev". The VSCode instance is typically
// hosted at codespacename.github.dev whereas the forwarded ports
// would be at codespacename-port.app.github.dev.
Url = codespaceUrlRewriter.RewriteUrl(uri)
};

remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot);
}
}

if (remappedUrls is not null)
{
var transformedUrls = from originalUrl in resourceEvent.Snapshot.Urls
select remappedUrls.TryGetValue(originalUrl, out var remappedUrl) ? remappedUrl : originalUrl;

await resourceNotificationService.PublishUpdateAsync(resourceEvent.Resource, resourceEvent.ResourceId, s => s with
{
Urls = transformedUrls.ToImmutableArray()
}).ConfigureAwait(false);
}
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
// When debugging sometimes we'll get cancelled here but we don't want
// to tear down the loop. We only want to crash out when the service's
// cancellation token is signaled.
logger.LogTrace(ex, "Codespace URL rewriting loop threw an exception but was ignored.");
}
} while (!stoppingToken.IsCancellationRequested);
}
}
74 changes: 15 additions & 59 deletions src/Aspire.Hosting/Codespaces/CodespacesUrlRewriter.cs
Original file line number Diff line number Diff line change
@@ -1,76 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Codespaces;

internal sealed class CodespacesUrlRewriter(ILogger<CodespacesUrlRewriter> logger, IOptions<CodespacesOptions> options, ResourceNotificationService resourceNotificationService) : BackgroundService
internal sealed class CodespacesUrlRewriter(IOptions<CodespacesOptions> options)
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
public string RewriteUrl(string url)
{
ArgumentNullException.ThrowIfNullOrWhiteSpace(url);

if (!options.Value.IsCodespace)
{
logger.LogTrace("Not running in Codespaces, skipping URL rewriting.");
return;
return url;
}

do
{
try
{
var resourceEvents = resourceNotificationService.WatchAsync(stoppingToken);

await foreach (var resourceEvent in resourceEvents.ConfigureAwait(false))
{
Dictionary<UrlSnapshot, UrlSnapshot>? remappedUrls = null;

foreach (var originalUrlSnapshot in resourceEvent.Snapshot.Urls)
{
var uri = new Uri(originalUrlSnapshot.Url);

if (!originalUrlSnapshot.IsInternal && (uri.Scheme == "http" || uri.Scheme == "https") && uri.Host == "localhost")
{
remappedUrls ??= new();

var newUrlSnapshot = originalUrlSnapshot with
{
// The format of GitHub Codespaces URLs comprises the codespace
// name (from the CODESPACE_NAME environment variable, the port,
// and the port forwarding domain (via GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
// which is typically ".app.github.dev". The VSCode instance is typically
// hosted at codespacename.github.dev whereas the forwarded ports
// would be at codespacename-port.app.github.dev.
Url = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}"
};

remappedUrls.Add(originalUrlSnapshot, newUrlSnapshot);
}
}
return RewriteUrl(new Uri(url, UriKind.Absolute));
}

if (remappedUrls is not null)
{
var transformedUrls = from originalUrl in resourceEvent.Snapshot.Urls
select remappedUrls.TryGetValue(originalUrl, out var remappedUrl) ? remappedUrl : originalUrl;
public string RewriteUrl(Uri uri)
{
if (!options.Value.IsCodespace)
{
return uri.ToString();
}

await resourceNotificationService.PublishUpdateAsync(resourceEvent.Resource, resourceEvent.ResourceId, s => s with
{
Urls = transformedUrls.ToImmutableArray()
}).ConfigureAwait(false);
}
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
// When debugging sometimes we'll get cancelled here but we don't want
// to tear down the loop. We only want to crash out when the service's
// cancellation token is signaled.
logger.LogTrace(ex, "Codespace URL rewriting loop threw an exception but was ignored.");
}
} while (!stoppingToken.IsCancellationRequested);
var codespacesUrl = $"{uri.Scheme}://{options.Value.CodespaceName}-{uri.Port}.{options.Value.PortForwardingDomain}{uri.AbsolutePath}";
return codespacesUrl;
}
}
14 changes: 10 additions & 4 deletions src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Aspire.Dashboard.ConsoleLogs;
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Codespaces;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Utils;
Expand All @@ -29,7 +30,8 @@ internal sealed class DashboardLifecycleHook(IConfiguration configuration,
ResourceLoggerService resourceLoggerService,
ILoggerFactory loggerFactory,
DcpNameGenerator nameGenerator,
IHostApplicationLifetime hostApplicationLifetime) : IDistributedApplicationLifecycleHook, IAsyncDisposable
IHostApplicationLifetime hostApplicationLifetime,
CodespacesUrlRewriter codespaceUrlRewriter) : IDistributedApplicationLifecycleHook, IAsyncDisposable
{
private Task? _dashboardLogsTask;
private CancellationTokenSource? _dashboardLogsCts;
Expand Down Expand Up @@ -235,14 +237,18 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)

// We need to print out the url so that dotnet watch can launch the dashboard
// technically this is too early, but it's late ne
if (StringUtils.TryGetUriFromDelimitedString(dashboardUrls, ";", out var firstDashboardUrl))
if (!StringUtils.TryGetUriFromDelimitedString(dashboardUrls, ";", out var firstDashboardUrl))
{
distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", firstDashboardUrl.ToString().TrimEnd('/'));
return;
}

var dashboardUrl = codespaceUrlRewriter.RewriteUrl(firstDashboardUrl.ToString());

distributedApplicationLogger.LogInformation("Now listening on: {DashboardUrl}", dashboardUrl.TrimEnd('/'));

if (!string.IsNullOrEmpty(browserToken))
{
LoggingHelpers.WriteDashboardUrl(distributedApplicationLogger, dashboardUrls, browserToken, isContainer: false);
LoggingHelpers.WriteDashboardUrl(distributedApplicationLogger, dashboardUrl, browserToken, isContainer: false);
}
}));
}
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)

// Codespaces
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CodespacesOptions>, ConfigureCodespacesOptions>());
_innerBuilder.Services.AddHostedService<CodespacesUrlRewriter>();
_innerBuilder.Services.AddSingleton<CodespacesUrlRewriter>();
_innerBuilder.Services.AddHostedService<CodespacesResourceUrlRewriterService>();

Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Globalization;
using System.Text.Json;
using System.Threading.Channels;
using Aspire.Hosting.Codespaces;
using Aspire.Hosting.ConsoleLogs;
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Dcp;
Expand Down Expand Up @@ -99,8 +100,13 @@ private static DashboardLifecycleHook CreateHook(
ResourceLoggerService resourceLoggerService,
ResourceNotificationService resourceNotificationService,
IConfiguration configuration,
ILoggerFactory? loggerFactory = null)
ILoggerFactory? loggerFactory = null,
IOptions<CodespacesOptions>? codespacesOptions = null
)
{
codespacesOptions ??= Options.Create(new CodespacesOptions());
var rewriter = new CodespacesUrlRewriter(codespacesOptions);

return new DashboardLifecycleHook(
configuration,
Options.Create(new DashboardOptions { DashboardPath = "test.dll" }),
Expand All @@ -111,7 +117,9 @@ private static DashboardLifecycleHook CreateHook(
resourceLoggerService,
loggerFactory ?? NullLoggerFactory.Instance,
new DcpNameGenerator(configuration, Options.Create(new DcpOptions())),
new TestHostApplicationLifetime());
new TestHostApplicationLifetime(),
rewriter
);
}

public static IEnumerable<object?[]> Data()
Expand Down

0 comments on commit bcfa2df

Please sign in to comment.