Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
94 changes: 89 additions & 5 deletions App.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,16 @@ private async Task InitializeServicesAsync()

var credentialManager = _services.GetRequiredService<ICredentialManager>();
var rpcController = _services.GetRequiredService<IRpcController>();
var settingsManager = _services.GetRequiredService<ISettingsManager<CoderConnectSettings>>();

AppBootstrapLogger.Info("Initializing credentials and RPC connection...");

using var credentialLoadCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var appStopping = _hostApplicationLifetime?.ApplicationStopping ?? CancellationToken.None;
using var credentialLoadCts = CancellationTokenSource.CreateLinkedTokenSource(appStopping);
credentialLoadCts.CancelAfter(TimeSpan.FromSeconds(15));

var loadCredentialsTask = credentialManager.LoadCredentials(credentialLoadCts.Token);
var reconnectTask = rpcController.Reconnect();
var reconnectTask = ReconnectWithStartupRetryAsync(rpcController, appStopping);

try
{
Expand All @@ -164,11 +168,91 @@ private async Task InitializeServicesAsync()
if (loadCredentialsTask.IsFaulted)
AppBootstrapLogger.Error("Credential initialization failed", loadCredentialsTask.Exception?.GetBaseException());

// reconnectTask logs its own errors internally; just note if it threw here
if (reconnectTask.IsFaulted)
AppBootstrapLogger.Error("RPC reconnect failed", reconnectTask.Exception?.GetBaseException());
else if (reconnectTask.IsCanceled)
AppBootstrapLogger.Warn("RPC reconnect canceled");
AppBootstrapLogger.Error("Startup reconnect failed unexpectedly", reconnectTask.Exception?.GetBaseException());
}

var reconnectSucceeded = reconnectTask is { IsCompletedSuccessfully: true, Result: true };

if (!reconnectSucceeded)
AppBootstrapLogger.Warn("Startup continuing in disconnected state after retry exhaustion");

try
{
await MaybeAutoStartVpnOnLaunchAsync(settingsManager, credentialManager, rpcController, reconnectSucceeded, appStopping);
}
catch (Exception ex)
{
AppBootstrapLogger.Error("ConnectOnLaunch failed", ex);
}
}

private async Task<bool> ReconnectWithStartupRetryAsync(IRpcController rpcController, CancellationToken ct)
{
TimeSpan[] delays =
[
TimeSpan.Zero,
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(8),
];

Exception? lastError = null;

for (var attempt = 0; attempt < delays.Length; attempt++)
{
if (attempt > 0)
await Task.Delay(delays[attempt], ct);

try
{
await rpcController.Reconnect(ct);
AppBootstrapLogger.Info($"RPC reconnect succeeded on attempt {attempt + 1}/{delays.Length}");
return true;
}
catch (Exception ex) when (!ct.IsCancellationRequested)
{
lastError = ex;
AppBootstrapLogger.Warn($"RPC reconnect attempt {attempt + 1}/{delays.Length} failed: {ex.Message}");
}
}

AppBootstrapLogger.Error("RPC reconnect exhausted startup retries", lastError);
return false;
}

private async Task MaybeAutoStartVpnOnLaunchAsync(
ISettingsManager<CoderConnectSettings> settingsManager,
ICredentialManager credentialManager,
IRpcController rpcController,
bool reconnectSucceeded,
CancellationToken ct)
{
var settings = await settingsManager.Read(ct);
if (!settings.ConnectOnLaunch)
return;

if (!reconnectSucceeded)
{
AppBootstrapLogger.Info("ConnectOnLaunch skipped because startup reconnect did not succeed");
return;
}

var creds = credentialManager.GetCachedCredentials();
var rpc = rpcController.GetState();

if (creds.State != CredentialState.Valid ||
rpc.RpcLifecycle != RpcLifecycle.Connected ||
rpc.VpnLifecycle != VpnLifecycle.Stopped)
{
AppBootstrapLogger.Info($"ConnectOnLaunch skipped (cred={creds.State}, rpc={rpc.RpcLifecycle}, vpn={rpc.VpnLifecycle})");
return;
}

await rpcController.StartVpn(ct);
AppBootstrapLogger.Info("ConnectOnLaunch started VPN successfully");
}

private void ConfigureTrayIcons(TrayIconViewModel trayIconViewModel)
Expand Down
2 changes: 1 addition & 1 deletion App.Avalonia/Views/Pages/SettingsMainPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<TextBlock Text="Start on login"
FontWeight="SemiBold" />
<TextBlock TextWrapping="Wrap"
Text="This setting controls whether the Coder Desktop app starts on Windows startup." />
Text="This setting controls whether the Coder Desktop app starts when you sign in." />
<ToggleSwitch IsChecked="{CompiledBinding StartOnLogin, Mode=TwoWay}"
IsEnabled="{CompiledBinding StartOnLoginDisabled, Converter={StaticResource InverseBoolConverter}}" />
</StackPanel>
Expand Down
20 changes: 12 additions & 8 deletions App.Shared/Services/RpcController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,19 @@ private async Task DisposeSpeaker()

private void SpeakerOnError(Exception e)
{
Debug.WriteLine($"Error: {e}");
try
{
using var _ = Reconnect(CancellationToken.None);
}
catch
_logger.LogWarning(e, "RPC speaker error; attempting immediate reconnect");

_ = Task.Run(async () =>
{
// best effort to immediately reconnect
}
try
{
await Reconnect(CancellationToken.None);
}
catch (Exception reconnectError)
{
_logger.LogWarning(reconnectError, "Immediate reconnect after speaker error failed");
}
});
}

private void AssertRpcConnected()
Expand Down
4 changes: 3 additions & 1 deletion App.Shared/ViewModels/TrayWindowDisconnectedViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public TrayWindowDisconnectedViewModel(IRpcController rpcController)

private void UpdateFromRpcModel(RpcModel rpcModel)
{
ReconnectButtonEnabled = rpcModel.RpcLifecycle != RpcLifecycle.Disconnected;
ReconnectButtonEnabled = rpcModel.RpcLifecycle == RpcLifecycle.Disconnected;
}

[RelayCommand]
Expand All @@ -36,12 +36,14 @@ public async Task Reconnect()
{
ReconnectFailed = false;
ErrorMessage = string.Empty;
ReconnectButtonEnabled = false;
await _rpcController.Reconnect();
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
ReconnectFailed = true;
ReconnectButtonEnabled = true;
}
}
}
Loading