Added initial work for selecting smartctl parser type.

Refactored smartctl version parsing, added tests.
This commit is contained in:
Alexander Shaduri
2022-02-11 16:00:21 +04:00
parent 582d4ad5cd
commit c1070e375f
12 changed files with 466 additions and 58 deletions
+3
View File
@@ -35,6 +35,9 @@ target_sources(applib PRIVATE
smartctl_executor.cpp
smartctl_executor_gui.h
smartctl_executor.h
smartctl_output_type.h
smartctl_version_parser.cpp
smartctl_version_parser.h
storage_detector.cpp
storage_detector.h
storage_detector_helpers.h
+3 -44
View File
@@ -23,6 +23,7 @@ Copyright:
#include "smartctl_ata_text_parser.h"
#include "ata_storage_property_descr.h"
#include "warning_colors.h"
#include "smartctl_version_parser.h"
#include "build_config.h"
@@ -208,7 +209,7 @@ bool SmartctlAtaTextParser::parse_full(const std::string& full, AtaStorageAttrib
// version info
std::string version, version_full;
if (!parse_version(s, version, version_full)) {
if (!SmartctlVersionParser::parse_version(s, version, version_full)) {
set_error_msg("Cannot extract smartctl version information.");
debug_out_warn("app", DBG_FUNC_MSG << "Cannot extract version information. Returning.\n");
return false;
@@ -231,7 +232,7 @@ bool SmartctlAtaTextParser::parse_full(const std::string& full, AtaStorageAttrib
add_property(p);
}
if (!check_parsed_version(version, version_full)) {
if (!SmartctlVersionParser::check_parsed_version(SmartctlOutputParserType::Text, version)) {
set_error_msg("Incompatible smartctl version.");
debug_out_warn("app", DBG_FUNC_MSG << "Incompatible smartctl version. Returning.\n");
return false;
@@ -275,48 +276,6 @@ bool SmartctlAtaTextParser::parse_full(const std::string& full, AtaStorageAttrib
// Supply output of "smartctl --version" here.
// returns false on failure. Non-unix newlines in s are ok.
bool SmartctlAtaTextParser::parse_version(const std::string& s, std::string& version, std::string& version_full)
{
// e.g.
// "smartctl version 5.37"
// "smartctl 5.39"
// "smartctl 5.39 2009-06-03 20:10" (cvs versions)
// "smartctl 5.39 2009-08-08 r2873" (svn versions)
if (!app_pcre_match(R"(/^smartctl (?:version )?(([0-9][^ \t\n\r]+)(?: [0-9 r:-]+)?)/mi)", s, &version_full, &version)) {
debug_out_error("app", DBG_FUNC_MSG << "No smartctl version information found in supplied string.\n");
return false;
}
hz::string_trim(version_full);
return true;
}
// check that the version of smartctl output can be parsed with this parser.
bool SmartctlAtaTextParser::check_parsed_version(const std::string& version_str, [[maybe_unused]] const std::string& version_full_str)
{
// tested with 5.1-xx versions (1 - 18), and 5.[20 - 38].
// note: 5.1-11 (maybe others too) with scsi disk gives non-parsable output (why?).
// 5.0-24, 5.0-36, 5.0-49 tested with data only, from smartmontool site.
// can't fully test 5.0-xx, they don't support sata and I have only sata.
const double minimum_req_version = 5.0;
double version = 0;
if (hz::string_is_numeric_nolocale<double>(version_str, version, false)) {
if (version >= minimum_req_version)
return true;
}
return false;
}
// convert e.g. "1,000,204,886,016 bytes" to 1.00 TB [931.51 GiB, 1000204886016 bytes].
// Note: this property is present since 5.33.
std::string SmartctlAtaTextParser::parse_byte_size(const std::string& str, int64_t& bytes, bool extended)
+5 -10
View File
@@ -28,16 +28,11 @@ class SmartctlAtaTextParser {
/// Parse full "smartctl -x" output
bool parse_full(const std::string& full, AtaStorageAttribute::DiskType disk_type);
/// Supply any output of smartctl here, the smartctl version will be retrieved.
static bool parse_version(const std::string& s, std::string& version, std::string& version_full);
/// Check that the version of smartctl output can be parsed with this parser.
static bool check_parsed_version(const std::string& version_str, const std::string& version_full_str);
/// Convert e.g. "1,000,204,886,016 bytes" to 1.00 TiB [931.51 GB, 1000204886016 bytes]
/// Convert e.g. "1,000,204,886,016 bytes" to "1.00 TiB [931.51 GB, 1000204886016 bytes]"
/// \param str String to parse
/// \param bytes Number of bytes
/// \param extended Return size in other units as well
/// \return Size as a displayable string
static std::string parse_byte_size(const std::string& str, int64_t& bytes, bool extended);
+53
View File
@@ -0,0 +1,53 @@
/******************************************************************************
License: GNU General Public License v3.0 only
Copyright:
(C) 2022 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup applib
/// \weakgroup applib
/// @{
#ifndef SMARTCTL_OUTPUT_TYPE_H
#define SMARTCTL_OUTPUT_TYPE_H
#include "local_glibmm.h"
#include "hz/enum_helper.h"
enum class SmartctlOutputParserType {
Auto,
Json,
Text,
};
/// Helper structure for enum-related functions
struct SmartctlOutputParserTypeExt
: public hz::EnumHelper<
SmartctlOutputParserType,
SmartctlOutputParserTypeExt,
Glib::ustring>
{
static constexpr inline SmartctlOutputParserType default_value = SmartctlOutputParserType::Auto;
static std::unordered_map<EnumType, std::pair<std::string, Glib::ustring>> build_enum_map()
{
return {
{SmartctlOutputParserType::Auto, {"auto", _("Automatic")}},
{SmartctlOutputParserType::Json, {"json", _("JSON")}},
{SmartctlOutputParserType::Text, {"text", _("Text")}},
};
}
};
#endif
/// @}
+88
View File
@@ -0,0 +1,88 @@
/******************************************************************************
License: GNU General Public License v3.0 only
Copyright:
(C) 2008 - 2022 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup applib
/// \weakgroup applib
/// @{
// #include "local_glibmm.h"
#include "hz/string_algo.h" // string_*
#include "hz/string_num.h" // string_is_numeric, number_to_string
#include "hz/debug.h" // debug_*
#include "app_pcrecpp.h"
#include "smartctl_version_parser.h"
bool SmartctlVersionParser::parse_version(const std::string& s, std::string& version_only, std::string& version_full)
{
// e.g.
// "smartctl version 5.37"
// "smartctl 5.39"
// "smartctl 5.39 2009-06-03 20:10" (cvs versions)
// "smartctl 5.39 2009-08-08 r2873" (svn versions)
// "smartctl 7.3 (build date Feb 11 2022)" (git versions)
if (!app_pcre_match(R"(/^smartctl (?:version )?(([0-9][^ \t\n\r]+)(?: [0-9 r:-]+)?)/mi)", s, &version_full, &version_only)) {
debug_out_error("app", DBG_FUNC_MSG << "No smartctl version information found in supplied string.\n");
return false;
}
hz::string_trim(version_only);
hz::string_trim(version_full);
return true;
}
std::optional<double> SmartctlVersionParser::get_numeric_version(const std::string& version_only)
{
double numeric_version = 0;
if (!hz::string_is_numeric_nolocale<double>(version_only, numeric_version, false)) {
return std::nullopt;
}
return numeric_version;
}
bool SmartctlVersionParser::check_parsed_version(SmartctlOutputParserType parser_type, const std::string& version_only)
{
if (auto numeric_version = get_numeric_version(version_only); numeric_version.has_value()) {
switch(parser_type) {
case SmartctlOutputParserType::Auto:
return numeric_version.value() >= minimum_req_text_version || numeric_version.value() >= minimum_req_json_version;
case SmartctlOutputParserType::Json:
return numeric_version.value() >= minimum_req_json_version;
case SmartctlOutputParserType::Text:
return numeric_version.value() >= minimum_req_text_version;
}
}
return false;
}
std::optional<SmartctlOutputParserType> SmartctlVersionParser::detect_supported_parser_type(const std::string& version_only)
{
if (check_parsed_version(SmartctlOutputParserType::Json, version_only)) {
return SmartctlOutputParserType::Json;
}
if (check_parsed_version(SmartctlOutputParserType::Text, version_only)) {
return SmartctlOutputParserType::Text;
}
return std::nullopt;
}
/// @}
+74
View File
@@ -0,0 +1,74 @@
/******************************************************************************
License: GNU General Public License v3.0 only
Copyright:
(C) 2008 - 2022 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup applib
/// \weakgroup applib
/// @{
#ifndef SMARTCTL_VERSION_PARSER_H
#define SMARTCTL_VERSION_PARSER_H
#include "local_glibmm.h"
#include <string>
#include <vector>
#include <optional>
#include "smartctl_output_type.h"
/// Smartctl version parser.
class SmartctlVersionParser {
public:
/// Supply any text (not JSON) output of smartctl here, the smartctl version will be retrieved.
/// The text does not have to be in Unix newline format.
/// \param command_output "smartctl -V" command output.
/// \param[out] version_only A string similar to "7.2"
/// \param[out] version_full A string similar to "smartctl 7.2 2020-12-30 r5155"
/// \return false if the version could not be parsed.
static bool parse_version(const std::string& command_output, std::string& version_only, std::string& version_full);
/// Get numeric version as a double from a parsed version.
/// \param version_only A string similar to "7.2", as parsed by parse_version().
/// \return Numeric version as a double, e.g. 7.2. std::nullopt if the version could not be parsed.
static std::optional<double> get_numeric_version(const std::string& version_only);
/// Check that the version of smartctl output can be parsed with a parser.
static bool check_parsed_version(SmartctlOutputParserType parser_type, const std::string& version_only);
/// Detect smartctl parser type based on smartctl version
static std::optional<SmartctlOutputParserType> detect_supported_parser_type(const std::string& version_only);
private:
// Text Parser:
// Tested with 5.1-xx versions (1 - 18), and 5.[20 - 38].
// Note: 5.1-11 (maybe others too) with scsi disk gives non-parsable output (why?).
// 5.0-24, 5.0-36, 5.0-49 tested with data only, from smartmontools site.
// Can't fully test 5.0-xx, they don't support sata, and I have only sata.
static constexpr double minimum_req_text_version = 5.0;
// JSON in 7.2 is still experimental, but we have to have some cut-off point.
static constexpr double minimum_req_json_version = 7.2;
};
#endif
/// @}
+2 -2
View File
@@ -22,7 +22,7 @@ Copyright:
#include "smartctl_ata_text_parser.h"
#include "storage_settings.h"
#include "smartctl_executor.h"
#include "smartctl_version_parser.h"
@@ -144,7 +144,7 @@ std::string StorageDevice::parse_basic_data(bool do_set_properties, bool emit_si
}
std::string version, version_full;
if (!SmartctlAtaTextParser::parse_version(this->info_output_, version, version_full)) // is this smartctl data at all?
if (!SmartctlVersionParser::parse_version(this->info_output_, version, version_full)) // is this smartctl data at all?
return _("Cannot get smartctl version information.");
// Detect type. note: we can't distinguish between sata and scsi (on linux, for -d ata switch).
+1
View File
@@ -15,6 +15,7 @@ endif()
add_library(applib_tests OBJECT)
target_sources(applib_tests PRIVATE
test_app_pcrecpp.cpp
test_smartctl_version_parser.cpp
)
target_link_libraries(applib_tests PRIVATE
applib
@@ -0,0 +1,105 @@
/******************************************************************************
License: BSD Zero Clause License
Copyright:
(C) 2022 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup applib_tests
/// \weakgroup applib_tests
/// @{
// Catch2 v3
//#include "catch2/catch_test_macros.hpp"
// Catch2 v2
#include "catch2/catch.hpp"
#include "applib/smartctl_version_parser.h"
TEST_CASE("SmartctlVersionParser", "[app][parser]")
{
std::string version_only, version_full;
SECTION("Parse with version keyword") {
SmartctlVersionParser::parse_version("smartctl version 5.37", version_only, version_full);
REQUIRE(version_only == "5.37");
REQUIRE(version_full == "5.37");
}
SECTION("Parse without version keyword") {
SmartctlVersionParser::parse_version("smartctl 5.39", version_only, version_full);
REQUIRE(version_only == "5.39");
REQUIRE(version_full == "5.39");
}
SECTION("Parse with date (CVS)") {
SmartctlVersionParser::parse_version("smartctl 5.39 2009-06-03 20:10", version_only, version_full);
REQUIRE(version_only == "5.39");
REQUIRE(version_full == "5.39 2009-06-03 20:10");
}
SECTION("Parse with date (SVN)") {
SmartctlVersionParser::parse_version("smartctl 5.39 2009-08-08 r2873", version_only, version_full);
REQUIRE(version_only == "5.39");
REQUIRE(version_full == "5.39 2009-08-08 r2873");
}
SECTION("Parse old 5.0") {
SmartctlVersionParser::parse_version("smartctl version 5.0-49", version_only, version_full);
REQUIRE(version_only == "5.0-49");
REQUIRE(version_full == "5.0-49");
}
SECTION("Parse full output (SVN)") {
std::string output =
R"(smartctl 7.2 2020-12-30 r5155 [x86_64-linux-5.3.18-lp152.66-default] (SUSE RPM)
Copyright (C) 2002-20, Bruce Allen, Christian Franke, www.smartmontools.org
smartctl comes with ABSOLUTELY NO WARRANTY. This is free
software, and you are welcome to redistribute it under
the terms of the GNU General Public License; either
version 2, or (at your option) any later version.
See http://www.gnu.org for further details.
smartmontools release 7.2 dated 2020-12-30 at 16:48:30 UTC
smartmontools SVN rev 5155 dated 2020-12-30 at 16:49:18
smartmontools build host: x86_64-suse-linux-gnu
smartmontools build with: C++14, GCC 7.5.0
smartmontools configure arguments: '--host=x86_64-suse-linux-gnu' '--build=x86_64-suse-linux-gnu' '--program-prefix=' '--prefix=/usr' '--exec-prefix=/usr' '--bindir=/usr/bin' '--sbindir=/usr/sbin' '--sysconfdir=/etc' '--datadir=/usr/share' '--includedir=/usr/include' '--libdir=/usr/lib64' '--libexecdir=/usr/lib' '--localstatedir=/var' '--sharedstatedir=/var/lib' '--mandir=/usr/share/man' '--infodir=/usr/share/info' '--disable-dependency-tracking' '--docdir=/usr/share/doc/packages/smartmontools' '--with-selinux' '--with-libsystemd' '--with-systemdsystemunitdir=/usr/lib/systemd/system' '--with-savestates' '--with-attributelog' '--with-nvme-devicescan' 'build_alias=x86_64-suse-linux-gnu' 'host_alias=x86_64-suse-linux-gnu' 'CXXFLAGS=-O2 -g -m64 -fmessage-length=0 -D_FORTIFY_SOURCE=2 -fstack-protector -funwind-tables -fasynchronous-unwind-tables -fPIE ' 'LDFLAGS=-pie' 'CFLAGS=-O2 -g -m64 -fmessage-length=0 -D_FORTIFY_SOURCE=2 -fstack-protector -funwind-tables -fasynchronous-unwind-tables -fPIE' 'PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'
)";
SmartctlVersionParser::parse_version(output, version_only, version_full);
REQUIRE(version_only == "7.2");
REQUIRE(version_full == "7.2 2020-12-30 r5155");
}
SECTION("Parse full output (git)") {
std::string output =
R"(smartctl 7.3 (build date Feb 11 2022) [x86_64-linux-5.3.18-lp152.66-default] (local build)
Copyright (C) 2002-22, Bruce Allen, Christian Franke, www.smartmontools.org
smartctl comes with ABSOLUTELY NO WARRANTY. This is free
software, and you are welcome to redistribute it under
the terms of the GNU General Public License; either
version 2, or (at your option) any later version.
See https://www.gnu.org for further details.
smartmontools release 7.3 dated 2020-12-30 at 16:48:30 UTC
smartmontools SVN rev is unknown
smartmontools build host: x86_64-pc-linux-gnu
smartmontools build with: C++11, GCC 7.5.0
smartmontools configure arguments: [no arguments given]
)";
SmartctlVersionParser::parse_version(output, version_only, version_full);
REQUIRE(version_only == "7.3");
REQUIRE(version_full == "7.3");
}
}
/// @}
+2 -2
View File
@@ -21,13 +21,13 @@ Copyright:
#include "hz/fs.h"
#include "rconfig/rconfig.h"
#include "applib/storage_detector.h"
#include "applib/smartctl_ata_text_parser.h"
#include "applib/gui_utils.h" // gui_show_error_dialog
#include "applib/smartctl_executor.h" // get_smartctl_binary()
#include "applib/smartctl_executor_gui.h"
#include "applib/app_gtkmm_tools.h" // app_gtkmm_*
#include "applib/warning_colors.h" // app_property_get_label_highlight_color
#include "applib/app_pcrecpp.h" // app_pcre_match
#include "applib/smartctl_version_parser.h"
#include "gsc_init.h" // app_quit()
#include "gsc_about_dialog.h"
@@ -118,7 +118,7 @@ GscMainWindow::GscMainWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
}
std::string version, version_full;
if (!SmartctlAtaTextParser::parse_version(output, version, version_full)) {
if (!SmartctlVersionParser::parse_version(output, version, version_full)) {
error_msg = _("Smartctl returned invalid output.");
break;
}
+1
View File
@@ -11,6 +11,7 @@ target_sources(hz INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/bad_cast_exception.h
${CMAKE_CURRENT_SOURCE_DIR}/data_file.h
${CMAKE_CURRENT_SOURCE_DIR}/debug.h
${CMAKE_CURRENT_SOURCE_DIR}/enum_helper.h
${CMAKE_CURRENT_SOURCE_DIR}/env_tools.h
${CMAKE_CURRENT_SOURCE_DIR}/error.h
${CMAKE_CURRENT_SOURCE_DIR}/error_holder.h
+129
View File
@@ -0,0 +1,129 @@
/******************************************************************************
License: GNU General Public License v3.0 only
Copyright:
(C) 2022 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup hz
/// \weakgroup hz
/// @{
#ifndef ENUM_HELPER_H
#define ENUM_HELPER_H
#include <string>
#include <vector>
#include <utility>
#include <unordered_map>
#include <algorithm>
namespace hz {
/// Helper class for defining enum-related functions.
/// In EnumExtClass it expects the following (accessible) members:
/// - static inline EnumType default_value = ...;
/// - static const std::unordered_map<EnumType, std::pair<std::string, DisplayableStringType>>& get_enum_static_map();
template <typename Enum, typename EnumExtClass, typename DisplayableStringType>
class EnumHelper {
public:
using EnumType = Enum;
using EnumMapType = std::unordered_map<EnumType, std::pair<std::string, DisplayableStringType>>;
/// Return storable name of an enum member
static std::string get_storable_name(EnumType enum_value)
{
const auto& m = get_enum_static_map();
// Iterator: enum -> pair{storable, displayable}
auto iter = m.find(enum_value);
return iter != m.end() ? std::string(iter->second.first) : std::string();
}
/// Return an enum member by its storable name
static EnumType get_by_storable_name(const std::string& storable_name,
EnumType default_value = EnumExtClass::default_value)
{
const auto& m = get_storable_enum_static_map();
// Iterator: storable_name -> enum
auto iter = m.find(storable_name);
return iter != m.end() ? iter->second : default_value;
}
/// Return displayable name of an enum member
static DisplayableStringType get_displayable_name(EnumType enum_value)
{
const auto& m = get_enum_static_map();
// Iterator: enum -> pair{storable, displayable}
auto iter = m.find(enum_value);
return iter != m.end() ? DisplayableStringType(iter->second.second) : DisplayableStringType();
}
/// Return all possible members of an enum
static std::vector<EnumType> getAllValues()
{
static const auto v = build_enum_value_list();
return v;
}
private:
/// Get a static map of storable names to enum values.
static const EnumMapType& get_enum_static_map()
{
static const auto m = EnumExtClass::build_enum_map();
return m;
}
/// Get a static map of storable names to enum values.
static const std::unordered_map<std::string, EnumType>& get_storable_enum_static_map()
{
static const auto m = build_storable_enum_map();
return m;
}
/// Build a map of storable names to enum values.
static std::unordered_map<std::string, EnumType> build_storable_enum_map()
{
std::unordered_map<std::string, EnumType> m;
for (const auto& [data, enum_value] : EnumExtClass::get_enum_static_map()) {
m.emplace(data.first, enum_value);
}
return m;
}
/// Build a list of enum values from get_enum_static_map()
static std::vector<EnumType> build_enum_value_list()
{
const auto& m = EnumExtClass::get_enum_static_map();
std::vector<EnumType> v;
v.reserve(m.size());
for (const auto& p : m) {
v.push_back(p.first);
}
std::sort(v.begin(), v.end());
return v;
}
};
}
#endif
/// @}