From c683bfc41d72c7e239c530a1cecb990e496ae085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sun, 15 Mar 2026 08:50:56 +0100 Subject: [PATCH 01/30] Add RefreshPolicy and Refresh() to IProbe interface Each probe now declares its refresh policy via GetRefreshPolicy(): - LinGeneralAnalyzer: 1h interval, proactive (log data can roll over) - SmartHealthAnalyzer: 4h interval, not proactive (data persists) - WinGeneralAnalyzer: 1h interval, proactive (WMI status transient) Probes collect data in Refresh() and cache results internally. GetStatus() and GetRawData() return cached data without I/O. LinGeneralAnalyzer no longer collects in constructor. Update IProbeMock and SmartHealthAnalyzer tests accordingly. --- src/agent/IProbe.h | 14 +++++++++ src/agent/SmartHealthAnalyzer.cpp | 34 ++++++++++++++++++++-- src/agent/SmartHealthAnalyzer.h | 10 +++++++ src/agent/linux/LinGeneralAnalyzer.cpp | 9 ++++-- src/agent/linux/LinGeneralAnalyzer.h | 5 ++-- src/agent/main.cpp | 4 +++ src/agent/windows/WinGeneralAnalyzer.cpp | 20 +++++++++++-- src/agent/windows/WinGeneralAnalyzer.h | 10 +++++++ tests/agent/IProbeMock.h | 2 ++ tests/agent/SmartHealthAnalyzerTests.cpp | 37 ++++++++++++++++++++++++ 10 files changed, 136 insertions(+), 9 deletions(-) diff --git a/src/agent/IProbe.h b/src/agent/IProbe.h index 1c253648..310d8c09 100644 --- a/src/agent/IProbe.h +++ b/src/agent/IProbe.h @@ -1,15 +1,29 @@ #pragma once +#include +#include + #include #include "common/GeneralHealth.h" #include "agent/Disk.h" +struct RefreshPolicy +{ + std::chrono::seconds interval{0}; + bool proactiveCollection = false; +}; + + class IProbe { public: virtual ~IProbe() = default; + + virtual RefreshPolicy GetRefreshPolicy() const = 0; + virtual void Refresh(const std::vector& disks) = 0; + virtual GeneralHealth::Health GetStatus(const Disk& _disk) = 0; virtual nlohmann::json GetRawData(const Disk& _disk) = 0; }; \ No newline at end of file diff --git a/src/agent/SmartHealthAnalyzer.cpp b/src/agent/SmartHealthAnalyzer.cpp index cd355e19..59418dc6 100644 --- a/src/agent/SmartHealthAnalyzer.cpp +++ b/src/agent/SmartHealthAnalyzer.cpp @@ -59,6 +59,22 @@ SmartHealthAnalyzer::SmartHealthAnalyzer(std::unique_ptr reader) SmartHealthAnalyzer::~SmartHealthAnalyzer() = default; +RefreshPolicy SmartHealthAnalyzer::GetRefreshPolicy() const +{ + return {std::chrono::hours(4), false}; +} + + +void SmartHealthAnalyzer::Refresh(const std::vector& disks) +{ + for (const auto& disk : disks) + { + m_cachedSmartData[disk.GetDeviceId()] = m_reader->ReadSMARTData(disk); + m_cachedTestStatus[disk.GetDeviceId()] = m_reader->ReadTestStatus(disk); + } +} + + const IVendorProfile& SmartHealthAnalyzer::profileFor(const std::string& vendor) { static const GenericProfile generic; @@ -76,7 +92,11 @@ const IVendorProfile& SmartHealthAnalyzer::profileFor(const std::string& vendor) GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) { - const auto smart = m_reader->ReadSMARTData(disk); + auto it = m_cachedSmartData.find(disk.GetDeviceId()); + if (it == m_cachedSmartData.end()) + return GeneralHealth::UNKNOWN; + + const auto& smart = it->second; const auto& profile = profileFor(disk.GetVendor()); auto worst = GeneralHealth::GOOD; @@ -128,7 +148,11 @@ GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk) { - const auto smart = m_reader->ReadSMARTData(disk); + auto it = m_cachedSmartData.find(disk.GetDeviceId()); + if (it == m_cachedSmartData.end()) + return nlohmann::json{{"type", "smart"}, {"attributes", nlohmann::json::array()}}; + + const auto& smart = it->second; nlohmann::json attrs = nlohmann::json::array(); for (const auto& attr : smart.attributes) @@ -145,7 +169,11 @@ nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk) auto j = nlohmann::json{{"type", "smart"}, {"attributes", attrs}}; - const auto testStatus = m_reader->ReadTestStatus(disk); + auto testIt = m_cachedTestStatus.find(disk.GetDeviceId()); + SmartTestStatus testStatus; + if (testIt != m_cachedTestStatus.end()) + testStatus = testIt->second; + j["selfTestStatus"] = { {"running", testStatus.running}, {"percentRemaining", testStatus.percentRemaining}, diff --git a/src/agent/SmartHealthAnalyzer.h b/src/agent/SmartHealthAnalyzer.h index 10476a2c..fa92b444 100644 --- a/src/agent/SmartHealthAnalyzer.h +++ b/src/agent/SmartHealthAnalyzer.h @@ -3,7 +3,11 @@ #include "IProbe.h" #include "ISmartReader.h" +#include #include +#include + +#include "common/SmartData.h" class IVendorProfile; @@ -13,11 +17,17 @@ class SmartHealthAnalyzer : public IProbe explicit SmartHealthAnalyzer(std::unique_ptr reader); ~SmartHealthAnalyzer() override; + RefreshPolicy GetRefreshPolicy() const override; + void Refresh(const std::vector& disks) override; + GeneralHealth::Health GetStatus(const Disk& disk) override; nlohmann::json GetRawData(const Disk& disk) override; private: std::unique_ptr m_reader; + std::map m_cachedSmartData; + std::map m_cachedTestStatus; + static const IVendorProfile& profileFor(const std::string& vendor); }; diff --git a/src/agent/linux/LinGeneralAnalyzer.cpp b/src/agent/linux/LinGeneralAnalyzer.cpp index 24baf2b9..0e888570 100644 --- a/src/agent/linux/LinGeneralAnalyzer.cpp +++ b/src/agent/linux/LinGeneralAnalyzer.cpp @@ -12,7 +12,12 @@ LinGeneralAnalyzer::LinGeneralAnalyzer(std::shared_ptr manager) : m_partitionsManager(manager) { - refreshState(); +} + + +RefreshPolicy LinGeneralAnalyzer::GetRefreshPolicy() const +{ + return {std::chrono::hours(1), true}; } @@ -47,7 +52,7 @@ nlohmann::json LinGeneralAnalyzer::GetRawData(const Disk& disk) } -void LinGeneralAnalyzer::refreshState() +void LinGeneralAnalyzer::Refresh(const std::vector&) { std::string output; std::array buffer; diff --git a/src/agent/linux/LinGeneralAnalyzer.h b/src/agent/linux/LinGeneralAnalyzer.h index 35a89058..db9cdd44 100644 --- a/src/agent/linux/LinGeneralAnalyzer.h +++ b/src/agent/linux/LinGeneralAnalyzer.h @@ -16,12 +16,13 @@ class LinGeneralAnalyzer : public IProbe public: LinGeneralAnalyzer(std::shared_ptr); + RefreshPolicy GetRefreshPolicy() const override; + void Refresh(const std::vector& disks) override; + GeneralHealth::Health GetStatus(const Disk& disk) override; nlohmann::json GetRawData(const Disk& disk) override; private: std::map> m_errors; std::shared_ptr m_partitionsManager; - - void refreshState(); }; diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 0f3de7de..393d64ce 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -84,6 +84,10 @@ void collectAndPublish(HttpServer& server, SystemUtilitiesFactory& factory) auto diskCollection = diskCollector->GetDisksList(); const auto probes = factory.getProbes(); + // Refresh all probes before reading cached data + for (const auto& probe : probes) + probe->Refresh(diskCollection); + DiscStatusCalculator calc; std::vector diskInfos; diff --git a/src/agent/windows/WinGeneralAnalyzer.cpp b/src/agent/windows/WinGeneralAnalyzer.cpp index 7bb3c32f..dbad016a 100644 --- a/src/agent/windows/WinGeneralAnalyzer.cpp +++ b/src/agent/windows/WinGeneralAnalyzer.cpp @@ -2,10 +2,26 @@ #include "CMDCommunication.h" -GeneralHealth::Health WinGeneralAnalyzer::GetStatus(const Disk& _disk) +RefreshPolicy WinGeneralAnalyzer::GetRefreshPolicy() const +{ + return {std::chrono::hours(1), true}; +} + + +void WinGeneralAnalyzer::Refresh(const std::vector& disks) { CMDCommunication reader; - return reader.CollectDiskStatus(_disk); + for (const auto& disk : disks) + m_cachedStatus[disk.GetDeviceId()] = reader.CollectDiskStatus(disk); +} + + +GeneralHealth::Health WinGeneralAnalyzer::GetStatus(const Disk& _disk) +{ + auto it = m_cachedStatus.find(_disk.GetDeviceId()); + if (it != m_cachedStatus.end()) + return it->second; + return GeneralHealth::UNKNOWN; } nlohmann::json WinGeneralAnalyzer::GetRawData(const Disk& _disk) diff --git a/src/agent/windows/WinGeneralAnalyzer.h b/src/agent/windows/WinGeneralAnalyzer.h index f310323f..0f1205a9 100644 --- a/src/agent/windows/WinGeneralAnalyzer.h +++ b/src/agent/windows/WinGeneralAnalyzer.h @@ -1,9 +1,19 @@ #pragma once + +#include +#include + #include "../IProbe.h" class WinGeneralAnalyzer : public IProbe { public: + RefreshPolicy GetRefreshPolicy() const override; + void Refresh(const std::vector& disks) override; + GeneralHealth::Health GetStatus(const Disk& _disk) override; nlohmann::json GetRawData(const Disk& _disk) override; + +private: + std::map m_cachedStatus; }; diff --git a/tests/agent/IProbeMock.h b/tests/agent/IProbeMock.h index 9f2ef7ec..2266bea9 100644 --- a/tests/agent/IProbeMock.h +++ b/tests/agent/IProbeMock.h @@ -7,6 +7,8 @@ class IProbeMock : public IProbe { public: + MOCK_METHOD(RefreshPolicy, GetRefreshPolicy, (), (const, override)); + MOCK_METHOD(void, Refresh, (const std::vector&), (override)); MOCK_METHOD(GeneralHealth::Health, GetStatus, (const Disk&), (override)); MOCK_METHOD(nlohmann::json, GetRawData, (const Disk&), (override)); }; \ No newline at end of file diff --git a/tests/agent/SmartHealthAnalyzerTests.cpp b/tests/agent/SmartHealthAnalyzerTests.cpp index 7923979c..e1830ae7 100644 --- a/tests/agent/SmartHealthAnalyzerTests.cpp +++ b/tests/agent/SmartHealthAnalyzerTests.cpp @@ -44,6 +44,8 @@ TEST_F(SmartHealthAnalyzerTest, AllHealthyAttributesReturnGood) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } @@ -58,6 +60,8 @@ TEST_F(SmartHealthAnalyzerTest, ValueBelowThresholdReturnsBad) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::BAD, m_analyzer->GetStatus(m_disk)); } @@ -71,6 +75,8 @@ TEST_F(SmartHealthAnalyzerTest, ValueEqualToThresholdReturnsBad) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::BAD, m_analyzer->GetStatus(m_disk)); } @@ -84,6 +90,8 @@ TEST_F(SmartHealthAnalyzerTest, CriticalAttributeWithNonZeroRawReturnsCheckStatu }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::CHECK_STATUS, m_analyzer->GetStatus(m_disk)); } @@ -98,6 +106,8 @@ TEST_F(SmartHealthAnalyzerTest, ProximityToThresholdReturnsCheckStatus) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::CHECK_STATUS, m_analyzer->GetStatus(m_disk)); } @@ -111,6 +121,8 @@ TEST_F(SmartHealthAnalyzerTest, ValueWellAboveThresholdIsGood) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } @@ -129,6 +141,8 @@ TEST_F(SmartHealthAnalyzerTest, SamsungProfileMasksRawReadErrorRate) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({samsungDisk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(samsungDisk)); } @@ -145,6 +159,8 @@ TEST_F(SmartHealthAnalyzerTest, SamsungProfileDetectsRealErrors) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({samsungDisk}); // ID 1 is not in the critical list, so non-zero raw alone doesn't trigger EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(samsungDisk)); @@ -162,6 +178,8 @@ TEST_F(SmartHealthAnalyzerTest, SeagateProfileMasksSeekErrorRate) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } @@ -179,6 +197,8 @@ TEST_F(SmartHealthAnalyzerTest, UnknownVendorUsesGenericProfile) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::CHECK_STATUS, m_analyzer->GetStatus(m_disk)); } @@ -194,6 +214,7 @@ TEST_F(SmartHealthAnalyzerTest, GetRawDataReturnsSmartJsonFormat) EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); auto json = m_analyzer->GetRawData(m_disk); @@ -220,6 +241,8 @@ TEST_F(SmartHealthAnalyzerTest, EmptySmartDataReturnsGood) SmartData data; // no attributes EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } @@ -234,6 +257,8 @@ TEST_F(SmartHealthAnalyzerTest, ZeroThresholdSkipsThresholdChecks) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } @@ -250,6 +275,8 @@ TEST_F(SmartHealthAnalyzerTest, NvmeCriticalWarningNonZeroReturnsBad) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::BAD, m_analyzer->GetStatus(m_disk)); } @@ -264,6 +291,8 @@ TEST_F(SmartHealthAnalyzerTest, NvmeMediaIntegrityErrorsReturnsBad) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::BAD, m_analyzer->GetStatus(m_disk)); } @@ -278,6 +307,8 @@ TEST_F(SmartHealthAnalyzerTest, NvmePercentageUsedHighReturnsCheckStatus) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::CHECK_STATUS, m_analyzer->GetStatus(m_disk)); } @@ -292,6 +323,8 @@ TEST_F(SmartHealthAnalyzerTest, NvmePercentageUsed100ReturnsBad) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::BAD, m_analyzer->GetStatus(m_disk)); } @@ -306,6 +339,8 @@ TEST_F(SmartHealthAnalyzerTest, NvmeAvailableSpareThresholdBreachReturnsBad) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::BAD, m_analyzer->GetStatus(m_disk)); } @@ -324,6 +359,8 @@ TEST_F(SmartHealthAnalyzerTest, NvmeHealthyReturnGood) }; EXPECT_CALL(*m_reader, ReadSMARTData(_)).WillOnce(Return(data)); + EXPECT_CALL(*m_reader, ReadTestStatus(_)).WillOnce(Return(SmartTestStatus{})); + m_analyzer->Refresh({m_disk}); EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } From 6edce9e15846ebd9eb9adf78fbbe59262f83b5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sun, 15 Mar 2026 08:54:55 +0100 Subject: [PATCH 02/30] Add per-probe refresh orchestration and SSE connect callback Replace monolithic collectAndPublish with per-probe staleness tracking: - refreshStaleProbes: only refreshes probes past their declared interval - publishFromCache: builds status from cached probe data without I/O - refreshAllProbes: used by POST /refresh for manual force-refresh Agent startup refreshes only proactive probes (log data). Non-proactive probes (SMART) stay uncollected until a monitor connects. SSE client connection triggers conditional refresh of stale probes via new HttpServer::setOnClientConnectedCallback. Background thread checks proactive probes every minute and pushes SSE updates when data changes. Graceful shutdown: signal handler sets atomic flag, background thread joins after server.listen() returns. --- src/agent/HttpServer.cpp | 11 +++ src/agent/HttpServer.h | 3 + src/agent/main.cpp | 164 ++++++++++++++++++++++++++++++--------- 3 files changed, 143 insertions(+), 35 deletions(-) diff --git a/src/agent/HttpServer.cpp b/src/agent/HttpServer.cpp index 5dd03666..4923c73f 100644 --- a/src/agent/HttpServer.cpp +++ b/src/agent/HttpServer.cpp @@ -40,6 +40,7 @@ struct HttpServer::Impl std::string lastRefreshed; std::function refreshCallback; + std::function onClientConnectedCallback; // Refresh cooldown static constexpr int RefreshCooldownSeconds = 600; // 10 minutes @@ -190,6 +191,10 @@ HttpServer::HttpServer(const std::string& agentName, unsigned int port) m_impl->sseClients.push_back(client); } + // Notify that a new monitor connected — may trigger conditional refresh + if (m_impl->onClientConnectedCallback) + m_impl->onClientConnectedCallback(); + res.set_header("Cache-Control", "no-cache"); res.set_header("Connection", "keep-alive"); @@ -269,3 +274,9 @@ void HttpServer::setRefreshCallback(std::function cb) { m_impl->refreshCallback = std::move(cb); } + + +void HttpServer::setOnClientConnectedCallback(std::function cb) +{ + m_impl->onClientConnectedCallback = std::move(cb); +} diff --git a/src/agent/HttpServer.h b/src/agent/HttpServer.h index 56dad081..df155e93 100644 --- a/src/agent/HttpServer.h +++ b/src/agent/HttpServer.h @@ -25,6 +25,9 @@ class HttpServer // Called to trigger data refresh; the callback does collection and calls setStatusData void setRefreshCallback(std::function cb); + // Called when a new SSE client connects + void setOnClientConnectedCallback(std::function cb); + private: struct Impl; std::unique_ptr m_impl; diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 393d64ce..792bdcfb 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -1,8 +1,13 @@ #include +#include +#include +#include #include #include #include +#include #include +#include #include "common/constants.hpp" #include "common/DiskSummary.h" @@ -15,13 +20,23 @@ namespace { HttpServer* g_server = nullptr; + std::atomic g_running{true}; + std::mutex g_bgMutex; + std::condition_variable g_bgCv; void signalHandler(int) { + g_running = false; if (g_server) g_server->stop(); } + struct ProbeEntry + { + std::unique_ptr probe; + std::chrono::steady_clock::time_point lastRefresh{}; + }; + DiskSummary buildSummary(const Disk& disk, const std::vector& probeStatuses) { DiskSummary summary; @@ -75,49 +90,85 @@ namespace return summary; } -} + // Returns true if any probe was refreshed + bool refreshStaleProbes(std::vector& entries, + const std::vector& disks, + bool proactiveOnly = false) + { + bool anyRefreshed = false; + const auto now = std::chrono::steady_clock::now(); -void collectAndPublish(HttpServer& server, SystemUtilitiesFactory& factory) -{ - auto diskCollector = factory.diskCollector(); - auto diskCollection = diskCollector->GetDisksList(); - const auto probes = factory.getProbes(); + for (auto& entry : entries) + { + const auto policy = entry.probe->GetRefreshPolicy(); - // Refresh all probes before reading cached data - for (const auto& probe : probes) - probe->Refresh(diskCollection); + if (proactiveOnly && !policy.proactiveCollection) + continue; - DiscStatusCalculator calc; - std::vector diskInfos; + const auto elapsed = std::chrono::duration_cast( + now - entry.lastRefresh); - for (const auto& disk : diskCollection) + if (elapsed >= policy.interval) + { + entry.probe->Refresh(disks); + entry.lastRefresh = now; + anyRefreshed = true; + } + } + + return anyRefreshed; + } + + void refreshAllProbes(std::vector& entries, + const std::vector& disks) { - std::vector probeStatuses; - probeStatuses.reserve(probes.size()); + const auto now = std::chrono::steady_clock::now(); + for (auto& entry : entries) + { + entry.probe->Refresh(disks); + entry.lastRefresh = now; + } + } + + void publishFromCache(HttpServer& server, + const std::vector& entries, + const std::vector& disks) + { + DiscStatusCalculator calc; + std::vector diskInfos; - for (const auto& probe : probes) + for (const auto& disk : disks) { - ProbeStatus status; - status.health = probe->GetStatus(disk); - status.rawData = probe->GetRawData(disk); - probeStatuses.push_back(status); + std::vector probeStatuses; + std::vector healthStatuses; + + for (const auto& entry : entries) + { + ProbeStatus status; + status.health = entry.probe->GetStatus(disk); + status.rawData = entry.probe->GetRawData(disk); + probeStatuses.push_back(status); + healthStatuses.push_back(status.health); + } + + DiskInfo info; + info.SetName(disk.GetDeviceId()); + info.SetHealth(calc.CalculateCumulativeStatus(healthStatuses)); + info.SetProbesStatuses(probeStatuses); + info.SetSummary(buildSummary(disk, probeStatuses)); + diskInfos.push_back(info); } - DiskInfo info; - info.SetName(disk.GetDeviceId()); - info.SetHealth(calc.CalculateDiskStatus(disk, probes)); - info.SetProbesStatuses(probeStatuses); - info.SetSummary(buildSummary(disk, probeStatuses)); - diskInfos.push_back(info); - } + std::vector statuses; + std::transform(diskInfos.begin(), diskInfos.end(), std::back_inserter(statuses), + [](const auto& di) { return di.GetHealth(); }); - std::vector statuses; - std::transform(diskInfos.begin(), diskInfos.end(), std::back_inserter(statuses), - [](const auto& di) { return di.GetHealth(); }); + auto overall = calc.CalculateCumulativeStatus(statuses); + server.setStatusData(overall, std::move(diskInfos)); + } - auto overall = calc.CalculateCumulativeStatus(statuses); - server.setStatusData(overall, std::move(diskInfos)); + std::mutex g_probeMutex; } @@ -152,15 +203,35 @@ int main(int argc, char** argv) for (const auto& disk : disks) std::cout << " " << disk.GetDeviceId() << '\n'; + // Create persistent probes + auto probeUptrs = systemUtilsFactory.getProbes(); + std::vector probeEntries; + for (auto& p : probeUptrs) + probeEntries.push_back({std::move(p), {}}); + // Create HTTP server HttpServer server(agentName, RDHMPort); - server.setRefreshCallback([&server, &systemUtilsFactory] { - collectAndPublish(server, systemUtilsFactory); + // POST /api/v1/refresh — force refresh all probes (manual trigger) + server.setRefreshCallback([&server, &probeEntries, &disks] { + std::lock_guard lock(g_probeMutex); + refreshAllProbes(probeEntries, disks); + publishFromCache(server, probeEntries, disks); + }); + + // SSE client connected — refresh stale probes + server.setOnClientConnectedCallback([&server, &probeEntries, &disks] { + std::lock_guard lock(g_probeMutex); + if (refreshStaleProbes(probeEntries, disks)) + publishFromCache(server, probeEntries, disks); }); - // Initial data collection - collectAndPublish(server, systemUtilsFactory); + // Initial proactive data collection + { + std::lock_guard lock(g_probeMutex); + refreshStaleProbes(probeEntries, disks, true); + publishFromCache(server, probeEntries, disks); + } // Start mDNS publisher MdnsPublisher mdns(agentName, ZeroConfServiceName, RDHMPort); @@ -173,10 +244,33 @@ int main(int argc, char** argv) std::cout << "Agent '" << agentName << "' listening on 0.0.0.0:" << RDHMPort << "\n"; + // Background thread for proactive probes + std::thread bgThread([&server, &probeEntries, &disks] { + while (g_running) + { + { + std::unique_lock lock(g_bgMutex); + g_bgCv.wait_for(lock, std::chrono::minutes(1), + [] { return !g_running.load(); }); + } + + if (!g_running) + break; + + std::lock_guard lock(g_probeMutex); + if (refreshStaleProbes(probeEntries, disks, true)) + publishFromCache(server, probeEntries, disks); + } + }); + // Blocking — runs the HTTP server server.listen(); // Cleanup + g_running = false; + g_bgCv.notify_all(); + bgThread.join(); + mdns.stop(); g_server = nullptr; From e8042d389aa1500b57ff112f94be1a5ee9e40ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sun, 15 Mar 2026 08:56:06 +0100 Subject: [PATCH 03/30] Use journalctl with cursor-file for incremental kernel log reading LinGeneralAnalyzer now detects journalctl at construction time. When available, Refresh() runs journalctl -k --cursor-file instead of dmesg. journalctl with --cursor-file reads only new entries since last cursor, enabling incremental error accumulation without re-reading the full kernel ring buffer. The cursor file at /tmp/rdhm-journal-cursor is naturally cleaned on reboot, matching -k (current boot) semantics. Falls back to popen(dmesg) on systems without systemd. --- src/agent/linux/LinGeneralAnalyzer.cpp | 28 ++++++++++++++++++++++++-- src/agent/linux/LinGeneralAnalyzer.h | 1 + 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/agent/linux/LinGeneralAnalyzer.cpp b/src/agent/linux/LinGeneralAnalyzer.cpp index 0e888570..12a9a128 100644 --- a/src/agent/linux/LinGeneralAnalyzer.cpp +++ b/src/agent/linux/LinGeneralAnalyzer.cpp @@ -12,6 +12,13 @@ LinGeneralAnalyzer::LinGeneralAnalyzer(std::shared_ptr manager) : m_partitionsManager(manager) { + FILE* pipe = popen("which journalctl", "r"); + if (pipe) + { + char buf[256]; + m_useJournalctl = (fgets(buf, sizeof(buf), pipe) != nullptr); + pclose(pipe); + } } @@ -57,7 +64,11 @@ void LinGeneralAnalyzer::Refresh(const std::vector&) std::string output; std::array buffer; - FILE* pipe = popen("dmesg", "r"); + const char* cmd = m_useJournalctl + ? "journalctl -k --cursor-file=/tmp/rdhm-journal-cursor --no-pager -q --output=short" + : "dmesg"; + + FILE* pipe = popen(cmd, "r"); if (pipe) { while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) @@ -65,5 +76,18 @@ void LinGeneralAnalyzer::Refresh(const std::vector&) pclose(pipe); } - m_errors = DmesgParser::parse(output, *m_partitionsManager); + auto newErrors = DmesgParser::parse(output, *m_partitionsManager); + + if (m_useJournalctl) + { + // journalctl with cursor-file returns only new entries; + // accumulate into existing errors + for (auto& [disk, errors] : newErrors) + m_errors[disk].merge(std::move(errors)); + } + else + { + // dmesg reads the full ring buffer; replace entirely + m_errors = std::move(newErrors); + } } diff --git a/src/agent/linux/LinGeneralAnalyzer.h b/src/agent/linux/LinGeneralAnalyzer.h index db9cdd44..33ca4001 100644 --- a/src/agent/linux/LinGeneralAnalyzer.h +++ b/src/agent/linux/LinGeneralAnalyzer.h @@ -25,4 +25,5 @@ class LinGeneralAnalyzer : public IProbe private: std::map> m_errors; std::shared_ptr m_partitionsManager; + bool m_useJournalctl = false; }; From aaa38472a0ce7d5014ccc5094187657750e645b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sun, 15 Mar 2026 08:57:30 +0100 Subject: [PATCH 04/30] Remove automatic POST /refresh from monitor observe flow Monitor no longer triggers a forced refresh on every observe(). Instead, connecting the SSE stream is sufficient: the agent-side SSE handler conditionally refreshes stale probes and pushes a statusChanged event as the first message. This avoids redundant refreshes when the monitor reconnects frequently. POST /api/v1/refresh remains available for manual user-triggered refresh via the UI button. --- src/monitor/AgentsStatusProvider.cpp | 46 ++++------------------------ src/monitor/AgentsStatusProvider.hpp | 1 - 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/src/monitor/AgentsStatusProvider.cpp b/src/monitor/AgentsStatusProvider.cpp index 7e3bd462..6c91b5db 100644 --- a/src/monitor/AgentsStatusProvider.cpp +++ b/src/monitor/AgentsStatusProvider.cpp @@ -52,8 +52,12 @@ void AgentsStatusProvider::observe(const AgentInformation& info) m_connections.insert(info, std::move(agentConn)); - // Fetch initial status, then trigger refresh, then connect SSE - fetchInitialStatus(info); + emit connectionStateChanged(info, ConnectionState::Connecting); + + // SSE connect triggers agent-side conditional refresh; + // the first SSE event carries initial status data + connectSse(info); + fetchAgentInfo(info); } @@ -70,44 +74,6 @@ void AgentsStatusProvider::unobserve(const AgentInformation& info) } -void AgentsStatusProvider::fetchInitialStatus(const AgentInformation& info) -{ - emit connectionStateChanged(info, ConnectionState::Connecting); - - const QUrl url = QStringLiteral("http://%1:%2/api/v1/refresh") - .arg(formatHost(info.host())) - .arg(info.port()); - - QNetworkRequest req(url); - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QNetworkReply* reply = m_nam.post(req, QByteArray()); - - QPointer self(this); - QObject::connect(reply, &QNetworkReply::finished, this, [self, info, reply]() { - reply->deleteLater(); - - if (!self) - return; - - if (reply->error() == QNetworkReply::NoError) - { - const QByteArray body = reply->readAll(); - self->parseStatusJson(info, std::string_view{body.constData(), static_cast(body.size())}); - emit self->connectionStateChanged(info, ConnectionState::Connected); - } - else - { - std::cerr << "Failed to fetch status from " << info.name().toStdString() - << ": " << reply->errorString().toStdString() << "\n"; - emit self->connectionStateChanged(info, ConnectionState::Error); - } - - self->connectSse(info); - self->fetchAgentInfo(info); - }); -} - - void AgentsStatusProvider::parseStatusJson(const AgentInformation& info, std::string_view json) { try diff --git a/src/monitor/AgentsStatusProvider.hpp b/src/monitor/AgentsStatusProvider.hpp index bf2f6a03..cab2dfc5 100644 --- a/src/monitor/AgentsStatusProvider.hpp +++ b/src/monitor/AgentsStatusProvider.hpp @@ -43,7 +43,6 @@ class AgentsStatusProvider: public IAgentsStatusProvider QHash m_connections; QTimer m_watchdog; - void fetchInitialStatus(const AgentInformation& info); void fetchAgentInfo(const AgentInformation& info); void connectSse(const AgentInformation& info); void handleSseEvent(const AgentInformation& info, const cpp_restapi::SseEvent& event); From 2d94b0668b875cf6bc0c7288f2baff4661f48db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:37:33 +0200 Subject: [PATCH 05/30] Always publish cached status to new SSE clients --- src/agent/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 792bdcfb..5eb24bb0 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -219,11 +219,11 @@ int main(int argc, char** argv) publishFromCache(server, probeEntries, disks); }); - // SSE client connected — refresh stale probes + // SSE client connected — refresh stale probes and always send current state server.setOnClientConnectedCallback([&server, &probeEntries, &disks] { std::lock_guard lock(g_probeMutex); - if (refreshStaleProbes(probeEntries, disks)) - publishFromCache(server, probeEntries, disks); + refreshStaleProbes(probeEntries, disks); + publishFromCache(server, probeEntries, disks); }); // Initial proactive data collection From d102ba47655b99ac59b2a5bf20d5bbb841ef2478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:38:14 +0200 Subject: [PATCH 06/30] Make SSE connect callback non-blocking by deferring refresh to background thread --- src/agent/main.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 5eb24bb0..51c265d5 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -219,11 +219,14 @@ int main(int argc, char** argv) publishFromCache(server, probeEntries, disks); }); - // SSE client connected — refresh stale probes and always send current state + // SSE client connected — immediately send cached state, then wake + // background thread so stale probes get refreshed asynchronously server.setOnClientConnectedCallback([&server, &probeEntries, &disks] { - std::lock_guard lock(g_probeMutex); - refreshStaleProbes(probeEntries, disks); - publishFromCache(server, probeEntries, disks); + { + std::lock_guard lock(g_probeMutex); + publishFromCache(server, probeEntries, disks); + } + g_bgCv.notify_one(); }); // Initial proactive data collection @@ -244,7 +247,7 @@ int main(int argc, char** argv) std::cout << "Agent '" << agentName << "' listening on 0.0.0.0:" << RDHMPort << "\n"; - // Background thread for proactive probes + // Background thread for periodic and on-demand stale-probe refresh std::thread bgThread([&server, &probeEntries, &disks] { while (g_running) { @@ -258,7 +261,7 @@ int main(int argc, char** argv) break; std::lock_guard lock(g_probeMutex); - if (refreshStaleProbes(probeEntries, disks, true)) + if (refreshStaleProbes(probeEntries, disks)) publishFromCache(server, probeEntries, disks); } }); From 7476c9344e1d48f0126d077a36db995a44717b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:38:40 +0200 Subject: [PATCH 07/30] Use XDG_RUNTIME_DIR for journalctl cursor file instead of /tmp --- src/agent/linux/LinGeneralAnalyzer.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/agent/linux/LinGeneralAnalyzer.cpp b/src/agent/linux/LinGeneralAnalyzer.cpp index 12a9a128..0a4176ca 100644 --- a/src/agent/linux/LinGeneralAnalyzer.cpp +++ b/src/agent/linux/LinGeneralAnalyzer.cpp @@ -1,13 +1,27 @@ #include +#include #include #include +#include #include "common/GeneralHealth.h" #include "LinGeneralAnalyzer.h" #include "DmesgParser.h" #include "IPartitionsManager.h" +namespace +{ + std::string getCursorFilePath() + { + if (const char* runDir = std::getenv("XDG_RUNTIME_DIR")) + return std::string(runDir) + "/rdhm-journal-cursor"; + + return "/run/rdhm/journal-cursor"; + } +} + + LinGeneralAnalyzer::LinGeneralAnalyzer(std::shared_ptr manager) : m_partitionsManager(manager) @@ -64,8 +78,12 @@ void LinGeneralAnalyzer::Refresh(const std::vector&) std::string output; std::array buffer; + const auto cursorPath = getCursorFilePath(); + const auto journalCmd = "journalctl -k --cursor-file=" + cursorPath + + " --no-pager -q --output=short"; + const char* cmd = m_useJournalctl - ? "journalctl -k --cursor-file=/tmp/rdhm-journal-cursor --no-pager -q --output=short" + ? journalCmd.c_str() : "dmesg"; FILE* pipe = popen(cmd, "r"); From e62f25c2e5210cdf04f48fec3e81ac55e661a9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:38:54 +0200 Subject: [PATCH 08/30] Document intentional permanent error accumulation in journalctl mode --- src/agent/linux/LinGeneralAnalyzer.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agent/linux/LinGeneralAnalyzer.cpp b/src/agent/linux/LinGeneralAnalyzer.cpp index 0a4176ca..3639119f 100644 --- a/src/agent/linux/LinGeneralAnalyzer.cpp +++ b/src/agent/linux/LinGeneralAnalyzer.cpp @@ -98,8 +98,10 @@ void LinGeneralAnalyzer::Refresh(const std::vector&) if (m_useJournalctl) { - // journalctl with cursor-file returns only new entries; - // accumulate into existing errors + // journalctl with cursor-file returns only new entries since last read. + // Errors are accumulated permanently — once a disk reports an error it stays BAD + // for the lifetime of the agent process. This is intentional: disk I/O errors + // warrant investigation even if they stop recurring. for (auto& [disk, errors] : newErrors) m_errors[disk].merge(std::move(errors)); } From d543f60d994d1e14836aad4c0dc1ff53a81984ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:39:13 +0200 Subject: [PATCH 09/30] Document lock acquisition hierarchy in agent main --- src/agent/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 51c265d5..a41cf7d0 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -168,6 +168,8 @@ namespace server.setStatusData(overall, std::move(diskInfos)); } + // Lock hierarchy (acquire in this order to avoid deadlocks): + // refreshMutex (HttpServer::Impl) → g_probeMutex → dataMutex → sseMutex std::mutex g_probeMutex; } From 66d5b9730d29e161e4ace9b9375ea826f87c3a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:42:23 +0200 Subject: [PATCH 10/30] Add unit test for SmartHealthAnalyzer::GetRefreshPolicy() --- tests/agent/SmartHealthAnalyzerTests.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/agent/SmartHealthAnalyzerTests.cpp b/tests/agent/SmartHealthAnalyzerTests.cpp index e1830ae7..43bd7424 100644 --- a/tests/agent/SmartHealthAnalyzerTests.cpp +++ b/tests/agent/SmartHealthAnalyzerTests.cpp @@ -364,3 +364,11 @@ TEST_F(SmartHealthAnalyzerTest, NvmeHealthyReturnGood) EXPECT_EQ(GeneralHealth::GOOD, m_analyzer->GetStatus(m_disk)); } + + +TEST_F(SmartHealthAnalyzerTest, RefreshPolicyReturnsExpectedValues) +{ + auto policy = m_analyzer->GetRefreshPolicy(); + EXPECT_EQ(std::chrono::hours(4), policy.interval); + EXPECT_FALSE(policy.proactiveCollection); +} From 77ab20c0f3c8bf8e6831055f965b75fc8ebc0147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Mon, 6 Apr 2026 22:43:02 +0200 Subject: [PATCH 11/30] Make g_server an atomic pointer for signal-handler safety --- src/agent/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index a41cf7d0..fbe3091f 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -19,7 +19,7 @@ namespace { - HttpServer* g_server = nullptr; + std::atomic g_server{nullptr}; std::atomic g_running{true}; std::mutex g_bgMutex; std::condition_variable g_bgCv; @@ -27,8 +27,8 @@ namespace void signalHandler(int) { g_running = false; - if (g_server) - g_server->stop(); + if (auto* srv = g_server.load(std::memory_order_relaxed)) + srv->stop(); } struct ProbeEntry From 01ce322f5582844e23c77a2bcc1f5fe13f341183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:32:50 +0200 Subject: [PATCH 12/30] Add const and [[nodiscard]] to IProbe getter methods --- src/agent/IProbe.h | 6 +++--- src/agent/SmartHealthAnalyzer.cpp | 4 ++-- src/agent/SmartHealthAnalyzer.h | 4 ++-- src/agent/linux/LinGeneralAnalyzer.cpp | 4 ++-- src/agent/linux/LinGeneralAnalyzer.h | 4 ++-- src/agent/windows/WinGeneralAnalyzer.cpp | 4 ++-- src/agent/windows/WinGeneralAnalyzer.h | 4 ++-- tests/agent/IProbeMock.h | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/agent/IProbe.h b/src/agent/IProbe.h index 310d8c09..fa6b3d2f 100644 --- a/src/agent/IProbe.h +++ b/src/agent/IProbe.h @@ -21,9 +21,9 @@ class IProbe public: virtual ~IProbe() = default; - virtual RefreshPolicy GetRefreshPolicy() const = 0; + [[nodiscard]] virtual RefreshPolicy GetRefreshPolicy() const = 0; virtual void Refresh(const std::vector& disks) = 0; - virtual GeneralHealth::Health GetStatus(const Disk& _disk) = 0; - virtual nlohmann::json GetRawData(const Disk& _disk) = 0; + [[nodiscard]] virtual GeneralHealth::Health GetStatus(const Disk& _disk) const = 0; + [[nodiscard]] virtual nlohmann::json GetRawData(const Disk& _disk) const = 0; }; \ No newline at end of file diff --git a/src/agent/SmartHealthAnalyzer.cpp b/src/agent/SmartHealthAnalyzer.cpp index 59418dc6..1e530b50 100644 --- a/src/agent/SmartHealthAnalyzer.cpp +++ b/src/agent/SmartHealthAnalyzer.cpp @@ -90,7 +90,7 @@ const IVendorProfile& SmartHealthAnalyzer::profileFor(const std::string& vendor) } -GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) +GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) const { auto it = m_cachedSmartData.find(disk.GetDeviceId()); if (it == m_cachedSmartData.end()) @@ -146,7 +146,7 @@ GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) } -nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk) +nlohmann::json SmartHealthAnalyzer::GetRawData(const Disk& disk) const { auto it = m_cachedSmartData.find(disk.GetDeviceId()); if (it == m_cachedSmartData.end()) diff --git a/src/agent/SmartHealthAnalyzer.h b/src/agent/SmartHealthAnalyzer.h index fa92b444..3e7245d4 100644 --- a/src/agent/SmartHealthAnalyzer.h +++ b/src/agent/SmartHealthAnalyzer.h @@ -20,8 +20,8 @@ class SmartHealthAnalyzer : public IProbe RefreshPolicy GetRefreshPolicy() const override; void Refresh(const std::vector& disks) override; - GeneralHealth::Health GetStatus(const Disk& disk) override; - nlohmann::json GetRawData(const Disk& disk) override; + GeneralHealth::Health GetStatus(const Disk& disk) const override; + nlohmann::json GetRawData(const Disk& disk) const override; private: std::unique_ptr m_reader; diff --git a/src/agent/linux/LinGeneralAnalyzer.cpp b/src/agent/linux/LinGeneralAnalyzer.cpp index 3639119f..707086e1 100644 --- a/src/agent/linux/LinGeneralAnalyzer.cpp +++ b/src/agent/linux/LinGeneralAnalyzer.cpp @@ -42,7 +42,7 @@ RefreshPolicy LinGeneralAnalyzer::GetRefreshPolicy() const } -GeneralHealth::Health LinGeneralAnalyzer::GetStatus(const Disk& disk) +GeneralHealth::Health LinGeneralAnalyzer::GetStatus(const Disk& disk) const { return m_errors.find(disk) == m_errors.end()? GeneralHealth::Health::GOOD : @@ -50,7 +50,7 @@ GeneralHealth::Health LinGeneralAnalyzer::GetStatus(const Disk& disk) } -nlohmann::json LinGeneralAnalyzer::GetRawData(const Disk& disk) +nlohmann::json LinGeneralAnalyzer::GetRawData(const Disk& disk) const { std::string result; diff --git a/src/agent/linux/LinGeneralAnalyzer.h b/src/agent/linux/LinGeneralAnalyzer.h index 33ca4001..da323a69 100644 --- a/src/agent/linux/LinGeneralAnalyzer.h +++ b/src/agent/linux/LinGeneralAnalyzer.h @@ -19,8 +19,8 @@ class LinGeneralAnalyzer : public IProbe RefreshPolicy GetRefreshPolicy() const override; void Refresh(const std::vector& disks) override; - GeneralHealth::Health GetStatus(const Disk& disk) override; - nlohmann::json GetRawData(const Disk& disk) override; + GeneralHealth::Health GetStatus(const Disk& disk) const override; + nlohmann::json GetRawData(const Disk& disk) const override; private: std::map> m_errors; diff --git a/src/agent/windows/WinGeneralAnalyzer.cpp b/src/agent/windows/WinGeneralAnalyzer.cpp index dbad016a..0cbd58e3 100644 --- a/src/agent/windows/WinGeneralAnalyzer.cpp +++ b/src/agent/windows/WinGeneralAnalyzer.cpp @@ -16,7 +16,7 @@ void WinGeneralAnalyzer::Refresh(const std::vector& disks) } -GeneralHealth::Health WinGeneralAnalyzer::GetStatus(const Disk& _disk) +GeneralHealth::Health WinGeneralAnalyzer::GetStatus(const Disk& _disk) const { auto it = m_cachedStatus.find(_disk.GetDeviceId()); if (it != m_cachedStatus.end()) @@ -24,7 +24,7 @@ GeneralHealth::Health WinGeneralAnalyzer::GetStatus(const Disk& _disk) return GeneralHealth::UNKNOWN; } -nlohmann::json WinGeneralAnalyzer::GetRawData(const Disk& _disk) +nlohmann::json WinGeneralAnalyzer::GetRawData(const Disk& _disk) const { return nlohmann::json{{"type", "text"}, {"value", std::string()}}; } diff --git a/src/agent/windows/WinGeneralAnalyzer.h b/src/agent/windows/WinGeneralAnalyzer.h index 0f1205a9..8ae1d3fe 100644 --- a/src/agent/windows/WinGeneralAnalyzer.h +++ b/src/agent/windows/WinGeneralAnalyzer.h @@ -11,8 +11,8 @@ class WinGeneralAnalyzer : public IProbe RefreshPolicy GetRefreshPolicy() const override; void Refresh(const std::vector& disks) override; - GeneralHealth::Health GetStatus(const Disk& _disk) override; - nlohmann::json GetRawData(const Disk& _disk) override; + GeneralHealth::Health GetStatus(const Disk& _disk) const override; + nlohmann::json GetRawData(const Disk& _disk) const override; private: std::map m_cachedStatus; diff --git a/tests/agent/IProbeMock.h b/tests/agent/IProbeMock.h index 2266bea9..7a13e430 100644 --- a/tests/agent/IProbeMock.h +++ b/tests/agent/IProbeMock.h @@ -9,6 +9,6 @@ class IProbeMock : public IProbe public: MOCK_METHOD(RefreshPolicy, GetRefreshPolicy, (), (const, override)); MOCK_METHOD(void, Refresh, (const std::vector&), (override)); - MOCK_METHOD(GeneralHealth::Health, GetStatus, (const Disk&), (override)); - MOCK_METHOD(nlohmann::json, GetRawData, (const Disk&), (override)); + MOCK_METHOD(GeneralHealth::Health, GetStatus, (const Disk&), (const, override)); + MOCK_METHOD(nlohmann::json, GetRawData, (const Disk&), (const, override)); }; \ No newline at end of file From 2dba3f859f5d80464bcc7a614e8d29bb700040f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:33:22 +0200 Subject: [PATCH 13/30] Replace thread-unsafe std::gmtime with gmtime_r --- src/agent/HttpServer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/agent/HttpServer.cpp b/src/agent/HttpServer.cpp index 4923c73f..6cbbfcdb 100644 --- a/src/agent/HttpServer.cpp +++ b/src/agent/HttpServer.cpp @@ -239,8 +239,10 @@ void HttpServer::setStatusData(GeneralHealth::Health overallHealth, std::vector< const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); + std::tm tm_buf{}; + gmtime_r(&time_t, &tm_buf); std::ostringstream oss; - oss << std::put_time(std::gmtime(&time_t), "%Y-%m-%dT%H:%M:%SZ"); + oss << std::put_time(&tm_buf, "%Y-%m-%dT%H:%M:%SZ"); m_impl->lastRefreshed = oss.str(); } From 1f79f965951c48bf4c490e5641287cac96d5575c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:34:51 +0200 Subject: [PATCH 14/30] Use early return on BAD in NVMe health checks for consistency --- src/agent/SmartHealthAnalyzer.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/agent/SmartHealthAnalyzer.cpp b/src/agent/SmartHealthAnalyzer.cpp index 1e530b50..b3b8d862 100644 --- a/src/agent/SmartHealthAnalyzer.cpp +++ b/src/agent/SmartHealthAnalyzer.cpp @@ -118,18 +118,15 @@ GeneralHealth::Health SmartHealthAnalyzer::GetStatus(const Disk& disk) const } // Layer 2b: NVMe critical fields — non-zero raw means trouble - if (isCriticalNvme(attr.name)) - { - if (attr.rawVal > 0) - worst = std::max(worst, GeneralHealth::BAD); - } + if (isCriticalNvme(attr.name) && attr.rawVal > 0) + return GeneralHealth::BAD; // Layer 2c: NVMe wear indicator if (isNvmePercentageUsed(attr.name)) { if (attr.rawVal >= 100) - worst = std::max(worst, GeneralHealth::BAD); - else if (attr.rawVal >= nvmeWearWarningPercent) + return GeneralHealth::BAD; + if (attr.rawVal >= nvmeWearWarningPercent) worst = std::max(worst, GeneralHealth::CHECK_STATUS); } From a5a27ec182401e8fc8111742eda45b443c379996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:35:10 +0200 Subject: [PATCH 15/30] Use std::string_view for SmartHealthAnalyzer::profileFor parameter --- src/agent/SmartHealthAnalyzer.cpp | 2 +- src/agent/SmartHealthAnalyzer.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/agent/SmartHealthAnalyzer.cpp b/src/agent/SmartHealthAnalyzer.cpp index b3b8d862..671c0589 100644 --- a/src/agent/SmartHealthAnalyzer.cpp +++ b/src/agent/SmartHealthAnalyzer.cpp @@ -75,7 +75,7 @@ void SmartHealthAnalyzer::Refresh(const std::vector& disks) } -const IVendorProfile& SmartHealthAnalyzer::profileFor(const std::string& vendor) +const IVendorProfile& SmartHealthAnalyzer::profileFor(std::string_view vendor) { static const GenericProfile generic; static const SamsungProfile samsung; diff --git a/src/agent/SmartHealthAnalyzer.h b/src/agent/SmartHealthAnalyzer.h index 3e7245d4..54193905 100644 --- a/src/agent/SmartHealthAnalyzer.h +++ b/src/agent/SmartHealthAnalyzer.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "common/SmartData.h" @@ -29,5 +30,5 @@ class SmartHealthAnalyzer : public IProbe std::map m_cachedSmartData; std::map m_cachedTestStatus; - static const IVendorProfile& profileFor(const std::string& vendor); + static const IVendorProfile& profileFor(std::string_view vendor); }; From e1648981c740b44ce54a97beadc4c9eab18de195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:35:32 +0200 Subject: [PATCH 16/30] Take HttpServer agentName by value and move into member --- src/agent/HttpServer.cpp | 4 ++-- src/agent/HttpServer.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent/HttpServer.cpp b/src/agent/HttpServer.cpp index 6cbbfcdb..3a10ff89 100644 --- a/src/agent/HttpServer.cpp +++ b/src/agent/HttpServer.cpp @@ -94,10 +94,10 @@ struct HttpServer::Impl }; -HttpServer::HttpServer(const std::string& agentName, unsigned int port) +HttpServer::HttpServer(std::string agentName, unsigned int port) : m_impl(std::make_unique()) { - m_impl->agentName = agentName; + m_impl->agentName = std::move(agentName); m_impl->port = port; // GET /api/v1/info diff --git a/src/agent/HttpServer.h b/src/agent/HttpServer.h index df155e93..5ea84055 100644 --- a/src/agent/HttpServer.h +++ b/src/agent/HttpServer.h @@ -13,7 +13,7 @@ class HttpServer { public: - HttpServer(const std::string& agentName, unsigned int port); + HttpServer(std::string agentName, unsigned int port); ~HttpServer(); void setStatusData(GeneralHealth::Health overallHealth, std::vector disks); From 0c1c11be3bac7406285f80b1d0b8aaf2cde7f6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:36:10 +0200 Subject: [PATCH 17/30] Modernize main.cpp: ranges::transform, aggregate init, simplify chrono comparison --- src/agent/main.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index fbe3091f..e189bbd3 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -106,10 +107,7 @@ namespace if (proactiveOnly && !policy.proactiveCollection) continue; - const auto elapsed = std::chrono::duration_cast( - now - entry.lastRefresh); - - if (elapsed >= policy.interval) + if ((now - entry.lastRefresh) >= policy.interval) { entry.probe->Refresh(disks); entry.lastRefresh = now; @@ -145,11 +143,9 @@ namespace for (const auto& entry : entries) { - ProbeStatus status; - status.health = entry.probe->GetStatus(disk); - status.rawData = entry.probe->GetRawData(disk); - probeStatuses.push_back(status); - healthStatuses.push_back(status.health); + probeStatuses.push_back({entry.probe->GetStatus(disk), + entry.probe->GetRawData(disk)}); + healthStatuses.push_back(probeStatuses.back().health); } DiskInfo info; @@ -161,8 +157,9 @@ namespace } std::vector statuses; - std::transform(diskInfos.begin(), diskInfos.end(), std::back_inserter(statuses), - [](const auto& di) { return di.GetHealth(); }); + statuses.reserve(diskInfos.size()); + std::ranges::transform(diskInfos, std::back_inserter(statuses), + &DiskInfo::GetHealth); auto overall = calc.CalculateCumulativeStatus(statuses); server.setStatusData(overall, std::move(diskInfos)); From 7fe0667699d09e3f5e94ab2f4024aee88ae44f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:36:37 +0200 Subject: [PATCH 18/30] Use std::ranges::all_of and std::ranges::find in HttpServer --- src/agent/HttpServer.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/agent/HttpServer.cpp b/src/agent/HttpServer.cpp index 3a10ff89..eb317607 100644 --- a/src/agent/HttpServer.cpp +++ b/src/agent/HttpServer.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -21,7 +22,7 @@ namespace bool isValidDiskName(const std::string& name) { return !name.empty() && name.size() <= 64 && - std::all_of(name.begin(), name.end(), [](char c) { + std::ranges::all_of(name, [](char c) { return std::isalnum(static_cast(c)) || c == '-' || c == '_'; }); } @@ -135,14 +136,12 @@ HttpServer::HttpServer(std::string agentName, unsigned int port) std::lock_guard lock(m_impl->dataMutex); - for (const auto& d : m_impl->disks) + auto it = std::ranges::find(m_impl->disks, name, &DiskInfo::GetName); + if (it != m_impl->disks.end()) { - if (d.GetName() == name) - { - nlohmann::json j = d; - res.set_content(j.dump(), "application/json"); - return; - } + nlohmann::json j = *it; + res.set_content(j.dump(), "application/json"); + return; } res.status = 404; res.set_content(R"({"error":"disk not found"})", "application/json"); From 13933bedb70dd2e52047c025f01c08cdf5277af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:36:59 +0200 Subject: [PATCH 19/30] Use std::ranges::any_of in SmartHealthAnalyzer --- src/agent/SmartHealthAnalyzer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/SmartHealthAnalyzer.cpp b/src/agent/SmartHealthAnalyzer.cpp index 671c0589..56e6f634 100644 --- a/src/agent/SmartHealthAnalyzer.cpp +++ b/src/agent/SmartHealthAnalyzer.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace { @@ -27,7 +28,7 @@ namespace bool isCriticalAta(uint8_t id, const std::string& name) { - return std::any_of(criticalAttrs.begin(), criticalAttrs.end(), + return std::ranges::any_of(criticalAttrs, [&](const CriticalAttr& ca) { return ca.id == id && name == ca.canonicalName; }); } From 2fd926228f345148daebe26e7da1b03069979029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:37:15 +0200 Subject: [PATCH 20/30] Use std::map::contains() in LinGeneralAnalyzer::GetStatus --- src/agent/linux/LinGeneralAnalyzer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent/linux/LinGeneralAnalyzer.cpp b/src/agent/linux/LinGeneralAnalyzer.cpp index 707086e1..9adcb462 100644 --- a/src/agent/linux/LinGeneralAnalyzer.cpp +++ b/src/agent/linux/LinGeneralAnalyzer.cpp @@ -44,9 +44,9 @@ RefreshPolicy LinGeneralAnalyzer::GetRefreshPolicy() const GeneralHealth::Health LinGeneralAnalyzer::GetStatus(const Disk& disk) const { - return m_errors.find(disk) == m_errors.end()? - GeneralHealth::Health::GOOD : - GeneralHealth::Health::BAD; + return m_errors.contains(disk) + ? GeneralHealth::Health::BAD + : GeneralHealth::Health::GOOD; } From 937a331093a4a6c24d44d856c609b7d8d1cdcd89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 20:37:35 +0200 Subject: [PATCH 21/30] Add defaulted operator== to RefreshPolicy --- src/agent/IProbe.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agent/IProbe.h b/src/agent/IProbe.h index fa6b3d2f..ed09a0a6 100644 --- a/src/agent/IProbe.h +++ b/src/agent/IProbe.h @@ -13,6 +13,8 @@ struct RefreshPolicy { std::chrono::seconds interval{0}; bool proactiveCollection = false; + + bool operator==(const RefreshPolicy&) const = default; }; From cbee8a6d59456501abeb3e226d0eb41671f57547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Tue, 7 Apr 2026 21:03:32 +0200 Subject: [PATCH 22/30] Use gmtime_s on Windows for MSVC compatibility --- src/agent/HttpServer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/agent/HttpServer.cpp b/src/agent/HttpServer.cpp index eb317607..b9bd2785 100644 --- a/src/agent/HttpServer.cpp +++ b/src/agent/HttpServer.cpp @@ -239,7 +239,11 @@ void HttpServer::setStatusData(GeneralHealth::Health overallHealth, std::vector< const auto now = std::chrono::system_clock::now(); const auto time_t = std::chrono::system_clock::to_time_t(now); std::tm tm_buf{}; +#ifdef _WIN32 + gmtime_s(&tm_buf, &time_t); +#else gmtime_r(&time_t, &tm_buf); +#endif std::ostringstream oss; oss << std::put_time(&tm_buf, "%Y-%m-%dT%H:%M:%SZ"); m_impl->lastRefreshed = oss.str(); From 1c4f4238fe2d8f76846ca8785517f6a3be3bdfc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Fri, 1 May 2026 22:12:05 +0200 Subject: [PATCH 23/30] Simplify a bit --- src/agent/main.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index e189bbd3..945abba3 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -118,7 +118,7 @@ namespace return anyRefreshed; } - void refreshAllProbes(std::vector& entries, + void refreshAllProbes(std::span entries, const std::vector& disks) { const auto now = std::chrono::steady_clock::now(); @@ -130,7 +130,7 @@ namespace } void publishFromCache(HttpServer& server, - const std::vector& entries, + const std::span entries, const std::vector& disks) { DiscStatusCalculator calc; @@ -204,9 +204,7 @@ int main(int argc, char** argv) // Create persistent probes auto probeUptrs = systemUtilsFactory.getProbes(); - std::vector probeEntries; - for (auto& p : probeUptrs) - probeEntries.push_back({std::move(p), {}}); + auto probeEntries = std::ranges::to>(std::views::transform(probeUptrs, [](auto&& probe) { return ProbeEntry(std::move(probe)); })); // Create HTTP server HttpServer server(agentName, RDHMPort); From bc02c07ee2da14dd8b4340d75772c7807561da60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 08:08:10 +0200 Subject: [PATCH 24/30] Add Utils files --- src/common/CMakeLists.txt | 5 ++- src/common/Utils.cpp | 74 +++++++++++++++++++++++++++++++++++++++ src/common/Utils.h | 8 +++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/common/Utils.cpp create mode 100644 src/common/Utils.h diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b42fc285..8f546ce0 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -10,7 +10,10 @@ add_library(common OBJECT DiskSummary.h DiskInfo.h DiskInfo.cpp - JsonSerialize.h) + JsonSerialize.h + Utils.cpp + Utils.h +) target_include_directories(common PUBLIC diff --git a/src/common/Utils.cpp b/src/common/Utils.cpp new file mode 100644 index 00000000..a432c67d --- /dev/null +++ b/src/common/Utils.cpp @@ -0,0 +1,74 @@ + +#include +#include +#include +#include + +#include "Utils.h" + + +std::string formatBytes(std::uint64_t bytes) +{ + static const std::array units = + { + "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" + }; + + double size = static_cast(bytes); + std::size_t unitIndex = 0; + + while(size >= 1024.0 && unitIndex < units.size() - 1) + { + size /= 1024.0; + ++unitIndex; + } + + std::ostringstream oss; + + if(unitIndex == 0) + oss << static_cast(size) << " " << units[unitIndex]; + else + oss << std::fixed << std::setprecision(2) << size << " " << units[unitIndex]; + + return oss.str(); +} + + +std::string formatTable(const std::span> rows) +{ + if(rows.empty()) + return {}; + + // liczba kolumn = max długość wiersza + std::size_t cols = 0; + for(const auto& r : rows) + cols = std::max(cols, r.size()); + + // szerokości kolumn + std::vector widths(cols, 0); + for(const auto& r : rows) + { + for(std::size_t c = 0; c < r.size(); ++c) + widths[c] = std::max(widths[c], r[c].size()); + } + + std::ostringstream oss; + + for(const auto& r : rows) + { + for(std::size_t c = 0; c < cols; ++c) + { + const std::string& cell = (c < r.size()) ? r[c] : ""; + + oss << cell; + + // padding (1 spacja odstępu między kolumnami) + std::size_t pad = widths[c] - cell.size(); + for(std::size_t i = 0; i < pad + 1; ++i) + oss << ' '; + } + oss << '\n'; + } + + return oss.str(); +} diff --git a/src/common/Utils.h b/src/common/Utils.h new file mode 100644 index 00000000..c668f719 --- /dev/null +++ b/src/common/Utils.h @@ -0,0 +1,8 @@ + +#pragma once + +#include +#include + +std::string formatBytes(std::uint64_t bytes); +std::string formatTable(const std::span> rows); From 751f76ddd533461f86588616a58ad835cfa52ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 08:08:26 +0200 Subject: [PATCH 25/30] Add todo --- src/monitor/Qml/AgentDetailPanel.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/monitor/Qml/AgentDetailPanel.qml b/src/monitor/Qml/AgentDetailPanel.qml index 3a1d40e0..30ba399f 100644 --- a/src/monitor/Qml/AgentDetailPanel.qml +++ b/src/monitor/Qml/AgentDetailPanel.qml @@ -15,6 +15,7 @@ Item { property var diskData: [] property string lastRefreshed: "" + // TODO: try using funcion from 'common' module function formatBytes(bytes) { if (bytes <= 0) return "" var units = ["B", "KB", "MB", "GB", "TB", "PB"] From 8d9762b9d3a43c7da7cc1797ee5491fea4956157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 08:08:42 +0200 Subject: [PATCH 26/30] Print disks info in a nice form --- src/agent/main.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 945abba3..8df0119a 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -12,6 +12,7 @@ #include "common/constants.hpp" #include "common/DiskSummary.h" +#include "common/Utils.h" #include "HttpServer.h" #include "MdnsPublisher.h" #include "SystemUtilitiesFactory.h" @@ -198,9 +199,21 @@ int main(int argc, char** argv) auto diskCollector = systemUtilsFactory.diskCollector(); const auto disks = diskCollector->GetDisksList(); - std::cout << "Found disks:\n"; + std::vector> disksInfo{ + {"ID", "type", "capacity", "vendor", "model"} + }; + for (const auto& disk : disks) - std::cout << " " << disk.GetDeviceId() << '\n'; + disksInfo.emplace_back( + std::vector { + disk.GetDeviceId(), + disk.GetDriveType(), + formatBytes(disk.GetCapacity()), + disk.GetVendor(), + disk.GetModel() + }); + + std::cout << "Found disks:\n" << formatTable(disksInfo); // Create persistent probes auto probeUptrs = systemUtilsFactory.getProbes(); From 7e611d202d6a83ae13f654ae0bb37b6f3a5ae303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 21:49:29 +0200 Subject: [PATCH 27/30] Add log for publish --- src/agent/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 8df0119a..61ab6a45 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -272,7 +272,10 @@ int main(int argc, char** argv) std::lock_guard lock(g_probeMutex); if (refreshStaleProbes(probeEntries, disks)) + { + std::cout << "Publishing new statues\n"; publishFromCache(server, probeEntries, disks); + } } }); From ca561ec82b9a00143dce7a6068e667bf6d343232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 21:56:50 +0200 Subject: [PATCH 28/30] Use string_view --- src/agent/HttpServer.cpp | 4 ++-- src/agent/HttpServer.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent/HttpServer.cpp b/src/agent/HttpServer.cpp index b9bd2785..62c992a1 100644 --- a/src/agent/HttpServer.cpp +++ b/src/agent/HttpServer.cpp @@ -95,10 +95,10 @@ struct HttpServer::Impl }; -HttpServer::HttpServer(std::string agentName, unsigned int port) +HttpServer::HttpServer(std::string_view agentName, unsigned int port) : m_impl(std::make_unique()) { - m_impl->agentName = std::move(agentName); + m_impl->agentName = agentName; m_impl->port = port; // GET /api/v1/info diff --git a/src/agent/HttpServer.h b/src/agent/HttpServer.h index 5ea84055..13ad891a 100644 --- a/src/agent/HttpServer.h +++ b/src/agent/HttpServer.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -13,7 +13,7 @@ class HttpServer { public: - HttpServer(std::string agentName, unsigned int port); + HttpServer(std::string_view agentName, unsigned int port); ~HttpServer(); void setStatusData(GeneralHealth::Health overallHealth, std::vector disks); From 16c124a052c03dddb53cdb050b3f3f2fc31be4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 21:59:46 +0200 Subject: [PATCH 29/30] Add missing include --- src/common/Utils.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/Utils.h b/src/common/Utils.h index c668f719..73b68c31 100644 --- a/src/common/Utils.h +++ b/src/common/Utils.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include + std::string formatBytes(std::uint64_t bytes); std::string formatTable(const std::span> rows); From c8c39a1ff99dc1a6073b1bac967c4d2cf2e4ea2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walenciak?= Date: Sat, 2 May 2026 22:16:10 +0200 Subject: [PATCH 30/30] Drop too new constructions for Debian --- src/agent/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent/main.cpp b/src/agent/main.cpp index 61ab6a45..69d01636 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -217,7 +217,8 @@ int main(int argc, char** argv) // Create persistent probes auto probeUptrs = systemUtilsFactory.getProbes(); - auto probeEntries = std::ranges::to>(std::views::transform(probeUptrs, [](auto&& probe) { return ProbeEntry(std::move(probe)); })); + std::vector probeEntries; + std::ranges::transform(probeUptrs, std::back_inserter(probeEntries), [](auto&& probe) { return ProbeEntry(std::move(probe)); }); // Create HTTP server HttpServer server(agentName, RDHMPort);