summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJos Dehaes <jos.dehaes@gmail.com>2021-09-28 23:37:03 +0200
committerJos Dehaes <jos.dehaes@gmail.com>2021-09-28 23:37:03 +0200
commit84a974695afaadc53d1d8576ea66255166ede482 (patch)
tree44596af09253be0043f956750dc533bda5caad7c
parentab013b989b5f3419fa58764c3ffe580d382c5633 (diff)
make it compile on macos (M1 - arm64). Does not run though
-rw-r--r--Makefile4
-rw-r--r--src/btop.cpp4
-rw-r--r--src/btop_tools.hpp7
-rw-r--r--src/osx/btop_collect.cpp1628
4 files changed, 1641 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index e6b4d44..e1b5618 100644
--- a/Makefile
+++ b/Makefile
@@ -81,8 +81,8 @@ OBJEXT := o
#? Flags, Libraries and Includes
override REQFLAGS := -std=c++20
WARNFLAGS := -Wall -Wextra -pedantic
-OPTFLAGS ?= -O2 -ftree-loop-vectorize -flto=$(THREADS)
-LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector -fstack-clash-protection $(ADDFLAGS)
+OPTFLAGS ?= -O2 #-ftree-loop-vectorize -flto=$(THREADS)
+LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions $(ADDFLAGS)
override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS)
override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS)
INC := -I$(INCDIR) -I$(SRCDIR)
diff --git a/src/btop.cpp b/src/btop.cpp
index 0bd5ab8..945df32 100644
--- a/src/btop.cpp
+++ b/src/btop.cpp
@@ -225,7 +225,11 @@ void clean_quit(int sig) {
//? Assume error if still not cleaned up and call quick_exit to avoid a segfault from Tools::atomic_lock destructor
if (Tools::active_locks > 0) {
+#ifdef __APPLE__
+ exit((sig != -1 ? sig : 0));
+#else
quick_exit((sig != -1 ? sig : 0));
+#endif
}
if (sig != -1) exit(sig);
diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp
index 7bb0445..adaaf02 100644
--- a/src/btop_tools.hpp
+++ b/src/btop_tools.hpp
@@ -32,6 +32,13 @@ tab-size = 4
using std::string, std::vector, std::atomic, std::to_string, std::regex, std::tuple, std::array;
+#ifndef HOST_NAME_MAX
+#if defined(__APPLE__)
+#define HOST_NAME_MAX 255
+#else
+#define HOST_NAME_MAX 64
+#endif /* __APPLE__ */
+#endif /* HOST_NAME_MAX */
//? ------------------------------------------------- NAMESPACES ------------------------------------------------------
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;
+ }
+
+}
+
+na