Files
process_explorer/main.cpp
T
egutierrez 477bcd00f0 chore: auto-commit (13 archivos)
- CMakeLists.txt
- agent_protocol.cpp
- agent_protocol.h
- app.md
- appicon.ico
- hosts_db.cpp
- hosts_db.h
- http_client.cpp
- http_client.h
- main.cpp
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:31:32 +02:00

656 lines
24 KiB
C++

#include <imgui.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "app_base.h"
#include "core/panel_menu.h"
#include "core/icons_tabler.h"
#include "core/logger.h"
#include "viz/kpi_card.h"
#include "viz/line_plot.h"
#include "agent_protocol.h"
#include "hosts_db.h"
#include "samples_db.h"
// =========================================================================
// Per-host runtime state
// =========================================================================
static constexpr int kHistoryCap = 600; // 5 min @ 1Hz + margen
static constexpr int kHistoryWindowSec = 300; // ventana visible plots
struct TimePoint { int64_t ts; float v; };
struct HistoryRing {
TimePoint buf[kHistoryCap] = {};
int count = 0;
int head = 0;
void push(int64_t ts, float v) {
buf[head] = {ts, v};
head = (head + 1) % kHistoryCap;
if (count < kHistoryCap) ++count;
}
// Vuelca solo los puntos dentro de la ventana [now-window, now].
// out_xs es seconds-from-window-start en [0, window].
// Devuelve count.
int flatten_window(int64_t now, int window_sec,
float* out_xs, float* out_ys, int cap) const {
int n = std::min(count, cap);
int start = (head - count + kHistoryCap) % kHistoryCap;
int64_t lo_ts = now - window_sec;
int written = 0;
for (int i = 0; i < n; ++i) {
const TimePoint& p = buf[(start + i) % kHistoryCap];
if (p.ts < lo_ts) continue;
if (written >= cap) break;
out_xs[written] = (float)(p.ts - lo_ts);
out_ys[written] = p.v;
++written;
}
return written;
}
// Solo Y para sparkline (KPI cards) — fade gradient implicito por orden.
int flatten_window_y(int64_t now, int window_sec, float* out_ys, int cap) const {
int n = std::min(count, cap);
int start = (head - count + kHistoryCap) % kHistoryCap;
int64_t lo_ts = now - window_sec;
int written = 0;
for (int i = 0; i < n; ++i) {
const TimePoint& p = buf[(start + i) % kHistoryCap];
if (p.ts < lo_ts) continue;
if (written >= cap) break;
out_ys[written] = p.v;
++written;
}
return written;
}
};
enum class HostKind { Local, Wsl, Http };
struct HostRuntime {
int64_t id = 0; // 0 = local pseudo-host, -1 = wsl pseudo
std::string name = "Local";
std::string url; // empty = local
std::string token;
std::string os;
HostKind kind = HostKind::Local;
bool is_local = true; // true para Local + Wsl (no HTTP remote)
bool online = true;
double last_poll_at = 0.0;
pex::StatsSnapshot stats;
std::vector<pex::ProcInfo> processes;
std::vector<pex::DeviceInfo> devices;
std::vector<pex::ServiceInfo> services;
std::vector<pex::NetConn> netconns;
bool netconns_loaded = false;
bool devices_loaded = false;
bool services_loaded = false;
HistoryRing hist_cpu, hist_ram, hist_disk, hist_net;
};
static void load_netconns(HostRuntime& r) {
switch (r.kind) {
case HostKind::Local: r.netconns = pex::fetch_netconns_local(); break;
case HostKind::Wsl: r.netconns = pex::fetch_netconns_wsl(); break;
case HostKind::Http: /* HTTP agent fetch ya en poll_runtime */ break;
}
r.netconns_loaded = true;
}
static void load_devices(HostRuntime& r) {
switch (r.kind) {
case HostKind::Local: r.devices = pex::fetch_devices_local(); break;
case HostKind::Wsl: r.devices = pex::fetch_devices_wsl(); break;
case HostKind::Http: break;
}
r.devices_loaded = true;
}
static void load_services(HostRuntime& r) {
switch (r.kind) {
case HostKind::Local: r.services = pex::fetch_services_local(); break;
case HostKind::Wsl: r.services = pex::fetch_services_wsl(); break;
case HostKind::Http: break;
}
r.services_loaded = true;
}
static std::vector<HostRuntime> g_runtimes;
static int64_t g_selected_host_id = 0; // panel-scoped selector
static constexpr double kPollIntervalSec = 1.0;
static constexpr int kPollWslIntervalMs = 2000;
// WSL polling se hace en un worker thread. Cada wsl.exe tarda ~300-500ms
// y bloquearia el render del main thread cada poll. Doble buffer con mutex.
#ifdef _WIN32
static std::thread g_wsl_worker;
static std::atomic<bool> g_wsl_stop{false};
static std::mutex g_wsl_mu;
static pex::StatsSnapshot g_wsl_stats_buf;
static std::vector<pex::ProcInfo> g_wsl_procs_buf;
static std::atomic<bool> g_wsl_fresh{false};
static void wsl_worker_thread_fn() {
while (!g_wsl_stop.load(std::memory_order_acquire)) {
auto stats = pex::fetch_stats_wsl();
auto procs = pex::fetch_processes_wsl();
{
std::lock_guard<std::mutex> lk(g_wsl_mu);
g_wsl_stats_buf = std::move(stats);
g_wsl_procs_buf = std::move(procs);
}
g_wsl_fresh.store(true, std::memory_order_release);
// Sleep en pasos de 100ms para reaccionar rapido a stop.
for (int i = 0; i < kPollWslIntervalMs / 100; ++i) {
if (g_wsl_stop.load(std::memory_order_acquire)) return;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
}
#endif
static pex::HostsDb g_hosts_db;
static pex::SamplesDb g_samples_db;
// Add-host modal state
static bool g_open_add_host = false;
static char g_add_name[64] = {};
static char g_add_url[256] = {};
static char g_add_token[256] = {};
static char g_add_os[16] = "linux";
// Panel toggles
static bool g_show_overview = true;
static bool g_show_processes = true;
static bool g_show_network = false;
static bool g_show_devices = false;
static bool g_show_services = false;
// =========================================================================
// Runtime management
// =========================================================================
static HostRuntime* find_runtime(int64_t id) {
for (auto& r : g_runtimes) if (r.id == id) return &r;
return nullptr;
}
static void seed_local_runtime() {
HostRuntime local;
local.id = 0;
local.kind = HostKind::Local;
local.is_local = true;
#ifdef _WIN32
local.name = "Windows";
local.os = "windows";
#else
local.name = "Linux";
local.os = "linux";
#endif
g_runtimes.push_back(std::move(local));
}
#ifdef _WIN32
static void seed_wsl_runtime() {
HostRuntime wsl;
wsl.id = -1;
wsl.name = "WSL";
wsl.os = "linux";
wsl.kind = HostKind::Wsl;
wsl.is_local = true; // accesible sin red
g_runtimes.push_back(std::move(wsl));
}
#endif
static void load_hosts_from_db() {
auto rows = g_hosts_db.list();
for (const auto& h : rows) {
if (find_runtime(h.id)) continue;
HostRuntime r;
r.id = h.id;
r.name = h.name;
r.url = h.url;
r.token = h.token;
r.os = h.os;
r.is_local = h.is_local;
r.kind = h.is_local ? HostKind::Local : HostKind::Http;
g_runtimes.push_back(std::move(r));
}
}
static void poll_runtime(HostRuntime& r) {
switch (r.kind) {
case HostKind::Local:
r.stats = pex::fetch_stats_local();
r.processes = pex::fetch_processes_local();
r.online = true;
// Network/Devices/Services se cargan on-demand (popen caro) desde los
// paneles via Refresh button — ver draw_network/devices/services.
break;
case HostKind::Wsl:
#ifdef _WIN32
// Non-blocking: consume buffer del worker. Si no fresh, mantiene
// ultimo valor — no para nunca la UI.
if (g_wsl_fresh.exchange(false, std::memory_order_acq_rel)) {
std::lock_guard<std::mutex> lk(g_wsl_mu);
r.stats = g_wsl_stats_buf;
r.processes = g_wsl_procs_buf;
r.online = (r.stats.ram_total_mb > 0.0f);
}
#else
r.stats = pex::fetch_stats_wsl();
r.processes = pex::fetch_processes_wsl();
r.online = (r.stats.ram_total_mb > 0.0f);
#endif
break;
case HostKind::Http:
r.stats = pex::fetch_stats(r.url, r.token);
r.processes = pex::fetch_processes(r.url, r.token);
r.devices = pex::fetch_devices(r.url, r.token);
r.services = pex::fetch_services(r.url, r.token);
r.netconns = pex::fetch_netconns(r.url, r.token);
r.online = (r.stats.ram_total_mb > 0.0f);
break;
}
int64_t ts = r.stats.unix_ts ? r.stats.unix_ts : (int64_t)std::time(nullptr);
r.hist_cpu.push(ts, r.stats.cpu_pct);
float ram_pct = r.stats.ram_total_mb > 0.0f
? (r.stats.ram_used_mb / r.stats.ram_total_mb) * 100.0f : 0.0f;
r.hist_ram.push(ts, ram_pct);
r.hist_disk.push(ts, r.stats.disk_read_mb_s + r.stats.disk_write_mb_s);
r.hist_net.push(ts, r.stats.net_rx_mb_s + r.stats.net_tx_mb_s);
pex::Sample s{};
s.host_id = r.id;
s.unix_ts = r.stats.unix_ts ? r.stats.unix_ts : (int64_t)std::time(nullptr);
s.cpu_pct = r.stats.cpu_pct;
s.ram_used_mb = r.stats.ram_used_mb;
s.ram_total_mb = r.stats.ram_total_mb;
s.disk_read_mb_s = r.stats.disk_read_mb_s;
s.disk_write_mb_s = r.stats.disk_write_mb_s;
s.net_rx_mb_s = r.stats.net_rx_mb_s;
s.net_tx_mb_s = r.stats.net_tx_mb_s;
s.gpu_pct = r.stats.gpu_pct;
g_samples_db.insert(s);
if (r.kind == HostKind::Http && r.online) g_hosts_db.touch_last_seen(r.id, s.unix_ts);
r.last_poll_at = ImGui::GetTime();
}
static void poll_all_due() {
double now = ImGui::GetTime();
for (auto& r : g_runtimes) {
if (now - r.last_poll_at >= kPollIntervalSec) poll_runtime(r);
}
}
// =========================================================================
// Host selector combo (single-host panels)
// =========================================================================
static void draw_host_selector(const char* label) {
HostRuntime* cur = find_runtime(g_selected_host_id);
if (!cur && !g_runtimes.empty()) {
g_selected_host_id = g_runtimes.front().id;
cur = &g_runtimes.front();
}
const char* preview = cur ? cur->name.c_str() : "(none)";
ImGui::SetNextItemWidth(180.0f);
if (ImGui::BeginCombo(label, preview)) {
for (auto& r : g_runtimes) {
bool selected = (r.id == g_selected_host_id);
std::string lbl = r.name;
if (r.kind == HostKind::Http) lbl += r.online ? " [online]" : " [offline]";
if (ImGui::Selectable(lbl.c_str(), selected)) g_selected_host_id = r.id;
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
}
// =========================================================================
// Add-host modal
// =========================================================================
static void draw_add_host_modal() {
if (g_open_add_host) {
ImGui::OpenPopup("Add host");
g_open_add_host = false;
}
if (ImGui::BeginPopupModal("Add host", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::InputText("Name", g_add_name, sizeof(g_add_name));
ImGui::InputText("URL", g_add_url, sizeof(g_add_url));
ImGui::InputText("Token", g_add_token, sizeof(g_add_token), ImGuiInputTextFlags_Password);
ImGui::InputText("OS", g_add_os, sizeof(g_add_os));
ImGui::TextDisabled("URL ej: http://aurgi-pc:8487 (process_agent, issue 0111)");
bool can_save = g_add_name[0] != 0 && g_add_url[0] != 0;
ImGui::BeginDisabled(!can_save);
if (ImGui::Button("Save")) {
pex::Host h;
h.name = g_add_name;
h.url = g_add_url;
h.token = g_add_token;
h.os = g_add_os;
h.is_local = false;
h.last_seen_unix = 0;
int64_t id = g_hosts_db.upsert(h);
if (id > 0) {
load_hosts_from_db();
std::memset(g_add_name, 0, sizeof(g_add_name));
std::memset(g_add_url, 0, sizeof(g_add_url));
std::memset(g_add_token, 0, sizeof(g_add_token));
}
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
}
// =========================================================================
// Panels
// =========================================================================
static void draw_host_kpis(HostRuntime& r) {
float hist[kHistoryCap];
int n;
int64_t now = (int64_t)std::time(nullptr);
ImGui::Columns(4, nullptr, false);
n = r.hist_cpu.flatten_window_y(now, kHistoryWindowSec, hist, kHistoryCap);
kpi_card("CPU", r.stats.cpu_pct, 0.0f, hist, n,
0.0f, 100.0f, "%.1f%%", TI_CPU);
ImGui::NextColumn();
n = r.hist_ram.flatten_window_y(now, kHistoryWindowSec, hist, kHistoryCap);
float ram_pct = r.stats.ram_total_mb > 0.0f
? (r.stats.ram_used_mb / r.stats.ram_total_mb) * 100.0f : 0.0f;
kpi_card("RAM", ram_pct, 0.0f, hist, n,
0.0f, 100.0f, "%.1f%%", TI_DATABASE);
ImGui::NextColumn();
n = r.hist_disk.flatten_window_y(now, kHistoryWindowSec, hist, kHistoryCap);
kpi_card("Disk I/O", r.stats.disk_read_mb_s + r.stats.disk_write_mb_s,
0.0f, hist, n, "%.1f MB/s", TI_DEVICE_FLOPPY);
ImGui::NextColumn();
n = r.hist_net.flatten_window_y(now, kHistoryWindowSec, hist, kHistoryCap);
kpi_card("Net I/O", r.stats.net_rx_mb_s + r.stats.net_tx_mb_s,
0.0f, hist, n, "%.1f MB/s", TI_WORLD);
ImGui::Columns(1);
}
static void draw_overview() {
if (!ImGui::Begin(TI_GAUGE " Overview", &g_show_overview)) { ImGui::End(); return; }
if (ImGui::Button(TI_PLUS " Add host")) g_open_add_host = true;
ImGui::SameLine();
ImGui::TextDisabled("%zu hosts", g_runtimes.size());
draw_add_host_modal();
ImGui::Spacing();
for (auto& r : g_runtimes) {
ImGui::PushID((int)r.id);
const char* status_icon = TI_HOME;
switch (r.kind) {
case HostKind::Local: status_icon = TI_HOME; break;
case HostKind::Wsl: status_icon = TI_TERMINAL_2; break;
case HostKind::Http: status_icon = r.online ? TI_PLUG_CONNECTED : TI_PLUG_CONNECTED_X; break;
}
ImVec4 status_col = r.online ? ImVec4(0.4f, 0.85f, 0.5f, 1.0f) : ImVec4(0.7f, 0.4f, 0.4f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Text, status_col);
ImGui::TextUnformatted(status_icon);
ImGui::PopStyleColor();
ImGui::SameLine();
ImGui::Text("%s", r.name.c_str());
if (r.kind == HostKind::Http) {
ImGui::SameLine();
ImGui::TextDisabled("(%s)", r.url.c_str());
}
draw_host_kpis(r);
// CPU history plot — ventana temporal fija [0, 300s], Y [0, 100].
// Aunque solo haya 30s de datos al inicio, la escala X NO se aplasta.
float xs[kHistoryCap], ys[kHistoryCap];
int64_t now = (int64_t)std::time(nullptr);
int m = r.hist_cpu.flatten_window(now, kHistoryWindowSec, xs, ys, kHistoryCap);
char plot_id[80];
std::snprintf(plot_id, sizeof(plot_id), "%s CPU %%##%lld", r.name.c_str(), (long long)r.id);
line_plot(plot_id, xs, ys, m,
0.0f, (float)kHistoryWindowSec,
0.0f, 100.0f, 100.0f);
ImGui::Separator();
ImGui::Spacing();
ImGui::PopID();
}
ImGui::End();
}
static void draw_processes() {
if (!ImGui::Begin(TI_LIST " Processes", &g_show_processes)) { ImGui::End(); return; }
draw_host_selector("Host##procs");
ImGui::SameLine();
HostRuntime* r = find_runtime(g_selected_host_id);
if (!r) { ImGui::TextDisabled("no host"); ImGui::End(); return; }
ImGui::Text("%zu procesos", r->processes.size());
ImGui::SameLine();
if (ImGui::Button("Refresh")) poll_runtime(*r);
if (ImGui::BeginTable("procs", 6,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("PID");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("State");
ImGui::TableSetupColumn("CPU%");
ImGui::TableSetupColumn("RAM MB");
ImGui::TableSetupColumn("Cmdline");
ImGui::TableHeadersRow();
for (const auto& p : r->processes) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%d", p.pid);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(p.name.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(p.state.c_str());
ImGui::TableSetColumnIndex(3); ImGui::Text("%.1f", p.cpu_pct);
ImGui::TableSetColumnIndex(4); ImGui::Text("%.1f", p.ram_mb);
ImGui::TableSetColumnIndex(5); ImGui::TextUnformatted(p.cmdline.c_str());
}
ImGui::EndTable();
}
ImGui::End();
}
static void draw_network() {
if (!ImGui::Begin(TI_NETWORK " Network", &g_show_network)) { ImGui::End(); return; }
draw_host_selector("Host##net");
HostRuntime* r = find_runtime(g_selected_host_id);
if (!r) { ImGui::TextDisabled("no host"); ImGui::End(); return; }
if (!r->netconns_loaded) load_netconns(*r);
ImGui::SameLine();
if (ImGui::Button("Refresh##net")) load_netconns(*r);
ImGui::SameLine();
ImGui::TextDisabled("%zu conexiones", r->netconns.size());
if (r->netconns.empty()) {
ImGui::TextDisabled("(vacio — fuente no disponible o sin conexiones)");
} else if (ImGui::BeginTable("conns", 5,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("PID");
ImGui::TableSetupColumn("Proto");
ImGui::TableSetupColumn("Local");
ImGui::TableSetupColumn("Remote");
ImGui::TableSetupColumn("State");
ImGui::TableHeadersRow();
for (const auto& c : r->netconns) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%d", c.pid);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(c.proto.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(c.local.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(c.remote.c_str());
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(c.state.c_str());
}
ImGui::EndTable();
}
ImGui::End();
}
static void draw_devices() {
if (!ImGui::Begin(TI_DEVICES_PC " Devices", &g_show_devices)) { ImGui::End(); return; }
draw_host_selector("Host##dev");
HostRuntime* r = find_runtime(g_selected_host_id);
if (!r) { ImGui::TextDisabled("no host"); ImGui::End(); return; }
if (!r->devices_loaded) load_devices(*r);
ImGui::SameLine();
if (ImGui::Button("Refresh##dev")) load_devices(*r);
ImGui::SameLine();
ImGui::TextDisabled("%zu dispositivos", r->devices.size());
if (r->devices.empty()) {
ImGui::TextDisabled("(vacio)");
} else if (ImGui::BeginTable("devs", 4,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Kind");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Detail");
ImGui::TableSetupColumn("Usage%");
ImGui::TableHeadersRow();
for (const auto& d : r->devices) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(d.kind.c_str());
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(d.name.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(d.detail.c_str());
ImGui::TableSetColumnIndex(3);
if (d.usage_pct >= 0.0f) ImGui::Text("%.1f", d.usage_pct);
else ImGui::TextDisabled("-");
}
ImGui::EndTable();
}
ImGui::End();
}
static void draw_services() {
if (!ImGui::Begin(TI_SERVER " Services", &g_show_services)) { ImGui::End(); return; }
draw_host_selector("Host##svc");
HostRuntime* r = find_runtime(g_selected_host_id);
if (!r) { ImGui::TextDisabled("no host"); ImGui::End(); return; }
if (!r->services_loaded) load_services(*r);
ImGui::SameLine();
if (ImGui::Button("Refresh##svc")) load_services(*r);
ImGui::SameLine();
ImGui::TextDisabled("%zu services", r->services.size());
if (r->services.empty()) {
ImGui::TextDisabled("(vacio — systemctl/sc no disponibles)");
} else if (ImGui::BeginTable("svc", 4,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("State");
ImGui::TableSetupColumn("Runtime");
ImGui::TableSetupColumn("Description");
ImGui::TableHeadersRow();
for (const auto& s : r->services) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(s.name.c_str());
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(s.state.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(s.runtime.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(s.description.c_str());
}
ImGui::EndTable();
}
ImGui::End();
}
// =========================================================================
// Render + Self-test + Entry
// =========================================================================
static void render() {
poll_all_due();
if (g_show_overview) draw_overview();
if (g_show_processes) draw_processes();
if (g_show_network) draw_network();
if (g_show_devices) draw_devices();
if (g_show_services) draw_services();
}
static int self_test() {
std::string hosts_path = fn::local_path("hosts.db");
std::string samples_path = fn::local_path("process_samples.db");
pex::HostsDb h; if (!h.open(hosts_path)) { std::fprintf(stderr, "self-test: hosts.db open failed\n"); return 1; }
pex::SamplesDb s; if (!s.open(samples_path)) { std::fprintf(stderr, "self-test: samples.db open failed\n"); return 1; }
auto stats = pex::fetch_stats_local();
if (stats.unix_ts == 0) { std::fprintf(stderr, "self-test: stats_local no ts\n"); return 1; }
std::fprintf(stdout, "self-test OK\n");
return 0;
}
int main(int argc, char** argv) {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--self-test") == 0) return self_test();
}
static fn_ui::PanelToggle panels[] = {
{ "Overview", TI_GAUGE, &g_show_overview },
{ "Processes", TI_LIST, &g_show_processes },
{ "Network", TI_NETWORK, &g_show_network },
{ "Devices", TI_DEVICES_PC, &g_show_devices },
{ "Services", TI_SERVER, &g_show_services },
};
fn::AppConfig cfg;
cfg.title = "process_explorer — cross-PC process & resource monitor";
cfg.about = { "process_explorer", "0.4.0",
"Process explorer cross-PC: CPU/RAM/disk/net/GPU + procesos en tiempo real" };
cfg.log = { "process_explorer.log", 1 };
cfg.panels = panels;
cfg.panel_count = sizeof(panels) / sizeof(panels[0]);
g_hosts_db.open(fn::local_path("hosts.db"));
g_samples_db.open(fn::local_path("process_samples.db"));
seed_local_runtime();
#ifdef _WIN32
seed_wsl_runtime();
g_wsl_worker = std::thread(wsl_worker_thread_fn);
#endif
load_hosts_from_db();
int rc = fn::run_app(cfg, render);
#ifdef _WIN32
g_wsl_stop.store(true, std::memory_order_release);
if (g_wsl_worker.joinable()) g_wsl_worker.join();
#endif
g_hosts_db.close();
g_samples_db.close();
return rc;
}