Watchdog-Heartbeat + LicenseLabrador-Lizenzgate im WinFormsHost
Bindet die beiden neuen Betriebsprojekte an: - WatchdogHeartbeatService: periodischer POST /api/heartbeat an watchdog.mhdf.de (Dead-Man's-Switch), stopping-Event beim Beenden, Konfiguration ueber AppSettings-Kategorie "Watchdog". Fehler sind best effort und beeintraechtigen die App nie. - LicenseGuard + LicenseDialog: Lizenzpruefung vor dem Start der MainForm, 12h-Revalidierung zur Laufzeit, Checksum-Haertung. Produkt-Slug/Endpoint/Public-Key sind einkompiliert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
02c5a4d6f4
commit
3a82397651
@@ -0,0 +1,137 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace Predictalytics.WinFormsHost.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Sends periodic dead-man's-switch heartbeats to the external Watchdog server
|
||||
/// (POST /api/heartbeat) so an outage of this app — or the whole machine — raises
|
||||
/// an alarm. A Watchdog outage must never impact the app: every call is best effort.
|
||||
/// </summary>
|
||||
public sealed class WatchdogHeartbeatService : IDisposable
|
||||
{
|
||||
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(10) };
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _apiKey;
|
||||
private readonly string _source;
|
||||
private readonly string _instance;
|
||||
private readonly int _intervalSeconds;
|
||||
private readonly Func<object?>? _metadataProvider;
|
||||
private readonly DateTime _startedUtc = DateTime.UtcNow;
|
||||
|
||||
private System.Threading.Timer? _timer;
|
||||
private bool _lastSendFailed;
|
||||
|
||||
public WatchdogHeartbeatService(
|
||||
string baseUrl,
|
||||
string apiKey,
|
||||
string source,
|
||||
string instance,
|
||||
int intervalSeconds,
|
||||
Func<object?>? metadataProvider = null)
|
||||
{
|
||||
_baseUrl = baseUrl.TrimEnd('/');
|
||||
_apiKey = apiKey;
|
||||
_source = source;
|
||||
_instance = string.IsNullOrWhiteSpace(instance) ? "default" : instance;
|
||||
_intervalSeconds = Math.Max(15, intervalSeconds);
|
||||
_metadataProvider = metadataProvider;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_timer = new System.Threading.Timer(
|
||||
async _ => await SendHeartbeatAsync("ok"),
|
||||
null, TimeSpan.Zero, TimeSpan.FromSeconds(_intervalSeconds));
|
||||
Log.Information("🐕 Watchdog heartbeat started → {Url} (source={Source}, every {Interval}s)",
|
||||
_baseUrl, _source, _intervalSeconds);
|
||||
}
|
||||
|
||||
/// <summary>Sends a lifecycle event (e.g. "stopping") so a planned shutdown is not alarmed as a crash.</summary>
|
||||
public void NotifyStopping()
|
||||
{
|
||||
try
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
source = _source,
|
||||
instance = _instance,
|
||||
kind = "stopping",
|
||||
severity = "info",
|
||||
message = "Predictalytics wird planmäßig beendet."
|
||||
};
|
||||
// Synchronous with a short cap: the form is closing and must not hang.
|
||||
PostAsync("/api/event", payload).Wait(TimeSpan.FromSeconds(4));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort only.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendHeartbeatAsync(string status)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
source = _source,
|
||||
instance = _instance,
|
||||
type = "heartbeat",
|
||||
status,
|
||||
message = (string?)null,
|
||||
metrics = new
|
||||
{
|
||||
uptimeSec = (long)(DateTime.UtcNow - _startedUtc).TotalSeconds,
|
||||
app = _metadataProvider?.Invoke()
|
||||
},
|
||||
group = "C# Applications",
|
||||
interval = _intervalSeconds
|
||||
};
|
||||
|
||||
await PostAsync("/api/heartbeat", payload);
|
||||
|
||||
if (_lastSendFailed)
|
||||
{
|
||||
_lastSendFailed = false;
|
||||
Log.Information("🐕 Watchdog heartbeat wieder erfolgreich zugestellt.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the first failure as warning, subsequent ones quietly (no log flood).
|
||||
if (!_lastSendFailed)
|
||||
{
|
||||
_lastSendFailed = true;
|
||||
Log.Warning("🐕 Watchdog heartbeat fehlgeschlagen (weitere Fehler werden unterdrückt): {Error}", ex.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug(ex, "Watchdog heartbeat failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PostAsync(string path, object payload)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, _baseUrl + path);
|
||||
request.Headers.Add("X-Watchdog-Key", _apiKey);
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await _http.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
throw new HttpRequestException($"Watchdog API HTTP {(int)response.StatusCode}: {body}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
_http.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user