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

Commit3afa871

Browse files
committed
feat: fetch hostname suffix from API
1 parent0f8201e commit3afa871

File tree

6 files changed

+304
-5
lines changed

6 files changed

+304
-5
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
newWindowsCredentialBackend(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/Services/HostnameSuffixGetter.cs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
usingSystem;
2+
usingSystem.Threading;
3+
usingSystem.Threading.Tasks;
4+
usingCoder.Desktop.App.Models;
5+
usingCoder.Desktop.CoderSdk.Coder;
6+
usingCoder.Desktop.Vpn.Utilities;
7+
usingMicrosoft.Extensions.Logging;
8+
9+
namespaceCoder.Desktop.App.Services;
10+
11+
publicinterfaceIHostnameSuffixGetter
12+
{
13+
publiceventEventHandler<string>SuffixChanged;
14+
15+
publicstringGetCachedSuffix();
16+
}
17+
18+
publicclassHostnameSuffixGetter:IHostnameSuffixGetter
19+
{
20+
privateconststringDefaultSuffix=".coder";
21+
22+
privatereadonlyICredentialManager_credentialManager;
23+
privatereadonlyICoderApiClientFactory_clientFactory;
24+
privatereadonlyILogger<HostnameSuffixGetter>_logger;
25+
26+
// _lock protects all private (non-readonly) values
27+
privatereadonlyRaiiSemaphoreSlim_lock=new(1,1);
28+
privatestring_domainSuffix=DefaultSuffix;
29+
privatebool_dirty=false;
30+
privatebool_getInProgress=false;
31+
privateCredentialModel_credentialModel=new(){State=CredentialState.Invalid};
32+
33+
publiceventEventHandler<string>?SuffixChanged;
34+
35+
publicHostnameSuffixGetter(ICredentialManagercredentialManager,ICoderApiClientFactoryapiClientFactory,
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+
privatevoidHandleCredentialsChanged(object?sender,CredentialModelcredentials)
51+
{
52+
usingvar_=_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+
privateasyncTaskRefresh()
66+
{
67+
_logger.LogDebug("refreshing domain suffix");
68+
CredentialModelcredentials;
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+
varclient=_clientFactory.Create(credentials.CoderUrl!.ToString());
82+
client.SetSessionToken(credentials.ApiToken!);
83+
usingvartimeoutSrc=newCancellationTokenSource(TimeSpan.FromSeconds(10));
84+
varconnInfo=awaitclient.GetAgentConnectionInfoGeneric(timeoutSrc.Token);
85+
86+
// older versions of Coder might not set this
87+
varsuffix=string.IsNullOrEmpty(connInfo.HostnameSuffix)
88+
?DefaultSuffix
89+
// and, it doesn't include the leading dot.
90+
:"."+connInfo.HostnameSuffix;
91+
92+
varchanged=false;
93+
using(_=await_lock.LockAsync(CancellationToken.None))
94+
{
95+
if(_domainSuffix!=suffix)changed=true;
96+
_domainSuffix=suffix;
97+
}
98+
99+
if(changed)
100+
{
101+
_logger.LogInformation("got new domain suffix '{suffix}'",suffix);
102+
// grab a local copy of the EventHandler to avoid TOCTOU race on the `?.` null-check
103+
vardel=SuffixChanged;
104+
del?.Invoke(this,suffix);
105+
}
106+
else
107+
{
108+
_logger.LogDebug("domain suffix unchanged '{suffix}'",suffix);
109+
}
110+
}
111+
112+
privateasyncTaskMaybeRefreshAgain(Taskprev)
113+
{
114+
if(prev.IsFaulted)
115+
{
116+
_logger.LogError(prev.Exception,"failed to query domain suffix");
117+
// back off here before retrying. We're just going to use a fixed, long
118+
// delay since this just affects UI stuff; we're not in a huge rush as
119+
// long as we eventually get the right value.
120+
awaitTask.Delay(TimeSpan.FromSeconds(10));
121+
}
122+
123+
usingvarl=await_lock.LockAsync(CancellationToken.None);
124+
if((_dirty||prev.IsFaulted)&&_credentialModel.State==CredentialState.Valid)
125+
{
126+
// we still have valid credentials and we're either dirty or the last Get failed.
127+
_logger.LogDebug("retrying domain suffix query");
128+
_=Task.Run(Refresh).ContinueWith(MaybeRefreshAgain);
129+
return;
130+
}
131+
132+
// Getting here means either the credentials are not valid or we don't need to
133+
// refresh anyway.
134+
// The next time we get new, valid credentials, HandleCredentialsChanged will kick off
135+
// a new Refresh
136+
_getInProgress=false;
137+
return;
138+
}
139+
140+
publicstringGetCachedSuffix()
141+
{
142+
usingvar_=_lock.Lock();
143+
return_domainSuffix;
144+
}
145+
}

‎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
privatereadonlyIRpcController_rpcController;
3636
privatereadonlyICredentialManager_credentialManager;
3737
privatereadonlyIAgentViewModelFactory_agentViewModelFactory;
38+
privatereadonlyIHostnameSuffixGetter_hostnameSuffixGetter;
3839

3940
privateFileSyncListWindow?_fileSyncListWindow;
4041

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

9293
[ObservableProperty]publicpartialstringDashboardUrl{get;set;}=DefaultDashboardUrl;
9394

94-
privatestring_hostnameSuffix=DefaultHostnameSuffix;
95-
9695
publicTrayWindowViewModel(IServiceProviderservices,IRpcControllerrpcController,
97-
ICredentialManagercredentialManager,IAgentViewModelFactoryagentViewModelFactory)
96+
ICredentialManagercredentialManager,IAgentViewModelFactoryagentViewModelFactory,IHostnameSuffixGetterhostnameSuffixGetter)
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
privatevoidUpdateFromRpcModel(RpcModelrpcModel)
@@ -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+
privatevoidHandleHostnameSuffixChanged(stringsuffix)
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(varagentinAgents)
290+
{
291+
agent.ConfiguredHostnameSuffix=suffix;
292+
}
293+
}
294+
276295
publicvoidVpnSwitch_Toggled(objectsender,RoutedEventArgse)
277296
{
278297
if(senderis notToggleSwitchtoggleSwitch)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
publicvoidSetSessionToken(stringtoken);
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
publicpartialinterfaceICoderApiClient
44
{
55
publicTask<WorkspaceAgent>GetWorkspaceAgent(stringid,CancellationTokenct=default);
6+
publicTask<AgentConnectionInfo>GetAgentConnectionInfoGeneric(CancellationTokenct=default);
7+
}
8+
9+
publicclassAgentConnectionInfo
10+
{
11+
publicstringHostnameSuffix{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
publicclassWorkspaceAgent
@@ -35,4 +43,9 @@ public Task<WorkspaceAgent> GetWorkspaceAgent(string id, CancellationToken ct =
3543
{
3644
returnSendRequestNoBodyAsync<WorkspaceAgent>(HttpMethod.Get,"/api/v2/workspaceagents/"+id,ct);
3745
}
46+
47+
publicTask<AgentConnectionInfo>GetAgentConnectionInfoGeneric(CancellationTokenct=default)
48+
{
49+
returnSendRequestNoBodyAsync<AgentConnectionInfo>(HttpMethod.Get,"/api/v2/workspaceagents/connection",ct);
50+
}
3851
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
usingSystem.ComponentModel.DataAnnotations;
2+
usingCoder.Desktop.App.Models;
3+
usingCoder.Desktop.App.Services;
4+
usingCoder.Desktop.CoderSdk.Coder;
5+
usingMicrosoft.Extensions.Hosting;
6+
usingMicrosoft.Extensions.Logging;
7+
usingMoq;
8+
usingSerilog;
9+
10+
namespaceCoder.Desktop.Tests.App.Services;
11+
12+
[TestFixture]
13+
publicclassHostnameSuffixGetterTest
14+
{
15+
conststringcoderUrl="https://coder.test/";
16+
17+
[SetUp]
18+
publicvoidSetupMocks()
19+
{
20+
Log.Logger=newLoggerConfiguration().MinimumLevel.Debug().WriteTo.NUnitOutput().CreateLogger();
21+
varbuilder=Host.CreateApplicationBuilder();
22+
builder.Services.AddSerilog();
23+
_logger=(ILogger<HostnameSuffixGetter>)builder.Build().Services
24+
.GetService(typeof(ILogger<HostnameSuffixGetter>))!;
25+
26+
_mCoderApiClientFactory=newMock<ICoderApiClientFactory>(MockBehavior.Strict);
27+
_mCredentialManager=newMock<ICredentialManager>(MockBehavior.Strict);
28+
_mCoderApiClient=newMock<ICoderApiClient>(MockBehavior.Strict);
29+
_mCoderApiClientFactory.Setup(m=>m.Create(coderUrl)).Returns(_mCoderApiClient.Object);
30+
}
31+
32+
privateMock<ICoderApiClientFactory>_mCoderApiClientFactory;
33+
privateMock<ICredentialManager>_mCredentialManager;
34+
privateMock<ICoderApiClient>_mCoderApiClient;
35+
privateILogger<HostnameSuffixGetter>_logger;
36+
37+
[Test(Description="Mainline no errors")]
38+
[CancelAfter(10_000)]
39+
publicasyncTaskMainline(CancellationTokenct)
40+
{
41+
_mCredentialManager.Setup(m=>m.GetCachedCredentials())
42+
.Returns(newCredentialModel(){State=CredentialState.Invalid});
43+
varhostnameSuffixGetter=
44+
newHostnameSuffixGetter(_mCredentialManager.Object,_mCoderApiClientFactory.Object,_logger);
45+
46+
// initially, we return the default
47+
Assert.That(hostnameSuffixGetter.GetCachedSuffix(),Is.EqualTo(".coder"));
48+
49+
// subscribed to suffix changes
50+
varsuffixCompletion=newTaskCompletionSource<string>();
51+
hostnameSuffixGetter.SuffixChanged+=(_,suffix)=>suffixCompletion.SetResult(suffix);
52+
53+
// set the client to return "test" as the suffix
54+
_mCoderApiClient.Setup(m=>m.SetSessionToken("test-token"));
55+
_mCoderApiClient.Setup(m=>m.GetAgentConnectionInfoGeneric(It.IsAny<CancellationToken>()))
56+
.Returns(Task.FromResult(newAgentConnectionInfo(){HostnameSuffix="test"}));
57+
58+
_mCredentialManager.Raise(m=>m.CredentialsChanged+=null,_mCredentialManager.Object,newCredentialModel
59+
{
60+
State=CredentialState.Valid,
61+
CoderUrl=newUri(coderUrl),
62+
ApiToken="test-token",
63+
});
64+
vargotSuffix=awaitTaskOrCancellation(suffixCompletion.Task,ct);
65+
Assert.That(gotSuffix,Is.EqualTo(".test"));
66+
67+
// now, we should return the .test domain going forward
68+
Assert.That(hostnameSuffixGetter.GetCachedSuffix(),Is.EqualTo(".test"));
69+
}
70+
71+
[Test(Description="Retries if error")]
72+
[CancelAfter(30_000)]
73+
// TODO: make this test not have to actually wait for the retry.
74+
publicasyncTaskRetryError(CancellationTokenct)
75+
{
76+
_mCredentialManager.Setup(m=>m.GetCachedCredentials())
77+
.Returns(newCredentialModel(){State=CredentialState.Invalid});
78+
varhostnameSuffixGetter=
79+
newHostnameSuffixGetter(_mCredentialManager.Object,_mCoderApiClientFactory.Object,_logger);
80+
81+
// subscribed to suffix changes
82+
varsuffixCompletion=newTaskCompletionSource<string>();
83+
hostnameSuffixGetter.SuffixChanged+=(_,suffix)=>suffixCompletion.SetResult(suffix);
84+
85+
// set the client to fail once, then return successfully
86+
_mCoderApiClient.Setup(m=>m.SetSessionToken("test-token"));
87+
varconnectionInfoCompletion=newTaskCompletionSource<AgentConnectionInfo>();
88+
_mCoderApiClient.SetupSequence(m=>m.GetAgentConnectionInfoGeneric(It.IsAny<CancellationToken>()))
89+
.Returns(Task.FromException<AgentConnectionInfo>(newException("a bad thing happened")))
90+
.Returns(Task.FromResult(newAgentConnectionInfo(){HostnameSuffix="test"}));
91+
92+
_mCredentialManager.Raise(m=>m.CredentialsChanged+=null,_mCredentialManager.Object,newCredentialModel
93+
{
94+
State=CredentialState.Valid,
95+
CoderUrl=newUri(coderUrl),
96+
ApiToken="test-token",
97+
});
98+
vargotSuffix=awaitTaskOrCancellation(suffixCompletion.Task,ct);
99+
Assert.That(gotSuffix,Is.EqualTo(".test"));
100+
101+
// now, we should return the .test domain going forward
102+
Assert.That(hostnameSuffixGetter.GetCachedSuffix(),Is.EqualTo(".test"));
103+
}
104+
105+
/// <summary>
106+
/// TaskOrCancellation waits for either the task to complete, or the given token to be canceled.
107+
/// </summary>
108+
internalstaticasyncTask<TResult>TaskOrCancellation<TResult>(Task<TResult>task,
109+
CancellationTokencancellationToken)
110+
{
111+
varcancellationTask=newTaskCompletionSource<TResult>();
112+
awaitusing(cancellationToken.Register(()=>cancellationTask.TrySetCanceled()))
113+
{
114+
// Wait for either the task or the cancellation
115+
varcompletedTask=awaitTask.WhenAny(task,cancellationTask.Task);
116+
// Await to propagate exceptions, if any
117+
returnawaitcompletedTask;
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp