Neues Projekt Predictalytics.Hosting nimmt auf, was bisher im windows-gebundenen WinFormsHost feststeckte, aber portabel ist: - PredictalyticsHost (aus EmbeddedWebServer): Kestrel- und Worker-Lifecycle, Wartungsaktionen, DB-Groesse. Meldet Zustandswechsel ueber StateChanged. - PredictalyticsOptions (aus AppSettings): ohne WinForms-Bezug. Die System.ComponentModel-Attribute sind plattformneutral und bleiben, damit das PropertyGrid Gruppen und Beschreibungen behaelt. - LoggingSetup (aus Program.cs): Serilog-Aufbau, Terminal-Sink als optionale Action statt fester RichTextBox. - LicenseGuard: GUI-frei. Periodische Revalidierung ueber PeriodicTimer statt WinForms-Timer, Abbruch ueber Callback statt Application.Exit. Der interaktive Dialogaufruf bleibt als LicenseGate im WinForms-Host. - WatchdogHeartbeatService unveraendert verschoben. Infrastructure: RichTextBoxSink -> DelegateSink umbenannt (war nie WinForms-abhaengig, nur missverstaendlich benannt). Einstellungen liegen jetzt unter %APPDATA%/Predictalytics bzw. ~/.config/Predictalytics statt neben der Programmdatei, mit einmaliger Uebernahme aus dem alten Ort. Das Installationsverzeichnis ist unter Linux ueblicherweise nicht beschreibbar. wwwroot wird ueber einen Content-Eintrag neben die Programmdatei kopiert; die frueheren Pfad-Heuristiken entfallen. Hosting und WinFormsHost nutzen Microsoft.NET.Sdk statt Sdk.Web: der Web-SDK globbt wwwroot automatisch als Static Web Asset und kollidiert mit dem Content-Eintrag. WebApplication kommt ueber FrameworkReference. Neu konfigurierbar (verhaltensgleiche Defaults): WebserverHost fuer die Kestrel-Bind-Adresse, DbSslMode fuer die MySQL-Verschluesselung. explorer.exe-Aufrufe durch ProcessStartInfo mit UseShellExecute ersetzt — funktioniert unter Windows und Linux. Build: 0 Fehler. Tests: 100 bestanden, 0 Fehler, 1 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
4.6 KiB
C#
142 lines
4.6 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Serilog;
|
|
|
|
namespace Predictalytics.Hosting;
|
|
|
|
/// <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>
|
|
/// Reports a planned shutdown so it is not alarmed as a crash.
|
|
/// Must be "stopped_graceful": the server's event_log.kind ENUM has no "stopping"
|
|
/// value, and that variant fails the insert with HTTP 500 after the state update.
|
|
/// </summary>
|
|
public void NotifyStopping()
|
|
{
|
|
try
|
|
{
|
|
var payload = new
|
|
{
|
|
source = _source,
|
|
instance = _instance,
|
|
kind = "stopped_graceful",
|
|
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();
|
|
}
|
|
}
|