#include #include #include #include #include #include #include #include #include #include #include #include #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 processes; std::vector devices; std::vector services; std::vector 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 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 g_wsl_stop{false}; static std::mutex g_wsl_mu; static pex::StatsSnapshot g_wsl_stats_buf; static std::vector g_wsl_procs_buf; static std::atomic 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 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 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; }