using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using RmmAgent.Models; using System.Diagnostics; using System.Runtime.InteropServices; namespace RmmAgent.Services; #pragma warning disable CA1416 public class HeartbeatService { private readonly ILogger _logger; private readonly ApiClient _api; private readonly IConfiguration _config; private readonly PerformanceCounter? _cpuCounter; private readonly PerformanceCounter? _ramAvailableCounter; private readonly long _ramTotalKb; private readonly Queue _offlineBuffer = new(); private readonly int _maxBufferPoints; private readonly object _bufferLock = new(); public HeartbeatService(ILogger logger, ApiClient api, IConfiguration config) { _logger = logger; _api = api; _config = config; _maxBufferPoints = config.GetValue("Rmm:OfflineBufferMaxPoints", 200); try { _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); _ramAvailableCounter = new PerformanceCounter("Memory", "Available KBytes"); _cpuCounter.NextValue(); } catch (Exception ex) { _logger.LogWarning(ex, "PerfCounter init failed"); } _ramTotalKb = GetTotalRamKb(); } public async Task SendAsync(CancellationToken ct) { var point = CapturePoint(); try { HeartbeatPoint[]? batch = null; lock (_bufferLock) { if (_offlineBuffer.Count > 0) { batch = _offlineBuffer.ToArray(); _offlineBuffer.Clear(); } } if (batch is { Length: > 0 }) _logger.LogInformation("Flushing {Count} buffered heartbeats", batch.Length); var req = new HeartbeatRequest( CpuPercent: point.CpuPercent, RamPercent: point.RamPercent, DiskPercent: point.DiskPercent, Uptime: point.Uptime, IdleSeconds: point.IdleSeconds, Batch: batch); var resp = await _api.SendHeartbeatAsync(req, ct); return resp?.Commands ?? Array.Empty(); } catch (Exception ex) { _logger.LogWarning("Heartbeat failed (offline?): {Msg} - buffering", ex.Message); lock (_bufferLock) { if (_offlineBuffer.Count >= _maxBufferPoints) _offlineBuffer.Dequeue(); _offlineBuffer.Enqueue(point); } return Array.Empty(); } } private HeartbeatPoint CapturePoint() { var cpu = _cpuCounter?.NextValue() ?? 0; var ramAvail = _ramAvailableCounter?.NextValue() ?? 0; var ramPct = _ramTotalKb > 0 ? (1.0 - (ramAvail / _ramTotalKb)) * 100 : 0; double? diskPct = null; try { var sysDrive = new DriveInfo(Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\"); if (sysDrive.IsReady && sysDrive.TotalSize > 0) diskPct = (1.0 - ((double)sysDrive.AvailableFreeSpace / sysDrive.TotalSize)) * 100; } catch { } var uptime = (int)(Environment.TickCount64 / 1000); var idle = GetIdleSeconds(); return new HeartbeatPoint( CpuPercent: Math.Round(cpu, 1), RamPercent: Math.Round(ramPct, 1), DiskPercent: diskPct.HasValue ? Math.Round(diskPct.Value, 1) : null, Uptime: uptime, IdleSeconds: idle, Timestamp: DateTime.UtcNow.ToString("o")); } [StructLayout(LayoutKind.Sequential)] private struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } [DllImport("user32.dll")] private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); private static int? GetIdleSeconds() { try { var info = new LASTINPUTINFO { cbSize = (uint)Marshal.SizeOf() }; if (!GetLastInputInfo(ref info)) return null; return (int)((Environment.TickCount - info.dwTime) / 1000); } catch { return null; } } private static long GetTotalRamKb() { try { using var searcher = new System.Management.ManagementObjectSearcher("SELECT TotalVisibleMemorySize FROM Win32_OperatingSystem"); foreach (var obj in searcher.Get()) return Convert.ToInt64(obj["TotalVisibleMemorySize"]); } catch { } return 0; } }