SIP Watchdog: automatischer Reconnect bei haengender Registrierung, Tray-Status/Benachrichtigung
This commit is contained in:
parent
ed18ffdd1d
commit
22c2ac1b2d
3 changed files with 143 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue