using System.Collections.Concurrent; using System.Diagnostics; using System.Net.NetworkInformation; using System.Text; namespace EonaCat.Connections.Helpers { // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/license for full license details. public class NetworkMonitor : IDisposable { private readonly string _targetHost; private readonly int _pingIntervalMs; private readonly int _maxHistory; private readonly ConcurrentQueue _samples = new ConcurrentQueue(); private CancellationTokenSource _cts; private Task _monitorTask; private volatile bool _isRunning; private CancellationTokenSource _autoReportCts; private Task _autoReportTask; private volatile bool _autoReportRunning; public event EventHandler OnOutageDetected; public event EventHandler OnOutageRecovered; public event EventHandler OnSampleRecorded; public bool IsRunning => _isRunning; public string TargetHost => _targetHost; /// /// Gets whether automatic HTML report generation is currently running. /// public bool IsAutoHtmlReportRunning => _autoReportRunning; public NetworkMonitor(string targetHost = "127.0.0.1", int pingIntervalMs = 5000, int maxHistory = 1000) { _targetHost = targetHost; _pingIntervalMs = Math.Max(1000, pingIntervalMs); _maxHistory = Math.Max(10, maxHistory); } public void Start() { if (_isRunning) { return; } _cts?.Dispose(); _cts = new CancellationTokenSource(); _isRunning = true; _monitorTask = Task.Run(() => MonitorLoopAsync(_cts.Token)); } public void Stop() { _isRunning = false; _cts?.Cancel(); try { _monitorTask?.Wait(TimeSpan.FromSeconds(5)); } catch (AggregateException) { } } private async Task MonitorLoopAsync(CancellationToken token) { bool wasReachable = true; DateTime? outageStartedAt = null; while (!token.IsCancellationRequested) { var sample = await CollectSampleAsync().ConfigureAwait(false); RecordSample(sample); OnSampleRecorded?.Invoke(this, sample); if (!sample.IsReachable && wasReachable) { outageStartedAt = sample.Timestamp; OnOutageDetected?.Invoke(this, new NetworkOutageEventArgs { TargetHost = _targetHost, OutageStartedAt = sample.Timestamp, Sample = sample }); } else if (sample.IsReachable && !wasReachable && outageStartedAt.HasValue) { OnOutageRecovered?.Invoke(this, new NetworkOutageEventArgs { TargetHost = _targetHost, OutageStartedAt = outageStartedAt.Value, OutageEndedAt = sample.Timestamp, OutageDuration = sample.Timestamp - outageStartedAt.Value, Sample = sample }); outageStartedAt = null; } wasReachable = sample.IsReachable; try { await Task.Delay(_pingIntervalMs, token).ConfigureAwait(false); } catch (OperationCanceledException) { break; } } } private async Task CollectSampleAsync() { var sample = new NetworkSample { Timestamp = DateTime.UtcNow, TargetHost = _targetHost }; try { using (var ping = new Ping()) { var sw = Stopwatch.StartNew(); var reply = await ping.SendPingAsync(_targetHost, 5000).ConfigureAwait(false); sw.Stop(); sample.RoundTripTimeMs = reply.RoundtripTime; sample.MeasuredLatencyMs = sw.ElapsedMilliseconds; sample.IsReachable = reply.Status == IPStatus.Success; sample.PingStatus = reply.Status; } } catch (PingException ex) { sample.IsReachable = false; sample.Error = ex.InnerException?.Message ?? ex.Message; } catch (Exception ex) { sample.IsReachable = false; sample.Error = ex.Message; } try { var interfaces = NetworkInterface.GetAllNetworkInterfaces(); long totalBytesReceived = 0; long totalBytesSent = 0; foreach (var ni in interfaces) { if (ni.OperationalStatus != OperationalStatus.Up) { continue; } if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) { continue; } var stats = ni.GetIPv4Statistics(); totalBytesReceived += stats.BytesReceived; totalBytesSent += stats.BytesSent; } sample.SystemBytesReceived = totalBytesReceived; sample.SystemBytesSent = totalBytesSent; } catch { // Network interface stats may not be available on all platforms } return sample; } private void RecordSample(NetworkSample sample) { _samples.Enqueue(sample); while (_samples.Count > _maxHistory) { _samples.TryDequeue(out _); } } public IReadOnlyList GetSamples() { return _samples.ToArray(); } public IReadOnlyList GetRecentSamples(int count) { var all = _samples.ToArray(); int skip = Math.Max(0, all.Length - count); var result = new NetworkSample[Math.Min(count, all.Length)]; Array.Copy(all, skip, result, 0, result.Length); return result; } public NetworkHealthReport GetHealthReport() { var samples = _samples.ToArray(); var report = new NetworkHealthReport { TargetHost = _targetHost, GeneratedAt = DateTime.UtcNow, TotalSamples = samples.Length }; if (samples.Length == 0) { return report; } var reachable = new List(); var unreachable = new List(); foreach (var s in samples) { if (s.IsReachable) { reachable.Add(s); } else { unreachable.Add(s); } } report.SuccessfulPings = reachable.Count; report.FailedPings = unreachable.Count; report.UptimePercentage = samples.Length > 0 ? (double)reachable.Count / samples.Length * 100.0 : 0; if (reachable.Count > 0) { long totalRtt = 0; long minRtt = long.MaxValue; long maxRtt = long.MinValue; foreach (var s in reachable) { totalRtt += s.RoundTripTimeMs; if (s.RoundTripTimeMs < minRtt) { minRtt = s.RoundTripTimeMs; } if (s.RoundTripTimeMs > maxRtt) { maxRtt = s.RoundTripTimeMs; } } report.AverageLatencyMs = (double)totalRtt / reachable.Count; report.MinLatencyMs = minRtt; report.MaxLatencyMs = maxRtt; // Calculate jitter (standard deviation of latency) double sumSquaredDiff = 0; foreach (var s in reachable) { var diff = s.RoundTripTimeMs - report.AverageLatencyMs; sumSquaredDiff += diff * diff; } report.JitterMs = Math.Sqrt(sumSquaredDiff / reachable.Count); } // Detect outage windows bool inOutage = false; DateTime outageStart = DateTime.MinValue; foreach (var s in samples) { if (!s.IsReachable && !inOutage) { inOutage = true; outageStart = s.Timestamp; } else if (s.IsReachable && inOutage) { inOutage = false; report.OutageWindows.Add(new OutageWindow { Start = outageStart, End = s.Timestamp, Duration = s.Timestamp - outageStart }); } } if (inOutage) { report.OutageWindows.Add(new OutageWindow { Start = outageStart, End = DateTime.UtcNow, Duration = DateTime.UtcNow - outageStart, IsOngoing = true }); } // Bandwidth estimate between last two samples if (samples.Length >= 2) { var prev = samples[samples.Length - 2]; var curr = samples[samples.Length - 1]; var elapsed = (curr.Timestamp - prev.Timestamp).TotalSeconds; if (elapsed > 0 && prev.SystemBytesReceived > 0 && curr.SystemBytesReceived > 0) { report.CurrentReceiveBytesPerSecond = (curr.SystemBytesReceived - prev.SystemBytesReceived) / elapsed; report.CurrentSendBytesPerSecond = (curr.SystemBytesSent - prev.SystemBytesSent) / elapsed; } } report.IsStable = report.UptimePercentage >= 99.0 && report.JitterMs < 50 && report.OutageWindows.Count(o => o.IsOngoing) == 0; return report; } public void Dispose() { Stop(); StopAutoHtmlReport(); _cts?.Dispose(); _autoReportCts?.Dispose(); } /// /// Starts periodic automatic generation of the network health HTML report. /// /// Directory where the HTML file is written. /// Interval in seconds between report generations. /// The HTML file name. public void StartAutoHtmlReport(string outputDirectory, int intervalSeconds = 60, string fileName = "status-network.html") { if (_autoReportRunning) { return; } _autoReportCts?.Dispose(); _autoReportCts = new CancellationTokenSource(); _autoReportRunning = true; var interval = TimeSpan.FromSeconds(Math.Max(5, intervalSeconds)); var token = _autoReportCts.Token; _autoReportTask = Task.Run(async () => { while (!token.IsCancellationRequested) { try { if (!Directory.Exists(outputDirectory)) { Directory.CreateDirectory(outputDirectory); } var report = GetHealthReport(); var html = report.GenerateHtmlReport(); var filePath = Path.Combine(outputDirectory, fileName); File.WriteAllText(filePath, html, Encoding.UTF8); } catch { // Swallow to keep the loop alive } try { await Task.Delay(interval, token).ConfigureAwait(false); } catch (OperationCanceledException) { break; } } }, token); } /// /// Stops the automatic HTML report generation. /// public void StopAutoHtmlReport() { if (!_autoReportRunning) { return; } _autoReportRunning = false; _autoReportCts?.Cancel(); try { _autoReportTask?.Wait(TimeSpan.FromSeconds(5)); } catch (AggregateException) { } } } public class NetworkSample { public DateTime Timestamp { get; set; } public string TargetHost { get; set; } public bool IsReachable { get; set; } public long RoundTripTimeMs { get; set; } public long MeasuredLatencyMs { get; set; } public IPStatus PingStatus { get; set; } public string Error { get; set; } public long SystemBytesReceived { get; set; } public long SystemBytesSent { get; set; } } public class NetworkOutageEventArgs : EventArgs { public string TargetHost { get; set; } public DateTime OutageStartedAt { get; set; } public DateTime? OutageEndedAt { get; set; } public TimeSpan? OutageDuration { get; set; } public NetworkSample Sample { get; set; } } public class OutageWindow { public DateTime Start { get; set; } public DateTime End { get; set; } public TimeSpan Duration { get; set; } public bool IsOngoing { get; set; } } public class NetworkHealthReport { public string TargetHost { get; set; } public DateTime GeneratedAt { get; set; } public int TotalSamples { get; set; } public int SuccessfulPings { get; set; } public int FailedPings { get; set; } public double UptimePercentage { get; set; } public double AverageLatencyMs { get; set; } public long MinLatencyMs { get; set; } public long MaxLatencyMs { get; set; } public double JitterMs { get; set; } public double CurrentReceiveBytesPerSecond { get; set; } public double CurrentSendBytesPerSecond { get; set; } public bool IsStable { get; set; } public List OutageWindows { get; set; } = new List(); public string GetSummary() { var sb = new StringBuilder(); sb.AppendLine($"=== Network Health Report ==="); sb.AppendLine($"Target: {TargetHost}"); sb.AppendLine($"Generated: {GeneratedAt:O}"); sb.AppendLine($"Samples: {TotalSamples}"); sb.AppendLine($"Success: {SuccessfulPings} / Failed: {FailedPings}"); sb.AppendLine($"Uptime: {UptimePercentage:F2}%"); sb.AppendLine($"Latency (avg): {AverageLatencyMs:F1} ms"); sb.AppendLine($"Latency (min): {MinLatencyMs} ms / (max): {MaxLatencyMs} ms"); sb.AppendLine($"Jitter: {JitterMs:F1} ms"); sb.AppendLine($"Recv BW: {CurrentReceiveBytesPerSecond:F0} B/s"); sb.AppendLine($"Send BW: {CurrentSendBytesPerSecond:F0} B/s"); sb.AppendLine($"Stable: {(IsStable ? "Yes" : "No")}"); sb.AppendLine($"Outages: {OutageWindows.Count}"); foreach (var o in OutageWindows) { var status = o.IsOngoing ? " (ONGOING)" : string.Empty; sb.AppendLine($" {o.Start:HH:mm:ss} - {o.End:HH:mm:ss} ({o.Duration.TotalSeconds:F0}s){status}"); } return sb.ToString(); } public string GenerateHtmlReport(string title = "Network Health Report") { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine($"{HtmlEncode(title)}"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("
"); sb.AppendLine($"

{HtmlEncode(title)}

"); // Status banner if (TotalSamples > 0) { var bannerClass = IsStable ? "status-stable" : "status-unstable"; var bannerText = IsStable ? "✔ Network is Stable" : "⚠ Network is Unstable"; sb.AppendLine($"
{bannerText}
"); } // Summary cards var uptimeClass = UptimePercentage >= 99 ? "ok" : UptimePercentage >= 95 ? "warn" : "error"; var latencyClass = AverageLatencyMs < 50 ? "ok" : AverageLatencyMs < 150 ? "warn" : "error"; var jitterClass = JitterMs < 20 ? "ok" : JitterMs < 50 ? "warn" : "error"; sb.AppendLine("
"); sb.AppendLine($"
Target
{HtmlEncode(TargetHost)}
"); sb.AppendLine($"
Samples
{TotalSamples}
"); sb.AppendLine($"
Uptime
{UptimePercentage:F1}%
"); sb.AppendLine($"
Avg Latency
{AverageLatencyMs:F1} ms
"); sb.AppendLine($"
Jitter
{JitterMs:F1} ms
"); sb.AppendLine($"
0 ? "error" : "ok")}\">
Failed Pings
{FailedPings}
"); sb.AppendLine("
"); // Uptime progress bar sb.AppendLine("

