From 7f688e0fa44092b3c546748e5fdf1d4588419cef Mon Sep 17 00:00:00 2001 From: Alexander Shaduri Date: Mon, 6 Mar 2023 19:18:48 +0400 Subject: [PATCH] Initial work on JSON parser. --- src/applib/smartctl_ata_json_parser.cpp | 135 +++++++++++++++++++++++- src/applib/smartctl_ata_json_parser.h | 2 +- src/applib/storage_device.cpp | 38 ++++++- 3 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/applib/smartctl_ata_json_parser.cpp b/src/applib/smartctl_ata_json_parser.cpp index f94199a5..5437b327 100644 --- a/src/applib/smartctl_ata_json_parser.cpp +++ b/src/applib/smartctl_ata_json_parser.cpp @@ -10,6 +10,13 @@ License: GNU General Public License v3.0 only /// @{ #include "smartctl_ata_json_parser.h" +#include "json/json.hpp" +#include "hz/debug.h" +#include "hz/string_algo.h" +#include "smartctl_version_parser.h" +#include "hz/string_num.h" + + /* Information not printed in JSON yet: @@ -48,7 +55,7 @@ Information not printed in JSON yet: _text_only/power_mode - Automatic Offline Data Collection toggle support - text_only/aodc_support + _text_only/aodc_support - Directory log supported We don't use this. @@ -68,11 +75,131 @@ _custom/smart_enabled */ +namespace { + + +/// Get json node data. The path is slash-separated string. +/// \throws std::runtime_error If not found or one of the paths is not an object +template +T get_node_data(const nlohmann::json& root, const std::string& path) +{ + using namespace std::literals; + + std::vector components; + hz::string_split(path, '/', components, true); + + const auto* curr = &root; + for (std::size_t comp_index = 0; comp_index < components.size(); ++comp_index) { + const std::string& comp_name = components[comp_index]; + + if (!curr->is_object()) { // we can't have non-object values in the middle of a path + throw std::runtime_error("Cannot get node data \""s + path + "\", component \"" + comp_name + "\" is not an object."); + } + if (auto iter = curr->find(comp_name); iter != curr->end()) { // path component exists + const auto& jval = iter.value(); + if (comp_index + 1 == components.size()) { // it's the "value" component + try { + return jval.get(); // may throw json::type_error + } + catch (nlohmann::json::type_error& ex) { + throw std::runtime_error(ex.what()); + } + } + // continue to the next component + curr = &jval; + + } else { // path component doesn't exist + throw std::runtime_error("Cannot get node data \""s + path + "\", component \"" + comp_name + "\" does not exist."); + } + } + + throw std::runtime_error("Cannot get node data \""s + path + "\": Internal error."); +} + + +/// Get json node data. The path is slash-separated string. +/// If an error is found, the default value is returned. +template +T get_node_data(const nlohmann::json& root, const std::string& path, const T& default_value) +{ + try { + return get_node_data(root, path); + } + catch (std::runtime_error& ex) { + return default_value; + } +} + + +} + + -bool SmartctlAtaJsonParser::parse_full(const std::string& full) +bool SmartctlAtaJsonParser::parse_full(const std::string& json_data_full) { - // TODO - return false; + this->set_data_full(json_data_full); + + if (hz::string_trim_copy(json_data_full).empty()) { + set_error_msg("Smartctl data is empty."); + debug_out_warn("app", DBG_FUNC_MSG << "Empty string passed as an argument. Returning.\n"); + return false; + } + + try { + const nlohmann::json root_node = nlohmann::json::parse(json_data_full); + + { + AtaStorageProperty p; + p.set_name("Smartctl version", "smartctl/version/_merged", "Smartctl Version"); + auto json_ver = get_node_data>(root_node, "smartctl/version", {}); + if (json_ver.size() >= 2) { + p.reported_value = hz::number_to_string_nolocale(json_ver.at(0)) + "." + hz::number_to_string_nolocale(json_ver.at(1)); + } + p.value = p.reported_value; // string-type value + p.section = AtaStorageProperty::Section::info; // add to info section + add_property(p); + } + // { + // AtaStorageProperty p; + // p.set_name("Smartctl version", "smartctl/version/_merged_full", "Smartctl Version"); + // p.reported_value = version_full; + // p.value = p.reported_value; // string-type value + // p.section = AtaStorageProperty::Section::info; // add to info section + // add_property(p); + // } + + // if (!SmartctlVersionParser::check_parsed_version(SmartctlParserType::Text, version)) { + // set_error_msg("Incompatible smartctl version."); + // debug_out_warn("app", DBG_FUNC_MSG << "Incompatible smartctl version. Returning.\n"); + // return false; + // } + + const std::unordered_map info_keys = { + {"model_family", _("Model Family")}, + }; + + for (const auto& [key, jval] : root_node.items()) { + if (auto found = info_keys.find(key); found != info_keys.end()) { + AtaStorageProperty p; + p.section = AtaStorageProperty::Section::info; + p.set_name(key, key, found->second); + p.reported_value = jval.get(); + p.value = p.reported_value; // string-type value + + // parse_section_info_property(p); // set type and the typed value. may change generic_name too. + + add_property(p); + } + } + + + } + catch (const nlohmann::json::parse_error& e) { + debug_out_warn("app", DBG_FUNC_MSG << "Error parsing smartctl output as JSON. Returning.\n"); + return false; + } + + return true; } diff --git a/src/applib/smartctl_ata_json_parser.h b/src/applib/smartctl_ata_json_parser.h index a7fa00da..d19507ca 100644 --- a/src/applib/smartctl_ata_json_parser.h +++ b/src/applib/smartctl_ata_json_parser.h @@ -29,7 +29,7 @@ class SmartctlAtaJsonParser : public SmartctlParser { SmartctlAtaJsonParser() = default; // Overridden - bool parse_full(const std::string& full) override; + bool parse_full(const std::string& json_data_full) override; }; diff --git a/src/applib/storage_device.cpp b/src/applib/storage_device.cpp index 6626597c..e9eec415 100644 --- a/src/applib/storage_device.cpp +++ b/src/applib/storage_device.cpp @@ -270,17 +270,26 @@ std::string StorageDevice::fetch_data_and_parse(const std::shared_ptrget_type_argument() == "scsi") { // not sure about correctness... FIXME probably fails with RAID/scsi // This doesn't do much yet, but just in case... // SCSI equivalent of -x: error_msg = execute_device_smartctl("--health --info --attributes --log=error --log=selftest --log=background --log=sasphy", smartctl_ex, output); + } else { - // ATA equivalent of -x: - error_msg = execute_device_smartctl("--health --info --get=all --capabilities --attributes --format=brief --log=xerror,50,error --log=xselftest,50,selftest --log=selective --log=directory --log=scttemp --log=scterc --log=devstat --log=sataphy", - smartctl_ex, output, true); // set type to invalid if needed + // ATA equivalent of -x. + std::string command_options = "--health --info --get=all --capabilities --attributes --format=brief --log=xerror,50,error --log=xselftest,50,selftest --log=selective --log=directory --log=scttemp --log=scterc --log=devstat --log=sataphy"; + if (default_parser_type == SmartctlParserSettingType::Json) { + // --json flags: o means include original output (just in case). + command_options += " --json=o"; + } + + error_msg = execute_device_smartctl(command_options, smartctl_ex, output, true); // set type to invalid if needed } + // See notes above (in fetch_basic_data_and_parse()). if (get_detected_type() == DetectedType::invalid && get_type_argument().empty()) { debug_out_info("app", "The device seems to be of different type than auto-detected, trying again with scsi.\n"); @@ -308,7 +317,28 @@ std::string StorageDevice::parse_data() disk_type = hdd_.value() ? AtaStorageAttribute::DiskType::Hdd : AtaStorageAttribute::DiskType::Ssd; } - auto parser = SmartctlParser::create(SmartctlParserType::Text); + std::string error_msg; + auto parser_type = leaf::try_handle_some( + [this]() -> leaf::result + { + return SmartctlParser::detect_output_type(this->full_output_); + }, + [&error_msg](leaf::match) -> SmartctlParserType + { + error_msg = "Empty input while trying to detect smartctl output format."; + return SmartctlParserType::Text; + }, + [&error_msg](leaf::match) -> SmartctlParserType + { + error_msg = "Unsupported format while trying to detect smartctl output format."; + return SmartctlParserType::Text; + } + ); + if (!error_msg.empty()) { + return error_msg; + } + + auto parser = SmartctlParser::create(parser_type.value()); DBG_ASSERT_RETURN(parser, "Cannot create parser"); if (parser->parse_full(this->full_output_)) { // try to parse it (parse only, set the properties after basic parsing).