From 831a8920411d842d71445afe9727417610efba20 Mon Sep 17 00:00:00 2001 From: localhorst Date: Tue, 28 Apr 2026 21:20:30 +0200 Subject: [PATCH 1/4] show HDD warnings based on sectors --- include/drive.h | 13 ++++- include/smart.h | 3 ++ include/tui.h | 2 +- src/drive.cpp | 23 ++++++++- src/reHDD.cpp | 2 +- src/smart.cpp | 129 +++++++++++++++++++++++++++++++++++++++++++++++- src/tui.cpp | 28 +++++++++-- 7 files changed, 191 insertions(+), 9 deletions(-) diff --git a/include/drive.h b/include/drive.h index bafe7e5..3220a3a 100644 --- a/include/drive.h +++ b/include/drive.h @@ -72,7 +72,10 @@ private: uint32_t u32ErrorCount = 0U; uint32_t u32PowerOnHours = 0U; // in hours uint32_t u32PowerCycles = 0U; - uint32_t u32Temperature = 0U; // in Fahrenheit, just kidding: degree Celsius + uint32_t u32Temperature = 0U; // in Fahrenheit, just kidding: degree Celsius + uint32_t u32ReallocatedSectors = 0U; // ID 0x05 - Reallocated Sectors Count + uint32_t u32PendingSectors = 0U; // ID 0xC5 - Current Pending Sector Count + uint32_t u32UncorrectableSectors = 0U; // ID 0xC6 - Offline Uncorrectable Sector Count } sSmartData; private: @@ -106,6 +109,9 @@ public: uint32_t getPowerOnHours(void); // in hours uint32_t getPowerCycles(void); uint32_t getTemperature(void); // in Fahrenheit, just kidding: degree Celsius + uint32_t getReallocatedSectors(void); + uint32_t getPendingSectors(void); + uint32_t getUncorrectableSectors(void); void checkFrozenDrive(void); void setDriveSMARTData(std::string modelFamily, @@ -115,7 +121,10 @@ public: uint32_t errorCount, uint32_t powerOnHours, uint32_t powerCycles, - uint32_t temperature); + uint32_t temperature, + uint32_t reallocatedSectors, + uint32_t pendingSectors, + uint32_t uncorrectableSectors); std::string sCapacityToText(); std::string sErrorCountToText(); diff --git a/include/smart.h b/include/smart.h index 2d4f4fd..7c7e340 100644 --- a/include/smart.h +++ b/include/smart.h @@ -28,6 +28,9 @@ private: 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); }; #endif // SMART_H_ \ No newline at end of file diff --git a/include/tui.h b/include/tui.h index 3826650..d4a201b 100644 --- a/include/tui.h +++ b/include/tui.h @@ -76,7 +76,7 @@ private: static WINDOW *createMenuView(int iXSize, int iYSize, int iXStart, int iYStart, struct MenuState menustate); static WINDOW *createDialog(int iXSize, int iYSize, int iXStart, int iYStart, std::string selectedTask, std::string optionA, std::string optionB); static WINDOW *createFrozenWarning(int iXSize, int iYSize, int iXStart, int iYStart, std::string sPath, std::string sModelFamily, std::string sModelName, std::string sSerial, std::string sProgress); - static WINDOW *createSmartWarning(int iXSize, int iYSize, int iXStart, int iYStart, std::string sPath, uint32_t u32PowerOnHours, uint32_t u32PowerCycles, uint32_t u32ErrorCount, uint32_t u32Temperature); + static WINDOW *createSmartWarning(int iXSize, int iYSize, int iXStart, int iYStart, std::string sPath, uint32_t u32PowerOnHours, uint32_t u32PowerCycles, uint32_t u32ErrorCount, uint32_t u32Temperature, uint32_t u32ReallocatedSectors, uint32_t u32PendingSectors, uint32_t u32UncorrectableSectors); static WINDOW *createZeroChecksumWarning(int iXSize, int iYSize, int iXStart, int iYStart, std::string sPath, std::string sModelFamily, std::string sModelName, std::string sSerial, uint32_t u32Checksum); void displaySelectedDrive(Drive &drive, int stdscrX, int stdscrY); diff --git a/src/drive.cpp b/src/drive.cpp index c1683ce..58ffb76 100644 --- a/src/drive.cpp +++ b/src/drive.cpp @@ -140,6 +140,21 @@ uint32_t Drive::getTemperature(void) return sSmartData.u32Temperature; } +uint32_t Drive::getReallocatedSectors(void) +{ + return sSmartData.u32ReallocatedSectors; +} + +uint32_t Drive::getPendingSectors(void) +{ + return sSmartData.u32PendingSectors; +} + +uint32_t Drive::getUncorrectableSectors(void) +{ + return sSmartData.u32UncorrectableSectors; +} + string Drive::sCapacityToText() { char acBuffer[16]; @@ -226,7 +241,10 @@ void Drive::setDriveSMARTData(string modelFamily, uint32_t errorCount, uint32_t powerOnHours, uint32_t powerCycle, - uint32_t temperature) + uint32_t temperature, + uint32_t reallocatedSectors, + uint32_t pendingSectors, + uint32_t uncorrectableSectors) { this->sSmartData.sModelFamily = modelFamily; this->sSmartData.sModelName = modelName; @@ -236,6 +254,9 @@ void Drive::setDriveSMARTData(string modelFamily, this->sSmartData.u32PowerOnHours = powerOnHours; this->sSmartData.u32PowerCycles = powerCycle; this->sSmartData.u32Temperature = temperature; + this->sSmartData.u32ReallocatedSectors = reallocatedSectors; + this->sSmartData.u32PendingSectors = pendingSectors; + this->sSmartData.u32UncorrectableSectors = uncorrectableSectors; } void Drive::setTimestamp() diff --git a/src/reHDD.cpp b/src/reHDD.cpp index e7b0a98..37707af 100644 --- a/src/reHDD.cpp +++ b/src/reHDD.cpp @@ -332,7 +332,7 @@ void reHDD::filterNewDrives(list *plistOldDrives, list *plistNewDr { itOld->bIsOffline = false; // drive is still attached // copy new smart data to existing drive - itOld->setDriveSMARTData(itNew->getModelFamily(), itNew->getModelName(), itNew->getSerial(), itNew->getCapacity(), itNew->getErrorCount(), itNew->getPowerOnHours(), itNew->getPowerCycles(), itNew->getTemperature()); + itOld->setDriveSMARTData(itNew->getModelFamily(), itNew->getModelName(), itNew->getSerial(), itNew->getCapacity(), itNew->getErrorCount(), itNew->getPowerOnHours(), itNew->getPowerCycles(), itNew->getTemperature(), itNew->getReallocatedSectors(), itNew->getPendingSectors(), itNew->getUncorrectableSectors()); #ifdef LOG_LEVEL_HIGH Logger::logThis()->info("Delete new drive, because already attached: " + itNew->getModelName()); #endif diff --git a/src/smart.cpp b/src/smart.cpp index 2a10855..eb0c127 100644 --- a/src/smart.cpp +++ b/src/smart.cpp @@ -23,6 +23,9 @@ void SMART::readSMARTData(Drive *drive) 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(); @@ -57,6 +60,9 @@ void SMART::readSMARTData(Drive *drive) SMART::parsePowerOnHours(sLine, powerOnHours); SMART::parsePowerCycles(sLine, powerCycles); SMART::parseTemperature(sLine, temperature); + SMART::parseReallocatedSectors(sLine, reallocatedSectors); + SMART::parsePendingSectors(sLine, pendingSectors); + SMART::parseUncorrectableSectors(sLine, uncorrectableSectors); } free(cLine); @@ -70,7 +76,7 @@ void SMART::readSMARTData(Drive *drive) } } - drive->setDriveSMARTData(modelFamily, modelName, serial, capacity, errorCount, powerOnHours, powerCycles, temperature); // write data in drive + drive->setDriveSMARTData(modelFamily, modelName, serial, capacity, errorCount, powerOnHours, powerCycles, temperature, reallocatedSectors, pendingSectors, uncorrectableSectors); // write data in drive } /** @@ -309,3 +315,124 @@ bool SMART::parseTemperature(string sLine, uint32_t &temperature) 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) + { + // 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()); + + if (!sLine.empty() && sLine.find_first_not_of("0123456789") == string::npos) + { + reallocatedSectors = stoul(sLine); + return true; + } + } + + return false; +} + +/** + * \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; +} + diff --git a/src/tui.cpp b/src/tui.cpp index 49e01b8..a21bd8c 100644 --- a/src/tui.cpp +++ b/src/tui.cpp @@ -110,10 +110,10 @@ void TUI::updateTUI(list *plistDrives, uint8_t u8SelectedEntry) bSelectedEntry = true; // mark this drive in entries list displaySelectedDrive(*it, u16StdscrX, u16StdscrY); - if ((it->getPowerOnHours() >= WORSE_HOURS) || (it->getPowerCycles() >= WORSE_POWERUP) || (it->getErrorCount() > 0) || (it->getTemperature() >= WORSE_TEMPERATURE)) + if ((it->getPowerOnHours() >= WORSE_HOURS) || (it->getPowerCycles() >= WORSE_POWERUP) || (it->getErrorCount() > 0) || (it->getTemperature() >= WORSE_TEMPERATURE) || (it->getReallocatedSectors() > 0) || (it->getPendingSectors() > 0) || (it->getUncorrectableSectors() > 0)) { // smart values are bad --> show warning - smartWarning = createSmartWarning(50, 10, ((u16StdscrX) - (int)(u16StdscrX / 2) + 35), (int)(u16StdscrY / 2) - 5, it->getPath(), it->getPowerOnHours(), it->getPowerCycles(), it->getErrorCount(), it->getTemperature()); + smartWarning = createSmartWarning(50, 14, ((u16StdscrX) - (int)(u16StdscrX / 2) + 35), (int)(u16StdscrY / 2) - 7, it->getPath(), it->getPowerOnHours(), it->getPowerCycles(), it->getErrorCount(), it->getTemperature(), it->getReallocatedSectors(), it->getPendingSectors(), it->getUncorrectableSectors()); wrefresh(smartWarning); } } @@ -721,7 +721,7 @@ void TUI::displaySelectedDrive(Drive &drive, int stdscrX, int stdscrY) } } -WINDOW *TUI::createSmartWarning(int iXSize, int iYSize, int iXStart, int iYStart, string sPath, uint32_t u32PowerOnHours, uint32_t u32PowerCycles, uint32_t u32ErrorCount, uint32_t u32Temperature) +WINDOW *TUI::createSmartWarning(int iXSize, int iYSize, int iXStart, int iYStart, string sPath, uint32_t u32PowerOnHours, uint32_t u32PowerCycles, uint32_t u32ErrorCount, uint32_t u32Temperature, uint32_t u32ReallocatedSectors, uint32_t u32PendingSectors, uint32_t u32UncorrectableSectors) { WINDOW *newWindow; newWindow = newwin(iYSize, iXSize, iYStart, iXStart); @@ -763,6 +763,28 @@ WINDOW *TUI::createSmartWarning(int iXSize, int iYSize, int iXStart, int iYStart { string sLineTmp = "Drive too hot: " + to_string(u32Temperature) + " C"; mvwaddstr(newWindow, u16Line++, (iXSize / 2) - (sLine01.size() / 2), sLineTmp.c_str()); + u16Line++; } + + if (u32ReallocatedSectors > 0) + { + string sLineTmp = "CRITICAL: Reallocated sectors detected: " + to_string(u32ReallocatedSectors); + mvwaddstr(newWindow, u16Line++, (iXSize / 2) - (sLine01.size() / 2), sLineTmp.c_str()); + u16Line++; + } + + if (u32PendingSectors > 0) + { + string sLineTmp = "CRITICAL: Pending sectors detected: " + to_string(u32PendingSectors); + mvwaddstr(newWindow, u16Line++, (iXSize / 2) - (sLine01.size() / 2), sLineTmp.c_str()); + u16Line++; + } + + if (u32UncorrectableSectors > 0) + { + string sLineTmp = "CRITICAL: Uncorrectable sectors: " + to_string(u32UncorrectableSectors); + mvwaddstr(newWindow, u16Line++, (iXSize / 2) - (sLine01.size() / 2), sLineTmp.c_str()); + } + return newWindow; } -- 2.54.0 From 6b72736f00ac8859d240167d8bb8226f0824d3b8 Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 1 May 2026 13:12:39 +0200 Subject: [PATCH 2/4] Fix error handling if shred failes (#96) fixes https://git.mosad.xyz/localhorst/reHDD/issues/95 Reviewed-on: https://git.mosad.xyz/localhorst/reHDD/pulls/96 Co-authored-by: localhorst Co-committed-by: localhorst --- include/reHDD.h | 2 +- src/shred.cpp | 340 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 277 insertions(+), 65 deletions(-) diff --git a/include/reHDD.h b/include/reHDD.h index 40f93e4..aaad511 100644 --- a/include/reHDD.h +++ b/include/reHDD.h @@ -8,7 +8,7 @@ #ifndef REHDD_H_ #define REHDD_H_ -#define REHDD_VERSION "V1.3.0" +#define REHDD_VERSION "V1.3.1" // Drive handling Settings #define WORSE_HOURS 19200 // mark drive if at this limit or beyond diff --git a/src/shred.cpp b/src/shred.cpp index 6cddd6c..c37299a 100644 --- a/src/shred.cpp +++ b/src/shred.cpp @@ -29,16 +29,20 @@ Shred::~Shred() /** * \brief shred drive with shred - * \param pointer of Drive instance - * \return void + * \param pointer of Drive instance + * \param file descriptor for signaling + * \return 0 on success, -1 on error */ int Shred::shredDrive(Drive *drive, int *ipSignalFd) { ostringstream address; address << (void const *)&(*drive); Logger::logThis()->info("Shred-Task started - Drive: " + drive->getModelName() + "-" + drive->getSerial() + " @" + address.str()); - drive->bWasShredStarted = true; // Mark drive as partly shredded + + // Mark as started but NOT shredded yet + drive->bWasShredStarted = true; drive->bWasShredded = false; + drive->bWasChecked = false; drive->setTaskPercentage(0.0); drive->u32DriveChecksumAfterShredding = UINT32_MAX; drive->state = Drive::TaskState::SHRED_ACTIVE; @@ -46,53 +50,137 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) #ifdef DRYRUN for (int i = 0; i <= 100; i++) { - drive->setTaskPercentage(i + 0.05); + if (drive->state.load() != Drive::TaskState::SHRED_ACTIVE) + { + Logger::logThis()->info("Shred-Task aborted during DRYRUN - Drive: " + drive->getSerial()); + drive->setTaskPercentage(i + 0.05); + drive->state = Drive::TaskState::NONE; + drive->bWasShredded = false; // CRITICAL: Mark as NOT shredded on abort + return -1; + } + drive->setTaskPercentage((double)i); write(*ipSignalFd, "A", 1); usleep(20000); } + + // Only mark as shredded if DRYRUN completed successfully drive->bWasShredded = true; + drive->setTaskPercentage(0.0); + drive->state = Drive::TaskState::NONE; + Logger::logThis()->info("DRYRUN completed - Drive: " + drive->getSerial()); + return 0; #endif #ifndef DRYRUN - const char *cpDrivePath = drive->getPath().c_str(); + string sDrivePath = drive->getPath(); + const char *cpDrivePath = sDrivePath.c_str(); unsigned char ucKey[TFNG_KEY_SIZE]; - // open random source + // Open random source + Logger::logThis()->info("Shred-Task: Opening random source: " + string(randomsrc) + " - Drive: " + drive->getSerial()); randomSrcFileDiscr = open(randomsrc, O_RDONLY | O_LARGEFILE); if (randomSrcFileDiscr == -1) { - std::string errorMsg(strerror(errno)); - Logger::logThis()->error("Shred-Task: Open random source failed! " + errorMsg + " - Drive: " + drive->getSerial()); - perror(randomsrc); - cleanup(); + int savedErrno = errno; + Logger::logThis()->error("Shred-Task: Open random source failed! Path: " + string(randomsrc) + + " - Error: " + strerror(savedErrno) + " (errno: " + to_string(savedErrno) + ")" + + " - Drive: " + drive->getSerial()); + + // Reset drive state on error - NOT shredded + drive->state = Drive::TaskState::NONE; + drive->setTaskPercentage(0.0); + drive->bWasShredStarted = false; + drive->bWasShredded = false; return -1; } + Logger::logThis()->info("Shred-Task: Random source opened successfully (fd: " + to_string(randomSrcFileDiscr) + ") - Drive: " + drive->getSerial()); - // open disk + // Open disk driveFileDiscr = open(cpDrivePath, O_RDWR | O_LARGEFILE); if (driveFileDiscr == -1) { - std::string errorMsg(strerror(errno)); - Logger::logThis()->error("Shred-Task: Open drive failed! " + errorMsg + " - Drive: " + drive->getSerial()); - perror(cpDrivePath); - cleanup(); + int savedErrno = errno; + string errorDetail; + + switch (savedErrno) + { + case ENOMEDIUM: + errorDetail = "No medium found (drive may be empty or disconnected)"; + break; + case EACCES: + errorDetail = "Permission denied (need root/sudo?)"; + break; + case ENOENT: + errorDetail = "Drive not found (device may have been removed)"; + break; + case EROFS: + errorDetail = "Read-only file system"; + break; + case EBUSY: + errorDetail = "Drive is busy (may be mounted or in use)"; + break; + case EINVAL: + errorDetail = "Invalid argument"; + break; + default: + errorDetail = strerror(savedErrno); + break; + } + + Logger::logThis()->error("Shred-Task: Open drive failed! Path: " + string(cpDrivePath) + + " - Error: " + errorDetail + " (errno: " + to_string(savedErrno) + ")" + + " - Drive: " + drive->getSerial() + " - Model: " + drive->getModelName()); + + // Close random source before returning + close(randomSrcFileDiscr); + randomSrcFileDiscr = -1; + + // Reset drive state on error - NOT shredded + drive->state = Drive::TaskState::NONE; + drive->setTaskPercentage(0.0); + drive->bWasShredStarted = false; + drive->bWasShredded = false; return -1; } + Logger::logThis()->info("Shred-Task: Drive opened successfully (fd: " + to_string(driveFileDiscr) + ") - Drive: " + drive->getSerial()); - // read key for random generator + // Read key for random generator + Logger::logThis()->info("Shred-Task: Reading random key - Drive: " + drive->getSerial()); ssize_t readRet = read(randomSrcFileDiscr, ucKey, sizeof(ucKey)); if (readRet <= 0) { - std::string errorMsg(strerror(errno)); - Logger::logThis()->error("Shred-Task: Read random key failed! " + errorMsg + " - Drive: " + drive->getSerial()); - perror(randomsrc); + int savedErrno = errno; + Logger::logThis()->error("Shred-Task: Read random key failed! Expected: " + to_string(sizeof(ucKey)) + + " bytes, Got: " + to_string(readRet) + " bytes" + + " - Error: " + strerror(savedErrno) + " (errno: " + to_string(savedErrno) + ")" + + " - Drive: " + drive->getSerial()); cleanup(); + + // Reset drive state on error - NOT shredded + drive->state = Drive::TaskState::NONE; + drive->setTaskPercentage(0.0); + drive->bWasShredStarted = false; + drive->bWasShredded = false; return -1; } + Logger::logThis()->info("Shred-Task: Random key read successfully (" + to_string(readRet) + " bytes) - Drive: " + drive->getSerial()); tfng_prng_seedkey(ucKey); this->ulDriveByteSize = getDriveSizeInBytes(driveFileDiscr); + if (this->ulDriveByteSize == 0) + { + Logger::logThis()->error("Shred-Task: Drive size is 0 bytes! Drive may be empty or size detection failed - Drive: " + drive->getSerial()); + cleanup(); + + // Reset drive state on error - NOT shredded + drive->state = Drive::TaskState::NONE; + drive->setTaskPercentage(0.0); + drive->bWasShredStarted = false; + drive->bWasShredded = false; + return -1; + } + Drive::ShredSpeed shredSpeed = drive->sShredSpeed.load(); shredSpeed.chronoShredTimestamp = std::chrono::system_clock::now(); // set inital timestamp for speed metric shredSpeed.ulSpeedMetricBytesWritten = 0U; // uses to calculate speed metric @@ -102,9 +190,12 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) Logger::logThis()->info("Shred-Task: Bytes-Size of Drive: " + to_string(this->ulDriveByteSize) + " - Drive: " + drive->getSerial()); #endif + // Main shredding loop for (unsigned int uiShredIterationCounter = 0U; uiShredIterationCounter < SHRED_ITERATIONS; uiShredIterationCounter++) { - unsigned long ulDriveByteCounter = 0U; // used for one shred-iteration to keep track of the current drive position + // Logger::logThis()->info("Shred-Task: Starting iteration " + to_string(uiShredIterationCounter + 1) + "/" + to_string(SHRED_ITERATIONS) + " - Drive: " + drive->getSerial()); + + unsigned long ulDriveByteCounter = 0U; if (uiShredIterationCounter == (SHRED_ITERATIONS - 1)) { @@ -114,11 +205,29 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) while (ulDriveByteCounter < ulDriveByteSize) { - int iBytesToShred = 0; // Bytes that will be overwritten in this chunk-iteration + // Check if task was aborted + if (drive->state.load() != Drive::TaskState::SHRED_ACTIVE) + { + Logger::logThis()->info("Shred-Task: Aborted by user at " + to_string(d32Percent) + + "% in iteration " + to_string(uiShredIterationCounter + 1) + + " - Drive: " + drive->getSerial()); + drive->setTaskPercentage(0); + d32Percent = 0.00; + d32TmpPercent = 0.00; + cleanup(); + + // CRITICAL: Mark as NOT shredded on abort + drive->state = Drive::TaskState::NONE; + drive->bWasShredded = false; + drive->bWasChecked = false; + return -1; + } + + int iBytesToShred = 0; if (uiShredIterationCounter != (SHRED_ITERATIONS - 1)) { - // NOT last shred iteration --> generate new random data + // Generate random data for this chunk tfng_prng_genrandom(caTfngData, TFNG_DATA_SIZE); } @@ -135,10 +244,20 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) if (iByteShredded <= 0) { - std::string errorMsg(strerror(errno)); - Logger::logThis()->error("Shred-Task: Write to drive failed! " + errorMsg + " - Drive: " + drive->getSerial()); - perror("unable to write random data"); + int savedErrno = errno; + Logger::logThis()->error("Shred-Task: Write to drive failed! Attempted: " + to_string(iBytesToShred) + + " bytes, Written: " + to_string(iByteShredded) + " bytes" + + " - Position: " + to_string(ulDriveByteCounter) + "/" + to_string(ulDriveByteSize) + + " - Iteration: " + to_string(uiShredIterationCounter + 1) + "/" + to_string(SHRED_ITERATIONS) + + " - Error: " + strerror(savedErrno) + " (errno: " + to_string(savedErrno) + ")" + + " - Drive: " + drive->getSerial()); cleanup(); + + // CRITICAL: Mark as NOT shredded on write failure + drive->state = Drive::TaskState::NONE; + drive->setTaskPercentage(0.0); + drive->bWasShredded = false; + drive->bWasChecked = false; return -1; } @@ -150,7 +269,10 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) ulDriveByteOverallCount += iByteShredded; d32Percent = this->calcProgress(); #ifdef LOG_LEVEL_HIGH - Logger::logThis()->info("Shred-Task: ByteCount: " + to_string(ulDriveByteCounter) + " - iteration: " + to_string((uiShredIterationCounter + 1)) + " - progress: " + to_string(d32Percent) + " - Drive: " + drive->getSerial()); + Logger::logThis()->info("Shred-Task: ByteCount: " + to_string(ulDriveByteCounter) + + " - iteration: " + to_string((uiShredIterationCounter + 1)) + + " - progress: " + to_string(d32Percent) + "%" + + " - Drive: " + drive->getSerial()); #endif if ((d32Percent - d32TmpPercent) >= 0.01) @@ -158,36 +280,37 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) // set shred percantage drive->setTaskPercentage(d32TmpPercent); d32TmpPercent = d32Percent; - // signal process in shreding + // signal process in shredding write(*ipSignalFd, "A", 1); } - - if (drive->state != Drive::TaskState::SHRED_ACTIVE) - { - drive->setTaskPercentage(0); - d32Percent = 0.00; - d32TmpPercent = 0.00; - ulDriveByteCounter = 0U; - Logger::logThis()->info("Aborted shred for: " + drive->getModelName() + "-" + drive->getSerial()); - cleanup(); - return -1; - } - // end one chunk write } + + Logger::logThis()->info("Shred-Task: Iteration " + to_string(uiShredIterationCounter + 1) + "/" + + to_string(SHRED_ITERATIONS) + " completed - Drive: " + drive->getSerial()); + + // Rewind drive for next iteration if (0 != iRewindDrive(driveFileDiscr)) { - Logger::logThis()->error("Shred-Task: Unable to rewind drive! - Drive: " + drive->getSerial()); + Logger::logThis()->error("Shred-Task: Unable to rewind drive after iteration " + + to_string(uiShredIterationCounter + 1) + " - Drive: " + drive->getSerial()); cleanup(); + + // CRITICAL: Mark as NOT shredded on rewind failure + drive->state = Drive::TaskState::NONE; + drive->setTaskPercentage(0.0); + drive->bWasShredded = false; + drive->bWasChecked = false; return -1; } - // end one shred iteration } - // end of all shred iteratio - tfng_prng_seedkey(NULL); // reset random generator + // All shred iterations completed successfully + tfng_prng_seedkey(NULL); + // ONLY mark as shredded if ALL iterations completed AND fsync succeeded drive->bWasShredded = true; - Logger::logThis()->info("Shred-Task finished - Drive: " + drive->getModelName() + "-" + drive->getSerial() + " @" + address.str()); + Logger::logThis()->info("Shred-Task finished successfully - Drive: " + drive->getModelName() + "-" + drive->getSerial() + " @" + address.str()); + #ifdef ZERO_CHECK drive->state = Drive::TaskState::CHECK_ACTIVE; Logger::logThis()->info("Check-Task started - Drive: " + drive->getModelName() + "-" + drive->getSerial() + " @" + address.str()); @@ -196,33 +319,49 @@ int Shred::shredDrive(Drive *drive, int *ipSignalFd) if (drive->u32DriveChecksumAfterShredding != 0) { drive->state = Drive::TaskState::CHECK_FAILED; - Logger::logThis()->info("Shred-Task: Checksum not zero: " + to_string(drive->u32DriveChecksumAfterShredding) + " - Drive: " + drive->getSerial()); + Logger::logThis()->error("Check-Task: Checksum verification failed! Expected: 0, Got: " + + to_string(drive->u32DriveChecksumAfterShredding) + " - Drive: " + drive->getSerial()); } else { drive->state = Drive::TaskState::CHECK_SUCCESSFUL; - Logger::logThis()->info("Shred-Task: Checksum zero: " + to_string(drive->u32DriveChecksumAfterShredding) + " - Drive: " + drive->getSerial()); + drive->bWasChecked = true; + Logger::logThis()->info("Check-Task: Checksum verification passed (zero) - Drive: " + drive->getSerial()); } #endif + cleanup(); #endif - if ((drive->state.load() == Drive::TaskState::SHRED_ACTIVE) || (drive->state.load() == Drive::TaskState::CHECK_SUCCESSFUL) || (drive->state == Drive::TaskState::CHECK_FAILED)) + // Final state handling - ONLY process if shred actually completed + Drive::TaskState finalState = drive->state.load(); + + // Only do final processing if we reached a completion state + // (not if we returned early with errors) + if ((finalState == Drive::TaskState::SHRED_ACTIVE) || + (finalState == Drive::TaskState::CHECK_SUCCESSFUL) || + (finalState == Drive::TaskState::CHECK_FAILED)) { - if (drive->state != Drive::TaskState::CHECK_FAILED) + if (finalState != Drive::TaskState::CHECK_FAILED) { + Logger::logThis()->info("Shred-Task: Triggering print for drive - Drive: " + drive->getSerial()); Printer::getPrinter()->print(drive); } + else + { + Logger::logThis()->warning("Shred-Task: Skipping print due to checksum failure - Drive: " + drive->getSerial()); + } + drive->state = Drive::TaskState::NONE; drive->setTaskPercentage(0.0); - Logger::logThis()->info("Finished shred/check for: " + drive->getModelName() + "-" + drive->getSerial()); + Logger::logThis()->info("Completed shred/check for: " + drive->getModelName() + "-" + drive->getSerial()); } + return 0; } + /** * \brief calc shredding progress in % - * \param current byte index of the drive - * \param current shred iteration * \return double percentage */ double Shred::calcProgress() @@ -232,54 +371,95 @@ double Shred::calcProgress() #ifdef ZERO_CHECK uiMaxShredIteration++; // increment because we will check after SHRED_ITERATIONS the drive for non-zero bytes #endif + if (this->ulDriveByteSize == 0) return 0.0; - return (double)(((double)ulDriveByteOverallCount) / ((double)this->ulDriveByteSize * uiMaxShredIteration)) * 100.0f; + + return (double)(((double)ulDriveByteOverallCount) / ((double)this->ulDriveByteSize * uiMaxShredIteration)) * 100.0; } +/** + * \brief rewind drive to beginning + * \param file descriptor + * \return 0 on success, -1 on error + */ int Shred::iRewindDrive(fileDescriptor file) { - if (0 != lseek(file, 0L, SEEK_SET)) + off_t result = lseek(file, 0L, SEEK_SET); + + if (result == -1) { - perror("unable to rewind drive"); - Logger::logThis()->info("Unable to rewind drive! - fileDescriptor: " + to_string(file)); + int savedErrno = errno; + Logger::logThis()->error("Unable to rewind drive! Error: " + string(strerror(savedErrno)) + + " (errno: " + to_string(savedErrno) + ") - fileDescriptor: " + to_string(file)); return -1; } - else + else if (result != 0) { - return 0; + Logger::logThis()->error("Rewind position mismatch! Expected: 0, Got: " + to_string(result) + + " - fileDescriptor: " + to_string(file)); + return -1; } + + return 0; } +/** + * \brief get drive size in bytes + * \param file descriptor + * \return size in bytes, 0 on error + */ long Shred::getDriveSizeInBytes(fileDescriptor file) { - long liDriveSizeTmp = lseek(file, 0L, SEEK_END); + off_t liDriveSizeTmp = lseek(file, 0L, SEEK_END); if (liDriveSizeTmp == -1) { - perror("unable to get drive size"); - Logger::logThis()->info("Unable to get drive size! - fileDescriptor: " + to_string(file)); + int savedErrno = errno; + Logger::logThis()->error("Unable to get drive size! Error: " + string(strerror(savedErrno)) + + " (errno: " + to_string(savedErrno) + ") - fileDescriptor: " + to_string(file)); return 0L; } if (0 != iRewindDrive(file)) { - liDriveSizeTmp = 0L; + Logger::logThis()->error("Unable to rewind after size detection - fileDescriptor: " + to_string(file)); + return 0L; } #ifdef DEMO_DRIVE_SIZE liDriveSizeTmp = DEMO_DRIVE_SIZE; + Logger::logThis()->info("DEMO_DRIVE_SIZE active - using size: " + to_string(liDriveSizeTmp) + " bytes"); #endif + return liDriveSizeTmp; } +/** + * \brief calculate checksum of drive (verify all zeros) + * \param file descriptor + * \param pointer to Drive instance + * \param signal file descriptor + * \return checksum value (0 = all zeros) + */ unsigned int Shred::uiCalcChecksum(fileDescriptor file, Drive *drive, int *ipSignalFd) { unsigned int uiChecksum = 0; unsigned long ulDriveByteCounter = 0U; + + Logger::logThis()->info("Check-Task: Starting checksum verification - Drive: " + drive->getSerial()); + while (ulDriveByteCounter < ulDriveByteSize) { + // Check if task was aborted + if (drive->state.load() != Drive::TaskState::CHECK_ACTIVE) + { + Logger::logThis()->info("Check-Task: Aborted by user at " + to_string(d32Percent) + "% - Drive: " + drive->getSerial()); + return UINT32_MAX; // Return non-zero to indicate incomplete check + } + int iBytesToCheck = 0; + if ((ulDriveByteSize - ulDriveByteCounter) < CHUNK_SIZE) { iBytesToCheck = (ulDriveByteSize - ulDriveByteCounter); @@ -289,6 +469,18 @@ unsigned int Shred::uiCalcChecksum(fileDescriptor file, Drive *drive, int *ipSig iBytesToCheck = CHUNK_SIZE; } int iReadBytes = read(file, caReadBuffer, iBytesToCheck); + + if (iReadBytes <= 0) + { + int savedErrno = errno; + Logger::logThis()->error("Check-Task: Read failed! Attempted: " + to_string(iBytesToCheck) + + " bytes, Read: " + to_string(iReadBytes) + " bytes" + + " - Position: " + to_string(ulDriveByteCounter) + "/" + to_string(ulDriveByteSize) + + " - Error: " + strerror(savedErrno) + " (errno: " + to_string(savedErrno) + ")" + + " - Drive: " + drive->getSerial()); + return UINT32_MAX; // Return non-zero to indicate read failure + } + for (int iReadBytesCounter = 0U; iReadBytesCounter < iReadBytes; iReadBytesCounter++) { uiChecksum += caReadBuffer[iReadBytesCounter]; @@ -301,7 +493,10 @@ unsigned int Shred::uiCalcChecksum(fileDescriptor file, Drive *drive, int *ipSig drive->sShredSpeed.store(shredSpeed); #ifdef LOG_LEVEL_HIGH - Logger::logThis()->info("Shred-Task (Checksum): ByteCount: " + to_string(ulDriveByteCounter) + " - progress: " + to_string(d32Percent) + " - Drive: " + drive->getSerial()); + Logger::logThis()->info("Check-Task: ByteCount: " + to_string(ulDriveByteCounter) + + " - progress: " + to_string(d32Percent) + "%" + + " - checksum so far: " + to_string(uiChecksum) + + " - Drive: " + drive->getSerial()); #endif if (((d32Percent - d32TmpPercent) >= 0.01) || (d32Percent == 100.0)) @@ -314,12 +509,29 @@ unsigned int Shred::uiCalcChecksum(fileDescriptor file, Drive *drive, int *ipSig write(*ipSignalFd, "A", 1); } } + + Logger::logThis()->info("Check-Task: Verification complete - Final checksum: " + to_string(uiChecksum) + " - Drive: " + drive->getSerial()); drive->bWasChecked = true; + return uiChecksum; } +/** + * \brief cleanup - close file descriptors + */ void Shred::cleanup() { - close(driveFileDiscr); - close(randomSrcFileDiscr); + if (driveFileDiscr != -1) + { + Logger::logThis()->info("Shred-Task: Closing drive file descriptor: " + to_string(driveFileDiscr)); + close(driveFileDiscr); + driveFileDiscr = -1; + } + + if (randomSrcFileDiscr != -1) + { + Logger::logThis()->info("Shred-Task: Closing random source file descriptor: " + to_string(randomSrcFileDiscr)); + close(randomSrcFileDiscr); + randomSrcFileDiscr = -1; + } } \ No newline at end of file -- 2.54.0 From 203b4a0c856a8080dbc7efd93829bd812ea7b7a2 Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 1 May 2026 14:52:16 +0200 Subject: [PATCH 3/4] 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; -} - -- 2.54.0 From 753dde833fa7f588af1be1eea9d2b577405934ac Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 1 May 2026 15:01:49 +0200 Subject: [PATCH 4/4] fix parsing and set dev version --- include/reHDD.h | 4 +- src/smart.cpp | 128 +++++++++++++++++++++++++----------------------- 2 files changed, 69 insertions(+), 63 deletions(-) diff --git a/include/reHDD.h b/include/reHDD.h index aaad511..d63f7f0 100644 --- a/include/reHDD.h +++ b/include/reHDD.h @@ -8,7 +8,7 @@ #ifndef REHDD_H_ #define REHDD_H_ -#define REHDD_VERSION "V1.3.1" +#define REHDD_VERSION "V1.4.0-dev" // Drive handling Settings #define WORSE_HOURS 19200 // mark drive if at this limit or beyond @@ -20,7 +20,7 @@ // Logger Settings #define LOG_PATH "./reHDD.log" -#define DESCRIPTION "reHDD - Copyright Hendrik Schutter 2025" +#define DESCRIPTION "reHDD - Copyright Hendrik Schutter 2026" #define DEVICE_ID "generic" #define SOFTWARE_VERSION REHDD_VERSION #define HARDWARE_VERSION "generic" diff --git a/src/smart.cpp b/src/smart.cpp index b444975..0733565 100644 --- a/src/smart.cpp +++ b/src/smart.cpp @@ -19,31 +19,31 @@ struct SMARTParseContext 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 + 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), @@ -69,15 +69,15 @@ 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); } @@ -91,18 +91,19 @@ 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'; }), + [](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); @@ -128,54 +129,54 @@ static void processLine(const string &line, SMARTParseContext &ctx) { 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) { @@ -183,7 +184,7 @@ static void processLine(const string &line, SMARTParseContext &ctx) ctx.temperature = extractIntegerValue(line); return; } - + // State machine for SMART attributes parsing switch (ctx.state) { @@ -205,7 +206,7 @@ static void processLine(const string &line, SMARTParseContext &ctx) ctx.currentAttributeId = 198; } break; - + case SMARTParseContext::IN_ATTRIBUTE_5: case SMARTParseContext::IN_ATTRIBUTE_197: case SMARTParseContext::IN_ATTRIBUTE_198: @@ -214,20 +215,21 @@ static void processLine(const string &line, SMARTParseContext &ctx) { ctx.state = SMARTParseContext::IN_RAW_SECTION; } - // Look for end of attribute object - else if (trimmed.find("},") == 0 || trimmed.find("}") == 0) + // Look for end of attribute object (more indented closing brace = end of attribute) + // " }," or " }" at attribute level (6 spaces) + else if (line.find(" },") == 0 || line.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) { @@ -241,11 +243,16 @@ static void processLine(const string &line, SMARTParseContext &ctx) { 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; + + // Stay in raw section - closing brace will exit + } + // Look for end of raw object (less indented = back to attribute level) + // " }" at raw level (8 spaces) + else if (line.find(" }") == 0) + { + // Return to attribute state (raw section closed) + ctx.state = (ctx.currentAttributeId == 5) ? SMARTParseContext::IN_ATTRIBUTE_5 : (ctx.currentAttributeId == 197) ? SMARTParseContext::IN_ATTRIBUTE_197 + : SMARTParseContext::IN_ATTRIBUTE_198; } break; } @@ -260,17 +267,17 @@ void SMART::readSMARTData(Drive *drive) { 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 -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 + " --json -d usbsunplus -a " // USB Sunplus }; - + for (const string &sSmartctlCommand : sSmartctlCommands) { // Build command with timeout @@ -278,9 +285,9 @@ void SMART::readSMARTData(Drive *drive) sCMD.append(sSmartctlCommand); sCMD.append(drive->getPath()); // 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) @@ -288,42 +295,42 @@ void SMART::readSMARTData(Drive *drive) 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); 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) { Logger::logThis()->warning("SMART: Command timed out (5s) - skipping to next variant"); continue; } - + // 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 @@ -339,12 +346,12 @@ void SMART::readSMARTData(Drive *drive) 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 @@ -352,18 +359,18 @@ void SMART::readSMARTData(Drive *drive) Logger::logThis()->warning("SMART: No valid data parsed (exit status: " + to_string(exitStatus) + ")"); } } - + // Check if we got ANY data if (ctx.modelName.empty() && ctx.serial.empty()) { Logger::logThis()->warning("SMART: No SMART data available for this drive - may not support SMART or need root privileges"); - + // 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"; } - + // Write parsed data to drive drive->setDriveSMARTData( ctx.modelFamily, @@ -376,6 +383,5 @@ void SMART::readSMARTData(Drive *drive) ctx.temperature, ctx.reallocatedSectors, ctx.pendingSectors, - ctx.uncorrectableSectors - ); + ctx.uncorrectableSectors); } -- 2.54.0