#include "client/discovery.h" #include "common/models.h" #include "common/protocol.h" #include #include #include #include #include #include #include // ═════════════════════════════════════════════════════════════════════════════ // 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 output into a vector of key→value maps. // Each map corresponds to one DMI block (e.g. one "Memory Device"). std::vector> 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"); if (output.empty()) return {}; std::vector> result; std::map 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(val * 1024); if (suffix == "TB" || suffix[0] == 'T') return static_cast(val * 1024 * 1024); return static_cast(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(val * 1024.0); if (u == 'G') return static_cast(val); if (u == 'M') return static_cast(val / 1024.0); if (u == 'K') return static_cast(val / (1024.0 * 1024.0)); // Assume raw bytes return static_cast(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 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 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 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 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 Discovery::discover_all() { std::vector all; for (auto& v : {discover_memory_sticks(), discover_memory_slots(), discover_cpus(), discover_cpu_slots(), discover_disks(), discover_nics()}) { all.insert(all.end(), v.begin(), v.end()); } return all; } std::vector 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(); std::cerr << "discover: unknown type '" << type_name << "'\n"; return {}; } // ═════════════════════════════════════════════════════════════════════════════ // Memory sticks // ═════════════════════════════════════════════════════════════════════════════ std::vector Discovery::discover_memory_sticks() { std::vector 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 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"]; result.push_back(std::move(p)); } #endif return result; } // ═════════════════════════════════════════════════════════════════════════════ // Memory slots // ═════════════════════════════════════════════════════════════════════════════ std::vector Discovery::discover_memory_slots() { std::vector 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) { 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(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 Discovery::discover_cpus() { std::vector 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; if (b.count("ID") && b["ID"] != "Not Specified") p.kv[K_SERIAL] = b["ID"]; 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(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"]; result.push_back(std::move(p)); } #endif return result; } // ═════════════════════════════════════════════════════════════════════════════ // CPU slots // ═════════════════════════════════════════════════════════════════════════════ std::vector Discovery::discover_cpu_slots() { std::vector 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 Discovery::discover_disks() { std::vector result; #ifdef __APPLE__ // ── macOS: diskutil info for each disk ──────────────────────────────────── // Enumerate whole disks via XML plist from diskutil list -plist std::vector all_disks; { std::string plist_out = run_cmd("diskutil list -plist 2>/dev/null"); if (!plist_out.empty()) { // Parse WholeDisksdisk0... size_t key_pos = plist_out.find("WholeDisks"); if (key_pos != std::string::npos) { size_t arr_start = plist_out.find("", key_pos); size_t arr_end = plist_out.find("", 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("", p)) != std::string::npos) { p += 8; size_t e = arr_content.find("", 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 KV std::map 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 = ""; 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 == "") { plist[cur_key] = "true"; cur_key = ""; } if (t == "") { 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(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 text output for partition names std::string parts_txt = run_cmd("diskutil list /dev/" + disk_name + " 2>/dev/null"); std::vector 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 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 Discovery::discover_nics() { std::vector 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 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 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(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_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 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(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; }