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>
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user