From 3ae781aa8a3834b0095bca8bf04521993826f82a Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Mar 2025 18:09:53 +1100 Subject: [PATCH 1/2] feat: show vpn start/stop failure in app --- App/Converters/AgentStatusToColorConverter.cs | 2 +- .../InverseBoolToVisibilityConverter.cs | 12 - .../VpnLifecycleToVisibilityConverter.cs | 14 -- App/Services/RpcController.cs | 33 ++- App/ViewModels/TrayWindowViewModel.cs | 83 +++++-- App/Views/Pages/TrayWindowMainPage.xaml | 227 +++++++++--------- 6 files changed, 202 insertions(+), 169 deletions(-) delete mode 100644 App/Converters/InverseBoolToVisibilityConverter.cs delete mode 100644 App/Converters/VpnLifecycleToVisibilityConverter.cs diff --git a/App/Converters/AgentStatusToColorConverter.cs b/App/Converters/AgentStatusToColorConverter.cs index 25f1f66..ebcabdd 100644 --- a/App/Converters/AgentStatusToColorConverter.cs +++ b/App/Converters/AgentStatusToColorConverter.cs @@ -9,7 +9,7 @@ namespace Coder.Desktop.App.Converters; public class AgentStatusToColorConverter : IValueConverter { private static readonly SolidColorBrush Green = new(Color.FromArgb(255, 52, 199, 89)); - private static readonly SolidColorBrush Yellow = new(Color.FromArgb(255, 204, 1, 0)); + private static readonly SolidColorBrush Yellow = new(Color.FromArgb(255, 255, 204, 1)); private static readonly SolidColorBrush Red = new(Color.FromArgb(255, 255, 59, 48)); private static readonly SolidColorBrush Gray = new(Color.FromArgb(255, 142, 142, 147)); diff --git a/App/Converters/InverseBoolToVisibilityConverter.cs b/App/Converters/InverseBoolToVisibilityConverter.cs deleted file mode 100644 index dd9c864..0000000 --- a/App/Converters/InverseBoolToVisibilityConverter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.UI.Xaml; - -namespace Coder.Desktop.App.Converters; - -public partial class InverseBoolToVisibilityConverter : BoolToObjectConverter -{ - public InverseBoolToVisibilityConverter() - { - TrueValue = Visibility.Collapsed; - FalseValue = Visibility.Visible; - } -} diff --git a/App/Converters/VpnLifecycleToVisibilityConverter.cs b/App/Converters/VpnLifecycleToVisibilityConverter.cs deleted file mode 100644 index bf83bea..0000000 --- a/App/Converters/VpnLifecycleToVisibilityConverter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Data; - -namespace Coder.Desktop.App.Converters; - -public partial class VpnLifecycleToVisibilityConverter : VpnLifecycleToBoolConverter, IValueConverter -{ - public new object Convert(object value, Type targetType, object parameter, string language) - { - var boolValue = base.Convert(value, targetType, parameter, language); - return boolValue is true ? Visibility.Visible : Visibility.Collapsed; - } -} diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index a02347f..1b3dac6 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -146,7 +146,8 @@ public async Task Reconnect(CancellationToken ct = default) Status = new StatusRequest(), }, ct); if (statusReply.MsgCase != ServiceMessage.MsgOneofCase.Status) - throw new InvalidOperationException($"Unexpected reply message type: {statusReply.MsgCase}"); + throw new VpnLifecycleException( + $"Failed to get VPN status. Unexpected reply message type: {statusReply.MsgCase}"); ApplyStatusUpdate(statusReply.Status); } @@ -172,8 +173,6 @@ public async Task StartVpn(CancellationToken ct = default) ApiToken = credentials.ApiToken, }, }, ct); - if (reply.MsgCase != ServiceMessage.MsgOneofCase.Start) - throw new InvalidOperationException($"Unexpected reply message type: {reply.MsgCase}"); } catch (Exception e) { @@ -181,11 +180,19 @@ public async Task StartVpn(CancellationToken ct = default) throw new RpcOperationException("Failed to send start command to service", e); } + if (reply.MsgCase != ServiceMessage.MsgOneofCase.Start) + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); + throw new VpnLifecycleException($"Failed to start VPN. Unexpected reply message type: {reply.MsgCase}"); + } + if (!reply.Start.Success) { + // We use Stopped instead of Unknown here as it's usually the case + // that a failed start got cleaned up successfully. MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); - throw new VpnLifecycleException("Failed to start VPN", - new InvalidOperationException($"Service reported failure: {reply.Start.ErrorMessage}")); + throw new VpnLifecycleException( + $"Failed to start VPN. Service reported failure: {reply.Start.ErrorMessage}"); } MutateState(state => { state.VpnLifecycle = VpnLifecycle.Started; }); @@ -212,16 +219,20 @@ public async Task StopVpn(CancellationToken ct = default) } finally { - // Technically the state is unknown now. - MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); } if (reply.MsgCase != ServiceMessage.MsgOneofCase.Stop) - throw new VpnLifecycleException("Failed to stop VPN", - new InvalidOperationException($"Unexpected reply message type: {reply.MsgCase}")); + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); + throw new VpnLifecycleException($"Failed to stop VPN. Unexpected reply message type: {reply.MsgCase}"); + } + if (!reply.Stop.Success) - throw new VpnLifecycleException("Failed to stop VPN", - new InvalidOperationException($"Service reported failure: {reply.Stop.ErrorMessage}")); + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); + throw new VpnLifecycleException($"Failed to stop VPN. Service reported failure: {reply.Stop.ErrorMessage}"); + } } public async ValueTask DisposeAsync() diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 1fccb7e..76530e4 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; using Coder.Desktop.Vpn.Proto; @@ -10,6 +11,7 @@ using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Exception = System.Exception; namespace Coder.Desktop.App.ViewModels; @@ -23,22 +25,46 @@ public partial class TrayWindowViewModel : ObservableObject private DispatcherQueue? _dispatcherQueue; - [ObservableProperty] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowEnableSection))] + [NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))] + [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowFailedSection))] + public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; // This is a separate property because we need the switch to be 2-way. [ObservableProperty] public partial bool VpnSwitchActive { get; set; } = false; - [ObservableProperty] public partial string? VpnFailedMessage { get; set; } = null; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowEnableSection))] + [NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))] + [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))] + [NotifyPropertyChangedFor(nameof(ShowFailedSection))] + public partial string? VpnFailedMessage { get; set; } = null; [ObservableProperty] - [NotifyPropertyChangedFor(nameof(NoAgents))] - [NotifyPropertyChangedFor(nameof(AgentOverflow))] [NotifyPropertyChangedFor(nameof(VisibleAgents))] + [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))] public partial List Agents { get; set; } = []; - public bool NoAgents => Agents.Count == 0; + public bool ShowEnableSection => VpnFailedMessage is null && VpnLifecycle is not VpnLifecycle.Started; + + public bool ShowWorkspacesHeader => VpnFailedMessage is null && VpnLifecycle is VpnLifecycle.Started; + + public bool ShowNoAgentsSection => + VpnFailedMessage is null && Agents.Count == 0 && VpnLifecycle is VpnLifecycle.Started; + + public bool ShowAgentsSection => + VpnFailedMessage is null && Agents.Count > 0 && VpnLifecycle is VpnLifecycle.Started; + + public bool ShowFailedSection => VpnFailedMessage is not null; - public bool AgentOverflow => Agents.Count > MaxAgents; + public bool ShowAgentOverflowButton => VpnFailedMessage is null && Agents.Count > MaxAgents; [ObservableProperty] [NotifyPropertyChangedFor(nameof(VisibleAgents))] @@ -190,24 +216,47 @@ public void VpnSwitch_Toggled(object sender, RoutedEventArgs e) { if (sender is not ToggleSwitch toggleSwitch) return; - VpnFailedMessage = ""; + VpnFailedMessage = null; + + // The start/stop methods will call back to update the state. + if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped) + _ = StartVpn(); // in the background + else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started) + _ = StopVpn(); // in the background + else + toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; + } + + private async Task StartVpn() + { try { - // The start/stop methods will call back to update the state. - if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped) - _rpcController.StartVpn(); - else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started) - _rpcController.StopVpn(); - else - toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; + await _rpcController.StartVpn(); } - catch + catch (Exception e) { - // TODO: display error - VpnFailedMessage = e.ToString(); + VpnFailedMessage = "Failed to start CoderVPN: " + MaybeUnwrapTunnelError(e); } } + private async Task StopVpn() + { + try + { + await _rpcController.StopVpn(); + } + catch (Exception e) + { + VpnFailedMessage = "Failed to stop CoderVPN: " + MaybeUnwrapTunnelError(e); + } + } + + private static string MaybeUnwrapTunnelError(Exception e) + { + if (e is VpnLifecycleException vpnError) return vpnError.Message; + return e.ToString(); + } + [RelayCommand] public void ToggleShowAllAgents() { diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index 66ec273..e466826 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -13,15 +13,11 @@ - - - + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + HorizontalTextAlignment="Left" + TextTrimming="CharacterEllipsis" + TextWrapping="NoWrap" + Width="180"> + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + From 1433fbd665df158565657a7a882c0ee2e1d3113b Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 7 Mar 2025 00:08:03 +1100 Subject: [PATCH 2/2] Remove property notifier for ShowFailedSection on VpnLifecycle --- App/ViewModels/TrayWindowViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 76530e4..204d9f0 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -30,7 +30,6 @@ public partial class TrayWindowViewModel : ObservableObject [NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))] [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] - [NotifyPropertyChangedFor(nameof(ShowFailedSection))] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; // This is a separate property because we need the switch to be 2-way. 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