Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: fetch hostname suffix from API#103

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
spikecurtis merged 2 commits intomainfromspike/49-fetch-suffix
May 20, 2025
Merged
Show file tree
Hide file tree
Changes fromall 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
1 change: 1 addition & 0 deletionsApp/App.xaml.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,6 +72,7 @@ public App()
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
services.AddSingleton<ICredentialManager, CredentialManager>();
services.AddSingleton<IRpcController, RpcController>();
services.AddSingleton<IHostnameSuffixGetter, HostnameSuffixGetter>();

services.AddOptions<MutagenControllerConfig>()
.Bind(builder.Configuration.GetSection(MutagenControllerConfigSection));
Expand Down
13 changes: 12 additions & 1 deletionApp/Models/CredentialModel.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
using System;
using Coder.Desktop.CoderSdk.Coder;

namespace Coder.Desktop.App.Models;

Expand All@@ -14,7 +15,7 @@ public enum CredentialState
Valid,
}

public class CredentialModel
public class CredentialModel : ICoderApiClientCredentialProvider
{
public CredentialState State { get; init; } = CredentialState.Unknown;

Expand All@@ -33,4 +34,14 @@ public CredentialModel Clone()
Username = Username,
};
}

public CoderApiClientCredential? GetCoderApiClientCredential()
{
if (State != CredentialState.Valid) return null;
return new CoderApiClientCredential
{
ApiToken = ApiToken!,
CoderUrl = CoderUrl!,
};
}
}
144 changes: 144 additions & 0 deletionsApp/Services/HostnameSuffixGetter.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Coder.Desktop.App.Models;
using Coder.Desktop.CoderSdk.Coder;
using Coder.Desktop.Vpn.Utilities;
using Microsoft.Extensions.Logging;

namespace Coder.Desktop.App.Services;

public interface IHostnameSuffixGetter
{
public event EventHandler<string> SuffixChanged;

public string GetCachedSuffix();
}

