diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index b895961..8372413 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -270,6 +270,7 @@ public partial class App : Application ToastNotificationManagerCompat.OnActivated += OnToastActivated; _sshTunnelService = new SshTunnelService(new AppLogger()); + _sshTunnelService.TunnelExited += OnSshTunnelExited; // First-run check if (string.IsNullOrWhiteSpace(_settings.Token)) @@ -2262,6 +2263,35 @@ public partial class App : Application return true; } + private async void OnSshTunnelExited(object? sender, EventArgs e) + { + if (_isExiting || _settings?.UseSshTunnel != true) return; + + // Attempt to restart the SSH tunnel with bounded exponential backoff. + // The gateway client's built-in reconnect loop will pick up automatically + // once the tunnel port is available again. + int[] retryDelays = [1000, 2000, 5000, 10000, 30000]; + for (int i = 0; i < retryDelays.Length; i++) + { + await Task.Delay(retryDelays[i]); + if (_isExiting || _settings?.UseSshTunnel != true) return; + try + { + if (EnsureSshTunnelConfigured()) + { + Logger.Info("SSH tunnel successfully restarted after unexpected exit"); + return; + } + } + catch (Exception ex) + { + Logger.Warn($"SSH tunnel restart attempt {i + 1} failed: {ex.Message}"); + } + } + + Logger.Error("SSH tunnel could not be restarted after all retry attempts"); + } + #endregion private Microsoft.UI.Dispatching.DispatcherQueue? AppDispatcherQueue => diff --git a/src/OpenClaw.Tray.WinUI/Services/SshTunnelService.cs b/src/OpenClaw.Tray.WinUI/Services/SshTunnelService.cs index 99ee08e..c3ac336 100644 --- a/src/OpenClaw.Tray.WinUI/Services/SshTunnelService.cs +++ b/src/OpenClaw.Tray.WinUI/Services/SshTunnelService.cs @@ -21,6 +21,11 @@ public sealed class SshTunnelService : IDisposable _logger = logger; } + /// + /// Raised when the SSH tunnel process exits unexpectedly (i.e., not as a result of ). + /// + public event EventHandler? TunnelExited; + public bool IsRunning => _process is { HasExited: false }; public void EnsureStarted(SettingsManager settings) @@ -130,6 +135,10 @@ public sealed class SshTunnelService : IDisposable else { _logger.Warn($"SSH tunnel exited unexpectedly (code {exitCode})"); + _process = null; + _lastSpec = null; + try { process.Dispose(); } catch (Exception disposeEx) { _logger.Warn($"SSH tunnel process dispose failed: {disposeEx.Message}"); } + TunnelExited?.Invoke(this, EventArgs.Empty); } };