Skip to content

Commit d3b6583

Browse files
committed
chore: PR comments
1 parent 5040670 commit d3b6583

16 files changed

+215
-132
lines changed

App/App.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using System.Diagnostics;
3-
using Coder.Desktop.App.Models;
43
using Coder.Desktop.App.Services;
4+
using Coder.Desktop.App.ViewModels;
55
using Coder.Desktop.App.Views;
66
using Coder.Desktop.App.Views.Pages;
77
using Microsoft.Extensions.DependencyInjection;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using Windows.UI;
3+
using Coder.Desktop.App.ViewModels;
4+
using Microsoft.UI.Xaml.Data;
5+
using Microsoft.UI.Xaml.Media;
6+
7+
namespace Coder.Desktop.App.Converters;
8+
9+
public class AgentStatusToColorConverter : IValueConverter
10+
{
11+
private static readonly SolidColorBrush Green = new(Color.FromArgb(255, 52, 199, 89));
12+
private static readonly SolidColorBrush Red = new(Color.FromArgb(255, 255, 59, 48));
13+
private static readonly SolidColorBrush Gray = new(Color.FromArgb(255, 142, 142, 147));
14+
15+
public object Convert(object value, Type targetType, object parameter, string language)
16+
{
17+
if (value is not AgentConnectionStatus status) return Gray;
18+
19+
return status switch
20+
{
21+
AgentConnectionStatus.Green => Green,
22+
AgentConnectionStatus.Red => Red,
23+
_ => Gray,
24+
};
25+
}
26+
27+
public object ConvertBack(object value, Type targetType, object parameter, string language)
28+
{
29+
throw new NotImplementedException();
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using Coder.Desktop.App.Models;
3+
using DependencyPropertyGenerator;
4+
using Microsoft.UI.Xaml;
5+
using Microsoft.UI.Xaml.Data;
6+
7+
namespace Coder.Desktop.App.Converters;
8+
9+
[DependencyProperty<bool>("Starting", DefaultValue = false)]
10+
[DependencyProperty<bool>("Started", DefaultValue = false)]
11+
[DependencyProperty<bool>("Stopping", DefaultValue = false)]
12+
[DependencyProperty<bool>("Stopped", DefaultValue = false)]
13+
public partial class VpnLifecycleToBoolConverter : DependencyObject, IValueConverter
14+
{
15+
public object Convert(object value, Type targetType, object parameter, string language)
16+
{
17+
if (value is not VpnLifecycle lifecycle) return Stopped;
18+
19+
return lifecycle switch
20+
{
21+
VpnLifecycle.Starting => Starting,
22+
VpnLifecycle.Started => Started,
23+
VpnLifecycle.Stopping => Stopping,
24+
VpnLifecycle.Stopped => Stopped,
25+
_ => Visibility.Collapsed,
26+
};
27+
}
28+
29+
public object ConvertBack(object value, Type targetType, object parameter, string language)
30+
{
31+
throw new NotImplementedException();
32+
}
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using Microsoft.UI.Xaml;
3+
using Microsoft.UI.Xaml.Data;
4+
5+
namespace Coder.Desktop.App.Converters;
6+
7+
public partial class VpnLifecycleToVisibilityConverter : VpnLifecycleToBoolConverter, IValueConverter
8+
{
9+
public new object Convert(object value, Type targetType, object parameter, string language)
10+
{
11+
var boolValue = base.Convert(value, targetType, parameter, language);
12+
return boolValue is true ? Visibility.Visible : Visibility.Collapsed;
13+
}
14+
}

App/Models/AgentModel.cs

Lines changed: 0 additions & 68 deletions
This file was deleted.

App/Models/RpcModel.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@ public class RpcModel
2323

2424
public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Stopped;
2525

26-
// TODO: write a type for this or maybe use the Rpc Type directly
27-
public List<object> VisibleAgents { get; set; } = [];
26+
public List<object> Agents { get; set; } = [];
2827

2928
public RpcModel Clone()
3029
{
3130
return new RpcModel
3231
{
3332
RpcLifecycle = RpcLifecycle,
3433
VpnLifecycle = VpnLifecycle,
35-
VisibleAgents = VisibleAgents,
34+
Agents = Agents,
3635
};
3736
}
3837
}

App/Services/CredentialManager.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
5858
if (coderUrl.Length > 128) throw new ArgumentOutOfRangeException(nameof(coderUrl), "Coder URL is too long");
5959
if (!Uri.TryCreate(coderUrl, UriKind.Absolute, out var uri))
6060
throw new ArgumentException($"Coder URL '{coderUrl}' is not a valid URL", nameof(coderUrl));
61-
if (uri.Scheme != "https") throw new ArgumentException("Coder URL must use HTTPS", nameof(coderUrl));
6261
if (uri.PathAndQuery != "/") throw new ArgumentException("Coder URL must be the root URL", nameof(coderUrl));
6362
if (string.IsNullOrWhiteSpace(apiToken)) throw new ArgumentException("API token is required", nameof(apiToken));
6463
apiToken = apiToken.Trim();
@@ -105,11 +104,15 @@ public void ClearCredentials()
105104

106105
private void UpdateState(CredentialModel newModel)
107106
{
108-
_latestCredentials = newModel;
109-
CredentialsChanged?.Invoke(this, _latestCredentials.Clone());
107+
using (_lock.Lock())
108+
{
109+
_latestCredentials = newModel.Clone();
110+
}
111+
112+
CredentialsChanged?.Invoke(this, newModel.Clone());
110113
}
111114

112-
private RawCredentials? ReadCredentials()
115+
private static RawCredentials? ReadCredentials()
113116
{
114117
var raw = NativeApi.ReadCredentials(CredentialsTargetName);
115118
if (raw == null) return null;
@@ -130,7 +133,7 @@ private void UpdateState(CredentialModel newModel)
130133
return credentials;
131134
}
132135

133-
private void WriteCredentials(RawCredentials credentials)
136+
private static void WriteCredentials(RawCredentials credentials)
134137
{
135138
var raw = JsonSerializer.Serialize(credentials);
136139
NativeApi.WriteCredentials(CredentialsTargetName, raw);
@@ -147,6 +150,7 @@ private static class NativeApi
147150
private const int CredentialTypeGeneric = 1;
148151
private const int PersistenceTypeLocalComputer = 2;
149152
private const int ErrorNotFound = 1168;
153+
private const int CredMaxCredentialBlobSize = 5 * 512;
150154

151155
public static string? ReadCredentials(string targetName)
152156
{
@@ -170,15 +174,17 @@ private static class NativeApi
170174

171175
public static void WriteCredentials(string targetName, string secret)
172176
{
173-
if (Encoding.Unicode.GetByteCount(secret) > 512)
174-
throw new ArgumentOutOfRangeException(nameof(secret), "The secret is greater than 512 bytes");
177+
var byteCount = Encoding.Unicode.GetByteCount(secret);
178+
if (byteCount > CredMaxCredentialBlobSize)
179+
throw new ArgumentOutOfRangeException(nameof(secret),
180+
$"The secret is greater than {CredMaxCredentialBlobSize} bytes");
175181

176182
var credentialBlob = Marshal.StringToHGlobalUni(secret);
177183
var cred = new CREDENTIAL
178184
{
179185
Type = CredentialTypeGeneric,
180186
TargetName = targetName,
181-
CredentialBlobSize = secret.Length * sizeof(char),
187+
CredentialBlobSize = byteCount,
182188
CredentialBlob = credentialBlob,
183189
Persist = PersistenceTypeLocalComputer,
184190
};

App/Services/RpcController.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ public async Task Reconnect(CancellationToken ct = default)
9696
{
9797
state.RpcLifecycle = RpcLifecycle.Connecting;
9898
state.VpnLifecycle = VpnLifecycle.Stopped;
99-
state.VisibleAgents.Clear();
99+
state.Agents.Clear();
100100
});
101101

102102
if (_speaker != null)
103103
try
104104
{
105-
await _speaker.DisposeAsync();
105+
await DisposeSpeaker();
106106
}
107107
catch (Exception e)
108108
{
@@ -126,7 +126,7 @@ public async Task Reconnect(CancellationToken ct = default)
126126
{
127127
state.RpcLifecycle = RpcLifecycle.Disconnected;
128128
state.VpnLifecycle = VpnLifecycle.Stopped;
129-
state.VisibleAgents.Clear();
129+
state.Agents.Clear();
130130
});
131131
throw new RpcOperationException("Failed to reconnect to the RPC server", e);
132132
}
@@ -136,14 +136,14 @@ public async Task Reconnect(CancellationToken ct = default)
136136
state.RpcLifecycle = RpcLifecycle.Connected;
137137
// TODO: fetch current state
138138
state.VpnLifecycle = VpnLifecycle.Stopped;
139-
state.VisibleAgents.Clear();
139+
state.Agents.Clear();
140140
});
141141
}
142142

143143
public async Task StartVpn(CancellationToken ct = default)
144144
{
145145
using var _ = await AcquireOperationLockNowAsync();
146-
EnsureRpcConnected();
146+
AssertRpcConnected();
147147

148148
var credentials = _credentialManager.GetCredentials();
149149
if (credentials.State != CredentialState.Valid)
@@ -184,7 +184,7 @@ public async Task StartVpn(CancellationToken ct = default)
184184
public async Task StopVpn(CancellationToken ct = default)
185185
{
186186
using var _ = await AcquireOperationLockNowAsync();
187-
EnsureRpcConnected();
187+
AssertRpcConnected();
188188

189189
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopping; });
190190

@@ -216,9 +216,14 @@ public async Task StopVpn(CancellationToken ct = default)
216216

217217
private void MutateState(Action<RpcModel> mutator)
218218
{
219-
using var _ = _stateLock.Lock();
220-
mutator(_state);
221-
StateChanged?.Invoke(this, _state.Clone());
219+
RpcModel newState;
220+
using (_stateLock.Lock())
221+
{
222+
mutator(_state);
223+
newState = _state.Clone();
224+
}
225+
226+
StateChanged?.Invoke(this, newState);
222227
}
223228

224229
private async Task<IDisposable> AcquireOperationLockNowAsync()
@@ -234,13 +239,22 @@ private void SpeakerOnReceive(ReplyableRpcMessage<ClientMessage, ServiceMessage>
234239
// TODO: this
235240
}
236241

