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

feat: add vpn start progress#114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
deansheather merged 5 commits intomainfromdean/progress
Jun 6, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

163 changes: 162 additions & 1 deletionApp/Models/RpcModel.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Coder.Desktop.App.Converters;
using Coder.Desktop.Vpn.Proto;

namespace Coder.Desktop.App.Models;
Expand All@@ -19,11 +22,168 @@ public enum VpnLifecycle
Stopping,
}

public enum VpnStartupStage
{
Unknown,
Initializing,
Downloading,
Finalizing,
}

public class VpnDownloadProgress
{
public ulong BytesWritten { get; set; } = 0;
public ulong? BytesTotal { get; set; } = null; // null means unknown total size

public double Progress
{
get
{
if (BytesTotal is > 0)
{
return (double)BytesWritten / BytesTotal.Value;
}
return 0.0;
}
}

public override string ToString()
{
// TODO: it would be nice if the two suffixes could match
var s = FriendlyByteConverter.FriendlyBytes(BytesWritten);
if (BytesTotal != null)
s += $" of {FriendlyByteConverter.FriendlyBytes(BytesTotal.Value)}";
else
s += " of unknown";
if (BytesTotal != null)
s += $" ({Progress:0%})";
return s;
}

public VpnDownloadProgress Clone()
{
return new VpnDownloadProgress
{
BytesWritten = BytesWritten,
BytesTotal = BytesTotal,
};
}

public static VpnDownloadProgress FromProto(StartProgressDownloadProgress proto)
{
return new VpnDownloadProgress
{
BytesWritten = proto.BytesWritten,
BytesTotal = proto.HasBytesTotal ? proto.BytesTotal : null,
};
}
}

public class VpnStartupProgress
{
public const string DefaultStartProgressMessage = "Starting Coder Connect...";

// Scale the download progress to an overall progress value between these
// numbers.
private const double DownloadProgressMin = 0.05;
private const double DownloadProgressMax = 0.80;

public VpnStartupStage Stage { get; init; } = VpnStartupStage.Unknown;
public VpnDownloadProgress? DownloadProgress { get; init; } = null;

// 0.0 to 1.0
public double Progress
{
get
{
switch (Stage)
{
case VpnStartupStage.Unknown:
case VpnStartupStage.Initializing:
return 0.0;
case VpnStartupStage.Downloading:
var progress = DownloadProgress?.Progress ?? 0.0;
return DownloadProgressMin + (DownloadProgressMax - DownloadProgressMin) * progress;
case VpnStartupStage.Finalizing:
return DownloadProgressMax;
default:
throw new ArgumentOutOfRangeException();
}
}
}

public override string ToString()
{
switch (Stage)
{
case VpnStartupStage.Unknown:
case VpnStartupStage.Initializing:
return DefaultStartProgressMessage;
case VpnStartupStage.Downloading:
var s = "Downloading Coder Connect binary...";
if (DownloadProgress is not null)
{
s += "\n" + DownloadProgress;
}

return s;
case VpnStartupStage.Finalizing:
return "Finalizing Coder Connect startup...";
default:
throw new ArgumentOutOfRangeException();
}
}

public VpnStartupProgress Clone()
{
return new VpnStartupProgress
{
Stage = Stage,
DownloadProgress = DownloadProgress?.Clone(),
};
}

public static VpnStartupProgress FromProto(StartProgress proto)
{
return new VpnStartupProgress
{
Stage = proto.Stage switch
{
StartProgressStage.Initializing => VpnStartupStage.Initializing,
StartProgressStage.Downloading => VpnStartupStage.Downloading,
StartProgressStage.Finalizing => VpnStartupStage.Finalizing,
_ => VpnStartupStage.Unknown,
},
DownloadProgress = proto.Stage is StartProgressStage.Downloading ?
VpnDownloadProgress.FromProto(proto.DownloadProgress) :
null,
};
}
}

public class RpcModel
{
public RpcLifecycle RpcLifecycle { get; set; } = RpcLifecycle.Disconnected;

public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
public VpnLifecycle VpnLifecycle
{
get;
set
{
if (VpnLifecycle != value && value == VpnLifecycle.Starting)
// Reset the startup progress when the VPN lifecycle changes to
// Starting.
VpnStartupProgress = null;
field = value;
}
}

// Nullable because it is only set when the VpnLifecycle is Starting
public VpnStartupProgress? VpnStartupProgress
{
get => VpnLifecycle is VpnLifecycle.Starting ? field ?? new VpnStartupProgress() : null;
set;
}

public IReadOnlyList<Workspace> Workspaces { get; set; } = [];

Expand All@@ -35,6 +195,7 @@ public RpcModel Clone()
{
RpcLifecycle = RpcLifecycle,
VpnLifecycle = VpnLifecycle,
VpnStartupProgress = VpnStartupProgress?.Clone(),
Workspaces = Workspaces,
Agents = Agents,
};
Expand Down
22 changes: 19 additions & 3 deletionsApp/Services/RpcController.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,7 +161,10 @@ public async Task StartVpn(CancellationToken ct = default)
throw new RpcOperationException(
$"Cannot start VPN without valid credentials, current state: {credentials.State}");

MutateState(state => { state.VpnLifecycle = VpnLifecycle.Starting; });
MutateState(state =>
{
state.VpnLifecycle = VpnLifecycle.Starting;
});

ServiceMessage reply;
try
Expand DownExpand Up@@ -283,15 +286,28 @@ private void ApplyStatusUpdate(Status status)
});
}

private void ApplyStartProgressUpdate(StartProgress message)
{
MutateState(state =>
{
// The model itself will ignore this value if we're not in the
// starting state.
state.VpnStartupProgress = VpnStartupProgress.FromProto(message);
});
}

