diff options
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | src/btop.cpp | 6 | ||||
-rw-r--r-- | src/btop_draw.cpp | 19 | ||||
-rw-r--r-- | src/btop_input.cpp | 6 | ||||
-rw-r--r-- | src/btop_tools.hpp | 7 | ||||
-rw-r--r-- | src/osx/btop_collect.cpp | 1193 |
6 files changed, 1238 insertions, 14 deletions
@@ -82,13 +82,26 @@ OBJEXT := o 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) +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) SU_USER := root SU_GROUP := root +ifdef DEBUG + override OPTFLAGS := -O0 -g +endif + +ifeq ($(PLATFORM), OSX) + override LDCXXFLAGS += -framework IOKit -framework CoreFoundation +endif +ifneq ($(ARCH),arm64) +ifneq ($(PLATFORM),OSX) + override LDCXXFLAGS += -fstack-protector -fstack-clash-protection +endif +endif + SOURCES := $(shell find $(SRCDIR) -maxdepth 1 -type f -name *.$(SRCEXT)) SOURCES += $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEXT)) @@ -124,7 +137,9 @@ help: @printf " clean Remove built objects\n" @printf " distclean Remove built objects and binaries\n" @printf " install Install btop++ to \$$PREFIX ($(PREFIX))\n" +ifneq ($(PLATFORM),OSX) @printf " setuid Set installed binary owner/group to \$$SU_USER/\$$SU_GROUP ($(SU_USER)/$(SU_GROUP)) and set SUID bit\n" +endif @printf " uninstall Uninstall btop++ from \$$PREFIX\n" @printf " info Display information about Environment,compiler and linker flags\n" @@ -154,6 +169,7 @@ install: @printf "\033[1;92mInstalling themes to: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop/themes\033[0m\n" @cp -pr themes $(DESTDIR)$(PREFIX)/share/btop +ifneq ($(PLATFORM),OSX) #? Set SUID bit for btop as $SU_USER in $SU_GROUP setuid: @printf "\033[1;97mFile: $(DESTDIR)$(PREFIX)/bin/btop\n" @@ -161,6 +177,7 @@ setuid: @chown $(SU_USER):$(SU_GROUP) $(DESTDIR)$(PREFIX)/bin/btop @printf "\033[1;92mSetting SUID bit\033[0m\n" @chmod u+s $(DESTDIR)$(PREFIX)/bin/btop +endif uninstall: @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/bin/btop\033[0m\n" @@ -179,7 +196,7 @@ btop: $(OBJECTS) @$(QUIET) || printf "\n\033[1;92mLinking and optimizing binary\033[37m...\033[0m\n" @$(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) || exit 1 @printf "\033[1;92m-> \033[1;37m$(TARGETDIR)/btop \033[100D\033[35C\033[1;93m(\033[1;97m$$(du -ah $(TARGETDIR)/btop | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" - printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n" + @printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n" #? Compile .ONESHELL: diff --git a/src/btop.cpp b/src/btop.cpp index f6a5978..2e0b01b 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); @@ -292,7 +296,7 @@ namespace Runner { pthread_mutex_t& pt_mutex; public: int status; - thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { status = pthread_mutex_lock(&pt_mutex); } + thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { pthread_mutex_init(&mtx, NULL); status = pthread_mutex_lock(&pt_mutex); } ~thread_lock() { if (status == 0) pthread_mutex_unlock(&pt_mutex); } }; diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp index d290832..609a336 100644 --- a/src/btop_draw.cpp +++ b/src/btop_draw.cpp @@ -815,7 +815,7 @@ namespace Mem { if (title.empty()) title = capitalize(name); const string humanized = floating_humanizer(mem.stats.at(name)); const string graphics = (use_graphs ? mem_graphs.at(name)(mem.percent.at(name), redraw or data_same) : mem_meters.at(name)(mem.percent.at(name).back())); - if (mem_size > 2) { + if (mem_size > 2 && mem.percent.at(name).size() > 0) { out += Mv::to(y+1+cy, x+1+cx) + divider + ljust(title, 4, false, false, not big_mem) + ljust(":", (big_mem ? 1 : 6)) + Mv::to(y+1+cy, x+cx + mem_width - 2 - humanized.size()) + trans(humanized) + Mv::to(y+2+cy, x+cx + (graph_height >= 2 ? 0 : 1)) + graphics + up + rjust(to_string(mem.percent.at(name).back()) + "%", 4); @@ -1313,14 +1313,15 @@ namespace Proc { if (item_fit >= 7) out += cjust(to_string(detailed.entry.threads), item_width); if (item_fit >= 8) out += cjust(to_string(detailed.entry.p_nice), item_width); - - const double mem_p = (double)detailed.mem_bytes.back() * 100 / totalMem; - string mem_str = to_string(mem_p); - mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4)); - out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + mem_str + "% ", (d_width / 3) - 2) - + Theme::c("inactive_fg") + Fx::ub + graph_bg * (d_width / 3) + Mv::l(d_width / 3) - + Theme::c("proc_misc") + detailed_mem_graph(detailed.mem_bytes, (redraw or data_same or not alive)) + ' ' - + Theme::c("title") + Fx::b + detailed.memory; + if (detailed.mem_bytes.size() > 0) { + const double mem_p = (double)detailed.mem_bytes.back() * 100 / totalMem; + string mem_str = to_string(mem_p); + mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4)); + out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + mem_str + "% ", (d_width / 3) - 2) + + Theme::c("inactive_fg") + Fx::ub + graph_bg * (d_width / 3) + Mv::l(d_width / 3) + + Theme::c("proc_misc") + detailed_mem_graph(detailed.mem_bytes, (redraw or data_same or not alive)) + ' ' + + Theme::c("title") + Fx::b + detailed.memory; + } } //? Check bounds of current selection and view diff --git a/src/btop_input.cpp b/src/btop_input.cpp index 2056bea..6e4fc02 100644 --- a/src/btop_input.cpp +++ b/src/btop_input.cpp @@ -32,6 +32,8 @@ using std::cin, std::vector, std::string_literals::operator""s; using namespace Tools; namespace rng = std::ranges; +extern void clean_quit(int sig); + namespace Input { //* Map for translating key codes to readable values @@ -187,7 +189,7 @@ namespace Input { if (not filtering) { bool keep_going = false; if (str_to_lower(key) == "q") { - exit(0); + clean_quit(0); } else if (is_in(key, "escape", "m")) { Menu::show(Menu::Menus::Main); @@ -483,4 +485,4 @@ namespace Input { } } -}
\ No newline at end of file +} diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp index 97e010a..ea3b543 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..789158a --- /dev/null +++ b/src/osx/btop_collect.cpp @@ -0,0 +1,1193 @@ +/* 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 <IOKit/ps/IOPSKeys.h> +#include <IOKit/ps/IOPowerSources.h> +#include <arpa/inet.h> +#include <ifaddrs.h> +#include <libproc.h> +#include <mach/mach_host.h> +#include <mach/mach_init.h> +#include <mach/mach_types.h> +#include <mach/processor_info.h> +#include <mach/vm_statistics.h> +#include <net/if.h> +#include <net/if_dl.h> +#include <netdb.h> +#include <netinet/tcp_fsm.h> +#include <pwd.h> +#include <sys/socket.h> +#include <sys/statvfs.h> +#include <sys/sysctl.h> +#include <sys/types.h> +#include <unistd.h> + +#include <btop_config.hpp> +#include <btop_shared.hpp> +#include <btop_tools.hpp> +#include <cmath> +#include <fstream> +#include <numeric> +#include <ranges> +#include <regex> +#include <string> + +using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; +using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; +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 Cpu + +namespace Mem { + double old_uptime; +} + +namespace Shared { + + fs::path passwd_path; + uint64_t totalMem; + long pageSize, clkTck, coreCount; + int totalMem_len; + + void init() { + //? Shared global variables init + + // 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."); + } + + int64_t memsize = 0; + size_t size = sizeof(memsize); + if (sysctlbyname("hw.memsize", &memsize, &size, NULL, 0) < 0) { + Logger::warning("Could not get memory size"); + } + totalMem = memsize; + + //? 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 Shared + +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; + char buffer[1024]; + size_t size = sizeof(buffer); + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &size, NULL, 0) < 0) { + Logger::error("Failed to get CPU name"); + return name; + } + name = string(buffer); + + 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 ® : {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() { + 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() { + unsigned int freq = 1; + size_t size = sizeof(freq); + + int mib[2]; + mib[0] = CTL_HW; + mib[1] = HW_CPU_FREQ; + + if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) { + // this fails on Apple Silicon macs. Apparently you're not allowed to know + return ""; + } + return std::to_string(freq / 1000.0 / 1000.0 / 1000.0).substr(0, 3); + } + + auto get_core_mapping() -> unordered_flat_map<int, int> { + unordered_flat_map<int, int> core_map; + if (cpu_temp_only) return core_map; + + natural_t cpu_count; + natural_t i; + processor_info_array_t info_array; + mach_msg_type_number_t info_count; + kern_return_t error; + + mach_port_t host_port = mach_host_self(); + error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count); + if (error != KERN_SUCCESS) { + Logger::error("Failed getting CPU info"); + return core_map; + } + for (i = 0; i < cpu_count; i++) { + core_map[i] = i; + } + + //? 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, ""}; + + uint32_t percent = -1; + long seconds = -1; + string status = "discharging"; + CFTypeRef ps_info = IOPSCopyPowerSourcesInfo(); + if (ps_info) { + CFArrayRef one_ps_descriptor = IOPSCopyPowerSourcesList(ps_info); + if (one_ps_descriptor) { + if (CFArrayGetCount(one_ps_descriptor)) { + CFDictionaryRef one_ps = IOPSGetPowerSourceDescription(ps_info, CFArrayGetValueAtIndex(one_ps_descriptor, 0)); + has_battery = true; + CFNumberRef remaining = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSTimeToEmptyKey)); + int32_t estimatedMinutesRemaining; + if (remaining) { + CFNumberGetValue(remaining, kCFNumberSInt32Type, &estimatedMinutesRemaining); + seconds = estimatedMinutesRemaining * 60; + } + CFNumberRef charge = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSCurrentCapacityKey)); + if (charge) { + CFNumberGetValue(charge, kCFNumberSInt32Type, &percent); + } + CFBooleanRef charging = (CFBooleanRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSIsChargingKey)); + if (charging) { + bool isCharging = CFBooleanGetValue(charging); + if (isCharging) { + status = "charging"; + } + } + if (percent == 100) { + status = "full"; + } + } else { + has_battery = false; + } + CFRelease(one_ps_descriptor); + } else { + has_battery = false; + } + CFRelease(ps_info); + } + 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; + + double avg[3]; + + if (getloadavg(avg, sizeof(avg)) < 0) { + Logger::error("failed to get load averages"); + } + + cpu.load_avg[0] = avg[0]; + cpu.load_avg[1] = avg[1]; + cpu.load_avg[2] = avg[2]; + natural_t cpu_count; + natural_t i; + processor_info_array_t info_array; + mach_msg_type_number_t info_count; + kern_return_t error; + processor_cpu_load_info_data_t *cpu_load_info = NULL; + + mach_port_t host_port = mach_host_self(); + error = host_processor_info(host_port, PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info_array, &info_count); + if (error != KERN_SUCCESS) { + Logger::error("Failed getting CPU load info"); + } + cpu_load_info = (processor_cpu_load_info_data_t *)info_array; + long long global_totals = 0; + long long global_idles = 0; + for (i = 0; i < cpu_count; i++) { + vector<long long> times; + long long total_sum = 0; + //? 0=user, 1=nice, 2=system, 3=idle, 4=iowait, 5=irq, 6=softirq, 7=steal, 8=guest, 9=guest_nice + times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_USER]); + times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_NICE]); + times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM]); + times.push_back(cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE]); + times.push_back(0); + times.push_back(0); + times.push_back(0); + times.push_back(0); + times.push_back(0); + times.push_back(0); + for (long long t : times) { + total_sum += t; + } + try { + //? 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)); + + global_totals += totals; + global_idles += idles; + //? Calculate cpu total for each core + if (i > Shared::coreCount) break; + const long long calc_totals = max(0ll, totals - core_old_totals.at(i)); + const long long calc_idles = max(0ll, idles - core_old_idles.at(i)); + core_old_totals.at(i) = totals; + core_old_idles.at(i) = idles; + + cpu.core_percent.at(i).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).size() > 40) cpu.core_percent.at(i).pop_front(); + + //? Populate cpu.cpu_percent with all fields from syscall + 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; + } + } catch (const std::exception &e) { + Logger::error("get_cpuHz() : " + (string)e.what()); + throw std::runtime_error("collect() : " + (string)e.what()); + } + } + const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals")); + const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles")); + cpu_old.at("totals") = global_totals; + cpu_old.at("idles") = global_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(); + + if (Config::getB("show_cpu_freq")) { + auto hz = get_cpuHz(); + if (hz != "") { + cpuHz = hz; + } + } + + 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 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{}; + + uint64_t get_totalMem() { + return Shared::totalMem; + } + + 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 &show_disks = Config::getB("show_disks"); + auto &swap_disk = Config::getB("swap_disk"); + auto &mem = current_mem; + static const bool snapped = (getenv("BTOP_SNAPPED") != NULL); + + vm_statistics64 p; + mach_msg_type_number_t info_size = HOST_VM_INFO64_COUNT; + if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&p, &info_size) == 0) { + mem.stats.at("available") = p.free_count * Shared::pageSize; + mem.stats.at("free") = p.free_count * Shared::pageSize; + mem.stats.at("cached") = p.external_page_count * Shared::pageSize; + mem.stats.at("used") = (p.active_count + p.inactive_count + p.wire_count) * Shared::pageSize; + } + + int mib[2] = {CTL_VM, VM_SWAPUSAGE}; + + struct xsw_usage swap; + size_t len = sizeof(struct xsw_usage); + if (sysctl(mib, 2, &swap, &len, NULL, 0) == 0) { + mem.stats.at("swap_total") = swap.xsu_total; + mem.stats.at("swap_free") = swap.xsu_avail; + mem.stats.at("swap_used") = swap.xsu_used; + } + + 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; + //? 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_disks) { + double uptime = system_uptime(); + auto &disks_filter = Config::getS("disks_filter"); + bool filter_exclude = false; + // auto &only_physical = Config::getB("only_physical"); + auto &disks = mem.disks; + 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); + } + } + + struct statfs *stfs; + int count = getmntinfo(&stfs, MNT_WAIT); + vector<string> found; + found.reserve(last_found.size()); + for (int i = 0; i < count; i++) { + std::error_code ec; + string mountpoint = stfs[i].f_mntonname; + string dev = stfs[i].f_mntfromname; + disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; + + //? 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; + } + + found.push_back(mountpoint); + if (not v_contains(last_found, mountpoint)) + redraw = true; + + if (disks.at(mountpoint).dev.empty()) + disks.at(mountpoint).dev = dev; + if (disks.at(mountpoint).name.empty()) + disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); + disks.at(mountpoint).free = stfs[i].f_bfree; + disks.at(mountpoint).total = stfs[i].f_iosize; + } + + //? 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); + + //? 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", "/dev")) + mem.disks_order.push_back(name); + + old_uptime = uptime; + } + return mem; + } + +} // namespace 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); + } + } + + unordered_flat_map<string, std::tuple<uint64_t, uint64_t>> ifstats; + int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0}; + size_t len; + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + Logger::error("failed getting network interfaces"); + } + char *buf = (char *)malloc(len); + if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { + Logger::error("failed getting network interfaces"); + } + char *lim = buf + len; + char *next = NULL; + for (next = buf; next < lim;) { + struct if_msghdr *ifm = (struct if_msghdr *)next; + next += ifm->ifm_msglen; + if (ifm->ifm_type == RTM_IFINFO2) { + struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; + struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); + char iface[32]; + strncpy(iface, sdl->sdl_data, sdl->sdl_nlen); + iface[sdl->sdl_nlen] = 0; + ifstats[iface] = std::tuple(if2m->ifm_data.ifi_ibytes, if2m->ifm_data.ifi_obytes); + } + } + + //? Get total recieved and transmitted bytes + device address if no ip was found + for (const auto &iface : interfaces) { + for (const string dir : {"download", "upload"}) { + auto &saved_stat = net.at(iface).stat.at(dir); + auto &bandwidth = net.at(iface).bandwidth.at(dir); + auto dirval = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]); + uint64_t val = saved_stat.last; + try { + val = max(dirval, val); + } catch (const std::invalid_argument &) { + } catch (const std::out_of_range &) { + } + + //? 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; + saved_stat.total = val - saved_stat.offset; + saved_stat.last = val; + + //? Add values to graph + bandwidth.push_back(saved_stat.speed); + while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front(); + + //? Set counters for auto scaling + if (net_auto and selected_iface == iface) { + if (saved_stat.speed > graph_max[dir]) { + ++max_count[dir][0]; + if (max_count[dir][1] > 0) --max_count[dir][1]; + } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) { + ++max_count[dir][1]; + if (max_count[dir][0] > 0) --max_count[dir][0]; + } + } + } + } + + //? Clean up net map if needed + if (net.size() > interfaces.size()) { + for (auto it = net.begin(); it != net.end();) { + if (not v_contains(interfaces, it->first)) + it = net.erase(it); + else + it++; + } + net.compact(); + } + + timestamp = new_timestamp; + } + //? Return empty net_info struct if no interfaces was found + if (net.empty()) + return empty_net; + + //? Find an interface to display if selected isn't set or valid + if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { + max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0; + redraw = true; + if (net_auto) rescale = true; + if (not config_iface.empty() and v_contains(interfaces, config_iface)) + selected_iface = config_iface; + else { + //? Sort interfaces by total upload + download bytes + auto sorted_interfaces = interfaces; + rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) { + return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total, + net.at(b).stat["download"].total + net.at(b).stat["upload"].total); + }); + selected_iface.clear(); + //? Try to set to a connected interface + for (const auto &iface : sorted_interfaces) { + if (net.at(iface).connected) selected_iface = iface; + brea |