Fix: SSH tunnel not restarted on unexpected exit

- Add TunnelExited event to SshTunnelService
- Null _process/_lastSpec and dispose the process on unexpected exit, then fire TunnelExited
- Subscribe to TunnelExited in App.xaml.cs with bounded retry backoff (1s/2s/5s/10s/30s)

Agent-Logs-Url: https://github.com/openclaw/openclaw-windows-node/sessions/a69d5952-fc73-46fe-9d2b-e5596c75ea4c

Co-authored-by: shanselman <2892+shanselman@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-01 17:27:28 +00:00 committed by GitHub
parent 9dcd12767a
commit 60f1ebde9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 39 additions and 0 deletions

View File

@ -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 =>

View File

@ -21,6 +21,11 @@ public sealed class SshTunnelService : IDisposable
_logger = logger;
}
/// <summary>
/// Raised when the SSH tunnel process exits unexpectedly (i.e., not as a result of <see cref="Stop"/>).
/// </summary>
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);
}
};