Uptime

"); var barColor = UptimePercentage >= 99 ? "#27ae60" : UptimePercentage >= 95 ? "#f39c12" : "#e74c3c"; var barWidth = Math.Max(0, Math.Min(100, UptimePercentage)); sb.AppendLine($"
{UptimePercentage:F2}%
"); // Latency / Bandwidth sb.AppendLine("

Performance

"); sb.AppendLine("
"); sb.AppendLine($"
Min Latency
{MinLatencyMs} ms
"); sb.AppendLine($"
Max Latency
{MaxLatencyMs} ms
"); sb.AppendLine($"
Recv Bandwidth
{FormatBytes(CurrentReceiveBytesPerSecond)}/s
"); sb.AppendLine($"
Send Bandwidth
{FormatBytes(CurrentSendBytesPerSecond)}/s
"); sb.AppendLine("
"); // Outage windows sb.AppendLine("

Outage History

"); if (OutageWindows.Count == 0) { sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("

No outages detected.

"); sb.AppendLine("
"); } else { sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); for (int i = 0; i < OutageWindows.Count; i++) { var o = OutageWindows[i]; var rowClass = o.IsOngoing ? "ongoing" : ""; var statusText = o.IsOngoing ? "ONGOING" : "Recovered"; sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine($""); sb.AppendLine(""); } sb.AppendLine(""); sb.AppendLine("
#Start (UTC)End (UTC)DurationStatus
{i + 1}{o.Start:yyyy-MM-dd HH:mm:ss}{(o.IsOngoing ? "-" : o.End.ToString("yyyy-MM-dd HH:mm:ss"))}{o.Duration.TotalSeconds:F0}s{statusText}
"); } sb.AppendLine($"
Generated at {GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC — EonaCat.Connections
"); sb.AppendLine("
"); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } public void SaveHtmlReport(string filePath, string title = "Network Health Report") { var html = GenerateHtmlReport(title); File.WriteAllText(filePath, html, Encoding.UTF8); } private static string FormatBytes(double bytes) { if (bytes >= 1_073_741_824) { return $"{bytes / 1_073_741_824:F2} GB"; } if (bytes >= 1_048_576) { return $"{bytes / 1_048_576:F2} MB"; } if (bytes >= 1024) { return $"{bytes / 1024:F2} KB"; } return $"{bytes:F0} B"; } private static string HtmlEncode(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } return value .Replace("&", "&") .Replace("<", "<") .Replace(">", ">") .Replace("\"", """) .Replace("'", "'"); } } }