Skip to content

Commit 51bf68e

Browse files
authored
feat: show vpn start/stop failure in app (#44)
Adds red text that appears when the VPN fails to start or stop. After an error, any manual start/stop operation will clear the error. Contributes to #40
1 parent e1d9774 commit 51bf68e

File tree

6 files changed

+201
-169
lines changed

6 files changed

+201
-169
lines changed

App/Converters/AgentStatusToColorConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Coder.Desktop.App.Converters;
99
public class AgentStatusToColorConverter : IValueConverter
1010
{
1111
private static readonly SolidColorBrush Green = new(Color.FromArgb(255, 52, 199, 89));
12-
private static readonly SolidColorBrush Yellow = new(Color.FromArgb(255, 204, 1, 0));
12+
private static readonly SolidColorBrush Yellow = new(Color.FromArgb(255, 255, 204, 1));
1313
private static readonly SolidColorBrush Red = new(Color.FromArgb(255, 255, 59, 48));
1414
private static readonly SolidColorBrush Gray = new(Color.FromArgb(255, 142, 142, 147));
1515

App/Converters/InverseBoolToVisibilityConverter.cs

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

App/Converters/VpnLifecycleToVisibilityConverter.cs

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

App/Services/RpcController.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ public async Task Reconnect(CancellationToken ct = default)
146146
Status = new StatusRequest(),
147147
}, ct);
148148
if (statusReply.MsgCase != ServiceMessage.MsgOneofCase.Status)
149-
throw new InvalidOperationException($"Unexpected reply message type: {statusReply.MsgCase}");
149+
throw new VpnLifecycleException(
150+
$"Failed to get VPN status. Unexpected reply message type: {statusReply.MsgCase}");
150151
ApplyStatusUpdate(statusReply.Status);
151152
}
152153

@@ -172,20 +173,26 @@ public async Task StartVpn(CancellationToken ct = default)
172173
ApiToken = credentials.ApiToken,
173174
},
174175
}, ct);
175-
if (reply.MsgCase != ServiceMessage.MsgOneofCase.Start)
176-
throw new InvalidOperationException($"Unexpected reply message type: {reply.MsgCase}");
177176
}
178177
catch (Exception e)
179178
{
180179
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; });
181180
throw new RpcOperationException("Failed to send start command to service", e);
182181
}
183182

183+
if (reply.MsgCase != ServiceMessage.MsgOneofCase.Start)
184+
{
185+
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; });
186+
throw new VpnLifecycleException($"Failed to start VPN. Unexpected reply message type: {reply.MsgCase}");
187+
}
188+
184189
if (!reply.Start.Success)
185190
{
191+
// We use Stopped instead of Unknown here as it's usually the case
192+
// that a failed start got cleaned up successfully.
186193
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; });
187-
throw new VpnLifecycleException("Failed to start VPN",
188-
new InvalidOperationException($"Service reported failure: {reply.Start.ErrorMessage}"));
194+
throw new VpnLifecycleException(
195+
$"Failed to start VPN. Service reported failure: {reply.Start.ErrorMessage}");
189196
}
190197

191198
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Started; });
@@ -212,16 +219,20 @@ public async Task StopVpn(CancellationToken ct = default)
212219
}
213220
finally
214221
{
215-
// Technically the state is unknown now.
216-
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; });
222+
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; });
217223
}
218224

219225
if (reply.MsgCase != ServiceMessage.MsgOneofCase.Stop)
220-
throw new VpnLifecycleException("Failed to stop VPN",
221-
new InvalidOperationException($"Unexpected reply message type: {reply.MsgCase}"));
226+
{
227+
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; });
228+
throw new VpnLifecycleException($"Failed to stop VPN. Unexpected reply message type: {reply.MsgCase}");
229+
}
230+
222231
if (!reply.Stop.Success)
223-
throw new VpnLifecycleException("Failed to stop VPN",
224-
new InvalidOperationException($"Service reported failure: {reply.Stop.ErrorMessage}"));
232+
{
233+
MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; });
234+
throw new VpnLifecycleException($"Failed to stop VPN. Service reported failure: {reply.Stop.ErrorMessage}");
235+
}
225236
}
226237

227238
public async ValueTask DisposeAsync()

App/ViewModels/TrayWindowViewModel.cs

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Threading.Tasks;
45
using Coder.Desktop.App.Models;
56
using Coder.Desktop.App.Services;
67
using Coder.Desktop.Vpn.Proto;
@@ -10,6 +11,7 @@
1011
using Microsoft.UI.Dispatching;
1112
using Microsoft.UI.Xaml;
1213
using Microsoft.UI.Xaml.Controls;
14+
using Exception = System.Exception;
1315

