From 5ba969c561dc3bd462d5eb2ba193da104f128cbc Mon Sep 17 00:00:00 2001 From: Alexander Shaduri Date: Tue, 14 May 2024 14:27:24 +0400 Subject: [PATCH] NVMe: Support self-test log and running self-tests (untested). --- src/applib/selftest.cpp | 321 +++++++++++++++++++++++++------- src/applib/selftest.h | 93 ++++++--- src/applib/storage_property.cpp | 24 --- src/applib/storage_property.h | 11 -- src/gsc_info_window.cpp | 79 +++++--- 5 files changed, 366 insertions(+), 162 deletions(-) diff --git a/src/applib/selftest.cpp b/src/applib/selftest.cpp index 6ac1e4e..41198e8 100644 --- a/src/applib/selftest.cpp +++ b/src/applib/selftest.cpp @@ -13,9 +13,16 @@ Copyright: #include // std::max, std::min #include // std::floor #include +#include #include +#include +#include +#include #include "app_pcrecpp.h" +#include "smartctl_parser_types.h" +#include "smartctl_parser.h" +#include "storage_device_detected_type.h" #include "storage_property.h" #include "smartctl_text_ata_parser.h" #include "selftest.h" @@ -24,10 +31,29 @@ Copyright: +SelfTestStatusSeverity get_self_test_status_severity(SelfTestStatus s) +{ + static const std::unordered_map m { + {SelfTestStatus::Unknown, SelfTestStatusSeverity::None}, + {SelfTestStatus::CompletedNoError, SelfTestStatusSeverity::None}, + {SelfTestStatus::ManuallyAborted, SelfTestStatusSeverity::Warning}, + {SelfTestStatus::Interrupted, SelfTestStatusSeverity::Warning}, + {SelfTestStatus::CompletedWithError, SelfTestStatusSeverity::Error}, + {SelfTestStatus::InProgress, SelfTestStatusSeverity::None}, + {SelfTestStatus::Reserved, SelfTestStatusSeverity::None}, + }; + if (auto iter = m.find(s); iter != m.end()) { + return iter->second; + } + return SelfTestStatusSeverity::None; +} + + + std::string SelfTest::get_test_displayable_name(SelfTest::TestType type) { static const std::unordered_map m { - {TestType::ImmediateOffline, _("Immediate Offline Test")}, +// {TestType::ImmediateOffline, _("Immediate Offline Test")}, {TestType::ShortTest, _("Short Self-Test")}, {TestType::LongTest, _("Extended Self-Test")}, {TestType::Conveyance, _("Conveyance Self-Test")}, @@ -40,6 +66,20 @@ std::string SelfTest::get_test_displayable_name(SelfTest::TestType type) +bool SelfTest::is_active() const +{ + return (status_ == SelfTestStatus::InProgress); +} + + + +int8_t SelfTest::get_remaining_percent() const +{ + return remaining_percent_; +} + + + // Returns estimated time of completion for the test. returns -1 if n/a or unknown. 0 is a valid value. std::chrono::seconds SelfTest::get_remaining_seconds() const { @@ -58,6 +98,27 @@ std::chrono::seconds SelfTest::get_remaining_seconds() const +SelfTest::TestType SelfTest::get_test_type() const +{ + return type_; +} + + + +SelfTestStatus SelfTest::get_status() const +{ + return status_; +} + + + +std::chrono::seconds SelfTest::get_poll_in_seconds() const +{ + return poll_in_seconds_; +} + + + // a drive reports a constant "test duration during idle" capability. std::chrono::seconds SelfTest::get_min_duration_seconds() const { @@ -69,9 +130,14 @@ std::chrono::seconds SelfTest::get_min_duration_seconds() const if (total_duration_ != -1s) // cache return total_duration_; + if (drive_->get_detected_type() == StorageDeviceDetectedType::Nvme) { + return -1s; // NVMe doesn't report this. + } + + // ATA std::string prop_name; switch(type_) { - case TestType::ImmediateOffline: prop_name = "ata_smart_data/offline_data_collection/completion_seconds"; break; +// case TestType::ImmediateOffline: prop_name = "ata_smart_data/offline_data_collection/completion_seconds"; break; case TestType::ShortTest: prop_name = "ata_smart_data/self_test/polling_minutes/short"; break; case TestType::LongTest: prop_name = "ata_smart_data/self_test/polling_minutes/extended"; break; case TestType::Conveyance: prop_name = "ata_smart_data/self_test/polling_minutes/conveyance"; break; @@ -91,63 +157,87 @@ bool SelfTest::is_supported() const if (!drive_) return false; - std::string prop_name; - switch(type_) { - case TestType::ImmediateOffline: - // prop_name = "ata_smart_data/capabilities/exec_offline_immediate_supported"; - // break; - return false; // disable this for now - it's unsupported. - case TestType::ShortTest: - case TestType::LongTest: // same for short and long - prop_name = "ata_smart_data/capabilities/self_tests_supported"; - break; - case TestType::Conveyance: prop_name = "ata_smart_data/capabilities/conveyance_self_test_supported"; break; + if (drive_->get_detected_type() == StorageDeviceDetectedType::Nvme) { + switch (type_) { +// case TestType::ImmediateOffline: + case TestType::Conveyance: + return false; // not supported by nvme + case TestType::ShortTest: + case TestType::LongTest: + // NVMe spec + return true; + } + + } else if (drive_->get_detected_type() == StorageDeviceDetectedType::AtaAny + || drive_->get_detected_type() == StorageDeviceDetectedType::AtaHdd + || drive_->get_detected_type() == StorageDeviceDetectedType::AtaSsd) { + + // Find appropriate capability + std::string prop_name; + switch(type_) { +// case TestType::ImmediateOffline: + // prop_name = "ata_smart_data/capabilities/exec_offline_immediate_supported"; + // break; +// return false; // disable this for now - it's unsupported by this application. + case TestType::ShortTest: + case TestType::LongTest: // same for short and long + prop_name = "ata_smart_data/capabilities/self_tests_supported"; + break; + case TestType::Conveyance: + prop_name = "ata_smart_data/capabilities/conveyance_self_test_supported"; + break; + } + + const StorageProperty p = drive_->get_property_repository().lookup_property(prop_name); + return (!p.empty() && p.get_value()); } - const StorageProperty p = drive_->get_property_repository().lookup_property(prop_name); - return (!p.empty() && p.get_value()); + return false; } // start the test -hz::ExpectedVoid SelfTest::start(const std::shared_ptr& smartctl_ex) +hz::ExpectedVoid SelfTest::start(const std::shared_ptr& smartctl_ex) { if (!drive_) { - return hz::Unexpected(SelfTestError::InternalError, _("Internal Error: Drive must not be NULL.")); + return hz::Unexpected(SelfTestExecutionError::InternalError, _("Internal Error: Drive must not be NULL.")); } if (drive_->get_test_is_active()) { - return hz::Unexpected(SelfTestError::AlreadyRunning, _("A test is already running on this drive.")); + return hz::Unexpected(SelfTestExecutionError::AlreadyRunning, _("A test is already running on this drive.")); } if (!this->is_supported()) { // Translators: {} is a test name - Short test, etc. - return hz::Unexpected(SelfTestError::UnsupportedTest, + return hz::Unexpected(SelfTestExecutionError::UnsupportedTest, std::vformat(_("{} is unsupported by this drive."), std::make_format_args(get_test_displayable_name(type_)))); } std::string test_param; switch(type_) { - case TestType::ImmediateOffline: test_param = "offline"; break; +// case TestType::ImmediateOffline: test_param = "offline"; break; case TestType::ShortTest: test_param = "short"; break; case TestType::LongTest: test_param = "long"; break; case TestType::Conveyance: test_param = "conveyance"; break; // no default - this way we get warned by compiler if we're not listing all of them. } if (test_param.empty()) { - return hz::Unexpected(SelfTestError::InvalidTestType, _("Invalid test specified.")); + return hz::Unexpected(SelfTestExecutionError::InvalidTestType, _("Invalid test specified.")); } std::string output; auto execute_status = drive_->execute_device_smartctl("--test=" + test_param, smartctl_ex, output); - if (!execute_status) { - return hz::Unexpected(SelfTestError::CommandFailed, + if (!execute_status.has_value()) { + return hz::Unexpected(SelfTestExecutionError::CommandFailed, std::vformat(_("Sending command to drive failed: {}"), std::make_format_args(execute_status.error().message()))); } - if (!app_pcre_match(R"(/^Drive command .* successful\.\nTesting has begun\.$/mi)", output)) { - return hz::Unexpected(SelfTestError::CommandUnknownError, _("Sending command to drive failed.")); + bool ata_test_started = app_pcre_match(R"(/^Drive command .* successful\.\nTesting has begun\.$/mi)", output); + bool nvme_test_started = app_pcre_match(R"(/^Self-test has begun$/mi)", output); + + if (!ata_test_started && !nvme_test_started) { + return hz::Unexpected(SelfTestExecutionError::CommandUnknownError, _("Sending command to drive failed.")); } // update our members @@ -160,7 +250,7 @@ hz::ExpectedVoid SelfTest::start(const std::shared_ptr90 timer reset. this way we won't be looking at @@ -178,40 +268,40 @@ hz::ExpectedVoid SelfTest::start(const std::shared_ptr SelfTest::force_stop(const std::shared_ptr& smartctl_ex) +hz::ExpectedVoid SelfTest::force_stop(const std::shared_ptr& smartctl_ex) { if (!drive_) { - return hz::Unexpected(SelfTestError::InternalError, _("Internal Error: Drive must not be NULL.")); + return hz::Unexpected(SelfTestExecutionError::InternalError, _("Internal Error: Drive must not be NULL.")); } if (!drive_->get_test_is_active()) { - return hz::Unexpected(SelfTestError::NotRunning, _("No test is currently running on this drive.")); + return hz::Unexpected(SelfTestExecutionError::NotRunning, _("No test is currently running on this drive.")); } // To abort immediate offline test, the device MUST have // "Abort Offline collection upon new command" capability, // any command (e.g. "--abort") will abort it. If it has "Suspend Offline...", // there's no way to abort such test. - if (type_ == TestType::ImmediateOffline) { - const StorageProperty p = drive_->get_property_repository().lookup_property( - "ata_smart_data/capabilities/offline_is_aborted_upon_new_cmd"); - if (!p.empty() && p.get_value()) { // if empty, give a chance to abort anyway. - return hz::Unexpected(SelfTestError::StopUnsupported, _("Aborting this test is unsupported by the drive.")); - } - // else, proceed as any other test - } +// if (type_ == TestType::ImmediateOffline) { +// const StorageProperty p = drive_->get_property_repository().lookup_property( +// "ata_smart_data/capabilities/offline_is_aborted_upon_new_cmd"); +// if (!p.empty() && p.get_value()) { // if empty, give a chance to abort anyway. +// return hz::Unexpected(SelfTestError::StopUnsupported, _("Aborting this test is unsupported by the drive.")); +// } +// // else, proceed as any other test +// } // To abort non-captive short, long and conveyance tests, use "--abort". std::string output; auto execute_status = drive_->execute_device_smartctl("--abort", smartctl_ex, output); if (!execute_status) { - return hz::Unexpected(SelfTestError::CommandFailed, + return hz::Unexpected(SelfTestExecutionError::CommandFailed, std::vformat(_("Sending command to drive failed: {}"), std::make_format_args(execute_status.error().message()))); } // this command prints success even if no test was running. if (!app_pcre_match("/^Self-testing aborted!$/mi", output)) { - return hz::Unexpected(SelfTestError::CommandUnknownError, _("Sending command to drive failed.")); + return hz::Unexpected(SelfTestExecutionError::CommandUnknownError, _("Sending command to drive failed.")); } // update our members @@ -219,8 +309,8 @@ hz::ExpectedVoid SelfTest::force_stop(const std::shared_ptr SelfTest::force_stop(const std::shared_ptr SelfTest::force_stop(const std::shared_ptr SelfTest::update(const std::shared_ptr& smartctl_ex) +hz::ExpectedVoid SelfTest::update(const std::shared_ptr& smartctl_ex) { using namespace std::literals; if (!drive_) { - return hz::Unexpected(SelfTestError::InternalError, _("Internal Error: Drive must not be NULL.")); + return hz::Unexpected(SelfTestExecutionError::InternalError, _("Internal Error: Drive must not be NULL.")); } std::string output; -// std::string error_message = drive_->execute_device_smartctl("--log=selftest", smartctl_ex, output); - auto execute_status = drive_->execute_device_smartctl("--capabilities", smartctl_ex, output); + // ATA shows status in capabilities; NVMe shows it in self-test log. + auto execute_status = drive_->execute_device_smartctl("--capabilities --log=selftest", smartctl_ex, output); if (!execute_status) { - return hz::Unexpected(SelfTestError::CommandFailed, + return hz::Unexpected(SelfTestExecutionError::CommandFailed, std::vformat(_("Sending command to drive failed: {}"), std::make_format_args(execute_status.error().message()))); } - auto parser = SmartctlParser::create(SmartctlParserType::Ata, SmartctlVersionParser::get_default_format(SmartctlParserType::Ata)); - DBG_ASSERT_RETURN(parser, hz::Unexpected(SelfTestError::ParseError, _("Cannot create parser."))); + + std::shared_ptr parser; + if (drive_->get_detected_type() == StorageDeviceDetectedType::Nvme) { + parser = SmartctlParser::create(SmartctlParserType::Nvme, SmartctlVersionParser::get_default_format(SmartctlParserType::Nvme)); + } else { + parser = SmartctlParser::create(SmartctlParserType::Ata, SmartctlVersionParser::get_default_format(SmartctlParserType::Ata)); + } + + DBG_ASSERT_RETURN(parser, hz::Unexpected(SelfTestExecutionError::ParseError, _("Cannot create parser."))); auto parse_status = parser->parse(output); if (!parse_status) { - return hz::Unexpected(SelfTestError::ParseError, + return hz::Unexpected(SelfTestExecutionError::ParseError, std::vformat(_("Cannot parse smartctl output: {}"), std::make_format_args(parse_status.error().message()))); } - auto property_repo = StoragePropertyProcessor::process_properties( + const auto property_repo = StoragePropertyProcessor::process_properties( parser->get_property_repository(), drive_->get_detected_type()); - // Note: Since the self-test log is sometimes late - // and in undetermined order (sorting by hours is too rough), - // we use the "self-test status" capability. - StorageProperty p; - for (const auto& e : property_repo.get_properties()) { - if (e.is_value_type() || e.get_value().test_num != 0 - || e.generic_name != "ata_smart_data/self_test/status/_merged") - continue; - p = e; + + if (drive_->get_detected_type() == StorageDeviceDetectedType::Nvme) { + + const StorageProperty current_operation = property_repo.lookup_property("nvme_self_test_log/current_self_test_operation/value/_decoded"); + + // If no test is active, the property may be absent, or set to None. + if (!current_operation.empty() + && current_operation.get_value() != NvmeSelfTestCurrentOperationTypeExt::get_storable_name(NvmeSelfTestCurrentOperationType::None)) { + status_ = SelfTestStatus::InProgress; + + auto remaining_percent = property_repo.lookup_property("nvme_self_test_log/current_self_test_operation/current_self_test_completion_percent"); + if (!remaining_percent.empty()) { + remaining_percent_ = static_cast(100 - remaining_percent.get_value()); + } + } else { // no test is active + // The first self-test table entry is the latest. + std::optional entry; + for (const auto& e : property_repo.get_properties()) { + if (e.is_value_type() && e.get_value().test_num == 1) { + entry = e.get_value(); + } + } + if (!entry) { + return hz::Unexpected(SelfTestExecutionError::ReportUnsupported, _("The drive doesn't report the test status.")); + } + + switch (entry->result) { + case NvmeSelfTestResultType::Unknown: + status_ = SelfTestStatus::Unknown; + break; + case NvmeSelfTestResultType::CompletedNoError: + status_ = SelfTestStatus::CompletedNoError; + break; + case NvmeSelfTestResultType::AbortedSelfTestCommand: + status_ = SelfTestStatus::ManuallyAborted; + break; + case NvmeSelfTestResultType::AbortedControllerReset: + case NvmeSelfTestResultType::AbortedNamespaceRemoved: + case NvmeSelfTestResultType::AbortedFormatNvmCommand: + case NvmeSelfTestResultType::AbortedUnknownReason: + case NvmeSelfTestResultType::AbortedSanitizeOperation: + status_ = SelfTestStatus::Interrupted; + break; + case NvmeSelfTestResultType::FatalOrUnknownTestError: + case NvmeSelfTestResultType::CompletedUnknownFailedSegment: + case NvmeSelfTestResultType::CompletedFailedSegments: + status_ = SelfTestStatus::CompletedWithError; + break; + } + } + + } else { + // ATA: + // Note: Since the self-test log is sometimes late + // and in undetermined order (sorting by hours is too rough), + // we use the "self-test status" capability. + StorageProperty p; + for (const auto& e : property_repo.get_properties()) { + if (e.is_value_type() || e.get_value().test_num != 0 + || e.generic_name != "ata_smart_data/self_test/status/_merged") + continue; + p = e; + } + if (p.empty()) { + return hz::Unexpected(SelfTestExecutionError::ReportUnsupported, _("The drive doesn't report the test status.")); + } + + switch (p.get_value().status) { + case AtaStorageSelftestEntry::Status::InProgress: + status_ = SelfTestStatus::InProgress; + break; + case AtaStorageSelftestEntry::Status::Unknown: + status_ = SelfTestStatus::Unknown; + break; + case AtaStorageSelftestEntry::Status::Reserved: + status_ = SelfTestStatus::Reserved; + break; + case AtaStorageSelftestEntry::Status::CompletedNoError: + status_ = SelfTestStatus::CompletedNoError; + break; + case AtaStorageSelftestEntry::Status::AbortedByHost: + status_ = SelfTestStatus::ManuallyAborted; + break; + case AtaStorageSelftestEntry::Status::Interrupted: + status_ = SelfTestStatus::Interrupted; + break; + case AtaStorageSelftestEntry::Status::FatalOrUnknown: + case AtaStorageSelftestEntry::Status::ComplUnknownFailure: + case AtaStorageSelftestEntry::Status::ComplElectricalFailure: + case AtaStorageSelftestEntry::Status::ComplServoFailure: + case AtaStorageSelftestEntry::Status::ComplReadFailure: + case AtaStorageSelftestEntry::Status::ComplHandlingDamage: + status_ = SelfTestStatus::CompletedWithError; + break; + } + + if (status_ == SelfTestStatus::InProgress) { + remaining_percent_ = p.get_value().remaining_percent; + } } - if (p.empty()) { - return hz::Unexpected(SelfTestError::ReportUnsupported, _("The drive doesn't report the test status.")); - } - - status_ = p.get_value().status; - const bool active = (status_ == AtaStorageSelftestEntry::Status::InProgress); - - // Note that the test needs 90% to complete, not 100. It starts at 90% // and reaches 00% on completion. That's 9 pieces. - if (active) { - - remaining_percent_ = p.get_value().remaining_percent; + if (status_ == SelfTestStatus::InProgress) { if (remaining_percent_ != last_seen_percent_) { last_seen_percent_ = remaining_percent_; timer_.start(); // restart the timer @@ -326,7 +503,7 @@ hz::ExpectedVoid SelfTest::update(const std::shared_ptrset_test_is_active(active); + drive_->set_test_is_active(status_ == SelfTestStatus::InProgress); return {}; // everything ok } diff --git a/src/applib/selftest.h b/src/applib/selftest.h index 518f64c..28434ab 100644 --- a/src/applib/selftest.h +++ b/src/applib/selftest.h @@ -13,6 +13,7 @@ Copyright: #define SELFTEST_H #include "local_glibmm.h" +#include #include #include #include @@ -20,9 +21,10 @@ Copyright: #include "storage_device.h" #include "command_executor.h" +#include "hz/error_container.h" -enum class SelfTestError { +enum class SelfTestExecutionError { InternalError, AlreadyRunning, UnsupportedTest, @@ -37,6 +39,59 @@ enum class SelfTestError { }; +/// Self-test status +enum class SelfTestStatus { + Unknown, + InProgress, + ManuallyAborted, + Interrupted, + CompletedNoError, + CompletedWithError, + Reserved, +}; + + + +/// Helper structure for enum-related functions +struct SelfTestStatusExt + : public hz::EnumHelper< + SelfTestStatus, + SelfTestStatusExt, + Glib::ustring> +{ + static constexpr EnumType default_value = EnumType::Unknown; + + static std::unordered_map> build_enum_map() + { + return { + {SelfTestStatus::Unknown, {"unknown", _("Unknown")}}, + {SelfTestStatus::InProgress, {"in_progress", _("In Progress")}}, + {SelfTestStatus::ManuallyAborted, {"manually_aborted", _("Manually Aborted")}}, + {SelfTestStatus::Interrupted, {"interrupted", _("Interrupted")}}, + {SelfTestStatus::CompletedNoError, {"completed_no_error", _("Completed Successfully")}}, + {SelfTestStatus::CompletedWithError, {"completed_with_error", _("Completed With Errors")}}, + {SelfTestStatus::Reserved, {"reserved", _("Reserved")}}, + }; + } + +}; + + + + +/// Self-test error severity +enum class SelfTestStatusSeverity { + None, + Warning, + Error, +}; + + +/// Get severity of error status +[[nodiscard]] SelfTestStatusSeverity get_self_test_status_severity(SelfTestStatus s); + + + /// SMART self-test runner. class SelfTest { @@ -44,7 +99,7 @@ class SelfTest { /// Test type enum class TestType { - ImmediateOffline, ///< Immediate offline, not supported +// ImmediateOffline, ///< Immediate offline, not supported ShortTest, ///< Short self-test LongTest, ///< Extended (a.k.a. long) self-test Conveyance ///< Conveyance self-test @@ -62,18 +117,12 @@ class SelfTest { /// Check if the test is currently active - [[nodiscard]] bool is_active() const - { - return (status_ == AtaStorageSelftestEntry::Status::InProgress); - } + [[nodiscard]] bool is_active() const; /// Get remaining time percent until the test completion. /// \return -1 if N/A or unknown. - [[nodiscard]] int8_t get_remaining_percent() const - { - return remaining_percent_; - } + [[nodiscard]] int8_t get_remaining_percent() const; /// Get estimated time of completion for the test. @@ -82,24 +131,16 @@ class SelfTest { /// Get test type - [[nodiscard]] TestType get_test_type() const - { - return type_; - } + [[nodiscard]] TestType get_test_type() const; /// Get test status - [[nodiscard]] AtaStorageSelftestEntry::Status get_status() const - { - return status_; - } + [[nodiscard]] SelfTestStatus get_status() const; /// Get the number of seconds after which the caller should call update(). - [[nodiscard]] std::chrono::seconds get_poll_in_seconds() const - { - return poll_in_seconds_; - } + /// Returns -1 if the test is not running. + [[nodiscard]] std::chrono::seconds get_poll_in_seconds() const; /// Get a constant "test duration during idle" capability drive's stored capabilities. -1 if N/A. @@ -113,17 +154,17 @@ class SelfTest { /// Start the test. Note that this object is not reusable, start() must be called /// only on newly constructed objects. /// \return error message on error, empty string on success. - hz::ExpectedVoid start(const std::shared_ptr& smartctl_ex = nullptr); + hz::ExpectedVoid start(const std::shared_ptr& smartctl_ex = nullptr); /// Abort the running test. /// \return error message on error, empty string on success. - hz::ExpectedVoid force_stop(const std::shared_ptr& smartctl_ex = nullptr); + hz::ExpectedVoid force_stop(const std::shared_ptr& smartctl_ex = nullptr); /// Update status variables. The user should call this every get_poll_in_seconds() seconds. /// \return error message on error, empty string on success. - hz::ExpectedVoid update(const std::shared_ptr& smartctl_ex = nullptr); + hz::ExpectedVoid update(const std::shared_ptr& smartctl_ex = nullptr); private: @@ -132,7 +173,7 @@ class SelfTest { TestType type_ = TestType::ShortTest; ///< Test type // status variables: - AtaStorageSelftestEntry::Status status_ = AtaStorageSelftestEntry::Status::Unknown; ///< Current status of the test as reported by the drive + SelfTestStatus status_ = SelfTestStatus::Unknown; ///< Current status of the test as reported by the drive int8_t remaining_percent_ = -1; ///< Remaining %. 0 means unknown, -1 means N/A. This is set to 100 on start. int8_t last_seen_percent_ = -1; ///< Last reported %, to detect changes in percentage (needed for timer update). mutable std::chrono::seconds total_duration_ = std::chrono::seconds(-1); ///< Total duration needed for the test, as reported by the drive. Constant. This variable acts as a cache. diff --git a/src/applib/storage_property.cpp b/src/applib/storage_property.cpp index c414d93..9ee4eb4 100644 --- a/src/applib/storage_property.cpp +++ b/src/applib/storage_property.cpp @@ -256,30 +256,6 @@ std::string AtaStorageSelftestEntry::get_readable_status_name(Status s) -AtaStorageSelftestEntry::StatusSeverity AtaStorageSelftestEntry::get_status_severity(AtaStorageSelftestEntry::Status s) -{ - static const std::unordered_map m { - {Status::Unknown, StatusSeverity::None}, - {Status::CompletedNoError, StatusSeverity::None}, - {Status::AbortedByHost, StatusSeverity::Warning}, - {Status::Interrupted, StatusSeverity::Warning}, - {Status::FatalOrUnknown, StatusSeverity::Error}, - {Status::ComplUnknownFailure, StatusSeverity::Error}, - {Status::ComplElectricalFailure, StatusSeverity::Error}, - {Status::ComplServoFailure, StatusSeverity::Error}, - {Status::ComplReadFailure, StatusSeverity::Error}, - {Status::ComplHandlingDamage, StatusSeverity::Error}, - {Status::InProgress, StatusSeverity::None}, - {Status::Reserved, StatusSeverity::None}, - }; - if (auto iter = m.find(s); iter != m.end()) { - return iter->second; - } - return StatusSeverity::None; -} - - - std::string AtaStorageSelftestEntry::get_readable_status() const { return (status == Status::Unknown ? status_str : get_readable_status_name(status)); diff --git a/src/applib/storage_property.h b/src/applib/storage_property.h index 955f216..919739a 100644 --- a/src/applib/storage_property.h +++ b/src/applib/storage_property.h @@ -194,20 +194,9 @@ class AtaStorageSelftestEntry { InProgress = 0xf, ///< Test in progress }; - /// Self-test error severity - enum class StatusSeverity { - None, - Warning, - Error - }; - /// Get log entry status displayable name [[nodiscard]] static std::string get_readable_status_name(Status s); - /// Get severity of error status - [[nodiscard]] static StatusSeverity get_status_severity(Status s); - - /// Get error status as a string [[nodiscard]] std::string get_readable_status() const; diff --git a/src/gsc_info_window.cpp b/src/gsc_info_window.cpp index d3643a6..e1e58cc 100644 --- a/src/gsc_info_window.cpp +++ b/src/gsc_info_window.cpp @@ -29,6 +29,7 @@ Copyright: #include "applib/gui_utils.h" // gui_show_error_dialog #include "applib/smartctl_executor_gui.h" #include "applib/storage_property.h" +#include "applib/storage_device_detected_type.h" #include "gsc_text_window.h" #include "gsc_info_window.h" @@ -399,7 +400,9 @@ void GscInfoWindow::fill_ui_with_info(bool scan, bool clear_ui, bool clear_tests note_page_box->set_visible(has_statistics); } - const bool has_selftest = prop_repo.has_properties_for_section(StoragePropertySection::SelftestLog); + bool has_selftest = prop_repo.has_properties_for_section(StoragePropertySection::SelftestLog); + // NVMe spec supports self-tests by default. + has_selftest = has_selftest || drive->get_detected_type() == StorageDeviceDetectedType::Nvme; if (note_page_box = lookup_widget("test_tab_vbox"); note_page_box != nullptr) { // Some USB flash drives erroneously report SMART as enabled. // note_page_box->set_visible(drive->get_smart_status() == StorageDevice::Status::Enabled); @@ -1353,17 +1356,17 @@ void GscInfoWindow::fill_ui_self_test_info() Gtk::TreeModel::Row row; - auto test_ioffline = std::make_shared(drive, SelfTest::TestType::ImmediateOffline); - if (test_ioffline->is_supported()) { - row = *(test_combo_model->append()); - row[test_combo_columns.name] = SelfTest::get_test_displayable_name(SelfTest::TestType::ImmediateOffline); - row[test_combo_columns.description] = - _("Immediate Offline Test (also known as Immediate Offline Data Collection)" - " is the manual version of Automatic Offline Data Collection, which, if enabled, is automatically run" - " every four hours. If an error occurs during this test, it will be reported in Error Log. Besides that," - " its effects are visible only in that it updates the \"Offline\" Attribute values."); - row[test_combo_columns.self_test] = test_ioffline; - } +// auto test_ioffline = std::make_shared(drive, SelfTest::TestType::ImmediateOffline); +// if (test_ioffline->is_supported()) { +// row = *(test_combo_model->append()); +// row[test_combo_columns.name] = SelfTest::get_test_displayable_name(SelfTest::TestType::ImmediateOffline); +// row[test_combo_columns.description] = +// _("Immediate Offline Test (also known as Immediate Offline Data Collection)" +// " is the manual version of Automatic Offline Data Collection, which, if enabled, is automatically run" +// " every four hours. If an error occurs during this test, it will be reported in Error Log. Besides that," +// " its effects are visible only in that it updates the \"Offline\" Attribute values."); +// row[test_combo_columns.self_test] = test_ioffline; +// } auto test_short = std::make_shared(drive, SelfTest::TestType::ShortTest); if (test_short->is_supported()) { @@ -1470,6 +1473,8 @@ void GscInfoWindow::fill_ui_self_test_log(const StoragePropertyRepository& prope WarningLevel max_tab_warning = WarningLevel::None; std::vector label_strings; // outside-of-tree properties + bool ata_entries_found = false; + for (auto&& p : props) { if (p.section != StoragePropertySection::SelftestLog || !p.show_in_ui) continue; @@ -1477,8 +1482,12 @@ void GscInfoWindow::fill_ui_self_test_log(const StoragePropertyRepository& prope if (p.generic_name == "ata_smart_self_test_log/_merged") // the whole section, we don't need it continue; + if (p.is_value_type()) { + ata_entries_found = true; + } + // add non-entry properties to label above - if (!p.is_value_type()) { + if (!p.is_value_type() && !p.is_value_type()) { label_strings.emplace_back(p.displayable_name + ": " + p.format_value(), &p); if (int(p.warning_level) > int(max_tab_warning)) @@ -1488,14 +1497,23 @@ void GscInfoWindow::fill_ui_self_test_log(const StoragePropertyRepository& prope Gtk::TreeRow row = *(list_store->append()); - const auto& sse = p.get_value(); + if (p.is_value_type()) { + const auto& entry = p.get_value(); + row[self_test_log_table_columns.log_entry_index] = entry.test_num; + row[self_test_log_table_columns.type] = Glib::Markup::escape_text(entry.type); + row[self_test_log_table_columns.status] = Glib::Markup::escape_text(entry.get_readable_status()); + row[self_test_log_table_columns.percent] = Glib::Markup::escape_text(hz::number_to_string_locale(100 - entry.remaining_percent) + "%"); + row[self_test_log_table_columns.hours] = Glib::Markup::escape_text(hz::number_to_string_locale(entry.lifetime_hours)); + row[self_test_log_table_columns.lba] = Glib::Markup::escape_text(entry.lba_of_first_error); - row[self_test_log_table_columns.log_entry_index] = sse.test_num; - row[self_test_log_table_columns.type] = Glib::Markup::escape_text(sse.type); - row[self_test_log_table_columns.status] = Glib::Markup::escape_text(sse.get_readable_status()); - row[self_test_log_table_columns.percent] = Glib::Markup::escape_text(hz::number_to_string_locale(100 - sse.remaining_percent) + "%"); - row[self_test_log_table_columns.hours] = Glib::Markup::escape_text(hz::number_to_string_locale(sse.lifetime_hours)); - row[self_test_log_table_columns.lba] = Glib::Markup::escape_text(sse.lba_of_first_error); + } else if (p.is_value_type()) { + const auto& entry = p.get_value(); + row[self_test_log_table_columns.log_entry_index] = entry.test_num; + row[self_test_log_table_columns.type] = Glib::Markup::escape_text(NvmeSelfTestTypeExt::get_displayable_name(entry.type)); + row[self_test_log_table_columns.status] = Glib::Markup::escape_text(NvmeSelfTestResultTypeExt::get_displayable_name(entry.result)); + row[self_test_log_table_columns.hours] = Glib::Markup::escape_text(hz::number_to_string_locale(entry.power_on_hours)); + row[self_test_log_table_columns.lba] = Glib::Markup::escape_text(entry.lba.has_value() ? hz::number_to_string_locale(entry.lba.value()) : std::string("-")); + } // There are no descriptions in self-test log entries, so don't display // "No description available" for all of them. // row[self_test_log_table_columns.tooltip] = p.get_description(); @@ -1505,6 +1523,9 @@ void GscInfoWindow::fill_ui_self_test_log(const StoragePropertyRepository& prope max_tab_warning = p.warning_level; } + // Hide percentage column if NVMe as there is no such field in output. + treeview->get_column(3)->set_visible(ata_entries_found); // % Completed + auto* label_vbox = lookup_widget("selftest_log_label_vbox"); app_set_top_labels(label_vbox, label_strings); @@ -1629,7 +1650,7 @@ void GscInfoWindow::fill_ui_ata_error_log(const StoragePropertyRepository& prope details_str = AtaStorageErrorBlock::format_readable_error_types(eb.reported_types); // parsed in Text } - row[error_log_table_columns.lba] = Glib::Markup::escape_text(std::to_string(eb.lba)); + row[error_log_table_columns.lba] = Glib::Markup::escape_text(hz::number_to_string_locale(eb.lba)); row[error_log_table_columns.details] = Glib::Markup::escape_text(details_str.empty() ? "-" : details_str); row[error_log_table_columns.tooltip] = p.get_description(); // markup row[error_log_table_columns.storage_property] = &p; @@ -2306,23 +2327,23 @@ gboolean GscInfoWindow::test_idle_callback(void* data) auto status = self->current_test->get_status(); bool aborted = false; - AtaStorageSelftestEntry::StatusSeverity severity = AtaStorageSelftestEntry::StatusSeverity::None; + SelfTestStatusSeverity severity = SelfTestStatusSeverity::None; std::string result_msg; if (!self->test_error_msg.empty()) { aborted = true; - severity = AtaStorageSelftestEntry::StatusSeverity::Error; + severity = SelfTestStatusSeverity::Error; result_msg = Glib::ustring::compose(_("Test aborted: %1"), Glib::Markup::escape_text(self->test_error_msg)); } else { - severity = AtaStorageSelftestEntry::get_status_severity(status); - if (status == AtaStorageSelftestEntry::Status::AbortedByHost) { + severity = get_self_test_status_severity(status); + if (status == SelfTestStatus::ManuallyAborted) { aborted = true; result_msg = ""s + _("Test was manually aborted.") + ""; // it's a StatusSeverity::none message } else { result_msg = Glib::ustring::compose(_("Test result: %1."), - Glib::Markup::escape_text(AtaStorageSelftestEntry::get_readable_status_name(status))); + Glib::Markup::escape_text(SelfTestStatusExt::get_displayable_name(status))); // It may not reach 100% somehow, so do it manually. if (test_completion_progressbar) @@ -2330,7 +2351,7 @@ gboolean GscInfoWindow::test_idle_callback(void* data) } } - if (severity != AtaStorageSelftestEntry::StatusSeverity::None) { + if (severity != SelfTestStatusSeverity::None) { result_msg += "\n"s + _("Check the Self-Test Log for more information."); } @@ -2348,9 +2369,9 @@ gboolean GscInfoWindow::test_idle_callback(void* data) test_stop_button->set_sensitive(false); Gtk::StockID stock_id = Gtk::Stock::DIALOG_ERROR; - if (severity == AtaStorageSelftestEntry::StatusSeverity::None) { + if (severity == SelfTestStatusSeverity::None) { stock_id = Gtk::Stock::DIALOG_INFO; - } else if (severity == AtaStorageSelftestEntry::StatusSeverity::Warning) { + } else if (severity == SelfTestStatusSeverity::Warning) { stock_id = Gtk::Stock::DIALOG_WARNING; }