Without root, dmidecode exits 0 but outputs only a header comment with no Handle blocks (DMI tables are root-only in sysfs). The previous empty-string check never triggered the sudo retry. Now checks for the presence of 'Handle ' lines: if absent, retries transparently with sudo. Users with passwordless sudo get full hardware detail (CPU slots, memory sticks/slots, cache, voltage) without needing to explicitly invoke sudo themselves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1584 lines
65 KiB
C++
1584 lines
65 KiB
C++
#include "client/discovery.h"
|
||
#include "common/models.h"
|
||
#include "common/protocol.h"
|
||
#include <algorithm>
|
||
#include <cerrno>
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <iostream>
|
||
#include <sstream>
|
||
#include <sys/stat.h>
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// Low-level helpers
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
// Run a command with popen, return trimmed stdout (at most 64 KB).
|
||
std::string Discovery::run_cmd(const std::string& cmd) {
|
||
FILE* pipe = popen(cmd.c_str(), "r");
|
||
if (!pipe) return "";
|
||
|
||
std::string result;
|
||
char buf[4096];
|
||
while (fgets(buf, sizeof(buf), pipe)) {
|
||
result += buf;
|
||
if (result.size() >= 65536) break;
|
||
}
|
||
pclose(pipe);
|
||
|
||
// Trim trailing whitespace
|
||
while (!result.empty() && std::isspace((unsigned char)result.back()))
|
||
result.pop_back();
|
||
return result;
|
||
}
|
||
|
||
// Parse dmidecode -t <type_num> output into a vector of key→value maps.
|
||
// Each map corresponds to one DMI block (e.g. one "Memory Device").
|
||
std::vector<std::map<std::string, std::string>>
|
||
Discovery::parse_dmi(const std::string& type_num) {
|
||
#ifdef __APPLE__
|
||
(void)type_num;
|
||
return {}; // dmidecode not available on macOS; callers check __APPLE__
|
||
#else
|
||
std::string output = run_cmd("dmidecode -t " + type_num + " 2>/dev/null");
|
||
// Without root, dmidecode outputs only a header comment with no Handle blocks.
|
||
// If no "Handle " lines appear, retry transparently with sudo.
|
||
if (output.find("Handle ") == std::string::npos)
|
||
output = run_cmd("sudo dmidecode -t " + type_num + " 2>/dev/null");
|
||
if (output.empty()) return {};
|
||
|
||
std::vector<std::map<std::string, std::string>> result;
|
||
std::map<std::string, std::string> current;
|
||
|
||
std::istringstream ss(output);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||
|
||
// Handle empty — signals end of a block
|
||
if (line.empty()) {
|
||
if (!current.empty()) {
|
||
result.push_back(std::move(current));
|
||
current.clear();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// "Handle 0x…" starts a new block
|
||
if (line.size() >= 6 && line.substr(0, 6) == "Handle") {
|
||
if (!current.empty()) {
|
||
result.push_back(std::move(current));
|
||
current.clear();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Tab-indented: " Key: Value" or just " Tag"
|
||
if (line[0] == '\t') {
|
||
std::string kv = line.substr(1);
|
||
size_t colon = kv.find(": ");
|
||
if (colon != std::string::npos) {
|
||
std::string k = kv.substr(0, colon);
|
||
std::string v = kv.substr(colon + 2);
|
||
while (!v.empty() && std::isspace((unsigned char)v.back()))
|
||
v.pop_back();
|
||
current[k] = v;
|
||
} else {
|
||
// Line with no colon is the block type header
|
||
while (!kv.empty() && std::isspace((unsigned char)kv.back()))
|
||
kv.pop_back();
|
||
if (!kv.empty()) current["__type__"] = kv;
|
||
}
|
||
} else if (line[0] != '#') {
|
||
// Non-indented, non-comment: block type header
|
||
while (!line.empty() && std::isspace((unsigned char)line.back()))
|
||
line.pop_back();
|
||
if (!line.empty()) current["__type__"] = line;
|
||
}
|
||
}
|
||
if (!current.empty()) result.push_back(std::move(current));
|
||
return result;
|
||
#endif
|
||
}
|
||
|
||
// ── Numeric / size helpers ────────────────────────────────────────────────────
|
||
namespace {
|
||
|
||
// "16384 MB", "16 GB", "No Module Installed", "" → MB as uint64_t
|
||
static uint64_t dmi_size_to_mb(const std::string& s) {
|
||
if (s.empty() || s.find("No Module") != std::string::npos) return 0;
|
||
size_t i = 0;
|
||
while (i < s.size() && (std::isdigit((unsigned char)s[i]) || s[i] == '.')) ++i;
|
||
if (i == 0) return 0;
|
||
double val = std::stod(s.substr(0, i));
|
||
std::string suffix = s.substr(i);
|
||
// trim leading whitespace from suffix
|
||
while (!suffix.empty() && std::isspace((unsigned char)suffix[0]))
|
||
suffix = suffix.substr(1);
|
||
if (suffix == "GB" || suffix == "GB" || suffix[0] == 'G') return static_cast<uint64_t>(val * 1024);
|
||
if (suffix == "TB" || suffix[0] == 'T') return static_cast<uint64_t>(val * 1024 * 1024);
|
||
return static_cast<uint64_t>(val); // assume MB
|
||
}
|
||
|
||
#ifndef __APPLE__
|
||
// "500G", "2T", "500M", "128K", raw bytes → GB as uint64_t
|
||
static uint64_t size_to_gb(const std::string& s) {
|
||
if (s.empty()) return 0;
|
||
size_t i = 0;
|
||
while (i < s.size() && (std::isdigit((unsigned char)s[i]) || s[i] == '.')) ++i;
|
||
if (i == 0) return 0;
|
||
double val = std::stod(s.substr(0, i));
|
||
std::string suffix = s.substr(i);
|
||
while (!suffix.empty() && std::isspace((unsigned char)suffix[0]))
|
||
suffix = suffix.substr(1);
|
||
char u = suffix.empty() ? '\0' : std::toupper((unsigned char)suffix[0]);
|
||
if (u == 'T') return static_cast<uint64_t>(val * 1024.0);
|
||
if (u == 'G') return static_cast<uint64_t>(val);
|
||
if (u == 'M') return static_cast<uint64_t>(val / 1024.0);
|
||
if (u == 'K') return static_cast<uint64_t>(val / (1024.0 * 1024.0));
|
||
// Assume raw bytes
|
||
return static_cast<uint64_t>(val / 1e9);
|
||
}
|
||
#endif // !__APPLE__
|
||
|
||
// Extract integer prefix from a string like "3600 MHz" → 3600
|
||
static uint64_t leading_uint(const std::string& s) {
|
||
size_t i = 0;
|
||
while (i < s.size() && std::isdigit((unsigned char)s[i])) ++i;
|
||
if (i == 0) return 0;
|
||
return std::stoull(s.substr(0, i));
|
||
}
|
||
|
||
#ifndef __APPLE__
|
||
// file-exists helper (Linux dhcp detection)
|
||
static bool file_exists(const std::string& path) {
|
||
struct stat st;
|
||
return stat(path.c_str(), &st) == 0;
|
||
}
|
||
#endif // !__APPLE__
|
||
|
||
// ── Minimal JSON helpers for lsblk / ip -j / system_profiler output ──────────
|
||
#ifndef __APPLE__
|
||
// Extract the value of a JSON string or scalar field by key.
|
||
// Returns "" for null/missing.
|
||
static std::string json_str(const std::string& json, const std::string& key) {
|
||
// Accept both "key":"v" and "key": "v"
|
||
for (auto sep : {std::string("\"") + key + "\":\"",
|
||
std::string("\"") + key + "\": \""}) {
|
||
size_t pos = json.find(sep);
|
||
if (pos == std::string::npos) continue;
|
||
pos += sep.size();
|
||
std::string result;
|
||
while (pos < json.size() && json[pos] != '"') {
|
||
if (json[pos] == '\\') {
|
||
++pos;
|
||
if (pos < json.size()) {
|
||
char c = json[pos];
|
||
if (c == 'n') result += '\n';
|
||
else if (c == 't') result += '\t';
|
||
else if (c == '\\') result += '\\';
|
||
else if (c == '"') result += '"';
|
||
else result += c;
|
||
}
|
||
} else {
|
||
result += json[pos];
|
||
}
|
||
++pos;
|
||
}
|
||
return result;
|
||
}
|
||
// Try null / numeric / bool (no quotes)
|
||
for (auto sep : {std::string("\"") + key + "\":",
|
||
std::string("\"") + key + "\": "}) {
|
||
size_t pos = json.find(sep);
|
||
if (pos == std::string::npos) continue;
|
||
pos += sep.size();
|
||
while (pos < json.size() && std::isspace((unsigned char)json[pos])) ++pos;
|
||
if (pos >= json.size() || json[pos] == '"') continue; // quoted handled above
|
||
if (json[pos] == 'n') return ""; // null
|
||
std::string result;
|
||
while (pos < json.size() && json[pos] != ',' && json[pos] != '}' &&
|
||
json[pos] != ']' && !std::isspace((unsigned char)json[pos]))
|
||
result += json[pos++];
|
||
return result;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
// Split a JSON array of objects into individual object strings (outer array only).
|
||
static std::vector<std::string> json_array_objects(const std::string& json,
|
||
const std::string& array_key) {
|
||
std::string search = "\"" + array_key + "\":";
|
||
size_t pos = json.find(search);
|
||
if (pos == std::string::npos) {
|
||
search = "\"" + array_key + "\": ";
|
||
pos = json.find(search);
|
||
}
|
||
if (pos == std::string::npos) return {};
|
||
pos = json.find('[', pos);
|
||
if (pos == std::string::npos) return {};
|
||
++pos; // skip '['
|
||
|
||
std::vector<std::string> result;
|
||
int depth = 0;
|
||
size_t obj_start = std::string::npos;
|
||
bool in_string = false;
|
||
char prev = '\0';
|
||
|
||
for (size_t i = pos; i < json.size(); ++i) {
|
||
char c = json[i];
|
||
// Track string boundaries to avoid treating braces inside strings
|
||
if (c == '"' && prev != '\\') { in_string = !in_string; }
|
||
if (!in_string) {
|
||
if (c == '{') {
|
||
if (depth == 0) obj_start = i;
|
||
++depth;
|
||
} else if (c == '}') {
|
||
--depth;
|
||
if (depth == 0 && obj_start != std::string::npos) {
|
||
result.push_back(json.substr(obj_start, i - obj_start + 1));
|
||
obj_start = std::string::npos;
|
||
}
|
||
} else if (c == ']' && depth == 0) {
|
||
break;
|
||
}
|
||
}
|
||
prev = c;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Extract all string values from a JSON array field (flat, one level).
|
||
static std::vector<std::string> json_array_strings(const std::string& json,
|
||
const std::string& key) {
|
||
std::string search = "\"" + key + "\":[";
|
||
size_t pos = json.find(search);
|
||
if (pos == std::string::npos) {
|
||
search = "\"" + key + "\": [";
|
||
pos = json.find(search);
|
||
}
|
||
if (pos == std::string::npos) return {};
|
||
pos = json.find('[', pos) + 1;
|
||
std::vector<std::string> result;
|
||
while (pos < json.size()) {
|
||
while (pos < json.size() && std::isspace((unsigned char)json[pos])) ++pos;
|
||
if (pos >= json.size() || json[pos] == ']') break;
|
||
if (json[pos] == '"') {
|
||
++pos;
|
||
std::string v;
|
||
while (pos < json.size() && json[pos] != '"') v += json[pos++];
|
||
if (pos < json.size()) ++pos; // skip closing '"'
|
||
result.push_back(v);
|
||
} else {
|
||
++pos; // skip other chars
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
#endif // !__APPLE__ (json_array_objects)
|
||
|
||
// Trim leading/trailing whitespace from a string.
|
||
static std::string trim(const std::string& s) {
|
||
size_t a = 0, b = s.size();
|
||
while (a < b && std::isspace((unsigned char)s[a])) ++a;
|
||
while (b > a && std::isspace((unsigned char)s[b-1])) --b;
|
||
return s.substr(a, b - a);
|
||
}
|
||
|
||
} // anonymous namespace
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// Public interface
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_all() {
|
||
std::vector<DiscoveredPart> all;
|
||
for (auto& v : {discover_memory_sticks(), discover_memory_slots(),
|
||
discover_cpus(), discover_cpu_slots(),
|
||
discover_disks(), discover_nics(),
|
||
discover_gpus()}) {
|
||
all.insert(all.end(), v.begin(), v.end());
|
||
}
|
||
return all;
|
||
}
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover(const std::string& type_name) {
|
||
if (type_name == PTYPE_MEMORY_STICK) return discover_memory_sticks();
|
||
if (type_name == PTYPE_MEMORY_SLOT) return discover_memory_slots();
|
||
if (type_name == PTYPE_CPU) return discover_cpus();
|
||
if (type_name == PTYPE_CPU_SLOT) return discover_cpu_slots();
|
||
if (type_name == PTYPE_DISK) return discover_disks();
|
||
if (type_name == PTYPE_NIC) return discover_nics();
|
||
if (type_name == PTYPE_GPU) return discover_gpus();
|
||
std::cerr << "discover: unknown type '" << type_name << "'\n";
|
||
return {};
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// Memory sticks
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_memory_sticks() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
// ── macOS: system_profiler SPMemoryDataType ───────────────────────────────
|
||
std::string out = run_cmd("system_profiler SPMemoryDataType 2>/dev/null");
|
||
if (out.empty()) {
|
||
std::cerr << "warning: system_profiler SPMemoryDataType returned no data\n";
|
||
return result;
|
||
}
|
||
|
||
// Each "BANK X/..." section is a slot; skip lines until we see "Size:"
|
||
// Parse simple indented key: value text
|
||
std::map<std::string, std::string> block;
|
||
std::string bank_name;
|
||
|
||
auto flush_block = [&]() {
|
||
if (block.empty()) return;
|
||
auto sit = block.find("Size");
|
||
// Skip empty/missing slots
|
||
if (sit == block.end() || sit->second == "Empty" || sit->second.empty()) {
|
||
block.clear(); return;
|
||
}
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_MEMORY_STICK;
|
||
p.kv[K_SIZE_MB] = std::to_string(dmi_size_to_mb(sit->second));
|
||
if (block.count("Speed")) p.kv[K_SPEED_MHZ] = std::to_string(leading_uint(block["Speed"]));
|
||
if (block.count("Manufacturer")) p.kv[K_MANUFACTURER] = block["Manufacturer"];
|
||
if (block.count("Serial Number") && block["Serial Number"] != "-")
|
||
p.kv[K_SERIAL] = block["Serial Number"];
|
||
if (block.count("Type"))
|
||
p.kv[K_OTHER_INFO] = block["Type"];
|
||
// channel_config heuristic from bank name
|
||
if (!bank_name.empty()) {
|
||
std::string lname = bank_name;
|
||
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
||
if (lname.find("channel") != std::string::npos)
|
||
p.kv[K_CHANNEL_CONFIG] = "dual";
|
||
}
|
||
result.push_back(std::move(p));
|
||
block.clear();
|
||
};
|
||
|
||
std::istringstream ss(out);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||
std::string t = trim(line);
|
||
if (t.empty()) continue;
|
||
// Bank header: e.g. " BANK 0/ChannelA-DIMM0:"
|
||
if (t.back() == ':' && t.find('/') != std::string::npos) {
|
||
flush_block();
|
||
bank_name = t.substr(0, t.size() - 1);
|
||
continue;
|
||
}
|
||
size_t colon = t.find(": ");
|
||
if (colon != std::string::npos)
|
||
block[t.substr(0, colon)] = t.substr(colon + 2);
|
||
}
|
||
flush_block();
|
||
|
||
#else
|
||
// ── Linux: dmidecode -t 17 ────────────────────────────────────────────────
|
||
auto blocks = parse_dmi("17");
|
||
if (blocks.empty()) {
|
||
std::cerr << "warning: dmidecode returned no Memory Device data "
|
||
"(may need root)\n";
|
||
return result;
|
||
}
|
||
|
||
for (auto& b : blocks) {
|
||
// Skip empty slots
|
||
if (!b.count("Size")) continue;
|
||
if (b["Size"] == "No Module Installed" || b["Size"].empty() ||
|
||
dmi_size_to_mb(b["Size"]) == 0) continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_MEMORY_STICK;
|
||
|
||
if (b.count("Serial Number") && b["Serial Number"] != "Not Specified" &&
|
||
b["Serial Number"] != "Unknown")
|
||
p.kv[K_SERIAL] = b["Serial Number"];
|
||
|
||
p.kv[K_SIZE_MB] = std::to_string(dmi_size_to_mb(b["Size"]));
|
||
|
||
if (b.count("Speed"))
|
||
p.kv[K_SPEED_MHZ] = std::to_string(leading_uint(b["Speed"]));
|
||
|
||
if (b.count("Manufacturer") && b["Manufacturer"] != "Not Specified")
|
||
p.kv[K_MANUFACTURER] = b["Manufacturer"];
|
||
|
||
// Channel config heuristic from Bank Locator
|
||
if (b.count("Bank Locator")) {
|
||
std::string bl = b["Bank Locator"];
|
||
std::transform(bl.begin(), bl.end(), bl.begin(), ::tolower);
|
||
if (bl.find("channel") != std::string::npos)
|
||
p.kv[K_CHANNEL_CONFIG] = "dual";
|
||
}
|
||
|
||
// other_info: Type + Form Factor
|
||
std::string other;
|
||
if (b.count("Type") && b["Type"] != "Unknown") other += b["Type"];
|
||
if (b.count("Form Factor") && b["Form Factor"] != "Unknown") {
|
||
if (!other.empty()) other += " ";
|
||
other += b["Form Factor"];
|
||
}
|
||
if (!other.empty()) p.kv[K_OTHER_INFO] = other;
|
||
|
||
if (b.count("Locator") && !b["Locator"].empty())
|
||
p.kv[K_LOCATOR] = b["Locator"];
|
||
|
||
// Extended memory fields
|
||
if (b.count("Type") && b["Type"] != "Unknown" && b["Type"] != "Not Specified")
|
||
p.kv[K_MEM_TYPE] = b["Type"];
|
||
|
||
if (b.count("Form Factor") && b["Form Factor"] != "Unknown" && b["Form Factor"] != "Not Specified")
|
||
p.kv[K_FORM_FACTOR] = b["Form Factor"];
|
||
|
||
if (b.count("Part Number")) {
|
||
std::string pn = trim(b["Part Number"]);
|
||
if (!pn.empty() && pn != "Unknown" && pn != "Not Specified")
|
||
p.kv[K_PART_NUMBER] = pn;
|
||
}
|
||
|
||
if (b.count("Rank")) {
|
||
uint32_t rank = static_cast<uint32_t>(leading_uint(b["Rank"]));
|
||
if (rank > 0) p.kv[K_RANK] = std::to_string(rank);
|
||
}
|
||
|
||
uint32_t data_width = 0;
|
||
if (b.count("Data Width")) {
|
||
data_width = static_cast<uint32_t>(leading_uint(b["Data Width"]));
|
||
if (data_width > 0) p.kv[K_DATA_WIDTH] = std::to_string(data_width);
|
||
}
|
||
|
||
// Compute theoretical bandwidth: speed_mhz × 2 × (data_width / 8) MB/s
|
||
if (data_width > 0 && p.kv.count(K_SPEED_MHZ)) {
|
||
uint64_t spd = std::stoull(p.kv[K_SPEED_MHZ]);
|
||
if (spd > 0) {
|
||
uint64_t bw = spd * 2ULL * (data_width / 8);
|
||
p.kv[K_BANDWIDTH_MBPS] = std::to_string(bw);
|
||
}
|
||
}
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// Memory slots
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_memory_slots() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
// On macOS, use system_profiler to infer slot count from bank info
|
||
std::string out = run_cmd("system_profiler SPMemoryDataType 2>/dev/null");
|
||
if (out.empty()) return result;
|
||
|
||
uint64_t allowed_speed = 0;
|
||
int slot_count = 0;
|
||
|
||
std::istringstream ss(out);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
std::string t = trim(line);
|
||
if (t.back() == ':' && t.find('/') != std::string::npos) {
|
||
++slot_count;
|
||
}
|
||
if (t.rfind("Speed:", 0) == 0 && allowed_speed == 0) {
|
||
allowed_speed = leading_uint(t.substr(7));
|
||
}
|
||
}
|
||
|
||
for (int s = 0; s < slot_count; ++s) {
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_MEMORY_SLOT;
|
||
if (allowed_speed) p.kv[K_ALLOWED_SPEED] = std::to_string(allowed_speed);
|
||
p.kv[K_INSTALLED_STICK] = "NULL";
|
||
result.push_back(std::move(p));
|
||
}
|
||
#else
|
||
// ── Linux: dmidecode -t 17 (all slots) + -t 16 for max capacity ───────────
|
||
auto blocks17 = parse_dmi("17");
|
||
if (blocks17.empty()) {
|
||
std::cerr << "warning: dmidecode returned no Memory Device data "
|
||
"(may need root)\n";
|
||
return result;
|
||
}
|
||
|
||
// Get max allowed size from dmidecode -t 16 (Physical Memory Array)
|
||
uint64_t max_size_mb = 0;
|
||
{
|
||
auto blocks16 = parse_dmi("16");
|
||
for (auto& b : blocks16) {
|
||
if (b.count("Maximum Capacity")) {
|
||
uint64_t mc = dmi_size_to_mb(b["Maximum Capacity"]);
|
||
if (mc > max_size_mb) max_size_mb = mc;
|
||
}
|
||
}
|
||
}
|
||
|
||
for (auto& b : blocks17) {
|
||
// Skip blocks without a Locator — these are malformed/phantom HP firmware entries
|
||
if (!b.count("Locator") || b["Locator"].empty()) continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_MEMORY_SLOT;
|
||
|
||
// Allowed speed
|
||
uint64_t spd = 0;
|
||
if (b.count("Configured Memory Speed"))
|
||
spd = leading_uint(b["Configured Memory Speed"]);
|
||
if (!spd && b.count("Speed"))
|
||
spd = leading_uint(b["Speed"]);
|
||
if (spd) p.kv[K_ALLOWED_SPEED] = std::to_string(spd);
|
||
|
||
// Allowed size (per-slot approximation: total max / slot count)
|
||
if (max_size_mb > 0 && !blocks17.empty())
|
||
p.kv[K_ALLOWED_SIZE] = std::to_string(
|
||
max_size_mb / static_cast<uint64_t>(blocks17.size()));
|
||
|
||
p.kv[K_INSTALLED_STICK] = "NULL";
|
||
if (b.count("Locator") && !b["Locator"].empty())
|
||
p.kv[K_LOCATOR] = b["Locator"];
|
||
result.push_back(std::move(p));
|
||
}
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// CPUs
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_cpus() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
std::string brand = run_cmd("sysctl -n machdep.cpu.brand_string 2>/dev/null");
|
||
std::string phys = run_cmd("sysctl -n hw.physicalcpu 2>/dev/null");
|
||
std::string logi = run_cmd("sysctl -n hw.logicalcpu 2>/dev/null");
|
||
std::string freq = run_cmd("sysctl -n hw.cpufrequency 2>/dev/null");
|
||
|
||
if (brand.empty()) {
|
||
std::cerr << "warning: sysctl cpu info unavailable\n";
|
||
return result;
|
||
}
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_CPU;
|
||
p.kv[K_NAME] = brand;
|
||
|
||
// Try to extract speed from brand string e.g. "@ 2.60GHz"
|
||
double speed_ghz = 0.0;
|
||
auto at_pos = brand.find(" @ ");
|
||
if (at_pos != std::string::npos) {
|
||
std::string sp = brand.substr(at_pos + 3);
|
||
try { speed_ghz = std::stod(sp); } catch (...) {}
|
||
}
|
||
if (speed_ghz == 0.0 && !freq.empty()) {
|
||
try { speed_ghz = std::stoull(freq) / 1e9; } catch (...) {}
|
||
}
|
||
if (speed_ghz > 0.0) {
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "%.2f", speed_ghz);
|
||
p.kv[K_SPEED_GHZ] = buf;
|
||
}
|
||
|
||
if (!phys.empty()) p.kv[K_CORES] = phys;
|
||
if (!logi.empty()) p.kv[K_THREADS] = logi;
|
||
|
||
// Manufacturer heuristic
|
||
std::string lower_brand = brand;
|
||
std::transform(lower_brand.begin(), lower_brand.end(), lower_brand.begin(), ::tolower);
|
||
if (lower_brand.find("intel") != std::string::npos) p.kv[K_MANUFACTURER] = "Intel";
|
||
else if (lower_brand.find("amd") != std::string::npos) p.kv[K_MANUFACTURER] = "AMD";
|
||
else if (lower_brand.find("apple") != std::string::npos) p.kv[K_MANUFACTURER] = "Apple";
|
||
|
||
result.push_back(std::move(p));
|
||
|
||
#else
|
||
// ── Linux: dmidecode -t 4 ─────────────────────────────────────────────────
|
||
auto blocks = parse_dmi("4");
|
||
if (blocks.empty()) {
|
||
std::cerr << "warning: dmidecode returned no Processor data "
|
||
"(may need root)\n";
|
||
return result;
|
||
}
|
||
|
||
for (auto& b : blocks) {
|
||
// Skip phantom entries (iLO processor, "Other" type, absent Status)
|
||
if (!b.count("Status") || b["Status"].find("Populated") == std::string::npos) continue;
|
||
if (b.count("Type") && b["Type"].find("Central Processor") == std::string::npos) continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_CPU;
|
||
|
||
// Do NOT use dmidecode "ID" (CPUID family/model/stepping) as serial —
|
||
// it is identical across matching CPUs in multi-socket systems.
|
||
// Socket designation is the actual unique natural key.
|
||
if (b.count("Version") && b["Version"] != "Not Specified")
|
||
p.kv[K_NAME] = b["Version"];
|
||
if (b.count("Manufacturer") && b["Manufacturer"] != "Not Specified")
|
||
p.kv[K_MANUFACTURER] = b["Manufacturer"];
|
||
|
||
if (b.count("Current Speed")) {
|
||
double mhz = static_cast<double>(leading_uint(b["Current Speed"]));
|
||
if (mhz > 0) {
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "%.3f", mhz / 1000.0);
|
||
p.kv[K_SPEED_GHZ] = buf;
|
||
}
|
||
}
|
||
if (b.count("Core Count")) p.kv[K_CORES] = b["Core Count"];
|
||
if (b.count("Thread Count")) p.kv[K_THREADS] = b["Thread Count"];
|
||
|
||
if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified")
|
||
p.kv[K_SOCKET] = b["Socket Designation"];
|
||
|
||
// Extended CPU fields from dmidecode type 4
|
||
if (b.count("Max Speed")) {
|
||
double max_mhz = static_cast<double>(leading_uint(b["Max Speed"]));
|
||
if (max_mhz > 0) {
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "%.3f", max_mhz / 1000.0);
|
||
p.kv[K_MAX_SPEED_GHZ] = buf;
|
||
}
|
||
}
|
||
if (b.count("External Clock")) {
|
||
uint64_t bus_mhz = leading_uint(b["External Clock"]);
|
||
if (bus_mhz > 0) p.kv[K_BUS_MHZ] = std::to_string(bus_mhz);
|
||
}
|
||
if (b.count("Voltage")) {
|
||
std::string vs = b["Voltage"];
|
||
// trim " V" suffix
|
||
while (!vs.empty() && (vs.back() == 'V' || vs.back() == ' ')) vs.pop_back();
|
||
if (!vs.empty()) {
|
||
try {
|
||
double v = std::stod(vs);
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "%.1f", v);
|
||
p.kv[K_VOLTAGE_V] = buf;
|
||
} catch (...) {}
|
||
}
|
||
}
|
||
if (b.count("Upgrade") && b["Upgrade"] != "Unknown" && b["Upgrade"] != "Other") {
|
||
p.kv[K_SOCKET_TYPE] = b["Upgrade"];
|
||
}
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
|
||
// Enrich CPUs with L1/L2/L3 cache sizes from sysfs
|
||
// Build a map: physical_package_id → first cpu index
|
||
{
|
||
std::map<int, int> pkg_to_first_cpu;
|
||
for (int ci = 0; ; ++ci) {
|
||
std::string pkg_path = "/sys/devices/system/cpu/cpu"
|
||
+ std::to_string(ci) + "/topology/physical_package_id";
|
||
FILE* pf = fopen(pkg_path.c_str(), "r");
|
||
if (!pf) break;
|
||
int pkg = -1;
|
||
fscanf(pf, "%d", &pkg);
|
||
fclose(pf);
|
||
if (pkg >= 0 && pkg_to_first_cpu.find(pkg) == pkg_to_first_cpu.end())
|
||
pkg_to_first_cpu[pkg] = ci;
|
||
}
|
||
|
||
// For each CPU entry in result (in order), pick socket 0, 1, 2 ...
|
||
int socket_idx = 0;
|
||
for (auto& p : result) {
|
||
if (p.type_name != PTYPE_CPU) { ++socket_idx; continue; }
|
||
auto it = pkg_to_first_cpu.find(socket_idx);
|
||
int cpu_idx = (it != pkg_to_first_cpu.end()) ? it->second : socket_idx;
|
||
|
||
// Walk cache indices for this cpu
|
||
uint32_t l1_kb = 0, l2_kb = 0, l3_kb = 0;
|
||
for (int idx = 0; idx < 8; ++idx) {
|
||
std::string base = "/sys/devices/system/cpu/cpu"
|
||
+ std::to_string(cpu_idx) + "/cache/index"
|
||
+ std::to_string(idx);
|
||
// Read level
|
||
int level = 0;
|
||
{
|
||
FILE* lf = fopen((base + "/level").c_str(), "r");
|
||
if (!lf) break;
|
||
fscanf(lf, "%d", &level);
|
||
fclose(lf);
|
||
}
|
||
// Read type
|
||
std::string cache_type;
|
||
{
|
||
FILE* tf = fopen((base + "/type").c_str(), "r");
|
||
if (tf) {
|
||
char tbuf[32] = {};
|
||
fscanf(tf, "%31s", tbuf);
|
||
fclose(tf);
|
||
cache_type = tbuf;
|
||
}
|
||
}
|
||
// Read size
|
||
std::string size_str;
|
||
{
|
||
FILE* sf = fopen((base + "/size").c_str(), "r");
|
||
if (sf) {
|
||
char sbuf[32] = {};
|
||
fscanf(sf, "%31s", sbuf);
|
||
fclose(sf);
|
||
size_str = sbuf;
|
||
}
|
||
}
|
||
if (size_str.empty()) continue;
|
||
// Parse size: "32K", "256K", "9216K", "6144K"
|
||
uint32_t kb = 0;
|
||
{
|
||
size_t ni = 0;
|
||
while (ni < size_str.size() && std::isdigit((unsigned char)size_str[ni])) ++ni;
|
||
if (ni > 0) {
|
||
kb = static_cast<uint32_t>(std::stoul(size_str.substr(0, ni)));
|
||
std::string unit = size_str.substr(ni);
|
||
if (!unit.empty() && (unit[0] == 'M' || unit[0] == 'm')) kb *= 1024;
|
||
}
|
||
}
|
||
if (kb == 0) continue;
|
||
if (level == 1 && cache_type == "Data") l1_kb += kb;
|
||
else if (level == 1 && cache_type == "Instruction") {} // skip icache
|
||
else if (level == 1 && cache_type == "Unified") l1_kb += kb;
|
||
else if (level == 2) l2_kb += kb;
|
||
else if (level == 3) l3_kb += kb;
|
||
}
|
||
|
||
if (l1_kb > 0) p.kv[K_CACHE_L1_KB] = std::to_string(l1_kb);
|
||
if (l2_kb > 0) p.kv[K_CACHE_L2_KB] = std::to_string(l2_kb);
|
||
if (l3_kb > 0) p.kv[K_CACHE_L3_KB] = std::to_string(l3_kb);
|
||
++socket_idx;
|
||
}
|
||
}
|
||
|
||
// /proc/cpuinfo fallback if dmidecode gave nothing
|
||
if (result.empty()) {
|
||
std::string cpuinfo = run_cmd("cat /proc/cpuinfo 2>/dev/null");
|
||
if (!cpuinfo.empty()) {
|
||
// Group by physical id
|
||
std::map<int, std::map<std::string,std::string>> by_phys;
|
||
std::map<std::string,std::string> cur_block;
|
||
int cur_phys = -1;
|
||
std::istringstream cs(cpuinfo);
|
||
std::string cl;
|
||
auto flush_cpuinfo = [&]() {
|
||
if (cur_phys < 0) return;
|
||
auto& target = by_phys[cur_phys];
|
||
for (auto& [k, v] : cur_block) {
|
||
if (target.find(k) == target.end())
|
||
target[k] = v;
|
||
}
|
||
};
|
||
while (std::getline(cs, cl)) {
|
||
if (cl.empty()) continue;
|
||
size_t colon = cl.find(':');
|
||
if (colon == std::string::npos) continue;
|
||
std::string key = trim(cl.substr(0, colon));
|
||
std::string val = trim(cl.substr(colon + 1));
|
||
if (key == "physical id") {
|
||
int phys = std::stoi(val);
|
||
if (phys != cur_phys) {
|
||
flush_cpuinfo();
|
||
cur_phys = phys;
|
||
cur_block.clear();
|
||
}
|
||
} else {
|
||
cur_block[key] = val;
|
||
}
|
||
}
|
||
flush_cpuinfo();
|
||
for (auto& [phys_id, fields] : by_phys) {
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_CPU;
|
||
if (fields.count("model name")) p.kv[K_NAME] = fields["model name"];
|
||
if (fields.count("cpu MHz")) {
|
||
try {
|
||
double mhz = std::stod(fields["cpu MHz"]);
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "%.3f", mhz / 1000.0);
|
||
p.kv[K_SPEED_GHZ] = buf;
|
||
} catch (...) {}
|
||
}
|
||
if (fields.count("cpu cores")) p.kv[K_CORES] = fields["cpu cores"];
|
||
if (fields.count("siblings")) p.kv[K_THREADS] = fields["siblings"];
|
||
if (fields.count("cache size")) {
|
||
// "9216 KB"
|
||
uint32_t kb = static_cast<uint32_t>(leading_uint(fields["cache size"]));
|
||
if (kb > 0) p.kv[K_CACHE_L3_KB] = std::to_string(kb);
|
||
}
|
||
// Manufacturer from model name heuristic
|
||
if (fields.count("model name")) {
|
||
std::string lower = fields["model name"];
|
||
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
||
if (lower.find("intel") != std::string::npos) p.kv[K_MANUFACTURER] = "Intel";
|
||
else if (lower.find("amd") != std::string::npos) p.kv[K_MANUFACTURER] = "AMD";
|
||
}
|
||
// Use "Proc <N>" style socket designation based on physical id order
|
||
p.kv[K_SOCKET] = "Proc " + std::to_string(phys_id + 1);
|
||
result.push_back(std::move(p));
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// CPU slots
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_cpu_slots() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
// On macOS we can only infer 1 slot from sysctl
|
||
if (run_cmd("sysctl -n machdep.cpu.brand_string 2>/dev/null").empty())
|
||
return result;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_CPU_SLOT;
|
||
p.kv[K_FORM_FACTOR] = "unknown";
|
||
p.kv[K_INSTALLED_CPU] = "NULL";
|
||
result.push_back(std::move(p));
|
||
#else
|
||
auto blocks = parse_dmi("4");
|
||
if (blocks.empty()) {
|
||
std::cerr << "warning: dmidecode returned no Processor data "
|
||
"(may need root)\n";
|
||
return result;
|
||
}
|
||
|
||
for (auto& b : blocks) {
|
||
// Only real CPU sockets: must have a recognizable Status (Populated or Unpopulated)
|
||
// and must be of type Central Processor
|
||
if (!b.count("Status")) continue;
|
||
const std::string& status = b["Status"];
|
||
bool is_populated = status.find("Populated") != std::string::npos;
|
||
bool is_unpopulated = status.find("Unpopulated") != std::string::npos;
|
||
if (!is_populated && !is_unpopulated) continue; // Skip "Other", empty, or phantom entries
|
||
if (b.count("Type") && b["Type"].find("Central Processor") == std::string::npos) continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_CPU_SLOT;
|
||
if (b.count("Upgrade")) p.kv[K_FORM_FACTOR] = b["Upgrade"];
|
||
else p.kv[K_FORM_FACTOR] = "unknown";
|
||
p.kv[K_INSTALLED_CPU] = "NULL";
|
||
if (b.count("Socket Designation") && b["Socket Designation"] != "Not Specified")
|
||
p.kv[K_SOCKET] = b["Socket Designation"];
|
||
result.push_back(std::move(p));
|
||
}
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// Disks
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_disks() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
// ── macOS: diskutil info for each disk ────────────────────────────────────
|
||
// Enumerate whole disks via XML plist from diskutil list -plist
|
||
std::vector<std::string> all_disks;
|
||
{
|
||
std::string plist_out = run_cmd("diskutil list -plist 2>/dev/null");
|
||
if (!plist_out.empty()) {
|
||
// Parse <key>WholeDisks</key><array><string>disk0</string>...</array>
|
||
size_t key_pos = plist_out.find("<key>WholeDisks</key>");
|
||
if (key_pos != std::string::npos) {
|
||
size_t arr_start = plist_out.find("<array>", key_pos);
|
||
size_t arr_end = plist_out.find("</array>", key_pos);
|
||
if (arr_start != std::string::npos && arr_end != std::string::npos) {
|
||
std::string arr_content = plist_out.substr(arr_start, arr_end - arr_start);
|
||
size_t p = 0;
|
||
while ((p = arr_content.find("<string>", p)) != std::string::npos) {
|
||
p += 8;
|
||
size_t e = arr_content.find("</string>", p);
|
||
if (e == std::string::npos) break;
|
||
all_disks.push_back(arr_content.substr(p, e - p));
|
||
p = e + 9;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (all_disks.empty()) {
|
||
// fall back: text diskutil list output
|
||
std::string txt = run_cmd("diskutil list 2>/dev/null");
|
||
std::istringstream ss(txt);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
std::string t = trim(line);
|
||
if (t.rfind("/dev/disk", 0) == 0) {
|
||
size_t sp = t.find(' ');
|
||
std::string dev = (sp != std::string::npos) ? t.substr(5, sp - 5) : t.substr(5);
|
||
if (dev.find('s') == std::string::npos) all_disks.push_back(dev);
|
||
}
|
||
}
|
||
}
|
||
if (all_disks.empty()) {
|
||
std::cerr << "warning: could not enumerate disks via diskutil\n";
|
||
return result;
|
||
}
|
||
}
|
||
|
||
for (auto& disk_name : all_disks) {
|
||
std::string info = run_cmd("diskutil info -plist /dev/" + disk_name + " 2>/dev/null");
|
||
if (info.empty()) continue;
|
||
|
||
// Parse plist-style XML (very simplified)
|
||
// Extract key/value pairs from <key>K</key><string>V</string>
|
||
std::map<std::string, std::string> plist;
|
||
std::istringstream ss(info);
|
||
std::string line;
|
||
std::string cur_key;
|
||
while (std::getline(ss, line)) {
|
||
std::string t = trim(line);
|
||
auto tag_val = [&](const std::string& tag) -> std::string {
|
||
std::string open = "<" + tag + ">";
|
||
std::string close = "</" + tag + ">";
|
||
size_t p0 = t.find(open);
|
||
if (p0 == std::string::npos) return "";
|
||
p0 += open.size();
|
||
size_t p1 = t.find(close, p0);
|
||
if (p1 == std::string::npos) return "";
|
||
return t.substr(p0, p1 - p0);
|
||
};
|
||
std::string k = tag_val("key");
|
||
if (!k.empty()) { cur_key = k; continue; }
|
||
if (!cur_key.empty()) {
|
||
for (auto& vtag : {"string", "integer", "real"}) {
|
||
std::string v = tag_val(vtag);
|
||
if (!v.empty()) { plist[cur_key] = v; cur_key = ""; break; }
|
||
}
|
||
if (t == "<true/>") { plist[cur_key] = "true"; cur_key = ""; }
|
||
if (t == "<false/>") { plist[cur_key] = "false"; cur_key = ""; }
|
||
}
|
||
}
|
||
|
||
// Skip virtual disks
|
||
auto ct = plist.count("VirtualOrPhysical") ? plist["VirtualOrPhysical"] : "";
|
||
if (ct == "Virtual") continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_DISK;
|
||
|
||
if (plist.count("MediaName")) p.kv[K_MODEL] = plist["MediaName"];
|
||
if (plist.count("IORegistryEntryName") && !p.kv.count(K_MODEL))
|
||
p.kv[K_MODEL] = plist["IORegistryEntryName"];
|
||
|
||
// Size
|
||
if (plist.count("TotalSize"))
|
||
p.kv[K_DISK_SIZE] = std::to_string(
|
||
static_cast<uint64_t>(std::stoull(plist["TotalSize"]) / 1000000000ULL));
|
||
|
||
// Type
|
||
bool solid = false;
|
||
if (plist.count("SolidState")) solid = (plist["SolidState"] == "true");
|
||
std::string protocol = plist.count("BusProtocol") ? plist["BusProtocol"] : "";
|
||
std::string conn = protocol;
|
||
std::string dtype;
|
||
std::string lower_proto = protocol;
|
||
std::transform(lower_proto.begin(), lower_proto.end(), lower_proto.begin(), ::tolower);
|
||
if (lower_proto.find("nvme") != std::string::npos ||
|
||
lower_proto.find("pcie") != std::string::npos) {
|
||
dtype = "nvme"; conn = "NVMe";
|
||
} else if (solid) {
|
||
dtype = "ssd";
|
||
} else {
|
||
dtype = "hdd";
|
||
}
|
||
if (!dtype.empty()) p.kv[K_DISK_TYPE] = dtype;
|
||
if (!conn.empty()) p.kv[K_CONN_TYPE] = conn;
|
||
|
||
// Speed heuristic
|
||
uint64_t conn_speed = 0;
|
||
if (lower_proto.find("nvme") != std::string::npos) conn_speed = 3500;
|
||
else if (lower_proto.find("sata") != std::string::npos) conn_speed = 600;
|
||
else if (lower_proto.find("usb") != std::string::npos) conn_speed = 480;
|
||
if (conn_speed) p.kv[K_CONN_SPEED] = std::to_string(conn_speed);
|
||
p.kv[K_DISK_SPEED] = "0";
|
||
p.kv[K_AGE_YEARS] = "-1";
|
||
|
||
// Partitions — use diskutil list <disk> text output for partition names
|
||
std::string parts_txt = run_cmd("diskutil list /dev/" + disk_name + " 2>/dev/null");
|
||
std::vector<std::string> part_ids, part_sizes;
|
||
{
|
||
std::istringstream ps(parts_txt);
|
||
std::string pline;
|
||
while (std::getline(ps, pline)) {
|
||
std::string pt = trim(pline);
|
||
// lines containing "disk0s1" etc.
|
||
if (pt.find(disk_name + "s") != std::string::npos) {
|
||
// Extract partition dev name (last token-ish)
|
||
std::istringstream ts(pt);
|
||
std::string tok, last_tok;
|
||
while (ts >> tok) last_tok = tok;
|
||
if (!last_tok.empty() && last_tok.find(disk_name) != std::string::npos)
|
||
part_ids.push_back(last_tok);
|
||
}
|
||
}
|
||
}
|
||
if (!part_ids.empty()) {
|
||
std::string pid_str, psz_str;
|
||
for (size_t ki = 0; ki < part_ids.size(); ++ki) {
|
||
if (ki) { pid_str += LS; psz_str += LS; }
|
||
pid_str += part_ids[ki];
|
||
psz_str += "0";
|
||
}
|
||
p.kv[K_PARTITION_IDS] = pid_str;
|
||
p.kv[K_PARTITION_SIZES] = psz_str;
|
||
}
|
||
|
||
p.kv[K_VM_HOSTNAMES] = "";
|
||
p.kv[K_VM_SERVER_IDS] = "";
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
|
||
#else
|
||
// ── Linux: lsblk -J ──────────────────────────────────────────────────────
|
||
std::string out = run_cmd(
|
||
"lsblk -J -o NAME,SIZE,TYPE,ROTA,SERIAL,MODEL,VENDOR,TRAN,"
|
||
"LOG-SEC,PHY-SEC,MOUNTPOINTS 2>/dev/null");
|
||
if (out.empty()) {
|
||
std::cerr << "warning: lsblk returned no data\n";
|
||
return result;
|
||
}
|
||
|
||
auto devices = json_array_objects(out, "blockdevices");
|
||
for (auto& dev : devices) {
|
||
// Only top-level disks
|
||
std::string type = json_str(dev, "type");
|
||
if (type != "disk") continue;
|
||
|
||
std::string dev_name = json_str(dev, "name");
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_DISK;
|
||
|
||
std::string serial = json_str(dev, "serial");
|
||
std::string model = trim(json_str(dev, "model"));
|
||
std::string vendor = trim(json_str(dev, "vendor"));
|
||
std::string rota = json_str(dev, "rota");
|
||
std::string tran = json_str(dev, "tran");
|
||
std::string size_str = json_str(dev, "size");
|
||
|
||
if (!serial.empty() && serial != "null") p.kv[K_SERIAL] = serial;
|
||
if (!model.empty()) p.kv[K_MODEL] = model;
|
||
if (!vendor.empty()) p.kv[K_MANUFACTURER] = vendor;
|
||
|
||
// disk_type
|
||
std::string lower_tran = tran;
|
||
std::transform(lower_tran.begin(), lower_tran.end(), lower_tran.begin(), ::tolower);
|
||
std::string dtype;
|
||
if (lower_tran.find("nvme") != std::string::npos) dtype = "nvme";
|
||
else if (rota == "0") dtype = "ssd";
|
||
else dtype = "hdd";
|
||
p.kv[K_DISK_TYPE] = dtype;
|
||
p.kv[K_CONN_TYPE] = tran.empty() ? "unknown" : tran;
|
||
|
||
// connection speed heuristic
|
||
uint64_t conn_speed = 0;
|
||
if (lower_tran.find("nvme") != std::string::npos) conn_speed = 3500;
|
||
else if (lower_tran.find("sata") != std::string::npos) conn_speed = 600;
|
||
else if (lower_tran.find("usb") != std::string::npos) conn_speed = 480;
|
||
else if (lower_tran.find("sas") != std::string::npos) conn_speed = 1200;
|
||
p.kv[K_CONN_SPEED] = std::to_string(conn_speed);
|
||
p.kv[K_DISK_SPEED] = "0";
|
||
p.kv[K_DISK_SIZE] = std::to_string(size_to_gb(size_str));
|
||
p.kv[K_AGE_YEARS] = "-1";
|
||
|
||
// Partitions from "children" array
|
||
auto children = json_array_objects(dev, "children");
|
||
std::vector<std::string> part_ids, part_sizes;
|
||
for (auto& child : children) {
|
||
if (json_str(child, "type") == "part") {
|
||
part_ids.push_back(json_str(child, "name"));
|
||
part_sizes.push_back(std::to_string(size_to_gb(json_str(child, "size"))));
|
||
}
|
||
}
|
||
if (!part_ids.empty()) {
|
||
std::string pid_str, psz_str;
|
||
for (size_t ki = 0; ki < part_ids.size(); ++ki) {
|
||
if (ki) { pid_str += LS; psz_str += LS; }
|
||
pid_str += part_ids[ki];
|
||
psz_str += part_sizes[ki];
|
||
}
|
||
p.kv[K_PARTITION_IDS] = pid_str;
|
||
p.kv[K_PARTITION_SIZES] = psz_str;
|
||
}
|
||
|
||
p.kv[K_VM_HOSTNAMES] = "";
|
||
p.kv[K_VM_SERVER_IDS] = "";
|
||
|
||
// Handle serial collisions: if another disk in result already has the same serial,
|
||
// differentiate both by appending the device name to the serial.
|
||
if (!dev_name.empty()) {
|
||
p.kv[K_DEVICE_NAME] = dev_name;
|
||
auto ser_it = p.kv.find(K_SERIAL);
|
||
if (ser_it != p.kv.end()) {
|
||
for (auto& prev : result) {
|
||
auto prev_ser = prev.kv.find(K_SERIAL);
|
||
if (prev_ser != prev.kv.end() && prev_ser->second == ser_it->second) {
|
||
// Collision: suffix both with their device names
|
||
auto prev_dev = prev.kv.find(K_DEVICE_NAME);
|
||
std::string prev_name = (prev_dev != prev.kv.end()) ? prev_dev->second : "";
|
||
if (!prev_name.empty())
|
||
prev_ser->second += ":" + prev_name;
|
||
ser_it->second += ":" + dev_name;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_nics() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
// ── macOS: networksetup + ifconfig ────────────────────────────────────────
|
||
// networksetup -listallhardwareports gives:
|
||
// Hardware Port: Ethernet
|
||
// Device: en0
|
||
// Ethernet Address: xx:xx:xx:xx:xx:xx
|
||
std::string ns_out = run_cmd("networksetup -listallhardwareports 2>/dev/null");
|
||
if (ns_out.empty()) {
|
||
std::cerr << "warning: networksetup not available\n";
|
||
return result;
|
||
}
|
||
|
||
struct NsPort { std::string port, device, mac; };
|
||
std::vector<NsPort> ports;
|
||
{
|
||
NsPort cur;
|
||
std::istringstream ss(ns_out);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
std::string t = trim(line);
|
||
auto extract = [&](const std::string& prefix) -> std::string {
|
||
if (t.rfind(prefix, 0) == 0) return trim(t.substr(prefix.size()));
|
||
return "";
|
||
};
|
||
auto pv = extract("Hardware Port: ");
|
||
if (!pv.empty()) { if (!cur.device.empty()) ports.push_back(cur); cur = {pv, "", ""}; continue; }
|
||
auto dv = extract("Device: ");
|
||
if (!dv.empty()) { cur.device = dv; continue; }
|
||
auto mv = extract("Ethernet Address: ");
|
||
if (!mv.empty()) { cur.mac = mv; continue; }
|
||
}
|
||
if (!cur.device.empty()) ports.push_back(cur);
|
||
}
|
||
|
||
for (auto& pt : ports) {
|
||
if (pt.device.empty()) continue;
|
||
// Skip loopback
|
||
if (pt.device == "lo0" || pt.device.rfind("lo", 0) == 0) continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_NIC;
|
||
p.kv[K_MAC] = pt.mac;
|
||
p.kv[K_MODEL] = pt.port;
|
||
|
||
// connection type
|
||
std::string lower_port = pt.port;
|
||
std::transform(lower_port.begin(), lower_port.end(), lower_port.begin(), ::tolower);
|
||
if (lower_port.find("wi-fi") != std::string::npos ||
|
||
lower_port.find("wifi") != std::string::npos ||
|
||
lower_port.find("wlan") != std::string::npos ||
|
||
lower_port.find("airport") != std::string::npos)
|
||
p.kv[K_CONN_TYPE] = "wifi";
|
||
else
|
||
p.kv[K_CONN_TYPE] = "ethernet";
|
||
|
||
// IPs via ifconfig
|
||
std::string ic_out = run_cmd("ifconfig " + pt.device + " 2>/dev/null");
|
||
std::vector<std::string> ips;
|
||
{
|
||
std::istringstream ss(ic_out);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
std::string t = trim(line);
|
||
if (t.rfind("inet ", 0) == 0 || t.rfind("inet6 ", 0) == 0) {
|
||
bool v6 = t[4] == '6';
|
||
std::string rest = t.substr(v6 ? 6 : 5);
|
||
// Extract addr and prefix/netmask
|
||
std::istringstream ts(rest);
|
||
std::string addr;
|
||
ts >> addr;
|
||
// Skip link-local for IPv6 unless it's the only address
|
||
if (v6 && addr.rfind("fe80", 0) == 0) continue;
|
||
std::string prefix_str;
|
||
std::string tok;
|
||
while (ts >> tok) {
|
||
if (tok == "prefixlen" || tok == "netmask") {
|
||
ts >> prefix_str;
|
||
break;
|
||
}
|
||
}
|
||
if (!prefix_str.empty() && prefix_str.rfind("0x", 0) == 0) {
|
||
// Convert hex netmask to prefix length
|
||
uint32_t mask = static_cast<uint32_t>(std::stoul(prefix_str, nullptr, 16));
|
||
int bits = 0;
|
||
while (mask) { bits += (mask & 1); mask >>= 1; }
|
||
prefix_str = std::to_string(bits);
|
||
}
|
||
std::string entry = addr;
|
||
if (!prefix_str.empty()) entry += "/" + prefix_str;
|
||
ips.push_back(entry);
|
||
}
|
||
}
|
||
}
|
||
if (!ips.empty()) {
|
||
std::string ips_str;
|
||
for (size_t ki = 0; ki < ips.size(); ++ki) {
|
||
if (ki) ips_str += LS;
|
||
ips_str += ips[ki];
|
||
}
|
||
p.kv[K_IPS] = ips_str;
|
||
}
|
||
|
||
p.kv[K_CONN_SPEED] = "0";
|
||
p.kv[K_AGE_YEARS] = "-1";
|
||
p.kv[K_DHCP] = "false";
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
|
||
#else
|
||
// ── Linux: ip -j link show + ip -j addr show ──────────────────────────────
|
||
std::string link_out = run_cmd("ip -j link show 2>/dev/null");
|
||
if (link_out.empty()) {
|
||
std::cerr << "warning: ip -j link show failed\n";
|
||
return result;
|
||
}
|
||
|
||
auto ifaces = json_array_objects(link_out, ""); // top-level array
|
||
// ip -j link show returns a JSON array at the root level, not under a key
|
||
// Re-parse: split the root array
|
||
{
|
||
// json_array_objects looks for a named key; for root arrays we parse directly
|
||
ifaces.clear();
|
||
int depth = 0;
|
||
size_t obj_start = std::string::npos;
|
||
bool in_string = false;
|
||
char prev = '\0';
|
||
for (size_t i = 0; i < link_out.size(); ++i) {
|
||
char c = link_out[i];
|
||
if (c == '"' && prev != '\\') in_string = !in_string;
|
||
if (!in_string) {
|
||
if (c == '{') { if (depth == 0) obj_start = i; ++depth; }
|
||
else if (c == '}') {
|
||
--depth;
|
||
if (depth == 0 && obj_start != std::string::npos) {
|
||
ifaces.push_back(link_out.substr(obj_start, i - obj_start + 1));
|
||
obj_start = std::string::npos;
|
||
}
|
||
}
|
||
}
|
||
prev = c;
|
||
}
|
||
}
|
||
|
||
for (auto& iface : ifaces) {
|
||
std::string ifname = json_str(iface, "ifname");
|
||
std::string mac = json_str(iface, "address");
|
||
std::string link_type= json_str(iface, "link_type");
|
||
std::string flags_str= json_str(iface, "flags"); // may be array
|
||
|
||
if (ifname.empty()) continue;
|
||
// Only include physical NICs: those with a /device symlink pointing to a PCI entry.
|
||
// Virtual interfaces (bridges, veth, flannel, fwbr, vmbr, tap, etc.) have no device symlink.
|
||
{
|
||
std::string dev_path = "/sys/class/net/" + ifname + "/device";
|
||
struct stat st;
|
||
if (lstat(dev_path.c_str(), &st) != 0 || !S_ISLNK(st.st_mode)) continue;
|
||
}
|
||
|
||
// Enrich with PCI device description via lspci
|
||
std::string pci_model;
|
||
std::string pci_vendor;
|
||
{
|
||
std::string link = run_cmd("readlink /sys/class/net/" + ifname + "/device 2>/dev/null");
|
||
// link is like "../../../0000:01:00.0" — extract last path component
|
||
size_t last_slash = link.rfind('/');
|
||
if (last_slash != std::string::npos) link = link.substr(last_slash + 1);
|
||
// trim whitespace
|
||
while (!link.empty() && std::isspace((unsigned char)link.back())) link.pop_back();
|
||
if (!link.empty()) {
|
||
std::string lspci_out = run_cmd("lspci -s " + link + " 2>/dev/null");
|
||
// Format: "0000:01:00.0 Ethernet controller: Broadcom Corporation NetXtreme II..."
|
||
// Find first ": " after the address
|
||
size_t colon = lspci_out.find(": ");
|
||
if (colon != std::string::npos) {
|
||
std::string desc = lspci_out.substr(colon + 2);
|
||
while (!desc.empty() && std::isspace((unsigned char)desc.back())) desc.pop_back();
|
||
// Try to split "VendorName Description" — look for another ": " for sub-vendor
|
||
size_t sub = desc.find(": ");
|
||
if (sub != std::string::npos) {
|
||
pci_vendor = desc.substr(0, sub);
|
||
pci_model = desc.substr(sub + 2);
|
||
} else {
|
||
pci_model = desc;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_NIC;
|
||
p.kv[K_NAME] = ifname;
|
||
p.kv[K_MAC] = mac;
|
||
p.kv[K_MODEL] = pci_model.empty() ? link_type : pci_model;
|
||
if (!pci_vendor.empty()) p.kv[K_MANUFACTURER] = pci_vendor;
|
||
|
||
// connection type
|
||
std::string lower_ifname = ifname;
|
||
std::transform(lower_ifname.begin(), lower_ifname.end(),
|
||
lower_ifname.begin(), ::tolower);
|
||
std::string conn_type = "ethernet";
|
||
if (lower_ifname.find("wl") != std::string::npos ||
|
||
lower_ifname.find("wifi") != std::string::npos)
|
||
conn_type = "wifi";
|
||
p.kv[K_CONN_TYPE] = conn_type;
|
||
|
||
// IPs
|
||
std::string addr_out = run_cmd("ip -j addr show " + ifname + " 2>/dev/null");
|
||
std::vector<std::string> ips;
|
||
if (!addr_out.empty()) {
|
||
// find addr_info array inside the single interface object
|
||
std::string search = "\"addr_info\":";
|
||
size_t ai_pos = addr_out.find(search);
|
||
if (ai_pos != std::string::npos) {
|
||
std::string addr_slice = addr_out.substr(ai_pos);
|
||
auto addr_objs = json_array_objects(addr_slice, "addr_info");
|
||
for (auto& ao : addr_objs) {
|
||
std::string local = json_str(ao, "local");
|
||
std::string prefixlen = json_str(ao, "prefixlen");
|
||
if (local.empty()) continue;
|
||
// skip link-local IPv6
|
||
std::string ll = local;
|
||
std::transform(ll.begin(), ll.end(), ll.begin(), ::tolower);
|
||
if (ll.rfind("fe80", 0) == 0) continue;
|
||
std::string entry = local;
|
||
if (!prefixlen.empty()) entry += "/" + prefixlen;
|
||
ips.push_back(entry);
|
||
}
|
||
}
|
||
}
|
||
if (!ips.empty()) {
|
||
std::string ips_str;
|
||
for (size_t ki = 0; ki < ips.size(); ++ki) {
|
||
if (ki) ips_str += LS;
|
||
ips_str += ips[ki];
|
||
}
|
||
p.kv[K_IPS] = ips_str;
|
||
}
|
||
|
||
// DHCP detection
|
||
bool dhcp = file_exists("/run/dhclient." + ifname + ".pid") ||
|
||
file_exists("/run/dhcp-lease-" + ifname) ||
|
||
file_exists("/var/lib/dhcp/dhclient." + ifname + ".leases");
|
||
p.kv[K_DHCP] = dhcp ? "true" : "false";
|
||
|
||
// Link speed via sysfs
|
||
uint64_t speed = 0;
|
||
{
|
||
std::string speed_path = "/sys/class/net/" + ifname + "/speed";
|
||
FILE* sf = fopen(speed_path.c_str(), "r");
|
||
if (sf) {
|
||
int raw = 0;
|
||
if (fscanf(sf, "%d", &raw) == 1 && raw > 0) speed = static_cast<uint64_t>(raw);
|
||
fclose(sf);
|
||
}
|
||
}
|
||
p.kv[K_CONN_SPEED] = std::to_string(speed);
|
||
p.kv[K_AGE_YEARS] = "-1";
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
#endif
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
// GPUs
|
||
// ═════════════════════════════════════════════════════════════════════════════
|
||
|
||
std::vector<DiscoveredPart> Discovery::discover_gpus() {
|
||
std::vector<DiscoveredPart> result;
|
||
|
||
#ifdef __APPLE__
|
||
// macOS GPU discovery not implemented
|
||
return result;
|
||
#else
|
||
std::string mm_out = run_cmd("lspci -mm 2>/dev/null");
|
||
if (mm_out.empty()) return result;
|
||
|
||
// Parse lspci -mm output. Each line:
|
||
// slot "class" "vendor" "device" ...
|
||
// (fields separated by spaces, quoted with double quotes)
|
||
auto parse_lspci_mm_line = [](const std::string& line)
|
||
-> std::vector<std::string> {
|
||
std::vector<std::string> fields;
|
||
size_t i = 0;
|
||
while (i < line.size()) {
|
||
while (i < line.size() && std::isspace((unsigned char)line[i])) ++i;
|
||
if (i >= line.size()) break;
|
||
if (line[i] == '"') {
|
||
++i;
|
||
std::string tok;
|
||
while (i < line.size() && line[i] != '"') tok += line[i++];
|
||
if (i < line.size()) ++i;
|
||
fields.push_back(tok);
|
||
} else {
|
||
std::string tok;
|
||
while (i < line.size() && !std::isspace((unsigned char)line[i])) tok += line[i++];
|
||
fields.push_back(tok);
|
||
}
|
||
}
|
||
return fields;
|
||
};
|
||
|
||
std::istringstream ss(mm_out);
|
||
std::string line;
|
||
while (std::getline(ss, line)) {
|
||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||
if (line.empty()) continue;
|
||
|
||
auto fields = parse_lspci_mm_line(line);
|
||
if (fields.size() < 4) continue;
|
||
|
||
std::string slot = fields[0];
|
||
std::string cls = fields[1];
|
||
std::string vendor = fields[2];
|
||
std::string device = fields[3];
|
||
|
||
// Filter for GPU classes
|
||
bool is_gpu = (cls.find("VGA compatible controller") != std::string::npos ||
|
||
cls.find("3D controller") != std::string::npos ||
|
||
cls.find("Display controller") != std::string::npos);
|
||
if (!is_gpu) continue;
|
||
|
||
DiscoveredPart p;
|
||
p.type_name = PTYPE_GPU;
|
||
p.kv[K_MANUFACTURER] = vendor;
|
||
p.kv[K_MODEL] = device;
|
||
p.kv[K_SERIAL] = slot; // PCI slot as stable ID
|
||
|
||
// NVIDIA VRAM via nvidia-smi
|
||
std::string lower_vendor = vendor;
|
||
std::transform(lower_vendor.begin(), lower_vendor.end(), lower_vendor.begin(), ::tolower);
|
||
if (lower_vendor.find("nvidia") != std::string::npos) {
|
||
std::string nsmi = run_cmd(
|
||
"nvidia-smi --query-gpu=index,name,memory.total --format=csv,noheader 2>/dev/null");
|
||
if (!nsmi.empty()) {
|
||
std::istringstream ns(nsmi);
|
||
std::string nline;
|
||
while (std::getline(ns, nline)) {
|
||
if (!nline.empty() && nline.back() == '\r') nline.pop_back();
|
||
size_t last_comma = nline.rfind(',');
|
||
if (last_comma == std::string::npos) continue;
|
||
std::string mem_str = trim(nline.substr(last_comma + 1));
|
||
uint64_t vram_mb = leading_uint(mem_str);
|
||
if (vram_mb > 0) {
|
||
p.kv[K_VRAM_MB] = std::to_string(vram_mb);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Find DRM card for this PCI slot
|
||
std::vector<std::string> outputs;
|
||
for (int card_idx = 0; card_idx < 8; ++card_idx) {
|
||
std::string card_name = "card" + std::to_string(card_idx);
|
||
std::string dev_link_path = "/sys/class/drm/" + card_name + "/device";
|
||
std::string resolved = run_cmd("readlink -f " + dev_link_path + " 2>/dev/null");
|
||
while (!resolved.empty() && std::isspace((unsigned char)resolved.back()))
|
||
resolved.pop_back();
|
||
if (resolved.empty()) continue;
|
||
|
||
bool matches = (resolved.find(slot) != std::string::npos);
|
||
if (!matches && slot.size() > 5 && slot[4] == ':') {
|
||
std::string short_addr = slot.substr(5);
|
||
matches = (resolved.find(short_addr) != std::string::npos);
|
||
}
|
||
if (!matches) continue;
|
||
|
||
// Found the card; check AMD VRAM
|
||
if (p.kv.find(K_VRAM_MB) == p.kv.end()) {
|
||
std::string vram_path = "/sys/class/drm/" + card_name + "/device/mem_info_vram_total";
|
||
FILE* vf = fopen(vram_path.c_str(), "r");
|
||
if (vf) {
|
||
uint64_t vram_bytes = 0;
|
||
fscanf(vf, "%llu", (unsigned long long*)&vram_bytes);
|
||
fclose(vf);
|
||
if (vram_bytes > 0) {
|
||
p.kv[K_VRAM_MB] = std::to_string(vram_bytes / (1024 * 1024));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Enumerate connectors
|
||
std::string ls_out = run_cmd("ls /sys/class/drm/ 2>/dev/null");
|
||
if (!ls_out.empty()) {
|
||
std::istringstream lss(ls_out);
|
||
std::string entry;
|
||
while (lss >> entry) {
|
||
std::string prefix = card_name + "-";
|
||
if (entry.rfind(prefix, 0) != 0) continue;
|
||
std::string connector_name = entry.substr(prefix.size());
|
||
std::string status_path = "/sys/class/drm/" + entry + "/status";
|
||
FILE* sf = fopen(status_path.c_str(), "r");
|
||
std::string status = "unknown";
|
||
if (sf) {
|
||
char sbuf[32] = {};
|
||
fscanf(sf, "%31s", sbuf);
|
||
fclose(sf);
|
||
status = sbuf;
|
||
}
|
||
outputs.push_back(connector_name + ":" + status);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
if (!outputs.empty()) {
|
||
std::string out_str;
|
||
for (size_t oi = 0; oi < outputs.size(); ++oi) {
|
||
if (oi) out_str += LS;
|
||
out_str += outputs[oi];
|
||
}
|
||
p.kv[K_DISPLAY_OUTPUTS] = out_str;
|
||
}
|
||
|
||
result.push_back(std::move(p));
|
||
}
|
||
|
||
return result;
|
||
#endif
|
||
}
|