From 5478a871f1dcd1c29727907807d5dcd2dc4986fb Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 1 May 2026 13:12:39 +0200 Subject: [PATCH 1/2] 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 From 55481b86fd97c28aeb6f525f4045a8ee3bc2e03a Mon Sep 17 00:00:00 2001 From: localhorst Date: Fri, 1 May 2026 15:03:15 +0200 Subject: [PATCH 2/2] Show HDD warnings based on sectors (#97) If one of the following metrics is >0 an warning is shown * Reallocated_Sector_Count * Current_Pending_Sector * Offline_Uncorrectable Reviewed-on: https://git.mosad.xyz/localhorst/reHDD/pulls/97 Co-authored-by: localhorst Co-committed-by: localhorst --- include/drive.h | 13 +- include/reHDD.h | 4 +- include/smart.h | 29 ++- include/tui.h | 2 +- src/drive.cpp | 23 +- src/reHDD.cpp | 2 +- src/smart.cpp | 612 +++++++++++++++++++++++++++--------------------- src/tui.cpp | 28 ++- 8 files changed, 423 insertions(+), 290 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/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/include/smart.h b/include/smart.h index 2d4f4fd..bafccc2 100644 --- a/include/smart.h +++ b/include/smart.h @@ -10,24 +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); + SMART(void); // Utility class - no instances }; -#endif // SMART_H_ \ No newline at end of file +#endif // SMART_H_ 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..0733565 100644 --- a/src/smart.cpp +++ b/src/smart.cpp @@ -6,306 +6,382 @@ */ #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 (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) + { + 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); + } + + // 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; + } +} + /** * \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; + SMARTParseContext ctx; + uint8_t exitStatus = 255U; - modelFamily.clear(); - modelName.clear(); - serial.clear(); + // 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 + }; - string sSmartctlCommands[] = {" --json -a ", " --json -d sntjmicron -a ", " --json -d sntasmedia -a ", " --json -d sntrealtek -a ", " --json -d sat -a "}; - - for (string sSmartctlCommand : sSmartctlCommands) + 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(); + // Note: stderr NOT suppressed for debugging - // Logger::logThis()->info(cpComand); + Logger::logThis()->info("SMART: Executing: " + sCMD); - 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) + // Execute smartctl with timeout protection + FILE *outputfileSmart = popen(sCMD.c_str(), "r"); + if (outputfileSmart == nullptr) { - string sLine = string(cLine); + Logger::logThis()->error("SMART: Failed to execute smartctl"); + continue; + } - 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); + // 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); + int pcloseStatus = pclose(outputfileSmart); - if (status == 0U) + 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); // 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; - } -} + 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)); -/** - * \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; - } -} + if (exitStatus != 0) + { + Logger::logThis()->info("SMART: Note - exit status " + to_string(exitStatus) + " indicates warnings/errors in SMART log"); + } -/** - * \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 + 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 + + // Check if we got ANY data + if (ctx.modelName.empty() && ctx.serial.empty()) { - return false; + 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, + ctx.modelName, + ctx.serial, + ctx.capacity, + ctx.errorCount, + ctx.powerOnHours, + ctx.powerCycles, + ctx.temperature, + ctx.reallocatedSectors, + ctx.pendingSectors, + ctx.uncorrectableSectors); } 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; }