mirror of
https://github.com/ashaduri/gsmartcontrol.git
synced 2026-09-26 22:05:34 +00:00
NVMe: Support self-test log and running self-tests (untested).
This commit is contained in:
+249
-72
@@ -13,9 +13,16 @@ Copyright:
|
||||
#include <algorithm> // std::max, std::min
|
||||
#include <cmath> // std::floor
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
#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<SelfTestStatus, SelfTestStatusSeverity> 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<TestType, std::string> 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<bool>());
|
||||
}
|
||||
|
||||
const StorageProperty p = drive_->get_property_repository().lookup_property(prop_name);
|
||||
return (!p.empty() && p.get_value<bool>());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// start the test
|
||||
hz::ExpectedVoid<SelfTestError> SelfTest::start(const std::shared_ptr<CommandExecutor>& smartctl_ex)
|
||||
hz::ExpectedVoid<SelfTestExecutionError> SelfTest::start(const std::shared_ptr<CommandExecutor>& 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<SelfTestError> SelfTest::start(const std::shared_ptr<CommandExe
|
||||
|
||||
// Set up everything so that the caller won't have to.
|
||||
|
||||
status_ = AtaStorageSelftestEntry::Status::InProgress;
|
||||
status_ = SelfTestStatus::InProgress;
|
||||
|
||||
remaining_percent_ = 100;
|
||||
// set to 90 to avoid the 100->90 timer reset. this way we won't be looking at
|
||||
@@ -178,40 +268,40 @@ hz::ExpectedVoid<SelfTestError> SelfTest::start(const std::shared_ptr<CommandExe
|
||||
|
||||
|
||||
// abort test.
|
||||
hz::ExpectedVoid<SelfTestError> SelfTest::force_stop(const std::shared_ptr<CommandExecutor>& smartctl_ex)
|
||||
hz::ExpectedVoid<SelfTestExecutionError> SelfTest::force_stop(const std::shared_ptr<CommandExecutor>& 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<bool>()) { // 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<bool>()) { // 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<SelfTestError> SelfTest::force_stop(const std::shared_ptr<Comma
|
||||
|
||||
// the thing is, update() may fail to actually update the statuses, so
|
||||
// do it manually.
|
||||
if (status_ == AtaStorageSelftestEntry::Status::InProgress) { // update() couldn't do its job
|
||||
status_ = AtaStorageSelftestEntry::Status::AbortedByHost;
|
||||
if (status_ == SelfTestStatus::InProgress) { // update() couldn't do its job
|
||||
status_ = SelfTestStatus::ManuallyAborted;
|
||||
remaining_percent_ = -1;
|
||||
last_seen_percent_ = -1;
|
||||
poll_in_seconds_ = std::chrono::seconds(-1);
|
||||
@@ -229,7 +319,7 @@ hz::ExpectedVoid<SelfTestError> SelfTest::force_stop(const std::shared_ptr<Comma
|
||||
}
|
||||
|
||||
if (!update_status) { // update can error out too.
|
||||
return hz::Unexpected(SelfTestError::UpdateError,
|
||||
return hz::Unexpected(SelfTestExecutionError::UpdateError,
|
||||
std::vformat(_("Error fetching test progress information: {}"), std::make_format_args(update_status.error().message())));
|
||||
}
|
||||
|
||||
@@ -240,58 +330,145 @@ hz::ExpectedVoid<SelfTestError> SelfTest::force_stop(const std::shared_ptr<Comma
|
||||
|
||||
// update status variables. note: the returned error is an error in logic,
|
||||
// not a hw defect error.
|
||||
hz::ExpectedVoid<SelfTestError> SelfTest::update(const std::shared_ptr<CommandExecutor>& smartctl_ex)
|
||||
hz::ExpectedVoid<SelfTestExecutionError> SelfTest::update(const std::shared_ptr<CommandExecutor>& 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<SmartctlParser> 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<AtaStorageSelftestEntry>() || e.get_value<AtaStorageSelftestEntry>().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<std::string>() != 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<int8_t>(100 - remaining_percent.get_value<int64_t>());
|
||||
}
|
||||
} else { // no test is active
|
||||
// The first self-test table entry is the latest.
|
||||
std::optional<NvmeStorageSelftestEntry> entry;
|
||||
for (const auto& e : property_repo.get_properties()) {
|
||||
if (e.is_value_type<NvmeStorageSelftestEntry>() && e.get_value<AtaStorageSelftestEntry>().test_num == 1) {
|
||||
entry = e.get_value<NvmeStorageSelftestEntry>();
|
||||
}
|
||||
}
|
||||
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<AtaStorageSelftestEntry>() || e.get_value<AtaStorageSelftestEntry>().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<AtaStorageSelftestEntry>().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<AtaStorageSelftestEntry>().remaining_percent;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.empty()) {
|
||||
return hz::Unexpected(SelfTestError::ReportUnsupported, _("The drive doesn't report the test status."));
|
||||
}
|
||||
|
||||
status_ = p.get_value<AtaStorageSelftestEntry>().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<AtaStorageSelftestEntry>().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<SelfTestError> SelfTest::update(const std::shared_ptr<CommandEx
|
||||
timer_.stop();
|
||||
}
|
||||
|
||||
drive_->set_test_is_active(active);
|
||||
drive_->set_test_is_active(status_ == SelfTestStatus::InProgress);
|
||||
|
||||
return {}; // everything ok
|
||||
}
|
||||
|
||||
+67
-26
@@ -13,6 +13,7 @@ Copyright:
|
||||
#define SELFTEST_H
|
||||
|
||||
#include "local_glibmm.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
@@ -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<EnumType, std::pair<std::string, Glib::ustring>> 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<SelfTestError> start(const std::shared_ptr<CommandExecutor>& smartctl_ex = nullptr);
|
||||
hz::ExpectedVoid<SelfTestExecutionError> start(const std::shared_ptr<CommandExecutor>& smartctl_ex = nullptr);
|
||||
|
||||
|
||||
/// Abort the running test.
|
||||
/// \return error message on error, empty string on success.
|
||||
hz::ExpectedVoid<SelfTestError> force_stop(const std::shared_ptr<CommandExecutor>& smartctl_ex = nullptr);
|
||||
hz::ExpectedVoid<SelfTestExecutionError> force_stop(const std::shared_ptr<CommandExecutor>& 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<SelfTestError> update(const std::shared_ptr<CommandExecutor>& smartctl_ex = nullptr);
|
||||
hz::ExpectedVoid<SelfTestExecutionError> update(const std::shared_ptr<CommandExecutor>& 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.
|
||||
|
||||
@@ -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<Status, StatusSeverity> 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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+50
-29
@@ -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<SelfTest>(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<SelfTest>(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<SelfTest>(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<PropertyLabel> 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<AtaStorageSelftestEntry>()) {
|
||||
ata_entries_found = true;
|
||||
}
|
||||
|
||||
// add non-entry properties to label above
|
||||
if (!p.is_value_type<AtaStorageSelftestEntry>()) {
|
||||
if (!p.is_value_type<AtaStorageSelftestEntry>() && !p.is_value_type<NvmeStorageSelftestEntry>()) {
|
||||
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<AtaStorageSelftestEntry>();
|
||||
if (p.is_value_type<AtaStorageSelftestEntry>()) {
|
||||
const auto& entry = p.get_value<AtaStorageSelftestEntry>();
|
||||
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<NvmeStorageSelftestEntry>()) {
|
||||
const auto& entry = p.get_value<NvmeStorageSelftestEntry>();
|
||||
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<Gtk::Box*>("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(_("<b>Test aborted:</b> %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 = "<b>"s + _("Test was manually aborted.") + "</b>"; // it's a StatusSeverity::none message
|
||||
|
||||
} else {
|
||||
result_msg = Glib::ustring::compose(_("<b>Test result:</b> %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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user