diff options
Diffstat (limited to 'src/osx')
-rw-r--r-- | src/osx/btop_collect.cpp | 1628 |
1 files changed, 1628 insertions, 0 deletions
diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp new file mode 100644 index 0000000..d8ea597 --- /dev/null +++ b/src/osx/btop_collect.cpp @@ -0,0 +1,1628 @@ +/* Copyright 2021 Aristocratos (jakob@qvantnet.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +indent = tab +tab-size = 4 +*/ + +#include <fstream> +#include <ranges> +#include <cmath> +#include <unistd.h> +#include <numeric> +#include <regex> +#include <sys/statvfs.h> +#include <netdb.h> +#include <ifaddrs.h> +#include <net/if.h> + +#include <btop_shared.hpp> +#include <btop_config.hpp> +#include <btop_tools.hpp> + +using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; +using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; +namespace fs = std::filesystem; +namespace rng = std::ranges; +using namespace Tools; + +//? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- + +namespace Cpu { + vector<long long> core_old_totals; + vector<long long> core_old_idles; + vector<string> available_fields; + vector<string> available_sensors = {"Auto"}; + cpu_info current_cpu; + fs::path freq_path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq"; + bool got_sensors = false, cpu_temp_only = false; + + //* Populate found_sensors map + bool get_sensors(); + + //* Get current cpu clock speed + string get_cpuHz(); + + //* Search /proc/cpuinfo for a cpu name + string get_cpuName(); + + struct Sensor { + fs::path path; + string label; + int64_t temp = 0; + int64_t high = 0; + int64_t crit = 0; + }; + + unordered_flat_map<string, Sensor> found_sensors; + string cpu_sensor; + vector<string> core_sensors; + unordered_flat_map<int, int> core_mapping; +} + +namespace Mem { + double old_uptime; +} + +namespace Shared { + + fs::path procPath, passwd_path; + uint64_t totalMem; + long pageSize, clkTck, coreCount; + int totalMem_len; + + void init() { + + //? Shared global variables init + procPath = (fs::is_directory(fs::path("/proc")) and access("/proc", R_OK) != -1) ? "/proc" : ""; + if (procPath.empty()) + throw std::runtime_error("Proc filesystem not found or no permission to read from it!"); + + passwd_path = (fs::is_regular_file(fs::path("/etc/passwd")) and access("/etc/passwd", R_OK) != -1) ? "/etc/passwd" : ""; + if (passwd_path.empty()) + Logger::warning("Could not read /etc/passwd, will show UID instead of username."); + + coreCount = sysconf(_SC_NPROCESSORS_ONLN); + if (coreCount < 1) { + coreCount = 1; + Logger::warning("Could not determine number of cores, defaulting to 1."); + } + + pageSize = sysconf(_SC_PAGE_SIZE); + if (pageSize <= 0) { + pageSize = 4096; + Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect."); + } + + clkTck = sysconf(_SC_CLK_TCK); + if (clkTck <= 0) { + clkTck = 100; + Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); + } + + ifstream meminfo(Shared::procPath / "meminfo"); + if (meminfo.good()) { + meminfo.ignore(SSmax, ':'); + meminfo >> totalMem; + totalMem_len = to_string(totalMem).size(); + totalMem <<= 10; + } + if (not meminfo.good() or totalMem == 0) + throw std::runtime_error("Could not get total memory size from /proc/meminfo"); + + //? Init for namespace Cpu + if (not fs::exists(Cpu::freq_path) or access(Cpu::freq_path.c_str(), R_OK) == -1) Cpu::freq_path.clear(); + Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); + Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {}); + Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0); + Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0); + Cpu::collect(); + for (auto& [field, vec] : Cpu::current_cpu.cpu_percent) { + if (not vec.empty()) Cpu::available_fields.push_back(field); + } + Cpu::cpuName = Cpu::get_cpuName(); + Cpu::got_sensors = Cpu::get_sensors(); + for (const auto& [sensor, ignored] : Cpu::found_sensors) { + Cpu::available_sensors.push_back(sensor); + } + Cpu::core_mapping = Cpu::get_core_mapping(); + + //? Init for namespace Mem + Mem::old_uptime = system_uptime(); + Mem::collect(); + + } + +} + +namespace Cpu { + string cpuName; + string cpuHz; + bool has_battery = true; + tuple<int, long, string> current_bat; + + const array<string, 10> time_names = {"user", "nice", "system", "idle", "iowait", "irq", "softirq", "steal", "guest", "guest_nice"}; + + unordered_flat_map<string, long long> cpu_old = { + {"totals", 0}, + {"idles", 0}, + {"user", 0}, + {"nice", 0}, + {"system", 0}, + {"idle", 0}, + {"iowait", 0}, + {"irq", 0}, + {"softirq", 0}, + {"steal", 0}, + {"guest", 0}, + {"guest_nice", 0} + }; + + string get_cpuName() { + string name; + ifstream cpuinfo(Shared::procPath / "cpuinfo"); + if (cpuinfo.good()) { + for (string instr; getline(cpuinfo, instr, ':') and not instr.starts_with("model name");) + cpuinfo.ignore(SSmax, '\n'); + if (cpuinfo.bad()) return name; + else if (not cpuinfo.eof()) { + cpuinfo.ignore(1); + getline(cpuinfo, name); + } + else if (fs::exists("/sys/devices")) { + for (const auto& d : fs::directory_iterator("/sys/devices")) { + if (string(d.path().filename()).starts_with("arm")) { + name = d.path().filename(); + break; + } + } + if (not name.empty()) { + auto name_vec = ssplit(name, '_'); + if (name_vec.size() < 2) return capitalize(name); + else return capitalize(name_vec.at(1)) + (name_vec.size() > 2 ? ' ' + capitalize(name_vec.at(2)) : ""); + } + + } + + auto name_vec = ssplit(name); + + if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) { + auto cpu_pos = v_index(name_vec, "CPU"s); + if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')')) + name = name_vec.at(cpu_pos + 1); + else + name.clear(); + } + else if (v_contains(name_vec, "Ryzen"s)) { + auto ryz_pos = v_index(name_vec, "Ryzen"s); + name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : ""); + } + else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) { + auto cpu_pos = v_index(name_vec, "CPU"s); + if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@") + name = name_vec.at(cpu_pos + 1); + else + name.clear(); + } + else + name.clear(); + + if (name.empty() and not name_vec.empty()) { + for (const auto& n : name_vec) { + if (n == "@") break; + name += n + ' '; + } + name.pop_back(); + for (const auto& reg : {regex("Processor"), regex("CPU"), regex("\\(R\\)"), regex("\\(TM\\)"), regex("Intel"), + regex("AMD"), regex("Core"), regex("\\d?\\.?\\d+[mMgG][hH][zZ]")}) { + name = std::regex_replace(name, reg, ""); + } + name = trim(name); + } + } + + return name; + } + + bool get_sensors() { + bool got_cpu = false, got_coretemp = false; + vector<fs::path> search_paths; + try { + //? Setup up paths to search for sensors + if (fs::exists(fs::path("/sys/class/hwmon")) and access("/sys/class/hwmon", R_OK) != -1) { + for (const auto& dir : fs::directory_iterator(fs::path("/sys/class/hwmon"))) { + fs::path add_path = fs::canonical(dir.path()); + if (v_contains(search_paths, add_path) or v_contains(search_paths, add_path / "device")) continue; + + if (s_contains(add_path, "coretemp")) + got_coretemp = true; + + if (fs::exists(add_path / "temp1_input")) { + search_paths.push_back(add_path); + } + else if (fs::exists(add_path / "device/temp1_input")) + search_paths.push_back(add_path / "device"); + } + } + if (not got_coretemp and fs::exists(fs::path("/sys/devices/platform/coretemp.0/hwmon"))) { + for (auto& d : fs::directory_iterator(fs::path("/sys/devices/platform/coretemp.0/hwmon"))) { + fs::path add_path = fs::canonical(d.path()); + + if (fs::exists(d.path() / "temp1_input") and not v_contains(search_paths, add_path)) { + search_paths.push_back(add_path); + got_coretemp = true; + } + } + } + //? Scan any found directories for temperature sensors + if (not search_paths.empty()) { + for (const auto& path : search_paths) { + const string pname = readfile(path / "name", path.filename()); + for (int i = 1; fs::exists(path / string("temp" + to_string(i) + "_input")); i++) { + const string basepath = path / string("temp" + to_string(i) + "_"); + const string label = readfile(fs::path(basepath + "label"), "temp" + to_string(i)); + const string sensor_name = pname + "/" + label; + const int64_t temp = stol(readfile(fs::path(basepath + "input"), "0")) / 1000; + const int64_t high = stol(readfile(fs::path(basepath + "max"), "80000")) / 1000; + const int64_t crit = stol(readfile(fs::path(basepath + "crit"), "95000")) / 1000; + + found_sensors[sensor_name] = {fs::path(basepath + "input"), label, temp, high, crit}; + + if (not got_cpu and (label.starts_with("Package id") or label.starts_with("Tdie"))) { + got_cpu = true; + cpu_sensor = sensor_name; + } + else if (label.starts_with("Core") or label.starts_with("Tccd")) { + got_coretemp = true; + if (not v_contains(core_sensors, sensor_name)) core_sensors.push_back(sensor_name); + } + } + } + } + //? If no good candidate for cpu temp has been found scan /sys/class/thermal + if (not got_cpu and fs::exists(fs::path("/sys/class/thermal"))) { + const string rootpath = fs::path("/sys/class/thermal/thermal_zone"); + for (int i = 0; fs::exists(fs::path(rootpath + to_string(i))); i++) { + const fs::path basepath = rootpath + to_string(i); + if (not fs::exists(basepath / "temp")) continue; + const string label = readfile(basepath / "type", "temp" + to_string(i)); + const string sensor_name = "thermal" + to_string(i) + "/" + label; + const int64_t temp = stol(readfile(basepath / "temp", "0")) / 1000; + + int64_t high, crit; + for (int ii = 0; fs::exists(basepath / string("trip_point_" + to_string(ii) + "_temp")); ii++) { + const string trip_type = readfile(basepath / string("trip_point_" + to_string(ii) + "_type")); + if (not is_in(trip_type, "high", "critical")) continue; + auto& val = (trip_type == "high" ? high : crit); + val = stol(readfile(basepath / string("trip_point_" + to_string(ii) + "_temp"), "0")) / 1000; + } + if (high < 1) high = 80; + if (crit < 1) crit = 95; + + found_sensors[sensor_name] = {basepath / "temp", label, temp, high, crit}; + } + } + + } + catch (...) {} + + if (not got_coretemp or core_sensors.empty()) cpu_temp_only = true; + if (cpu_sensor.empty() and not found_sensors.empty()) { + for (const auto& [name, sensor] : found_sensors) { + if (s_contains(str_to_lower(name), "cpu")) { + cpu_sensor = name; + break; + } + } + if (cpu_sensor.empty()) { + cpu_sensor = found_sensors.begin()->first; + Logger::warning("No good candidate for cpu sensor found, using random from all found sensors."); + } + } + + return not found_sensors.empty(); + } + + void update_sensors() { + if (cpu_sensor.empty()) return; + + const auto& cpu_sensor = (not Config::getS("cpu_sensor").empty() and found_sensors.contains(Config::getS("cpu_sensor")) ? Config::getS("cpu_sensor") : Cpu::cpu_sensor); + + found_sensors.at(cpu_sensor).temp = stol(readfile(found_sensors.at(cpu_sensor).path, "0")) / 1000; + current_cpu.temp.at(0).push_back(found_sensors.at(cpu_sensor).temp); + current_cpu.temp_max = found_sensors.at(cpu_sensor).crit; + if (current_cpu.temp.at(0).size() > 20) current_cpu.temp.at(0).pop_front(); + + if (Config::getB("show_coretemp") and not cpu_temp_only) { + vector<string> done; + for (const auto& sensor : core_sensors) { + if (v_contains(done, sensor)) continue; + found_sensors.at(sensor).temp = stol(readfile(found_sensors.at(sensor).path, "0")) / 1000; + done.push_back(sensor); + } + for (const auto& [core, temp] : core_mapping) { + if (cmp_less(core + 1, current_cpu.temp.size()) and cmp_less(temp, core_sensors.size())) { + current_cpu.temp.at(core + 1).push_back(found_sensors.at(core_sensors.at(temp)).temp); + if (current_cpu.temp.at(core + 1).size() > 20) current_cpu.temp.at(core + 1).pop_front(); + } + } + } + } + + string get_cpuHz() { + static int failed = 0; + if (failed > 4) return ""s; + string cpuhz; + try { + double hz = 0.0; + //? Try to get freq from /sys/devices/system/cpu/cpufreq/policy first (faster) + if (not freq_path.empty()) { + hz = stod(readfile(freq_path, "0.0")) / 1000; + if (hz <= 0.0 and ++failed >= 2) + freq_path.clear(); + } + //? If freq from /sys failed or is missing try to use /proc/cpuinfo + if (hz <= 0.0) { + ifstream cpufreq(Shared::procPath / "cpuinfo"); + if (cpufreq.good()) { + while (cpufreq.ignore(SSmax, '\n')) { + if (cpufreq.peek() == 'c') { + cpufreq.ignore(SSmax, ' '); + if (cpufreq.peek() == 'M') { + cpufreq.ignore(SSmax, ':'); + cpufreq.ignore(1); + cpufreq >> hz; + break; + } + } + } + } + } + + if (hz <= 1 or hz >= 1000000) throw std::runtime_error("Failed to read /sys/devices/system/cpu/cpufreq/policy and /proc/cpuinfo."); + + if (hz >= 1000) { + if (hz >= 10000) cpuhz = to_string((int)round(hz / 1000)); // Future proof until we reach THz speeds :) + else cpuhz = to_string(round(hz / 100) / 10.0).substr(0, 3); + cpuhz += " GHz"; + } + else if (hz > 0) + cpuhz = to_string((int)round(hz)) + " MHz"; + + } + catch (const std::exception& e) { + if (++failed < 5) return ""s; + else { + Logger::warning("get_cpuHZ() : " + (string)e.what()); + return ""s; + } + } + + return cpuhz; + } + + auto get_core_mapping() -> unordered_flat_map<int, int> { + unordered_flat_map<int, int> core_map; + if (cpu_temp_only) return core_map; + + //? Try to get core mapping from /proc/cpuinfo + ifstream cpuinfo(Shared::procPath / "cpuinfo"); + if (cpuinfo.good()) { + int cpu, core, n = 0; + for (string instr; cpuinfo >> instr;) { + if (instr == "processor") { + cpuinfo.ignore(SSmax, ':'); + cpuinfo >> cpu; + } + else if (instr.starts_with("core")) { + cpuinfo.ignore(SSmax, ':'); + cpuinfo >> core; + if (std::cmp_greater_equal(core, core_sensors.size())) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[cpu] = n++; + } + else + core_map[cpu] = core; + } + cpuinfo.ignore(SSmax, '\n'); + } + } + + //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc. + if (cmp_less(core_map.size(), Shared::coreCount)) { + if (Shared::coreCount % 2 == 0 and (long)core_map.size() == Shared::coreCount / 2) { + for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[Shared::coreCount / 2 + i] = n++; + } + } + else { + core_map.clear(); + for (int i = 0, n = 0; i < Shared::coreCount; i++) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[i] = n++; + } + } + } + + //? Apply user set custom mapping if any + const auto& custom_map = Config::getS("cpu_core_map"); + if (not custom_map.empty()) { + try { + for (const auto& split : ssplit(custom_map)) { + const auto vals = ssplit(split, ':'); + if (vals.size() != 2) continue; + int change_id = std::stoi(vals.at(0)); + int new_id = std::stoi(vals.at(1)); + if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue; + core_map.at(change_id) = new_id; + } + } + catch (...) {} + } + + return core_map; + } + + auto get_battery() -> tuple<int, long, string> { + if (not has_battery) return {0, 0, ""}; + static fs::path bat_dir, energy_now_path, energy_full_path, power_now_path, status_path, online_path; + static bool use_energy = true; + + //? Get paths to needed files and check for valid values on first run + if (bat_dir.empty() and has_battery) { + if (fs::exists("/sys/class/power_supply")) { + for (const auto& d : fs::directory_iterator("/sys/class/power_supply")) { + if (const string dir_name = d.path().filename(); d.is_directory() and (dir_name.starts_with("BAT") or s_contains(str_to_lower(dir_name), "battery"))) { + bat_dir = d.path(); + break; + } + } + } + if (bat_dir.empty()) { + has_battery = false; + return {0, 0, ""}; + } + else { + if (fs::exists(bat_dir / "energy_now")) energy_now_path = bat_dir / "energy_now"; + else if (fs::exists(bat_dir / "charge_now")) energy_now_path = bat_dir / "charge_now"; + else use_energy = false; + + if (fs::exists(bat_dir / "energy_full")) energy_full_path = bat_dir / "energy_full"; + else if (fs::exists(bat_dir / "charge_full")) energy_full_path = bat_dir / "charge_full"; + else use_energy = false; + + if (not use_energy and not fs::exists(bat_dir / "capacity")) { + has_battery = false; + return {0, 0, ""}; + } + + if (fs::exists(bat_dir / "power_now")) power_now_path = bat_dir / "power_now"; + else if (fs::exists(bat_dir / "current_now")) power_now_path = bat_dir / "current_now"; + + if (fs::exists(bat_dir / "AC0/online")) online_path = bat_dir / "AC0/online"; + else if (fs::exists(bat_dir / "AC/online")) online_path = bat_dir / "AC/online"; + } + } + + int percent = -1; + long seconds = -1; + + //? Try to get battery percentage + if (use_energy) { + try { + percent = round(100.0 * stoll(readfile(energy_now_path, "-1")) / stoll(readfile(energy_full_path, "1"))); + } + catch (const std::invalid_argument&) { } + catch (const std::out_of_range&) { } + } + if (percent < 0) { + try { + percent = stoll(readfile(bat_dir / "capacity", "-1")); + } + catch (const std::invalid_argument&) { } + catch (const std::out_of_range&) { } + } + if (percent < 0) { + has_battery = false; + return {0, 0, ""}; + } + + //? Get charging/discharging status + string status = str_to_lower(readfile(bat_dir / "status", "unknown")); + if (status == "unknown" and not online_path.empty()) { + const auto online = readfile(online_path, "0"); + if (online == "1" and percent < 100) status = "charging"; + else if (online == "1") status = "full"; + else status = "discharging"; + } + + //? Get seconds to empty + if (not is_in(status, "charging", "full")) { + if (use_energy and not power_now_path.empty()) { + try { + seconds = round((double)stoll(readfile(energy_now_path, "0")) / stoll(readfile(power_now_path, "1")) * 3600); + } + catch (const std::invalid_argument&) { } + catch (const std::out_of_range&) { } + } + if (seconds < 0 and fs::exists(bat_dir / "time_to_empty")) { + try { + seconds = stoll(readfile(bat_dir / "time_to_empty", "0")) * 60; + } + catch (const std::invalid_argument&) { } + catch (const std::out_of_range&) { } + } + } + + return {percent, seconds, status}; + } + + auto collect(const bool no_update) -> cpu_info& { + if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) return current_cpu; + auto& cpu = current_cpu; + + ifstream cread; + + try { + //? Get cpu load averages from /proc/loadavg + cread.open(Shared::procPath / "loadavg"); + if (cread.good()) { + cread >> cpu.load_avg[0] >> cpu.load_avg[1] >> cpu.load_avg[2]; + } + cread.close(); + + //? Get cpu total times for all cores from /proc/stat + cread.open(Shared::procPath / "stat"); + for (int i = 0; cread.good() and cread.peek() == 'c'; i++) { + cread.ignore(SSmax, ' '); + + //? Expected on kernel 2.6.3> : 0=user, 1=nice, 2=system, 3=idle, 4=iowait, 5=irq, 6=softirq, 7=steal, 8=guest, 9=guest_nice + vector<long long> times; + long long total_sum = 0; + + for (uint64_t val; cread >> val; total_sum += val) { + times.push_back(val); + } + cread.clear(); + if (times.size() < 4) throw std::runtime_error("Malformatted /proc/stat"); + + //? Subtract fields 8-9 and any future unknown fields + const long long totals = max(0ll, total_sum - (times.size() > 8 ? std::accumulate(times.begin() + 8, times.end(), 0) : 0)); + + //? Add iowait field if present + const long long idles = max(0ll, times.at(3) + (times.size() > 4 ? times.at(4) : 0)); + + //? Calculate values for totals from first line of stat + if (i == 0) { + const long long calc_totals = max(1ll, totals - cpu_old.at("totals")); + const long long calc_idles = max(1ll, idles - cpu_old.at("idles")); + cpu_old.at("totals") = totals; + cpu_old.at("idles") = idles; + + //? Total usage of cpu + cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); + + //? Reduce size if there are more values than needed for graph + while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front(); + + //? Populate cpu.cpu_percent with all fields from stat + for (int ii = 0; const auto& val : times) { + cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll)); + cpu_old.at(time_names.at(ii)) = val; + + //? Reduce size if there are more values than needed for graph + while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); + + if (++ii == 10) break; + } + } + //? Calculate cpu total for each core + else { + if (i > Shared::coreCount) break; + const long long calc_totals = max(0ll, totals - core_old_totals.at(i-1)); + const long long calc_idles = max(0ll, idles - core_old_idles.at(i-1)); + core_old_totals.at(i-1) = totals; + core_old_idles.at(i-1) = idles; + + cpu.core_percent.at(i-1).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); + + //? Reduce size if there are more values than needed for graph + if (cpu.core_percent.at(i-1).size() > 40) cpu.core_percent.at(i-1).pop_front(); + + } + } + } + catch (const std::exception& e) { + Logger::debug("get_cpuHz() : " + (string)e.what()); + if (cread.bad()) throw std::runtime_error("Failed to read /proc/stat"); + else throw std::runtime_error("collect() : " + (string)e.what()); + } + + if (Config::getB("show_cpu_freq")) + cpuHz = get_cpuHz(); + + if (Config::getB("check_temp") and got_sensors) + update_sensors(); + + if (Config::getB("show_battery") and has_battery) + current_bat = get_battery(); + + return cpu; + } +} + +namespace Mem { + bool has_swap = false; + vector<string> fstab; + fs::file_time_type fstab_time; + int disk_ios = 0; + vector<string> last_found; + + mem_info current_mem {}; + + auto collect(const bool no_update) -> mem_info& { + if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) return current_mem; + auto& show_swap = Config::getB("show_swap"); + auto& swap_disk = Config::getB("swap_disk"); + auto& show_disks = Config::getB("show_disks"); + auto& mem = current_mem; + static const bool snapped = (getenv("BTOP_SNAPPED") != NULL); + + mem.stats.at("swap_total") = 0; + + //? Read memory info from /proc/meminfo + ifstream meminfo(Shared::procPath / "meminfo"); + if (meminfo.good()) { + bool got_avail = false; + for (string label; meminfo >> label;) { + if (label == "MemFree:") { + meminfo >> mem.stats.at("free"); + mem.stats.at("free") <<= 10; + } + else if (label == "MemAvailable:") { + meminfo >> mem.stats.at("available"); + mem.stats.at("available") <<= 10; + got_avail = true; + } + else if (label == "Cached:") { + meminfo >> mem.stats.at("cached"); + mem.stats.at("cached") <<= 10; + if (not show_swap and not swap_disk) break; + } + else if (label == "SwapTotal:") { + meminfo >> mem.stats.at("swap_total"); + mem.stats.at("swap_total") <<= 10; + } + else if (label == "SwapFree:") { + meminfo >> mem.stats.at("swap_free"); + mem.stats.at("swap_free") <<= 10; + break; + } + meminfo.ignore(SSmax, '\n'); + } + if (not got_avail) mem.stats.at("available") = mem.stats.at("free") + mem.stats.at("cached"); + mem.stats.at("used") = Shared::totalMem - mem.stats.at("available"); + if (mem.stats.at("swap_total") > 0) mem.stats.at("swap_used") = mem.stats.at("swap_total") - mem.stats.at("swap_free"); + } + else + throw std::runtime_error("Failed to read /proc/meminfo"); + + meminfo.close(); + + //? Calculate percentages + for (const auto& name : mem_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); + while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); + } + + if (show_swap and mem.stats.at("swap_total") > 0) { + for (const auto& name : swap_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total"))); + while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); + } + has_swap = true; + } + else + has_swap = false; + + //? Get disks stats + if (show_disks) { + double uptime = system_uptime(); + try { + auto& disks_filter = Config::getS("disks_filter"); + bool filter_exclude = false; + auto& use_fstab = Config::getB("use_fstab"); + auto& only_physical = Config::getB("only_physical"); + auto& disks = mem.disks; + ifstream diskread; + + vector<string> filter; + if (not disks_filter.empty()) { + filter = ssplit(disks_filter); + if (filter.at(0).starts_with("exclude=")) { + filter_exclude = true; + filter.at(0) = filter.at(0).substr(8); + } + } + + //? Get list of "real" filesystems from /proc/filesystems + vector<string> fstypes; + if (only_physical and not use_fstab) { + fstypes = {"zfs", "wslfs", "drvfs"}; + diskread.open(Shared::procPath / "filesystems"); + if (diskread.good()) { + for (string fstype; diskread >> fstype;) { + if (not is_in(fstype, "nodev", "squashfs", "nullfs")) + fstypes.push_back(fstype); + diskread.ignore(SSmax, '\n'); + } + } + else + throw std::runtime_error("Failed to read /proc/filesystems"); + diskread.close(); + } + + //? Get disk list to use from fstab if enabled + if (use_fstab and fs::last_write_time("/etc/fstab") != fstab_time) { + fstab.clear(); + fstab_time = fs::last_write_time("/etc/fstab"); + diskread.open("/etc/fstab"); + if (diskread.good()) { + for (string instr; diskread >> instr;) { + if (not instr.starts_with('#')) { + diskread >> instr; + if (snapped and instr == "/") fstab.push_back("/mnt"); + else if (not is_in(instr, "none", "swap")) fstab.push_back(instr); + } + diskread.ignore(SSmax, '\n'); + } + } + else + throw std::runtime_error("Failed to read /etc/fstab"); + diskread.close(); + } + + //? Get mounts from /etc/mtab or /proc/self/mounts + diskread.open((fs::exists("/etc/mtab") ? fs::path("/etc/mtab") : Shared::procPath / "self/mounts")); + if (diskread.good()) { + vector<string> found; + found.reserve(last_found.size()); + string dev, mountpoint, fstype; + while (not diskread.eof()) { + std::error_code ec; + diskread >> dev >> mountpoint >> fstype; + + //? Match filter if not empty + if (not filter.empty()) { + bool match = v_contains(filter, mountpoint); + if ((filter_exclude and match) or (not filter_exclude and not match)) + continue; + } + + if ((not use_fstab and not only_physical) + or (use_fstab and v_contains(fstab, mountpoint)) + or (not use_fstab and only_physical and v_contains(fstypes, fstype))) { + found.push_back(mountpoint); + if (not v_contains(last_found, mountpoint)) redraw = true; + + //? Save mountpoint, name, dev path and path to /sys/block stat file + if (not disks.contains(mountpoint)) { + disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; + if (disks.at(mountpoint).dev.empty()) disks.at(mountpoint).dev = dev; + if (disks.at(mountpoint).name.empty()) disks.at(mountpoint).name = (mountpoint == "/" or (snapped and mountpoint == "/mnt") ? "root" : mountpoint); + string devname = disks.at(mountpoint).dev.filename(); + while (devname.size() >= 2) { + if (fs::exists("/sys/block/" + devname + "/stat", ec) and access(string("/sys/block/" + devname + "/stat").c_str(), R_OK) == 0) { + disks.at(mountpoint).stat = "/sys/block/" + devname + "/stat"; + break; + } + devname.resize(devname.size() - 1); + } + } + + } + diskread.ignore(SSmax, '\n'); + } + //? Remove disks no longer mounted or filtered out + if (swap_disk and has_swap) found.push_back("swap"); + for (auto it = disks.begin(); it != disks.end();) { + if (not v_contains(found, it->first)) + it = disks.erase(it); + else + it++; + } + if (found.size() != last_found.size()) redraw = true; + last_found = std::move(found); + } + else + throw std::runtime_error("Failed to get mounts from /etc/mtab and /proc/self/mounts"); + diskread.close(); + + //? Get disk/partition stats + for (auto& [mountpoint, disk] : disks) { + if (std::error_code ec; not fs::exists(mountpoint, ec)) continue; + struct statvfs vfs; + if (statvfs(mountpoint.c_str(), &vfs) < 0) { + Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); + continue; + } + disk.total = vfs.f_blocks * vfs.f_frsize; + disk.free = vfs.f_bfree * vfs.f_frsize; + disk.used = disk.total - disk.free; + disk.used_percent = round((double)disk.used * 100 / disk.total); + disk.free_percent = 100 - disk.used_percent; + } + + //? Setup disks order in UI and add swap if enabled + mem.disks_order.clear(); + if (snapped and disks.contains("/mnt")) mem.disks_order.push_back("/mnt"); + else if (disks.contains("/")) mem.disks_order.push_back("/"); + if (swap_disk and has_swap) { + mem.disks_order.push_back("swap"); + if (not disks.contains("swap")) disks["swap"] = {"", "swap"}; + disks.at("swap").total = mem.stats.at("swap_total"); + disks.at("swap").used = mem.stats.at("swap_used"); + disks.at("swap").free = mem.stats.at("swap_free"); + disks.at("swap").used_percent = mem.percent.at("swap_used").back(); + disks.at("swap").free_percent = mem.percent.at("swap_free").back(); + } + for (const auto& name : last_found) + if (not is_in(name, "/", "swap")) mem.disks_order.push_back(name); + + //? Get disks IO + int64_t sectors_read, sectors_write, io_ticks; + disk_ios = 0; + for (auto& [ignored, disk] : disks) { + if (disk.stat.empty() or access(disk.stat.c_str(), R_OK) != 0) continue; + diskread.open(disk.stat); + if (diskread.good()) { + disk_ios++; + for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> sectors_read; + if (disk.io_read.empty()) + disk.io_read.push_back(0); + else + disk.io_read.push_back(max((int64_t)0, (sectors_read - disk.old_io.at(0)) * 512)); + disk.old_io.at(0) = sectors_read; + while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + + for (int i = 0; i < 3; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> sectors_write; + if (disk.io_write.empty()) + disk.io_write.push_back(0); + else + disk.io_write.push_back(max((int64_t)0, (sectors_write - disk.old_io.at(1)) * 512)); + disk.old_io.at(1) = sectors_write; + while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + + for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> io_ticks; + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(clamp((long)round((double)(io_ticks - disk.old_io.at(2)) / (uptime - old_uptime) / 10), 0l, 100l)); + disk.old_io.at(2) = io_ticks; + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + } + diskread.close(); + } + old_uptime = uptime; + } + catch (const std::exception& e) { + Logger::warning("Error in Mem::collect() : " + (string)e.what()); + } + } + + return mem; + } + +} + +namespace Net { + unordered_flat_map<string, net_info> current_net; + net_info empty_net = {}; + vector<string> interfaces; + string selected_iface; + int errors = 0; + unordered_flat_map<string, uint64_t> graph_max = { {"download", {}}, {"upload", {}} }; + unordered_flat_map<string, array<int, 2>> max_count = { {"download", {}}, {"upload", {}} }; + bool rescale = true; + uint64_t timestamp = 0; + + //* RAII wrapper for getifaddrs + class getifaddr_wrapper { + struct ifaddrs* ifaddr; + public: + int status; + getifaddr_wrapper() { status = getifaddrs(&ifaddr); } + ~getifaddr_wrapper() { freeifaddrs(ifaddr); } + auto operator()() -> struct ifaddrs* { return ifaddr; } + }; + + auto collect(const bool no_update) -> net_info& { + auto& net = current_net; + auto& config_iface = Config::getS("net_iface"); + auto& net_sync = Config::getB("net_sync"); + auto& net_auto = Config::getB("net_auto"); + auto new_timestamp = time_ms(); + + if (not no_update and errors < 3) { + //? Get interface list using getifaddrs() wrapper + getifaddr_wrapper if_wrap {}; + if (if_wrap.status != 0) { + errors++; + Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); + redraw = true; + return empty_net; + } + int family = 0; + char ip[NI_MAXHOST]; + interfaces.clear(); + string ipv4, ipv6; + + //? Iteration over all items in getifaddrs() list + for (auto* ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) continue; + family = ifa->ifa_addr->sa_family; + const auto& iface = ifa->ifa_name; + + //? Get IPv4 address + if (family == AF_INET) { + if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) + net[iface].ipv4 = ip; + } + //? Get IPv6 address + else if (family == AF_INET6) { + if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) + net[iface].ipv6 = ip; + } + + //? Update available interfaces vector and get status of interface + if (not v_contains(interfaces, iface)) { + interfaces.push_back(iface); + net[iface].connected = (ifa->ifa_flags & IFF_RUNNING); + } + } + + //? Get total recieved and transmitted bytes + device address if no ip was found + for (const auto& iface : interfaces) { + if (net.at(iface).ipv4.empty() and net.at(iface).ipv6.empty()) + net.at(iface).ipv4 = readfile("/sys/class/net/" + iface + "/address"); + + for (const string dir : {"download", "upload"}) { + const fs::path sys_file = "/sys/class/net/" + iface + "/statistics/" + (dir == "download" ? "rx_bytes" : "tx_bytes"); + auto& saved_stat = net.at(iface).stat.at(dir); + auto& bandwidth = net.at(iface).bandwidth.at(dir); + + const uint64_t val = max((uint64_t)stoul(readfile(sys_file, "0")), saved_stat.last); + + //? Update speed, total and top values + saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); + if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; + if (saved_stat.offset > val) saved_stat.offset = 0; |