1416
namespace Coder.Desktop.App.ViewModels;
1517

@@ -23,22 +25,45 @@ public partial class TrayWindowViewModel : ObservableObject
2325

2426
private DispatcherQueue? _dispatcherQueue;
2527

26-
[ObservableProperty] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
28+
[ObservableProperty]
29+
[NotifyPropertyChangedFor(nameof(ShowEnableSection))]
30+
[NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))]
31+
[NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))]
32+
[NotifyPropertyChangedFor(nameof(ShowAgentsSection))]
33+
public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
2734

2835
// This is a separate property because we need the switch to be 2-way.
2936
[ObservableProperty] public partial bool VpnSwitchActive { get; set; } = false;
3037

31-
[ObservableProperty] public partial string? VpnFailedMessage { get; set; } = null;
38+
[ObservableProperty]
39+
[NotifyPropertyChangedFor(nameof(ShowEnableSection))]
40+
[NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))]
41+
[NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))]
42+
[NotifyPropertyChangedFor(nameof(ShowAgentsSection))]
43+
[NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))]
44+
[NotifyPropertyChangedFor(nameof(ShowFailedSection))]
45+
public partial string? VpnFailedMessage { get; set; } = null;
3246

3347
[ObservableProperty]
34-
[NotifyPropertyChangedFor(nameof(NoAgents))]
35-
[NotifyPropertyChangedFor(nameof(AgentOverflow))]
3648
[NotifyPropertyChangedFor(nameof(VisibleAgents))]
49+
[NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))]
50+
[NotifyPropertyChangedFor(nameof(ShowAgentsSection))]
51+
[NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))]
3752
public partial List<AgentViewModel> Agents { get; set; } = [];
3853

39-
public bool NoAgents => Agents.Count == 0;
54+
public bool ShowEnableSection => VpnFailedMessage is null && VpnLifecycle is not VpnLifecycle.Started;
55+
56+
public bool ShowWorkspacesHeader => VpnFailedMessage is null && VpnLifecycle is VpnLifecycle.Started;
57+
58+
public bool ShowNoAgentsSection =>
59+
VpnFailedMessage is null && Agents.Count == 0 && VpnLifecycle is VpnLifecycle.Started;
60+
61+
public bool ShowAgentsSection =>
62+
VpnFailedMessage is null && Agents.Count > 0 && VpnLifecycle is VpnLifecycle.Started;
63+
64+
public bool ShowFailedSection => VpnFailedMessage is not null;
4065

41-
public bool AgentOverflow => Agents.Count > MaxAgents;
66+
public bool ShowAgentOverflowButton => VpnFailedMessage is null && Agents.Count > MaxAgents;
4267

4368
[ObservableProperty]
4469
[NotifyPropertyChangedFor(nameof(VisibleAgents))]
@@ -190,24 +215,47 @@ public void VpnSwitch_Toggled(object sender, RoutedEventArgs e)
190215
{
191216
if (sender is not ToggleSwitch toggleSwitch) return;
192217

193-
VpnFailedMessage = "";
218+
VpnFailedMessage = null;
219+
220+
// The start/stop methods will call back to update the state.
221+
if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped)
222+
_ = StartVpn(); // in the background
223+
else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started)
224+
_ = StopVpn(); // in the background
225+
else
226+
toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started;
227+
}
228+
229+
private async Task StartVpn()
230+
{
194231
try
195232
{
196-
// The start/stop methods will call back to update the state.
197-
if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped)
198-
_rpcController.StartVpn();
199-
else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started)
200-
_rpcController.StopVpn();
201-
else
202-
toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started;
233+
await _rpcController.StartVpn();
203234
}
204-
catch
235+
catch (Exception e)
205236
{
206-
// TODO: display error
207-
VpnFailedMessage = e.ToString();
237+
VpnFailedMessage = "Failed to start CoderVPN: " + MaybeUnwrapTunnelError(e);
208238
}
209239
}
210240

241+
private async Task StopVpn()
242+
{
243+
try
244+
{
245+
await _rpcController.StopVpn();
246+
}
247+
catch (Exception e)
248+
{
249+
VpnFailedMessage = "Failed to stop CoderVPN: " + MaybeUnwrapTunnelError(e);
250+
}
251+
}
252+
253+
private static string MaybeUnwrapTunnelError(Exception e)
254+
{
255+
if (e is VpnLifecycleException vpnError) return vpnError.Message;
256+
return e.ToString();
257+
}
258+
211259
[RelayCommand]
212260
public void ToggleShowAllAgents()
213261
{

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