private void SpeakerOnReceive(ReplyableRpcMessage<ClientMessage, ServiceMessage> message)
{
switch (message.Message.MsgCase)
{
case ServiceMessage.MsgOneofCase.Start:
case ServiceMessage.MsgOneofCase.Stop:
case ServiceMessage.MsgOneofCase.Status:
ApplyStatusUpdate(message.Message.Status);
break;
case ServiceMessage.MsgOneofCase.Start:
case ServiceMessage.MsgOneofCase.Stop:
case ServiceMessage.MsgOneofCase.StartProgress:
ApplyStartProgressUpdate(message.Message.StartProgress);
break;
case ServiceMessage.MsgOneofCase.None:
default:
// TODO: log unexpected message
Expand Down
37 changes: 35 additions & 2 deletionsApp/ViewModels/TrayWindowViewModel.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
{
private const int MaxAgents = 5;
private const string DefaultDashboardUrl = "https://coder.com";
private const string DefaultHostnameSuffix = ".coder";

private readonly IServiceProvider _services;
private readonly IRpcController _rpcController;
Expand All@@ -53,6 +52,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowEnableSection))]
[NotifyPropertyChangedFor(nameof(ShowVpnStartProgressSection))]
[NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))]
[NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))]
[NotifyPropertyChangedFor(nameof(ShowAgentsSection))]
Expand All@@ -63,14 +63,33 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowEnableSection))]
[NotifyPropertyChangedFor(nameof(ShowVpnStartProgressSection))]
[NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))]
[NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))]
[NotifyPropertyChangedFor(nameof(ShowAgentsSection))]
[NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))]
[NotifyPropertyChangedFor(nameof(ShowFailedSection))]
public partial string? VpnFailedMessage { get; set; } = null;

public bool ShowEnableSection => VpnFailedMessage is null && VpnLifecycle is not VpnLifecycle.Started;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(VpnStartProgressIsIndeterminate))]
[NotifyPropertyChangedFor(nameof(VpnStartProgressValueOrDefault))]
public partial int? VpnStartProgressValue { get; set; } = null;

public int VpnStartProgressValueOrDefault => VpnStartProgressValue ?? 0;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(VpnStartProgressMessageOrDefault))]
public partial string? VpnStartProgressMessage { get; set; } = null;

public string VpnStartProgressMessageOrDefault =>
string.IsNullOrEmpty(VpnStartProgressMessage) ? VpnStartupProgress.DefaultStartProgressMessage : VpnStartProgressMessage;

public bool VpnStartProgressIsIndeterminate => VpnStartProgressValueOrDefault == 0;

public bool ShowEnableSection => VpnFailedMessage is null && VpnLifecycle is not VpnLifecycle.Starting and not VpnLifecycle.Started;

public bool ShowVpnStartProgressSection => VpnFailedMessage is null && VpnLifecycle is VpnLifecycle.Starting;

public bool ShowWorkspacesHeader => VpnFailedMessage is null && VpnLifecycle is VpnLifecycle.Started;

Expand DownExpand Up@@ -170,6 +189,20 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
VpnLifecycle = rpcModel.VpnLifecycle;
VpnSwitchActive = rpcModel.VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started;

// VpnStartupProgress is only set when the VPN is starting.
if (rpcModel.VpnLifecycle is VpnLifecycle.Starting && rpcModel.VpnStartupProgress != null)
{
// Convert 0.00-1.00 to 0-100.
var progress = (int)(rpcModel.VpnStartupProgress.Progress * 100);
VpnStartProgressValue = Math.Clamp(progress, 0, 100);
VpnStartProgressMessage = rpcModel.VpnStartupProgress.ToString();
}
else
{
VpnStartProgressValue = null;
VpnStartProgressMessage = null;
}

// Add every known agent.
HashSet<ByteString> workspacesWithAgents = [];
List<AgentViewModel> agents = [];
Expand Down
2 changes: 1 addition & 1 deletionApp/Views/Pages/TrayWindowLoginRequiredPage.xaml
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@
</HyperlinkButton>

<HyperlinkButton
Command="{x:Bind ViewModel.ExitCommand, Mode=OneWay}"
Command="{x:Bind ViewModel.ExitCommand}"
Margin="-12,-8,-12,-5"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
Expand Down
11 changes: 10 additions & 1 deletionApp/Views/Pages/TrayWindowMainPage.xaml
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,6 +43,8 @@
<ProgressRing
Grid.Column="1"
IsActive="{x:Bind ViewModel.VpnLifecycle, Converter={StaticResource ConnectingBoolConverter}, Mode=OneWay}"
IsIndeterminate="{x:Bind ViewModel.VpnStartProgressIsIndeterminate, Mode=OneWay}"
Value="{x:Bind ViewModel.VpnStartProgressValueOrDefault, Mode=OneWay}"
Width="24"
Height="24"
Margin="10,0"
Expand DownExpand Up@@ -74,6 +76,13 @@
Visibility="{x:Bind ViewModel.ShowEnableSection, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />

<TextBlock
Text="{x:Bind ViewModel.VpnStartProgressMessageOrDefault, Mode=OneWay}"
TextWrapping="Wrap"
Margin="0,6,0,6"
Visibility="{x:Bind ViewModel.ShowVpnStartProgressSection, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />

<TextBlock
Text="Workspaces"
FontWeight="semibold"
Expand DownExpand Up@@ -344,7 +353,7 @@
Command="{x:Bind ViewModel.ExitCommand, Mode=OneWay}"
Margin="-12,-8,-12,-5"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
HorizontalContentAlignment="Left">

<TextBlockText="Exit"Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" />
</HyperlinkButton>
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp