btop: add port

This commit is contained in:
Matheus
2026-06-09 18:09:00 -03:00
parent 3f78b4ec13
commit 0492348131
3 changed files with 563 additions and 0 deletions
+527
View File
@@ -0,0 +1,527 @@
diff --git btop-clean/Makefile btop-workdir/Makefile
index 0bb9cbd..3dad342 100644
--- btop-clean/Makefile
+++ btop-workdir/Makefile
@@ -132,6 +132,10 @@ else ifeq ($(PLATFORM_LC),netbsd)
override ADDFLAGS += -lkvm -lprop
export MAKE = gmake
SU_GROUP := wheel
+else ifeq ($(PLATFORM_LC),astral)
+ PLATFORM_DIR := astral
+ THREADS := $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)
+ SU_GROUP := root
else
$(error $(shell printf "\033[1;91mERROR: \033[97mUnsupported platform ($(PLATFORM))\033[0m"))
endif
diff --git btop-workdir/src/astral/btop_collect.cpp btop-workdir/src/astral/btop_collect.cpp
new file mode 100644
index 0000000..97b9358
--- /dev/null
+++ btop-workdir/src/astral/btop_collect.cpp
@@ -0,0 +1,472 @@
+#include <algorithm>
+#include <arpa/inet.h>
+#include <array>
+#include <astral/sysctl.h>
+#include <cmath>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <ifaddrs.h>
+#include <iterator>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <pwd.h>
+#include <ranges>
+#include <stdexcept>
+#include <string>
+#include <sys/sysinfo.h>
+#include <unistd.h>
+#include <vector>
+
+#include "btop_config.hpp"
+#include "btop_shared.hpp"
+#include "btop_tools.hpp"
+
+using namespace Tools;
+using std::cmp_greater;
+
+namespace {
+ constexpr double load_scale = 1 << SI_LOAD_SHIFT;
+
+ auto read_sysinfo() -> struct sysinfo {
+ struct sysinfo info {};
+ if (sysinfo(&info) != 0)
+ throw std::runtime_error("sysinfo() failed");
+ return info;
+ }
+
+ auto mem_bytes(unsigned long pages, unsigned int unit) -> uint64_t {
+ return static_cast<uint64_t>(pages) * static_cast<uint64_t>(unit ? unit : 1);
+ }
+
+ auto user_name(uid_t uid) -> string {
+ if (auto *pw = getpwuid(uid); pw != nullptr and pw->pw_name != nullptr)
+ return pw->pw_name;
+ return std::to_string(uid);
+ }
+
+ auto proc_runtime_seconds(const sysctl_proc_info_t& info) -> uint64_t {
+ return static_cast<uint64_t>(info.total_runtime.tv_sec);
+ }
+
+ auto proc_start_seconds(const sysctl_proc_info_t& info) -> uint64_t {
+ return static_cast<uint64_t>(info.start_timestamp.tv_sec);
+ }
+
+ auto proc_state(int state) -> char {
+ (void)state;
+ // TODO: Astral sysctl currently exposes a placeholder process state.
+ return 'W';
+ }
+
+ auto get_processes() -> vector<sysctl_proc_info_t> {
+ size_t limit = 256;
+ for (;;) {
+ int mib[] = {
+ SYS_CTL_KERN,
+ SYS_CTL_KERN_PROC,
+ SYS_CTL_KERN_PROC_ALL,
+ 0,
+ static_cast<int>(sizeof(sysctl_proc_info_t)),
+ static_cast<int>(limit),
+ };
+
+ size_t len = sizeof(sysctl_proc_info_t) * limit;
+ vector<sysctl_proc_info_t> procs(limit);
+ if (sysctl(mib, std::size(mib), procs.data(), &len, nullptr, 0) == 0) {
+ procs.resize(len / sizeof(sysctl_proc_info_t));
+ return procs;
+ }
+
+ if (errno != ENOMEM)
+ throw std::runtime_error("Astral process sysctl failed");
+ limit *= 2;
+ }
+ }
+}
+
+namespace Cpu {
+ string cpuName = "Astral";
+ string cpuHz;
+ vector<string> available_fields = {"Auto", "total"};
+ vector<string> available_sensors;
+ bool got_sensors{};
+ bool cpu_temp_only{};
+ bool supports_watts{};
+ bool has_battery = false; // TODO: no existing battery ABI.
+ tuple<int, float, long, string> current_bat = {0, 0.0F, 0, ""};
+ std::unordered_map<int, int> core_mapping;
+ cpu_info current_cpu;
+
+ auto get_cpuHz() -> string {
+ return "";
+ }
+
+ auto get_battery() -> tuple<int, float, long, string> {
+ // TODO: no existing battery ABI.
+ return current_bat;
+ }
+
+ auto get_core_mapping() -> std::unordered_map<int, int> {
+ return {};
+ }
+
+ auto collect(bool no_update) -> cpu_info& {
+ if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty()))
+ return current_cpu;
+
+ auto info = read_sysinfo();
+ current_cpu.load_avg = {
+ static_cast<double>(info.loads[0]) / load_scale,
+ static_cast<double>(info.loads[1]) / load_scale,
+ static_cast<double>(info.loads[2]) / load_scale,
+ };
+
+ for (auto& [field, values] : current_cpu.cpu_percent) {
+ // TODO: Astral does not expose CPU tick counters to userland yet.
+ values.push_back(0);
+ while (cmp_greater(values.size(), width * 2))
+ values.pop_front();
+ }
+
+ if (current_cpu.core_percent.size() != static_cast<size_t>(Shared::coreCount))
+ current_cpu.core_percent.assign(Shared::coreCount, {});
+ for (auto& values : current_cpu.core_percent) {
+ // TODO: no per-core CPU utilization API is currently available.
+ values.push_back(0);
+ while (cmp_greater(values.size(), width * 2))
+ values.pop_front();
+ }
+
+ current_cpu.temp.assign(Shared::coreCount + 1, {});
+ current_cpu.temp_max = 0; // TODO: no sensors API.
+ current_cpu.usage_watts = 0; // TODO: no power sensors API.
+ return current_cpu;
+ }
+}
+
+namespace Mem {
+ bool has_swap{};
+ int disk_ios{};
+ mem_info current_mem {};
+
+ uint64_t get_totalMem() {
+ auto info = read_sysinfo();
+ return mem_bytes(info.totalram, info.mem_unit);
+ }
+
+ auto collect(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 info = read_sysinfo();
+ auto total = mem_bytes(info.totalram, info.mem_unit);
+ auto free = mem_bytes(info.freeram, info.mem_unit);
+ auto cached = mem_bytes(info.bufferram, info.mem_unit);
+ auto available = std::min(total, free + cached);
+
+ current_mem.stats.at("free") = free;
+ current_mem.stats.at("cached") = cached;
+ current_mem.stats.at("available") = available;
+ current_mem.stats.at("used") = total > available ? total - available : 0;
+ current_mem.stats.at("swap_total") = mem_bytes(info.totalswap, info.mem_unit);
+ current_mem.stats.at("swap_free") = mem_bytes(info.freeswap, info.mem_unit);
+ current_mem.stats.at("swap_used") = current_mem.stats.at("swap_total") > current_mem.stats.at("swap_free")
+ ? current_mem.stats.at("swap_total") - current_mem.stats.at("swap_free")
+ : 0;
+
+ for (const auto& name : mem_names) {
+ current_mem.percent.at(name).push_back(total == 0 ? 0 : std::round(static_cast<double>(current_mem.stats.at(name)) * 100.0 / total));
+ while (cmp_greater(current_mem.percent.at(name).size(), width * 2))
+ current_mem.percent.at(name).pop_front();
+ }
+
+ has_swap = show_swap and current_mem.stats.at("swap_total") > 0;
+ if (has_swap) {
+ for (const auto& name : swap_names) {
+ current_mem.percent.at(name).push_back(std::round(static_cast<double>(current_mem.stats.at(name)) * 100.0 / current_mem.stats.at("swap_total")));
+ while (cmp_greater(current_mem.percent.at(name).size(), width * 2))
+ current_mem.percent.at(name).pop_front();
+ }
+ }
+
+ current_mem.disks.clear();
+ current_mem.disks_order.clear();
+ disk_ios = 0; // TODO: no existing disk enumeration or disk IO API.
+ return current_mem;
+ }
+}
+
+namespace Shared {
+ long pageSize, clkTck, coreCount, page_size, clk_tck;
+
+ void init() {
+ coreCount = sysconf(_SC_NPROCESSORS_ONLN);
+ if (coreCount < 1)
+ coreCount = 1;
+
+ pageSize = sysconf(_SC_PAGESIZE);
+ if (pageSize <= 0)
+ pageSize = 4096;
+ page_size = pageSize;
+
+ clkTck = sysconf(_SC_CLK_TCK);
+ if (clkTck <= 0)
+ clkTck = 100;
+ clk_tck = clkTck;
+
+ (void)read_sysinfo();
+
+ Cpu::current_cpu.core_percent.assign(coreCount, {});
+ Cpu::current_cpu.temp.assign(coreCount + 1, {});
+ Cpu::cpuName = "Astral";
+ Cpu::cpuHz.clear();
+ Cpu::has_battery = false; // TODO: no battery API.
+ Cpu::got_sensors = false; // TODO: no sensors API.
+ Cpu::supports_watts = false; // TODO: no power sensors API.
+ Cpu::available_fields.clear();
+ for (const auto& [field, ignored] : Cpu::current_cpu.cpu_percent)
+ Cpu::available_fields.push_back(field);
+ Cpu::core_mapping = Cpu::get_core_mapping();
+ Cpu::collect();
+
+ Mem::collect();
+ Proc::collect();
+ Net::collect();
+
+ Logger::debug("Shared::init() : Initialized Astral collector.");
+ }
+}
+
+namespace Net {
+ std::unordered_map<string, net_info> current_net;
+ net_info empty_net = {};
+ vector<string> interfaces;
+ string selected_iface;
+ std::unordered_map<string, uint64_t> graph_max = {{"download", {}}, {"upload", {}}};
+ bool rescale{true};
+
+ auto collect(bool no_update) -> net_info& {
+ if (Runner::stopping)
+ return empty_net;
+ if (no_update and not selected_iface.empty() and current_net.contains(selected_iface))
+ return current_net.at(selected_iface);
+
+ IfAddrsPtr if_addrs {};
+ if (if_addrs.get_status() != 0)
+ return empty_net;
+
+ interfaces.clear();
+ char ip[INET6_ADDRSTRLEN] {};
+
+ for (auto *ifa = if_addrs.get(); ifa != nullptr; ifa = ifa->ifa_next) {
+ if (ifa->ifa_addr == nullptr or ifa->ifa_name == nullptr)
+ continue;
+
+ const string iface = ifa->ifa_name;
+ if (not v_contains(interfaces, iface)) {
+ interfaces.push_back(iface);
+ current_net[iface].connected = (ifa->ifa_flags & IFF_RUNNING) != 0;
+ current_net[iface].ipv4.clear();
+ current_net[iface].ipv6.clear();
+ }
+
+ if (ifa->ifa_addr->sa_family == AF_INET and current_net[iface].ipv4.empty()) {
+ auto *addr = reinterpret_cast<struct sockaddr_in*>(ifa->ifa_addr);
+ if (inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)) != nullptr)
+ current_net[iface].ipv4 = ip;
+ }
+ else if (ifa->ifa_addr->sa_family == AF_INET6 and current_net[iface].ipv6.empty()) {
+ auto *addr = reinterpret_cast<struct sockaddr_in6*>(ifa->ifa_addr);
+ if (inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)) != nullptr)
+ current_net[iface].ipv6 = ip;
+ }
+ }
+
+ for (const auto& iface : interfaces) {
+ for (const string dir : {"download", "upload"}) {
+ auto& netif = current_net[iface];
+ auto& stat = netif.stat.at(dir);
+ auto& bandwidth = netif.bandwidth.at(dir);
+ // TODO: getifaddrs() exposes addresses, not Astral RX/TX byte counters.
+ stat.speed = 0;
+ stat.total = 0;
+ bandwidth.push_back(0);
+ while (cmp_greater(bandwidth.size(), width * 2))
+ bandwidth.pop_front();
+ }
+ }
+
+ for (auto it = current_net.begin(); it != current_net.end();) {
+ if (not v_contains(interfaces, it->first))
+ it = current_net.erase(it);
+ else
+ ++it;
+ }
+
+ if (current_net.empty())
+ return empty_net;
+
+ auto& config_iface = Config::getS("net_iface");
+ if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) {
+ if (not config_iface.empty() and v_contains(interfaces, config_iface))
+ selected_iface = config_iface;
+ else
+ selected_iface = interfaces.front();
+ redraw = true;
+ rescale = true;
+ }
+
+ return current_net.at(selected_iface);
+ }
+}
+
+namespace Proc {
+ vector<proc_info> current_procs;
+ string current_sort;
+ string current_filter;
+ bool current_rev{};
+ bool is_tree_mode{};
+ atomic<int> numpids{};
+ int filter_found{};
+ int collapse = -1, expand = -1, toggle_children = -1, collapse_all = -1;
+ detail_container detailed;
+
+ static void collect_details(size_t pid, vector<proc_info>& procs) {
+ auto p_info = std::ranges::find(procs, pid, &proc_info::pid);
+ if (p_info == procs.end())
+ return;
+ if (pid != detailed.last_pid) {
+ detailed = {};
+ detailed.last_pid = pid;
+ }
+ detailed.entry = *p_info;
+ detailed.status = proc_states.contains(detailed.entry.state) ? proc_states.at(detailed.entry.state) : "Unknown";
+ detailed.parent.clear();
+ if (auto parent = std::ranges::find(procs, detailed.entry.ppid, &proc_info::pid); parent != procs.end())
+ detailed.parent = parent->name;
+ detailed.memory = floating_humanizer(detailed.entry.mem);
+ detailed.mem_bytes.push_back(detailed.entry.mem);
+ while (cmp_greater(detailed.mem_bytes.size(), width))
+ detailed.mem_bytes.pop_front();
+ // TODO: no per-process IO counters are exposed by the current API.
+ detailed.io_read = "0 B";
+ detailed.io_write = "0 B";
+ }
+
+ auto collect(bool no_update) -> vector<proc_info>& {
+ if (Runner::stopping)
+ return current_procs;
+
+ const auto& sorting = Config::getS("proc_sorting");
+ auto reverse = Config::getB("proc_reversed");
+ const auto& filter = Config::getS("proc_filter");
+ auto tree = Config::getB("proc_tree");
+ auto show_detailed = Config::getB("show_detailed");
+ const size_t detailed_pid = Config::getI("detailed_pid");
+ bool should_filter = current_filter != filter;
+ bool sorted_change = sorting != current_sort or reverse != current_rev or should_filter;
+ bool tree_mode_change = tree != is_tree_mode;
+ if (should_filter)
+ current_filter = filter;
+ if (sorted_change) {
+ current_sort = sorting;
+ current_rev = reverse;
+ }
+ if (tree_mode_change)
+ is_tree_mode = tree;
+
+ if (not no_update) {
+ auto total_mem = Mem::get_totalMem();
+ vector<size_t> found;
+ auto snapshot = get_processes();
+
+ for (const auto& proc : snapshot) {
+ found.push_back(proc.pid);
+ auto existing = std::ranges::find(current_procs, static_cast<size_t>(proc.pid), &proc_info::pid);
+ if (existing == current_procs.end()) {
+ current_procs.push_back({static_cast<size_t>(proc.pid)});
+ existing = current_procs.end() - 1;
+ }
+
+ auto& entry = *existing;
+ entry.name = proc.name[0] ? proc.name : std::to_string(proc.pid);
+ entry.cmd = entry.name; // TODO: Astral sysctl does not expose argv yet.
+ entry.short_cmd = entry.cmd;
+ entry.threads = proc.thread_count;
+ entry.user = user_name(proc.uid);
+ entry.ppid = proc.ppid;
+ entry.p_nice = proc.nice;
+ entry.state = proc_state(proc.state);
+ entry.cpu_s = proc_start_seconds(proc);
+ entry.cpu_t = proc_runtime_seconds(proc);
+ entry.cpu_p = 0.0; // TODO: no process CPU percentage ABI.
+ entry.cpu_c = 0.0; // TODO: no process CPU counters suitable for cumulative percentages.
+ entry.mem = static_cast<uint64_t>(proc.physical_pages) * Shared::pageSize;
+ if (entry.mem == 0)
+ entry.mem = static_cast<uint64_t>(proc.virtual_pages) * Shared::pageSize;
+ if (entry.mem > total_mem)
+ entry.mem = total_mem;
+ // TODO: RSS/virtual memory stay zero when Astral sysctl returns zero page counts.
+ }
+
+ auto eraser = std::ranges::remove_if(current_procs, [&](const auto& entry) {
+ return not v_contains(found, entry.pid);
+ });
+ current_procs.erase(eraser.begin(), eraser.end());
+ should_filter = true;
+ }
+
+ if (show_detailed)
+ collect_details(detailed_pid, current_procs);
+
+ if (should_filter) {
+ filter_found = 0;
+ for (auto& proc : current_procs) {
+ if (not tree and not filter.empty() and not matches_filter(proc, filter)) {
+ proc.filtered = true;
+ filter_found++;
+ }
+ else
+ proc.filtered = false;
+ }
+ }
+
+ if (sorted_change or tree_mode_change or not no_update)
+ proc_sorter(current_procs, sorting, reverse, tree);
+
+ if (tree and not current_procs.empty() and (not no_update or should_filter or sorted_change)) {
+ if (collapse_all != -1) {
+ toggle_tree_collapse(current_procs);
+ collapse_all = -1;
+ }
+ if (auto find_pid = collapse != -1 ? collapse : expand; find_pid != -1) {
+ if (auto collapser = std::ranges::find(current_procs, static_cast<size_t>(find_pid), &proc_info::pid); collapser != current_procs.end())
+ collapser->collapsed = collapse != -1;
+ collapse = expand = -1;
+ }
+
+ vector<tree_proc> tree_procs;
+ tree_procs.reserve(current_procs.size());
+ std::ranges::stable_sort(current_procs, std::ranges::less{}, &proc_info::ppid);
+ for (auto& proc : std::ranges::equal_range(current_procs, current_procs.front().ppid, std::ranges::less{}, &proc_info::ppid))
+ _tree_gen(proc, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter);
+
+ int index = 0;
+ tree_sort(tree_procs, sorting, reverse, false, index, current_procs.size());
+ for (auto t = tree_procs.begin(); t != tree_procs.end(); ++t)
+ _collect_prefixes(*t, t == tree_procs.end() - 1);
+ std::ranges::stable_sort(current_procs, std::ranges::less{}, &proc_info::tree_index);
+ }
+
+ numpids = static_cast<int>(current_procs.size()) - filter_found;
+ return current_procs;
+ }
+}
+
+namespace Tools {
+ double system_uptime() {
+ return static_cast<double>(read_sysinfo().uptime);
+ }
+}
diff --git btop-clean/src/btop.cpp btop-workdir/src/btop.cpp
index c04295c..e2c1772 100644
--- btop-clean/src/btop.cpp
+++ btop-workdir/src/btop.cpp
@@ -214,7 +214,7 @@ void clean_quit(int sig) {
Global::quitting = true;
Runner::stop();
if (Global::_runner_started) {
- #if defined __APPLE__ || defined __OpenBSD__ || defined __NetBSD__
+ #if defined __APPLE__ || defined __OpenBSD__ || defined __NetBSD__ || defined __astral__
if (pthread_join(Runner::runner_id, nullptr) != 0) {
Logger::warning("Failed to join _runner thread on exit!");
pthread_cancel(Runner::runner_id);
diff --git btop-clean/src/btop_shared.cpp btop-workdir/src/btop_shared.cpp
index 462cba7..a88a9e4 100644
--- btop-clean/src/btop_shared.cpp
+++ btop-workdir/src/btop_shared.cpp
@@ -95,10 +95,16 @@ namespace Gpu {
namespace Proc {
bool set_priority(pid_t pid, int priority) {
+#ifdef __astral__
+ (void)pid;
+ (void)priority;
+ return false;
+#else
if (setpriority(PRIO_PROCESS, pid, priority) == 0) {
return true;
}
return false;
+#endif
}
void proc_sorter(vector<proc_info>& proc_vec, const string& sorting, bool reverse, bool tree) {