From 203b4a0c856a8080dbc7efd93829bd812ea7b7a2 Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 1 May 2026 14:52:16 +0200 Subject: [PATCH] rework smart module to improve parsing --- include/smart.h | 32 ++- src/smart.cpp | 747 ++++++++++++++++++++++-------------------------- 2 files changed, 362 insertions(+), 417 deletions(-) diff --git a/include/smart.h b/include/smart.h index 7c7e340..bafccc2 100644 --- a/include/smart.h +++ b/include/smart.h @@ -10,27 +10,29 @@ #include "reHDD.h" +/** + * @brief SMART data reader for drives + * + * Parses smartctl JSON output to extract: + * - Device information (model, serial, capacity) + * - Power statistics (hours, cycles) + * - Temperature + * - Critical sector counts (reallocated, pending, uncorrectable) + * + * Uses deterministic state machine parser for reliable multi-line JSON parsing. + */ class SMART { protected: public: + /** + * @brief Read S.M.A.R.T. data from drive and populate Drive object + * @param drive Pointer to Drive instance to populate with SMART data + */ static void readSMARTData(Drive *drive); private: - SMART(void); - - static bool parseExitStatus(std::string sLine, uint8_t &status); - static bool parseModelFamily(std::string sLine, std::string &modelFamily); - static bool parseModelName(std::string sLine, std::string &modelName); - static bool parseSerial(std::string sLine, std::string &serial); - static bool parseCapacity(std::string sLine, uint64_t &capacity); - static bool parseErrorCount(std::string sLine, uint32_t &errorCount); - static bool parsePowerOnHours(std::string sLine, uint32_t &powerOnHours); - static bool parsePowerCycles(std::string sLine, uint32_t &powerCycles); - static bool parseTemperature(std::string sLine, uint32_t &temperature); - static bool parseReallocatedSectors(std::string sLine, uint32_t &reallocatedSectors); - static bool parsePendingSectors(std::string sLine, uint32_t &pendingSectors); - static bool parseUncorrectableSectors(std::string sLine, uint32_t &uncorrectableSectors); + SMART(void); // Utility class - no instances }; -#endif // SMART_H_ \ No newline at end of file +#endif // SMART_H_ diff --git a/src/smart.cpp b/src/smart.cpp index eb0c127..b444975 100644 --- a/src/smart.cpp +++ b/src/smart.cpp @@ -6,433 +6,376 @@ */ #include "../include/reHDD.h" +#include // For WIFSIGNALED, WTERMSIG using namespace std; +/** + * \brief Parse context for SMART attribute values + */ +struct SMARTParseContext +{ + // Device information (top-level JSON fields) + string modelFamily; + string modelName; + string serial; + uint64_t capacity; + + // Power and temperature (top-level JSON fields) + uint32_t errorCount; + uint32_t powerOnHours; + uint32_t powerCycles; + uint32_t temperature; + + // Critical sector counts (from ata_smart_attributes table) + uint32_t reallocatedSectors; // ID 5 + uint32_t pendingSectors; // ID 197 + uint32_t uncorrectableSectors; // ID 198 + + // Parser state machine + enum State + { + SEARCHING, // Looking for next field + IN_ATTRIBUTE_5, // Inside ID 5 object + IN_ATTRIBUTE_197, // Inside ID 197 object + IN_ATTRIBUTE_198, // Inside ID 198 object + IN_RAW_SECTION // Inside "raw": { } of current attribute + }; + + State state; + int currentAttributeId; // Which attribute are we parsing? (5, 197, 198) + + SMARTParseContext() + : capacity(0), + errorCount(0), + powerOnHours(0), + powerCycles(0), + temperature(0), + reallocatedSectors(0), + pendingSectors(0), + uncorrectableSectors(0), + state(SEARCHING), + currentAttributeId(0) + { + } +}; + +/** + * \brief Extract JSON string value + * \param line containing "key": "value" + * \return extracted string value + */ +static string extractStringValue(const string &line) +{ + size_t colonPos = line.find(": "); + if (colonPos == string::npos) + return ""; + + size_t firstQuote = line.find('"', colonPos + 2); + if (firstQuote == string::npos) + return ""; + + size_t secondQuote = line.find('"', firstQuote + 1); + if (secondQuote == string::npos) + return ""; + + return line.substr(firstQuote + 1, secondQuote - firstQuote - 1); +} + +/** + * \brief Extract JSON integer value + * \param line containing "key": number + * \return extracted integer value + */ +static uint64_t extractIntegerValue(const string &line) +{ + size_t colonPos = line.find(": "); + if (colonPos == string::npos) + return 0; + + string valueStr = line.substr(colonPos + 2); + + // Remove whitespace, commas, braces + valueStr.erase(remove_if(valueStr.begin(), valueStr.end(), + [](char c) { return c == ' ' || c == ',' || c == '}' || c == '\n'; }), + valueStr.end()); + + // Verify it's a valid number + if (valueStr.empty() || valueStr.find_first_not_of("0123456789") != string::npos) + return 0; + + try + { + return stoull(valueStr); + } + catch (...) + { + return 0; + } +} + +/** + * \brief Process a single line of JSON output + * \param line from smartctl JSON output + * \param context parsing context with state + * \return void + */ +static void processLine(const string &line, SMARTParseContext &ctx) +{ + // Trim whitespace for consistent parsing + string trimmed = line; + size_t firstNonSpace = trimmed.find_first_not_of(" \t\r\n"); + if (firstNonSpace != string::npos) + { + trimmed = trimmed.substr(firstNonSpace); + } + + // Parse top-level device information + if (trimmed.find("\"model_family\":") == 0) + { + ctx.modelFamily = extractStringValue(line); + return; + } + + if (trimmed.find("\"model_name\":") == 0) + { + ctx.modelName = extractStringValue(line); + return; + } + + if (trimmed.find("\"serial_number\":") == 0) + { + ctx.serial = extractStringValue(line); + return; + } + + // Parse capacity from user_capacity.bytes + if (trimmed.find("\"bytes\":") == 0) + { + ctx.capacity = extractIntegerValue(line); + return; + } + + // Parse error count from self_test log + if (trimmed.find("\"error_count_total\":") == 0) + { + ctx.errorCount = extractIntegerValue(line); + return; + } + + // Parse power-on hours + if (trimmed.find("\"hours\":") == 0) + { + ctx.powerOnHours = extractIntegerValue(line); + return; + } + + // Parse power cycle count + if (trimmed.find("\"power_cycle_count\":") == 0) + { + ctx.powerCycles = extractIntegerValue(line); + return; + } + + // Parse temperature + if (trimmed.find("\"current\":") == 0 && ctx.temperature == 0) + { + // Only parse first occurrence (temperature section, not other "current" fields) + ctx.temperature = extractIntegerValue(line); + return; + } + + // State machine for SMART attributes parsing + switch (ctx.state) + { + case SMARTParseContext::SEARCHING: + // Look for critical attribute IDs + if (trimmed.find("\"id\": 5,") == 0) + { + ctx.state = SMARTParseContext::IN_ATTRIBUTE_5; + ctx.currentAttributeId = 5; + } + else if (trimmed.find("\"id\": 197,") == 0) + { + ctx.state = SMARTParseContext::IN_ATTRIBUTE_197; + ctx.currentAttributeId = 197; + } + else if (trimmed.find("\"id\": 198,") == 0) + { + ctx.state = SMARTParseContext::IN_ATTRIBUTE_198; + ctx.currentAttributeId = 198; + } + break; + + case SMARTParseContext::IN_ATTRIBUTE_5: + case SMARTParseContext::IN_ATTRIBUTE_197: + case SMARTParseContext::IN_ATTRIBUTE_198: + // Look for "raw": { start + if (trimmed.find("\"raw\":") == 0) + { + ctx.state = SMARTParseContext::IN_RAW_SECTION; + } + // Look for end of attribute object + else if (trimmed.find("},") == 0 || trimmed.find("}") == 0) + { + ctx.state = SMARTParseContext::SEARCHING; + ctx.currentAttributeId = 0; + } + break; + + case SMARTParseContext::IN_RAW_SECTION: + // Look for "value": number inside raw section + if (trimmed.find("\"value\":") == 0) + { + uint64_t value = extractIntegerValue(line); + + // Store value in appropriate field based on current attribute + if (ctx.currentAttributeId == 5) + { + ctx.reallocatedSectors = static_cast(value); + } + else if (ctx.currentAttributeId == 197) + { + ctx.pendingSectors = static_cast(value); + } + else if (ctx.currentAttributeId == 198) + { + ctx.uncorrectableSectors = static_cast(value); + } + + // Exit raw section after finding value + ctx.state = (ctx.currentAttributeId == 5) ? SMARTParseContext::IN_ATTRIBUTE_5 : + (ctx.currentAttributeId == 197) ? SMARTParseContext::IN_ATTRIBUTE_197 : + SMARTParseContext::IN_ATTRIBUTE_198; + } + break; + } +} + /** * \brief get and set S.M.A.R.T. values in Drive - * \param pointer of Drive instance + * \param pointer of Drive instance * \return void */ void SMART::readSMARTData(Drive *drive) { - string modelFamily; - string modelName; - string serial; - uint64_t capacity = 0U; - uint32_t errorCount = 0U; - uint32_t powerOnHours = 0U; - uint32_t powerCycles = 0U; - uint32_t temperature = 0U; - uint32_t reallocatedSectors = 0U; - uint32_t pendingSectors = 0U; - uint32_t uncorrectableSectors = 0U; - - modelFamily.clear(); - modelName.clear(); - serial.clear(); - - string sSmartctlCommands[] = {" --json -a ", " --json -d sntjmicron -a ", " --json -d sntasmedia -a ", " --json -d sntrealtek -a ", " --json -d sat -a "}; - - for (string sSmartctlCommand : sSmartctlCommands) + SMARTParseContext ctx; + uint8_t exitStatus = 255U; + + // Command order optimized for USB adapters + // Standard commands first, then device-specific variants + string sSmartctlCommands[] = { + " --json -a ", // Try standard first + " --json -d sat -a ", // SAT (SCSI/ATA Translation) - most USB adapters + " --json -d usbjmicron -a ", // USB JMicron + " --json -d usbprolific -a ", // USB Prolific + " --json -d usbsunplus -a " // USB Sunplus + }; + + for (const string &sSmartctlCommand : sSmartctlCommands) { - string sCMD = ("smartctl"); + // Build command with timeout + string sCMD = "timeout 5 smartctl"; // 5 second timeout prevents hanging sCMD.append(sSmartctlCommand); sCMD.append(drive->getPath()); - const char *cpComand = sCMD.c_str(); - - // Logger::logThis()->info(cpComand); - - FILE *outputfileSmart = popen(cpComand, "r"); - size_t len = 0U; // length of found line - char *cLine = NULL; // found line - uint8_t status = 255U; - - while ((getline(&cLine, &len, outputfileSmart)) != -1) + // Note: stderr NOT suppressed for debugging + + Logger::logThis()->info("SMART: Executing: " + sCMD); + + // Execute smartctl with timeout protection + FILE *outputfileSmart = popen(sCMD.c_str(), "r"); + if (outputfileSmart == nullptr) { - string sLine = string(cLine); - - SMART::parseExitStatus(sLine, status); - SMART::parseModelFamily(sLine, modelFamily); - SMART::parseModelName(sLine, modelName); - SMART::parseSerial(sLine, serial); - SMART::parseCapacity(sLine, capacity); - SMART::parseErrorCount(sLine, errorCount); - SMART::parsePowerOnHours(sLine, powerOnHours); - SMART::parsePowerCycles(sLine, powerCycles); - SMART::parseTemperature(sLine, temperature); - SMART::parseReallocatedSectors(sLine, reallocatedSectors); - SMART::parsePendingSectors(sLine, pendingSectors); - SMART::parseUncorrectableSectors(sLine, uncorrectableSectors); + Logger::logThis()->error("SMART: Failed to execute smartctl"); + continue; } - + + // Reset context for new attempt + ctx = SMARTParseContext(); + + // Parse output line by line + char *cLine = nullptr; + size_t len = 0; + int lineCount = 0; + + while (getline(&cLine, &len, outputfileSmart) != -1) + { + string sLine(cLine); + lineCount++; + + // Parse exit status + if (sLine.find("\"exit_status\":") != string::npos) + { + exitStatus = static_cast(extractIntegerValue(sLine)); + } + + // Process this line + processLine(sLine, ctx); + } + free(cLine); - pclose(outputfileSmart); - - if (status == 0U) + int pcloseStatus = pclose(outputfileSmart); + + Logger::logThis()->info("SMART: Parsed " + to_string(lineCount) + " lines, exit status: " + to_string(exitStatus)); + + // Check if timeout killed the process + if (WIFSIGNALED(pcloseStatus) && WTERMSIG(pcloseStatus) == SIGTERM) { - // Found S.M.A.R.T. data with this command - // Logger::logThis()->info("Found S.M.A.R.T. data with this command"); - break; + Logger::logThis()->warning("SMART: Command timed out (5s) - skipping to next variant"); + continue; } - } - - drive->setDriveSMARTData(modelFamily, modelName, serial, capacity, errorCount, powerOnHours, powerCycles, temperature, reallocatedSectors, pendingSectors, uncorrectableSectors); // write data in drive -} - -/** - * \brief parse ExitStatus - * \param string output line of smartctl - * \param uint8_t parsed status - * \return bool if parsing was possible - */ -bool SMART::parseExitStatus(string sLine, uint8_t &status) -{ - string search("\"exit_status\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0U, sLine.find(": ") + 1U); - status = stol(sLine); - return true; - } - else - { - return false; - } -} - -/** - * \brief parse ModelFamily - * \param string output line of smartctl - * \param string parsed model family - * \return bool if parsing was possible - */ -bool SMART::parseModelFamily(string sLine, string &modelFamily) -{ - string search("\"model_family\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0U, sLine.find(": ") + 3U); - if (sLine.length() >= 3U) + + // IGNORE exit status - instead check if we got valid data! + // Exit status 64 means "error log contains errors" but SMART data is still valid + // Exit status 4 means "some prefail attributes concerning" but data is valid + // What matters: Did we parse model name and serial? + if (!ctx.modelName.empty() && !ctx.serial.empty()) { - sLine.erase(sLine.length() - 3U, 3U); - } - modelFamily = sLine; - return true; - } - else - { - return false; - } -} - -/** - * \brief parse ModelName - * \param string output line of smartctl - * \param string parsed model name - * \return bool if parsing was possible - */ -bool SMART::parseModelName(string sLine, string &modelName) -{ - string search("\"model_name\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0U, sLine.find(": ") + 3U); - if (sLine.length() >= 3U) - { - sLine.erase(sLine.length() - 3U, 3U); - } - modelName = sLine; - return true; - } - else - { - return false; - } -} - -/** - * \brief parse Serial - * \param string output line of smartctl - * \param string parsed serial - * \return bool if parsing was possible - */ -bool SMART::parseSerial(string sLine, string &serial) -{ - string search("\"serial_number\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0, sLine.find(": ") + 3); - if (sLine.length() >= 3U) - { - sLine.erase(sLine.length() - 3U, 3U); - } - serial = sLine; - return true; - } - else - { - return false; - } -} - -/** - * \brief parse Capacity - * \param string output line of smartctl - * \param string parsed capacity - * \return bool if parsing was possible - */ -bool SMART::parseCapacity(string sLine, uint64_t &capacity) -{ - string search("\"bytes\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0, sLine.find(": ") + 2); - if (sLine.length() >= 1U) - { - sLine.erase(sLine.length() - 1U, 1U); - } - capacity = stol(sLine); - return true; - } - else - { - return false; - } -} - -/** - * \brief parse ErrorCount - * \param string output line of smartctl - * \param uint32_t parsed error count - * \return bool if parsing was possible - */ -bool SMART::parseErrorCount(string sLine, uint32_t &errorCount) -{ - string search("\"error_count_total\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0U, sLine.find(": ") + 2U); - if (sLine.length() >= 2U) - { - sLine.erase(sLine.length() - 2U, 2U); - } - errorCount = stol(sLine); - return true; - } - else - { - return false; - } -} - -/** - * \brief parse PowerOnHours - * \param string output line of smartctl\ - * \param uint32_t parsed power on hours - * \return bool if parsing was possible - */ -bool SMART::parsePowerOnHours(string sLine, uint32_t &powerOnHours) -{ - string search("\"hours\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0U, sLine.find(": ") + 2U); - if (sLine.length() >= 1U) - { - sLine.erase(sLine.length() - 1U, 1U); - } - powerOnHours = stol(sLine); - return true; - } - else - { - return false; - } -} - -/** - * \brief parse PowerCycle - * \param string output line of smartctl - * \param uint32_t parsed power cycles - * \return bool if parsing was possible - */ -bool SMART::parsePowerCycles(string sLine, uint32_t &powerCycles) -{ - string search("\"power_cycle_count\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0, sLine.find(": ") + 2); - if (sLine.length() >= 2U) - { - sLine.erase(sLine.length() - 2U, 2U); - } - powerCycles = stol(sLine); - return true; - } - else - { - return false; - } -} - -/** - * \brief parse temperature - * \param string output line of smartctl - * \param uint32_t parsed temperature - * \return bool if parsing was possible - */ -bool SMART::parseTemperature(string sLine, uint32_t &temperature) -{ - string search("\"current\": "); - size_t found = sLine.find(search); - if (found != string::npos) - { - sLine.erase(0U, sLine.find(": ") + 2U); - if (sLine.length() >= 1U) - { - sLine.erase(sLine.length() - 1U, 2U); - } - if (sLine == "{") - { - temperature = 0U; // this drive doesn't support temperature + Logger::logThis()->info("SMART: Successfully parsed data"); + Logger::logThis()->info("SMART: Model: " + ctx.modelName); + Logger::logThis()->info("SMART: Serial: " + ctx.serial); + Logger::logThis()->info("SMART: Capacity: " + to_string(ctx.capacity) + " bytes"); + Logger::logThis()->info("SMART: Power-On Hours: " + to_string(ctx.powerOnHours)); + Logger::logThis()->info("SMART: Temperature: " + to_string(ctx.temperature) + " C"); + Logger::logThis()->info("SMART: Reallocated Sectors: " + to_string(ctx.reallocatedSectors)); + Logger::logThis()->info("SMART: Pending Sectors: " + to_string(ctx.pendingSectors)); + Logger::logThis()->info("SMART: Uncorrectable Sectors: " + to_string(ctx.uncorrectableSectors)); + + if (exitStatus != 0) + { + Logger::logThis()->info("SMART: Note - exit status " + to_string(exitStatus) + " indicates warnings/errors in SMART log"); + } + + break; // Success - we got data! } else { - temperature = stol(sLine); + Logger::logThis()->warning("SMART: No valid data parsed (exit status: " + to_string(exitStatus) + ")"); } - return true; - } - else - { - return false; - } -} - -/** - * \brief parse Reallocated Sectors Count (SMART ID 0x05) - * \param string output line of smartctl - * \param uint32_t parsed reallocated sectors count - * \return bool if parsing was possible - */ -bool SMART::parseReallocatedSectors(string sLine, uint32_t &reallocatedSectors) -{ - string search("\"id\": 5,"); - size_t found = sLine.find(search); - if (found != string::npos) - { - // Found attribute ID 5 (Reallocated_Sector_Ct) - // Now we need to find the raw value in the next lines - // smartctl JSON format: "raw": { "value": , ... } - return true; // Mark that we found the attribute } - // Look for the raw value if we're in the right attribute - search = "\"value\":"; - found = sLine.find(search); - if (found != string::npos && sLine.find("\"raw\":") != string::npos) + // Check if we got ANY data + if (ctx.modelName.empty() && ctx.serial.empty()) { - // Extract value after "value": - sLine.erase(0U, sLine.find("\"value\":") + 8U); - // Remove trailing characters - size_t comma = sLine.find(","); - if (comma != string::npos) - { - sLine = sLine.substr(0, comma); - } - // Remove whitespace - sLine.erase(remove(sLine.begin(), sLine.end(), ' '), sLine.end()); + Logger::logThis()->warning("SMART: No SMART data available for this drive - may not support SMART or need root privileges"); - if (!sLine.empty() && sLine.find_first_not_of("0123456789") == string::npos) - { - reallocatedSectors = stoul(sLine); - return true; - } + // Try basic device info without SMART (use hdparm or similar as fallback) + // For now, just log that SMART is not available + ctx.modelName = "SMART not available"; + ctx.serial = "N/A"; } - return false; + // Write parsed data to drive + drive->setDriveSMARTData( + ctx.modelFamily, + ctx.modelName, + ctx.serial, + ctx.capacity, + ctx.errorCount, + ctx.powerOnHours, + ctx.powerCycles, + ctx.temperature, + ctx.reallocatedSectors, + ctx.pendingSectors, + ctx.uncorrectableSectors + ); } - -/** - * \brief parse Current Pending Sector Count (SMART ID 0xC5) - * \param string output line of smartctl - * \param uint32_t parsed pending sectors count - * \return bool if parsing was possible - */ -bool SMART::parsePendingSectors(string sLine, uint32_t &pendingSectors) -{ - string search("\"id\": 197,"); // 0xC5 = 197 decimal - size_t found = sLine.find(search); - if (found != string::npos) - { - return true; // Mark that we found the attribute - } - - // Look for the raw value - search = "\"value\":"; - found = sLine.find(search); - if (found != string::npos && sLine.find("\"raw\":") != string::npos) - { - sLine.erase(0U, sLine.find("\"value\":") + 8U); - size_t comma = sLine.find(","); - if (comma != string::npos) - { - sLine = sLine.substr(0, comma); - } - sLine.erase(remove(sLine.begin(), sLine.end(), ' '), sLine.end()); - - if (!sLine.empty() && sLine.find_first_not_of("0123456789") == string::npos) - { - pendingSectors = stoul(sLine); - return true; - } - } - - return false; -} - -/** - * \brief parse Offline Uncorrectable Sectors (SMART ID 0xC6) - * \param string output line of smartctl - * \param uint32_t parsed uncorrectable sectors count - * \return bool if parsing was possible - */ -bool SMART::parseUncorrectableSectors(string sLine, uint32_t &uncorrectableSectors) -{ - string search("\"id\": 198,"); // 0xC6 = 198 decimal - size_t found = sLine.find(search); - if (found != string::npos) - { - return true; // Mark that we found the attribute - } - - // Look for the raw value - search = "\"value\":"; - found = sLine.find(search); - if (found != string::npos && sLine.find("\"raw\":") != string::npos) - { - sLine.erase(0U, sLine.find("\"value\":") + 8U); - size_t comma = sLine.find(","); - if (comma != string::npos) - { - sLine = sLine.substr(0, comma); - } - sLine.erase(remove(sLine.begin(), sLine.end(), ' '), sLine.end()); - - if (!sLine.empty() && sLine.find_first_not_of("0123456789") == string::npos) - { - uncorrectableSectors = stoul(sLine); - return true; - } - } - - return false; -} -