public class HostnameSuffixGetter : IHostnameSuffixGetter
{
private const string DefaultSuffix = ".coder";

private readonly ICredentialManager _credentialManager;
private readonly ICoderApiClientFactory _clientFactory;
private readonly ILogger<HostnameSuffixGetter> _logger;

// _lock protects all private (non-readonly) values
private readonly RaiiSemaphoreSlim _lock = new(1, 1);
private string _domainSuffix = DefaultSuffix;
private bool _dirty = false;
private bool _getInProgress = false;
private CredentialModel _credentialModel = new() { State = CredentialState.Invalid };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We should just fetch this on-demand from the CredentialManager (since it's cheap) rather than storing it here.

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I tried this, but it complicates things considerably in terms of race conditions because you can't synchronize changes toGetCachedCredentials with the other items we track here like_dirty, so its hard to guarantee the correct behavior. Writing updates to this while holding the lock ends up being much simpler.


public event EventHandler<string>? SuffixChanged;

public HostnameSuffixGetter(ICredentialManager credentialManager, ICoderApiClientFactory apiClientFactory,
ILogger<HostnameSuffixGetter> logger)
{
_credentialManager = credentialManager;
_clientFactory = apiClientFactory;
_logger = logger;
credentialManager.CredentialsChanged += HandleCredentialsChanged;
HandleCredentialsChanged(this, _credentialManager.GetCachedCredentials());
}

~HostnameSuffixGetter()
{
_credentialManager.CredentialsChanged -= HandleCredentialsChanged;
}

private void HandleCredentialsChanged(object? sender, CredentialModel credentials)
{
using var _ = _lock.Lock();
_logger.LogDebug("credentials updated with state {state}", credentials.State);
_credentialModel = credentials;
if (credentials.State != CredentialState.Valid) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Should you set the stored domain back to the default in this case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Maybe it would be better to just always create the task if the credentials changed (i.e. remove this check here) and do the check once in the task.

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I don't think we should set the stored domain back to the default. The most common scenario for getting invalid creds is probably a logout or expired token --- so it's not particularly likely that the Default is more correct.

In any case, once we sign back in we'll get the correct value.


In terms of avoiding the check and always creating the task, seems like a lot of churn (locking, creating tasks, dropping logs) to avoid one conditional.


_dirty = true;
if (!_getInProgress)
{
_getInProgress = true;
Task.Run(Refresh).ContinueWith(MaybeRefreshAgain);
}
}

private async Task Refresh()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This should probably just take the credentials model as an argument.

Copy link
CollaboratorAuthor

@spikecurtisspikecurtisMay 16, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We want to clear the_dirty flag while grabbing the most up to date credential (holding the lock), then any later valid credentials that come in will re-set the_dirty flag. That doesn't work if we pass the credential as an argument because we can't guarantee it's the "latest" if we're not holding the lock.

{
_logger.LogDebug("refreshing domain suffix");
CredentialModel credentials;
using (_ = await _lock.LockAsync())
{
credentials = _credentialModel;
if (credentials.State != CredentialState.Valid)
{
_logger.LogDebug("abandoning refresh because credentials are now invalid");
return;
}

_dirty = false;
}

var client = _clientFactory.Create(credentials);
using var timeoutSrc = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var connInfo = await client.GetAgentConnectionInfoGeneric(timeoutSrc.Token);

// older versions of Coder might not set this
var suffix = string.IsNullOrEmpty(connInfo.HostnameSuffix)
? DefaultSuffix
// and, it doesn't include the leading dot.
: "." + connInfo.HostnameSuffix;

var changed = false;
using (_ = await _lock.LockAsync(CancellationToken.None))
{
if (_domainSuffix != suffix) changed = true;
_domainSuffix = suffix;
}

if (changed)
{
_logger.LogInformation("got new domain suffix '{suffix}'", suffix);
// grab a local copy of the EventHandler to avoid TOCTOU race on the `?.` null-check
var del = SuffixChanged;
del?.Invoke(this, suffix);
}
else
{
_logger.LogDebug("domain suffix unchanged '{suffix}'", suffix);
}
}

private async Task MaybeRefreshAgain(Task prev)
{
if (prev.IsFaulted)
{
_logger.LogError(prev.Exception, "failed to query domain suffix");
// back off here before retrying. We're just going to use a fixed, long
// delay since this just affects UI stuff; we're not in a huge rush as
// long as we eventually get the right value.
await Task.Delay(TimeSpan.FromSeconds(10));
}

using var l = await _lock.LockAsync(CancellationToken.None);
if ((_dirty || prev.IsFaulted) && _credentialModel.State == CredentialState.Valid)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

_dirty seems to only be true here if the credentials became invalid between when the event was handled and the task started. So the only way for this to trigger would be if credentials became valid, then became invalid, then became valid again, which seems very unlikely to actually happen. The user would need to sign in or start the app, then sign out, then enter their credentials and sign back in (with API auth check latency included).

You could probably also accomplish the same thing without needing a bool by throwing in the credential state guard at the top ofRefresh(), and only relying onprev.IsFaulted here

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The idea of_dirty is that while we are making the network call a new, valid credential for a different deployment could come in. That is, just because we had a valid credential when we started our work, and have a valid credential at the end, it doesn't mean thatit was the same credential. If that happens then we refresh to ensure we have the correct value from the new credential.

{
// we still have valid credentials and we're either dirty or the last Get failed.
_logger.LogDebug("retrying domain suffix query");
_ = Task.Run(Refresh).ContinueWith(MaybeRefreshAgain);
return;
}

// Getting here means either the credentials are not valid or we don't need to
// refresh anyway.
// The next time we get new, valid credentials, HandleCredentialsChanged will kick off
// a new Refresh
_getInProgress = false;
return;
}

public string GetCachedSuffix()
{
using var _ = _lock.Lock();
return _domainSuffix;
}
}
29 changes: 24 additions & 5 deletionsApp/ViewModels/TrayWindowViewModel.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
private readonly IRpcController _rpcController;
private readonly ICredentialManager _credentialManager;
private readonly IAgentViewModelFactory _agentViewModelFactory;
private readonly IHostnameSuffixGetter _hostnameSuffixGetter;

private FileSyncListWindow? _fileSyncListWindow;

Expand DownExpand Up@@ -91,15 +92,14 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost

[ObservableProperty] public partial string DashboardUrl { get; set; } = DefaultDashboardUrl;

private string _hostnameSuffix = DefaultHostnameSuffix;

public TrayWindowViewModel(IServiceProvider services, IRpcController rpcController,
ICredentialManager credentialManager, IAgentViewModelFactory agentViewModelFactory)
ICredentialManager credentialManager, IAgentViewModelFactory agentViewModelFactory, IHostnameSuffixGetter hostnameSuffixGetter)
{
_services = services;
_rpcController = rpcController;
_credentialManager = credentialManager;
_agentViewModelFactory = agentViewModelFactory;
_hostnameSuffixGetter = hostnameSuffixGetter;

// Since the property value itself never changes, we add event
// listeners for the underlying collection changing instead.
Expand DownExpand Up@@ -139,6 +139,9 @@ public void Initialize(DispatcherQueue dispatcherQueue)

_credentialManager.CredentialsChanged += (_, credentialModel) => UpdateFromCredentialModel(credentialModel);
UpdateFromCredentialModel(_credentialManager.GetCachedCredentials());

_hostnameSuffixGetter.SuffixChanged += (_, suffix) => HandleHostnameSuffixChanged(suffix);
HandleHostnameSuffixChanged(_hostnameSuffixGetter.GetCachedSuffix());
}

private void UpdateFromRpcModel(RpcModel rpcModel)
Expand DownExpand Up@@ -195,7 +198,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
this,
uuid,
fqdn,
_hostnameSuffix,
_hostnameSuffixGetter.GetCachedSuffix(),
connectionStatus,
credentialModel.CoderUrl,
workspace?.Name));
Expand All@@ -214,7 +217,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
// Workspace ID is fine as a stand-in here, it shouldn't
// conflict with any agent IDs.
uuid,
_hostnameSuffix,
_hostnameSuffixGetter.GetCachedSuffix(),
AgentConnectionStatus.Gray,
credentialModel.CoderUrl,
workspace.Name));
Expand DownExpand Up@@ -273,6 +276,22 @@ private void UpdateFromCredentialModel(CredentialModel credentialModel)
DashboardUrl = credentialModel.CoderUrl?.ToString() ?? DefaultDashboardUrl;
}

private void HandleHostnameSuffixChanged(string suffix)
{
// Ensure we're on the UI thread.
if (_dispatcherQueue == null) return;
if (!_dispatcherQueue.HasThreadAccess)
{
_dispatcherQueue.TryEnqueue(() => HandleHostnameSuffixChanged(suffix));
return;
}

foreach (var agent in Agents)
{
agent.ConfiguredHostnameSuffix = suffix;
}
}

public void VpnSwitch_Toggled(object sender, RoutedEventArgs e)
{
if (sender is not ToggleSwitch toggleSwitch) return;
Expand Down
1 change: 1 addition & 0 deletionsCoderSdk/Coder/CoderApiClient.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,6 +49,7 @@ public partial interface ICoderApiClient
public void SetSessionToken(string token);
}

[JsonSerializable(typeof(AgentConnectionInfo))]
[JsonSerializable(typeof(BuildInfo))]
[JsonSerializable(typeof(Response))]
[JsonSerializable(typeof(User))]
Expand Down
13 changes: 13 additions & 0 deletionsCoderSdk/Coder/WorkspaceAgents.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,14 @@ namespace Coder.Desktop.CoderSdk.Coder;
public partial interface ICoderApiClient
{
public Task<WorkspaceAgent> GetWorkspaceAgent(string id, CancellationToken ct = default);
public Task<AgentConnectionInfo> GetAgentConnectionInfoGeneric(CancellationToken ct = default);
}

public class AgentConnectionInfo
{
public string HostnameSuffix { get; set; } = string.Empty;
// note that we're leaving out several fields including the DERP Map because
// we don't use that information, and it's a complex object to define.
}

public class WorkspaceAgent
Expand DownExpand Up@@ -35,4 +43,9 @@ public Task<WorkspaceAgent> GetWorkspaceAgent(string id, CancellationToken ct =
{
return SendRequestNoBodyAsync<WorkspaceAgent>(HttpMethod.Get, "/api/v2/workspaceagents/" + id, ct);
}

public Task<AgentConnectionInfo> GetAgentConnectionInfoGeneric(CancellationToken ct = default)
{
return SendRequestNoBodyAsync<AgentConnectionInfo>(HttpMethod.Get, "/api/v2/workspaceagents/connection", ct);
}
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp