Skip to content

Commit be72f80

Browse files
authored
feat: fetch hostname suffix from API (#103)
Fixes #49 Adds support to query the hostname suffix from Coder server, and then propagates any changes to the agent view models.
1 parent 9e50acd commit be72f80

File tree

7 files changed

+316
-6
lines changed

7 files changed

+316
-6
lines changed

App/App.xaml.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public App()
7272
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
7373
services.AddSingleton<ICredentialManager, CredentialManager>();
7474
services.AddSingleton<IRpcController, RpcController>();
75+
services.AddSingleton<IHostnameSuffixGetter, HostnameSuffixGetter>();
7576

7677
services.AddOptions<MutagenControllerConfig>()
7778
.Bind(builder.Configuration.GetSection(MutagenControllerConfigSection));

App/Models/CredentialModel.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using Coder.Desktop.CoderSdk.Coder;
23

34
namespace Coder.Desktop.App.Models;
45

@@ -14,7 +15,7 @@ public enum CredentialState
1415
Valid,
1516
}
1617

17-
public class CredentialModel
18+
public class CredentialModel : ICoderApiClientCredentialProvider
1819
{
1920
public CredentialState State { get; init; } = CredentialState.Unknown;
2021

@@ -33,4 +34,14 @@ public CredentialModel Clone()
3334
Username = Username,
3435
};
3536
}
37+
38+
public CoderApiClientCredential? GetCoderApiClientCredential()
39+
{
40+
if (State != CredentialState.Valid) return null;
41+
return new CoderApiClientCredential
42+
{
43+
ApiToken = ApiToken!,
44+
CoderUrl = CoderUrl!,
45+
};
46+
}
3647
}