242+
private async Task DisposeSpeaker()
243+
{
244+
if (_speaker == null) return;
245+
_speaker.Receive -= SpeakerOnReceive;
246+
_speaker.Error -= SpeakerOnError;
247+
await _speaker.DisposeAsync();
248+
_speaker = null;
249+
}
250+
237251
private void SpeakerOnError(Exception e)
238252
{
239253
Debug.WriteLine($"Error: {e}");
240254
Reconnect(CancellationToken.None).Wait();
241255
}
242256

243-
private void EnsureRpcConnected()
257+
private void AssertRpcConnected()
244258
{
245259
if (_speaker == null)
246260
throw new InvalidOperationException("Not connected to the RPC server");

App/ViewModels/AgentViewModel.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Windows.ApplicationModel.DataTransfer;
2+
using CommunityToolkit.Mvvm.Input;
3+
using Microsoft.UI.Xaml;
4+
using Microsoft.UI.Xaml.Controls;
5+
using Microsoft.UI.Xaml.Controls.Primitives;
6+
7+
namespace Coder.Desktop.App.ViewModels;
8+
9+
public enum AgentConnectionStatus
10+
{
11+
Green,
12+
Red,
13+
Gray,
14+
}
15+
16+
public partial class AgentViewModel
17+
{
18+
public required string Hostname { get; set; }
19+
20+
public required string HostnameSuffix { get; set; } // including leading dot
21+
22+
public required AgentConnectionStatus ConnectionStatus { get; set; }
23+
24+
public string FullHostname => Hostname + HostnameSuffix;
25+
26+
public required string DashboardUrl { get; set; }
27+
28+
[RelayCommand]
29+
private void CopyHostname(object parameter)
30+
{
31+
var dataPackage = new DataPackage
32+
{
33+
RequestedOperation = DataPackageOperation.Copy,
34+
};
35+
dataPackage.SetText(FullHostname);
36+
Clipboard.SetContent(dataPackage);
37+
38+
if (parameter is not FrameworkElement frameworkElement) return;
39+
40+
var flyout = new Flyout
41+
{
42+
Content = new TextBlock
43+
{
44+
Text = "DNS Copied",
45+
Margin = new Thickness(4),
46+
},
47+
};
48+
FlyoutBase.SetAttachedFlyout(frameworkElement, flyout);
49+
FlyoutBase.ShowAttachedFlyout(frameworkElement);
50+
}
51+
}

App/Models/TrayWindowDisconnectedViewModel.cs renamed to App/ViewModels/TrayWindowDisconnectedViewModel.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
using System.Threading.Tasks;
2+
using Coder.Desktop.App.Models;
23
using Coder.Desktop.App.Services;
34
using CommunityToolkit.Mvvm.ComponentModel;
45
using CommunityToolkit.Mvvm.Input;
56

6-
namespace Coder.Desktop.App.Models;
7+
namespace Coder.Desktop.App.ViewModels;
78

89
public partial class TrayWindowDisconnectedViewModel : ObservableObject
910
{

0 commit comments

Comments
 (0)