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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(pages) * static_cast(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(info.total_runtime.tv_sec); + } + + auto proc_start_seconds(const sysctl_proc_info_t& info) -> uint64_t { + return static_cast(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 { + size_t limit = 256; + for (;;) { + int mib[] = { + SYS_CTL_KERN, + SYS_CTL_KERN_PROC, + SYS_CTL_KERN_PROC_ALL, + 0, + static_cast(sizeof(sysctl_proc_info_t)), + static_cast(limit), + }; + + size_t len = sizeof(sysctl_proc_info_t) * limit; + vector 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 available_fields = {"Auto", "total"}; + vector available_sensors; + bool got_sensors{}; + bool cpu_temp_only{}; + bool supports_watts{}; + bool has_battery = false; // TODO: no existing battery ABI. + tuple current_bat = {0, 0.0F, 0, ""}; + std::unordered_map core_mapping; + cpu_info current_cpu; + + auto get_cpuHz() -> string { + return ""; + } + + auto get_battery() -> tuple { + // TODO: no existing battery ABI. + return current_bat; + } + + auto get_core_mapping() -> std::unordered_map { + 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(info.loads[0]) / load_scale, + static_cast(info.loads[1]) / load_scale, + static_cast(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(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(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(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 current_net; + net_info empty_net = {}; + vector interfaces; + string selected_iface; + std::unordered_map 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(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(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 current_procs; + string current_sort; + string current_filter; + bool current_rev{}; + bool is_tree_mode{}; + atomic 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& 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& { + 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 found; + auto snapshot = get_processes(); + + for (const auto& proc : snapshot) { + found.push_back(proc.pid); + auto existing = std::ranges::find(current_procs, static_cast(proc.pid), &proc_info::pid); + if (existing == current_procs.end()) { + current_procs.push_back({static_cast(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(proc.physical_pages) * Shared::pageSize; + if (entry.mem == 0) + entry.mem = static_cast(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(find_pid), &proc_info::pid); collapser != current_procs.end()) + collapser->collapsed = collapse != -1; + collapse = expand = -1; + } + + vector 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(current_procs.size()) - filter_found; + return current_procs; + } +} + +namespace Tools { + double system_uptime() { + return static_cast(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_vec, const string& sorting, bool reverse, bool tree) {