App/Services/HostnameSuffixGetter.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Coder.Desktop.App.Models;
5+
using Coder.Desktop.CoderSdk.Coder;
6+
using Coder.Desktop.Vpn.Utilities;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Coder.Desktop.App.Services;
10+
11+
public interface IHostnameSuffixGetter
12+
{
13+
public event EventHandler<string> SuffixChanged;
14+
15+
public string GetCachedSuffix();
16+
}
17+
18+
public class HostnameSuffixGetter : IHostnameSuffixGetter
19+
{
20+
private const string DefaultSuffix = ".coder";
21+
22+
private readonly ICredentialManager _credentialManager;
23+
private readonly ICoderApiClientFactory _clientFactory;
24+
private readonly ILogger<HostnameSuffixGetter> _logger;
25+
26+
// _lock protects all private (non-readonly) values
27+
private readonly RaiiSemaphoreSlim _lock = new(1, 1);
28+
private string _domainSuffix = DefaultSuffix;
29+
private bool _dirty = false;
30+
private bool _getInProgress = false;
31+
private CredentialModel _credentialModel = new() { State = CredentialState.Invalid };
32+
33+
public event EventHandler<string>? SuffixChanged;
34+
35+
public HostnameSuffixGetter(ICredentialManager credentialManager, ICoderApiClientFactory apiClientFactory,
36+
ILogger<HostnameSuffixGetter> logger)
37+
{
38+
_credentialManager = credentialManager;
39+
_clientFactory = apiClientFactory;
40+
_logger = logger;
41+
credentialManager.CredentialsChanged += HandleCredentialsChanged;
42+
HandleCredentialsChanged(this, _credentialManager.GetCachedCredentials());
43+
}
44+
45+
~HostnameSuffixGetter()
46+
{
47+
_credentialManager.CredentialsChanged -= HandleCredentialsChanged;
48+
}
49+
50+
private void HandleCredentialsChanged(object? sender, CredentialModel credentials)
51+
{
52+
using var _ = _lock.Lock();
53+
_logger.LogDebug("credentials updated with state {state}", credentials.State);
54+
_credentialModel = credentials;
55+
if (credentials.State != CredentialState.Valid) return;
56+
57+
_dirty = true;
58+
if (!_getInProgress)
59+
{
60+
_getInProgress = true;
61+
Task.Run(Refresh).ContinueWith(MaybeRefreshAgain);
62+
}
63+
}
64+
65+
private async Task Refresh()
66+
{
67+
_logger.LogDebug("refreshing domain suffix");
68+
CredentialModel credentials;
69+
using (_ = await _lock.LockAsync())
70+
{
71+
credentials = _credentialModel;
72+
if (credentials.State != CredentialState.Valid)
73+
{
74+
_logger.LogDebug("abandoning refresh because credentials are now invalid");
75+
return;
76+
}
77+
78+
_dirty = false;
79+
}
80+
81+
var client = _clientFactory.Create(credentials);
82+
using var timeoutSrc = new CancellationTokenSource(TimeSpan.FromSeconds(10));
83+
var connInfo = await client.GetAgentConnectionInfoGeneric(timeoutSrc.Token);
84+
85+
// older versions of Coder might not set this
86+
var suffix = string.IsNullOrEmpty(connInfo.HostnameSuffix)
87+
? DefaultSuffix
88+
// and, it doesn't include the leading dot.
89+
: "." + connInfo.HostnameSuffix;
90+
91+
var changed = false;
92+
using (_ = await _lock.LockAsync(CancellationToken.None))
93+
{
94+
if (_domainSuffix != suffix) changed = true;
95+
_domainSuffix = suffix;
96+
}
97+
98+
if (changed)
99+
{
100+
_logger.LogInformation("got new domain suffix '{suffix}'", suffix);
101+
// grab a local copy of the EventHandler to avoid TOCTOU race on the `?.` null-check
102+
var del = SuffixChanged;
103+
del?.Invoke(this, suffix);
104+
}
105+
else
106+
{
107+
_logger.LogDebug("domain suffix unchanged '{suffix}'", suffix);
108+
}
109+
}
110+
111+
private async Task MaybeRefreshAgain(Task prev)
112+
{
113+
if (prev.IsFaulted)
114+
{
115+
_logger.LogError(prev.Exception, "failed to query domain suffix");
116+
// back off here before retrying. We're just going to use a fixed, long
117+
// delay since this just affects UI stuff; we're not in a huge rush as
118+
// long as we eventually get the right value.
119+
await Task.Delay(TimeSpan.FromSeconds(10));
120+
}
121+
122+
using var l = await _lock.LockAsync(CancellationToken.None);
123+
if ((_dirty || prev.IsFaulted) && _credentialModel.State == CredentialState.Valid)
124+
{
125+
// we still have valid credentials and we're either dirty or the last Get failed.
126+
_logger.LogDebug("retrying domain suffix query");
127+
_ = Task.Run(Refresh).ContinueWith(MaybeRefreshAgain);
128+
return;
129+
}
130+
131+
// Getting here means either the credentials are not valid or we don't need to
132+
// refresh anyway.
133+
// The next time we get new, valid credentials, HandleCredentialsChanged will kick off
134+
// a new Refresh
135+
_getInProgress = false;
136+
return;
137+
}
138+
139+
public string GetCachedSuffix()
140+
{
141+
using var _ = _lock.Lock();
142+
return _domainSuffix;
143+
}
144+
}

App/ViewModels/TrayWindowViewModel.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
3535
private readonly IRpcController _rpcController;
3636
private readonly ICredentialManager _credentialManager;
3737
private readonly IAgentViewModelFactory _agentViewModelFactory;
38+
private readonly IHostnameSuffixGetter _hostnameSuffixGetter;
3839

3940
private FileSyncListWindow? _fileSyncListWindow;
4041

@@ -91,15 +92,14 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
9192

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

94-
private string _hostnameSuffix = DefaultHostnameSuffix;
95-
9695
public TrayWindowViewModel(IServiceProvider services, IRpcController rpcController,
97-
ICredentialManager credentialManager, IAgentViewModelFactory agentViewModelFactory)
96+
ICredentialManager credentialManager, IAgentViewModelFactory agentViewModelFactory, IHostnameSuffixGetter hostnameSuffixGetter)
9897
{
9998
_services = services;
10099
_rpcController = rpcController;
101100
_credentialManager = credentialManager;
102101
_agentViewModelFactory = agentViewModelFactory;
102+
_hostnameSuffixGetter = hostnameSuffixGetter;
103103

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

140140
_credentialManager.CredentialsChanged += (_, credentialModel) => UpdateFromCredentialModel(credentialModel);
141141
UpdateFromCredentialModel(_credentialManager.GetCachedCredentials());
142+
143+
_hostnameSuffixGetter.SuffixChanged += (_, suffix) => HandleHostnameSuffixChanged(suffix);
144+
HandleHostnameSuffixChanged(_hostnameSuffixGetter.GetCachedSuffix());
142145
}
143146

144147
private void UpdateFromRpcModel(RpcModel rpcModel)
@@ -195,7 +198,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
195198
this,
196199
uuid,
197200
fqdn,
198-
_hostnameSuffix,
201+
_hostnameSuffixGetter.GetCachedSuffix(),
199202
connectionStatus,
200203
credentialModel.CoderUrl,
201204
workspace?.Name));
@@ -214,7 +217,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
214217
// Workspace ID is fine as a stand-in here, it shouldn't
215218
// conflict with any agent IDs.
216219
uuid,
217-
_hostnameSuffix,
220+
_hostnameSuffixGetter.GetCachedSuffix(),
218221
AgentConnectionStatus.Gray,
219222
credentialModel.CoderUrl,
220223
workspace.Name));
@@ -273,6 +276,22 @@ private void UpdateFromCredentialModel(CredentialModel credentialModel)
273276
DashboardUrl = credentialModel.CoderUrl?.ToString() ?? DefaultDashboardUrl;
274277
}
275278

279+
private void HandleHostnameSuffixChanged(string suffix)
280+
{
281+
// Ensure we're on the UI thread.
282+
if (_dispatcherQueue == null) return;
283+
if (!_dispatcherQueue.HasThreadAccess)
284+
{
285+
_dispatcherQueue.TryEnqueue(() => HandleHostnameSuffixChanged(suffix));
286+
return;
287+
}
288+
289+
foreach (var agent in Agents)
290+
{
291+
agent.ConfiguredHostnameSuffix = suffix;
292+
}
293+
}
294+
276295
public void VpnSwitch_Toggled(object sender, RoutedEventArgs e)
277296
{
278297
if (sender is not ToggleSwitch toggleSwitch) return;

CoderSdk/Coder/CoderApiClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public partial interface ICoderApiClient
4949
public void SetSessionToken(string token);
5050
}
5151

52+
[JsonSerializable(typeof(AgentConnectionInfo))]
5253
[JsonSerializable(typeof(BuildInfo))]
5354
[JsonSerializable(typeof(Response))]
5455
[JsonSerializable(typeof(User))]

CoderSdk/Coder/WorkspaceAgents.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ namespace Coder.Desktop.CoderSdk.Coder;
33
public partial interface ICoderApiClient
44
{
55
public Task<WorkspaceAgent> GetWorkspaceAgent(string id, CancellationToken ct = default);
6+
public Task<AgentConnectionInfo> GetAgentConnectionInfoGeneric(CancellationToken ct = default);
7+
}
8+
9+
public class AgentConnectionInfo
10+
{
11+
public string HostnameSuffix { get; set; } = string.Empty;
12+
// note that we're leaving out several fields including the DERP Map because
13+
// we don't use that information, and it's a complex object to define.
614
}
715

816
public class WorkspaceAgent
@@ -35,4 +43,9 @@ public Task<WorkspaceAgent> GetWorkspaceAgent(string id, CancellationToken ct =
3543
{
3644
return SendRequestNoBodyAsync<WorkspaceAgent>(HttpMethod.Get, "/api/v2/workspaceagents/" + id, ct);
3745
}
46+
47+
public Task<AgentConnectionInfo> GetAgentConnectionInfoGeneric(CancellationToken ct = default)
48+
{
49+
return SendRequestNoBodyAsync<AgentConnectionInfo>(HttpMethod.Get, "/api/v2/workspaceagents/connection", ct);
50+
}
3851
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy