-
Notifications
You must be signed in to change notification settings - Fork 526
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve auto-forwarding for GitHub Codespaces and devcontainers. (#6780)
Improve auto-forwarding for GitHub Codespaces and devcontainers. (#6780)
- Loading branch information
1 parent
d9a9826
commit c331e53
Showing
16 changed files
with
260 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
src/Aspire.Hosting/Devcontainers/DevcontainerPortForwardingHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Globalization; | ||
using System.Text.Json.Nodes; | ||
|
||
namespace Aspire.Hosting.Devcontainers; | ||
|
||
internal class DevcontainerPortForwardingHelper | ||
{ | ||
private const string CodespaceSettingsPath = "/home/vscode/.vscode-remote/data/Machine/settings.json"; | ||
private const string LocalDevcontainerSettingsPath = "/home/vscode/.vscode-server/data/Machine/settings.json"; | ||
private const string PortAttributesFieldName = "remote.portsAttributes"; | ||
private const int WriteLockTimeoutMs = 2000; | ||
private static readonly SemaphoreSlim s_writeLock = new SemaphoreSlim(1); | ||
|
||
public static async Task SetPortAttributesAsync(int port, string protocol, string label, CancellationToken cancellationToken = default) | ||
{ | ||
ArgumentNullException.ThrowIfNullOrEmpty(protocol); | ||
ArgumentNullException.ThrowIfNullOrEmpty(label); | ||
|
||
var settingsPath = GetSettingsPath(); | ||
|
||
var acquired = await s_writeLock.WaitAsync(WriteLockTimeoutMs, cancellationToken).ConfigureAwait(false); | ||
|
||
if (!acquired) | ||
{ | ||
throw new DistributedApplicationException($"Failed to acquire semaphore for settings file: {settingsPath}"); | ||
} | ||
|
||
var settingsContent = await File.ReadAllTextAsync(settingsPath, cancellationToken).ConfigureAwait(false); | ||
var settings = (JsonObject)JsonObject.Parse(settingsContent)!; | ||
|
||
JsonObject? portsAttributes; | ||
if (!settings.TryGetPropertyValue(PortAttributesFieldName, out var portsAttributesNode)) | ||
{ | ||
portsAttributes = new JsonObject(); | ||
settings.Add(PortAttributesFieldName, portsAttributes); | ||
} | ||
else | ||
{ | ||
portsAttributes = (JsonObject)portsAttributesNode!; | ||
} | ||
|
||
var portAsString = port.ToString(CultureInfo.InvariantCulture); | ||
|
||
JsonObject? portAttributes; | ||
if (!portsAttributes.TryGetPropertyValue(portAsString, out var portAttributeNode)) | ||
{ | ||
portAttributes = new JsonObject(); | ||
portsAttributes.Add(portAsString, portAttributes); | ||
} | ||
else | ||
{ | ||
portAttributes = (JsonObject)portAttributeNode!; | ||
} | ||
|
||
portAttributes["label"] = label; | ||
portAttributes["protocol"] = protocol; | ||
portAttributes["onAutoForward"] = "notify"; | ||
|
||
settingsContent = settings.ToString(); | ||
await File.WriteAllTextAsync(settingsPath, settingsContent, cancellationToken).ConfigureAwait(false); | ||
|
||
s_writeLock.Release(); | ||
|
||
static string GetSettingsPath() | ||
{ | ||
// For some reason the machine settings path is different between Codespaces and local Devcontainers | ||
// so we probe for it here. We could use options to figure out which one to look for here but that | ||
// would require taking a dependency on the options system here which seems like overkill. | ||
if (File.Exists(CodespaceSettingsPath)) | ||
{ | ||
return CodespaceSettingsPath; | ||
} | ||
else if (File.Exists(LocalDevcontainerSettingsPath)) | ||
{ | ||
return LocalDevcontainerSettingsPath; | ||
} | ||
else | ||
{ | ||
throw new DistributedApplicationException("Could not find a devcontainer settings file."); | ||
} | ||
} | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
src/Aspire.Hosting/Devcontainers/DevcontainerPortForwardingLifecycleHook.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Aspire.Hosting.ApplicationModel; | ||
using Aspire.Hosting.Devcontainers.Codespaces; | ||
using Aspire.Hosting.Lifecycle; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Aspire.Hosting.Devcontainers; | ||
|
||
internal sealed class DevcontainerPortForwardingLifecycleHook : IDistributedApplicationLifecycleHook | ||
{ | ||
private readonly ILogger _hostingLogger; | ||
private readonly IOptions<CodespacesOptions> _codespacesOptions; | ||
private readonly IOptions<DevcontainersOptions> _devcontainersOptions; | ||
|
||
public DevcontainerPortForwardingLifecycleHook(ILoggerFactory loggerFactory, IOptions<CodespacesOptions> codespacesOptions, IOptions<DevcontainersOptions> devcontainersOptions) | ||
{ | ||
_hostingLogger = loggerFactory.CreateLogger("Aspire.Hosting"); | ||
_codespacesOptions = codespacesOptions; | ||
_devcontainersOptions = devcontainersOptions; | ||
} | ||
|
||
public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) | ||
{ | ||
if (!_devcontainersOptions.Value.IsDevcontainer && !_codespacesOptions.Value.IsCodespace) | ||
{ | ||
// We aren't a codespace so there is nothing to do here. | ||
return; | ||
} | ||
|
||
foreach (var resource in appModel.Resources) | ||
{ | ||
if (resource.Name == KnownResourceNames.AspireDashboard) | ||
{ | ||
// We don't configure the dashboard here because if we print out the URL | ||
// the dashboard will launch immediately but it hasn't actually started | ||
// which would lead to a poor experience. So we'll let the dashboard | ||
// URL writing logic call the helper directly. | ||
continue; | ||
} | ||
|
||
if (resource is not IResourceWithEndpoints resourceWithEndpoints) | ||
{ | ||
continue; | ||
} | ||
|
||
foreach (var endpoint in resourceWithEndpoints.Annotations.OfType<EndpointAnnotation>()) | ||
{ | ||
if (_codespacesOptions.Value.IsCodespace && !(endpoint.UriScheme is "https" or "http")) | ||
{ | ||
// Codespaces only does port forwarding over HTTPS. If the protocol is not HTTP or HTTPS | ||
// it cannot be forwarded because it can't intercept access to the endpoint without breaking | ||
// the non-HTTP protocol to do GitHub auth. | ||
continue; | ||
} | ||
|
||
// TODO: This is inefficient because we are opening the file, parsing it, updating it | ||
// and writing it each time. Its like this for now beause I need to use the logic | ||
// in a few places (here and when we print out the Dashboard URL) - but will need | ||
// to come back and optimize this to support some kind of batching. | ||
await DevcontainerPortForwardingHelper.SetPortAttributesAsync( | ||
endpoint.AllocatedEndpoint!.Port, | ||
endpoint.UriScheme, | ||
$"{resource.Name}-{endpoint.Name}", | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
_hostingLogger.LogInformation("Port forwarding: {Url}", endpoint.AllocatedEndpoint!.UriString); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.