SIP Watchdog: automatischer Reconnect bei haengender Registrierung, Tray-Status/Benachrichtigung

This commit is contained in:
dimedtec 2026-08-28 17:48:24 +02:00
parent ed18ffdd1d
commit 22c2ac1b2d
3 changed files with 143 additions and 7 deletions

View file

@ -109,6 +109,24 @@ public partial class App : System.Windows.Application
CallPopupWindow.CloseByCallId(args.CallId);
});
};
var hadOutage = false;
sipMonitor.RegistrationStatusChanged += (_, registered) =>
{
_trayIconManager?.SetSipStatus(registered);
if (!registered)
{
hadOutage = true;
_trayIconManager?.ShowNotification(
"SIP-Verbindung verloren", "Versuche automatisch, die Registrierung wiederherzustellen ...");
}
else if (hadOutage)
{
hadOutage = false;
_trayIconManager?.ShowNotification("SIP wieder verbunden", "Die Registrierung wurde wiederhergestellt.");
}
};
}
protected override async void OnExit(System.Windows.ExitEventArgs e)

View file

@ -35,6 +35,13 @@ public sealed class TrayIconManager : IDisposable
public void Show() => _notifyIcon.Visible = true;
public void SetSipStatus(bool registered)
{
_notifyIcon.Text = registered
? "Anrufmonitor - verbunden"
: "Anrufmonitor - SIP getrennt";
}
public void ShowNotification(string title, string message)
{
_notifyIcon.Visible = true;

View file

@ -1,3 +1,4 @@
using System.Net;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -12,9 +13,18 @@ namespace Anrufmonitor.Sip;
/// eingehende Anrufe an mehrere registrierte Kontakte forkt (Nebenstelle/Konto teilen
/// sich mehrere Geraete) - der eigentliche Anruf wird weiterhin vom echten Telefon
/// angenommen, dieser Client dient nur der Anzeige des Anrufers.
///
/// Robustheit: ein Watchdog prueft periodisch, ob die letzte erfolgreiche Registrierung
/// nicht laenger als erwartet zurueckliegt. Falls doch (z.B. weil die interne
/// Neu-Registrierung aus welchem Grund auch immer haengen geblieben ist), wird der
/// komplette SIP-Stack (Transport, User-Agent, Registrierung) verworfen und neu
/// aufgebaut, statt dass die Ueberwachung stillschweigend tot bleibt.
/// </summary>
public sealed class SipCallMonitor : BackgroundService
{
private static readonly TimeSpan ReconnectDelay = TimeSpan.FromSeconds(15);
private static readonly TimeSpan WatchdogInterval = TimeSpan.FromSeconds(60);
private readonly SipOptions _options;
private readonly ILogger<SipCallMonitor> _logger;
@ -22,6 +32,9 @@ public sealed class SipCallMonitor : BackgroundService
private SIPUserAgent? _userAgent;
private SIPRegistrationUserAgent? _registrationAgent;
private DateTime _lastRegistrationSuccessUtc;
private bool _isRegistered;
public SipCallMonitor(IOptions<SipOptions> options, ILogger<SipCallMonitor> logger)
{
_options = options.Value;
@ -32,6 +45,9 @@ public sealed class SipCallMonitor : BackgroundService
public event EventHandler<CallEndedEventArgs>? CallEnded;
/// <summary>Feuert bei jedem Wechsel des Registrierungsstatus (true = registriert, false = verloren).</summary>
public event EventHandler<bool>? RegistrationStatusChanged;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!_options.Enabled)
@ -40,8 +56,47 @@ public sealed class SipCallMonitor : BackgroundService
return;
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
await RunOnceAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(
ex, "SIP-Monitor unterbrochen, versuche Neuverbindung in {Delay}s", ReconnectDelay.TotalSeconds);
}
SetRegistered(false);
TeardownCurrent();
if (stoppingToken.IsCancellationRequested)
{
break;
}
try
{
await Task.Delay(ReconnectDelay, stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
}
TeardownCurrent();
}
private async Task RunOnceAsync(CancellationToken stoppingToken)
{
_transport = new SIPTransport();
_transport.AddSIPChannel(new SIPUDPChannel(new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0)));
_transport.AddSIPChannel(new SIPUDPChannel(new IPEndPoint(IPAddress.Any, 0)));
_userAgent = new SIPUserAgent(_transport, null);
_userAgent.OnIncomingCall += OnIncomingCall;
@ -65,27 +120,84 @@ public sealed class SipCallMonitor : BackgroundService
_options.Username,
_options.Password,
_options.Server,
_options.RegisterExpirySeconds);
_options.RegisterExpirySeconds,
maxRegistrationAttemptTimeout: 60,
registerFailureRetryInterval: 30,
maxRegisterAttempts: 3,
exitOnUnequivocalFailure: false);
_registrationAgent.RegistrationSuccessful += (uri, resp) =>
{
_logger.LogInformation("SIP-Registrierung erfolgreich: {Uri}", uri);
_lastRegistrationSuccessUtc = DateTime.UtcNow;
SetRegistered(true);
};
_registrationAgent.RegistrationFailed += (uri, resp, err) =>
{
_logger.LogError("SIP-Registrierung fehlgeschlagen ({Uri}): {Error}", uri, err);
SetRegistered(false);
};
_registrationAgent.RegistrationTemporaryFailure += (uri, resp, msg) =>
_logger.LogWarning("SIP-Registrierung temporaer fehlgeschlagen ({Uri}): {Message}", uri, msg);
_registrationAgent.RegistrationRemoved += (uri, resp) =>
{
_logger.LogWarning("SIP-Registrierung entfernt: {Uri}", uri);
SetRegistered(false);
};
// Startzeitpunkt als Referenz setzen, damit der Watchdog der ersten (noch
// laufenden) Registrierung eine Gnadenfrist gibt, statt sofort auszuloesen.
_lastRegistrationSuccessUtc = DateTime.UtcNow;
_registrationAgent.Start();
var maxAge = TimeSpan.FromSeconds(_options.RegisterExpirySeconds) + WatchdogInterval + WatchdogInterval;
using var timer = new PeriodicTimer(WatchdogInterval);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
var age = DateTime.UtcNow - _lastRegistrationSuccessUtc;
if (age > maxAge)
{
throw new TimeoutException(
$"Keine erfolgreiche SIP-Registrierung seit {age.TotalMinutes:0.0} Minuten - erzwinge Neuverbindung.");
}
}
}
private void SetRegistered(bool registered)
{
if (_isRegistered == registered)
{
return;
}
_isRegistered = registered;
RegistrationStatusChanged?.Invoke(this, registered);
}
private void TeardownCurrent()
{
try
{
_registrationAgent?.Stop();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Fehler beim Stoppen des Registrierungs-Agents (ignoriert)");
}
try
{
await Task.Delay(Timeout.Infinite, stoppingToken);
_transport?.Shutdown();
}
catch (OperationCanceledException)
catch (Exception ex)
{
// erwartetes Verhalten beim Beenden
_logger.LogDebug(ex, "Fehler beim Herunterfahren des SIP-Transports (ignoriert)");
}
_registrationAgent = null;
_userAgent = null;
_transport = null;
}
private void OnIncomingCall(SIPUserAgent userAgent, SIPRequest inviteRequest)
@ -118,8 +230,7 @@ public sealed class SipCallMonitor : BackgroundService
public override void Dispose()
{
_registrationAgent?.Stop();
_transport?.Shutdown();
TeardownCurrent();
base.Dispose();
}
}