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:
2026-05-19 00:31:32 +02:00
commit 477bcd00f0
13 changed files with 1821 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
add_imgui_app(process_explorer
main.cpp
http_client.cpp
hosts_db.cpp
samples_db.cpp
agent_protocol.cpp
# Funciones del registry usadas:
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
)
target_include_directories(process_explorer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# SQLite for hosts.db + samples.db. fn_framework already links sqlite3 transitively,
# but declaring it here is explicit.
find_package(SQLite3 QUIET)
if(SQLite3_FOUND)
target_link_libraries(process_explorer PRIVATE SQLite::SQLite3)
endif()
# fn_table_viz: provides data_table::render(), viz_render, TQL engine, Lua, LLM.
# Guard keeps the app compilable in builds where vendor/lua is absent.
if(TARGET fn_table_viz)
target_link_libraries(process_explorer PRIVATE fn_table_viz)
endif()
if(WIN32)
target_link_libraries(process_explorer PRIVATE ws2_32 psapi)
set_target_properties(process_explorer PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
+600
View File
@@ -0,0 +1,600 @@
#include "agent_protocol.h"
#include "http_client.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fstream>
#include <sstream>
#include <string>
#ifdef __linux__
#include <dirent.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#endif
namespace pex {
// =========================================================================
// HTTP-agent placeholders (issue 0111)
// =========================================================================
std::vector<ProcInfo> fetch_processes(const std::string& base_url, const std::string& token) {
if (base_url.empty()) return {};
auto resp = pex_http::get(base_url + "/api/processes", token);
if (resp.status != 200) return {};
return {};
}
StatsSnapshot fetch_stats(const std::string& base_url, const std::string& token) {
StatsSnapshot s;
s.unix_ts = (int64_t)std::time(nullptr);
if (base_url.empty()) return s;
auto resp = pex_http::get(base_url + "/api/stats", token);
if (resp.status != 200) return s;
return s;
}
std::vector<DeviceInfo> fetch_devices(const std::string& base_url, const std::string& token) {
if (base_url.empty()) return {};
auto resp = pex_http::get(base_url + "/api/devices", token);
if (resp.status != 200) return {};
return {};
}
std::vector<ServiceInfo> fetch_services(const std::string& base_url, const std::string& token) {
if (base_url.empty()) return {};
auto resp = pex_http::get(base_url + "/api/services", token);
if (resp.status != 200) return {};
return {};
}
std::vector<NetConn> fetch_netconns(const std::string& base_url, const std::string& token) {
if (base_url.empty()) return {};
auto resp = pex_http::get(base_url + "/api/netconns", token);
if (resp.status != 200) return {};
return {};
}
// =========================================================================
// LINUX local
// =========================================================================
#ifdef __linux__
static std::string read_file(const char* path) {
std::ifstream f(path);
if (!f) return {};
std::stringstream ss; ss << f.rdbuf();
return ss.str();
}
// CPU%: diff de /proc/stat entre dos llamadas.
struct CpuJiffies { unsigned long long user, nice, system, idle, iowait, irq, softirq, steal; };
static bool parse_proc_stat(CpuJiffies& out) {
std::ifstream f("/proc/stat");
if (!f) return false;
std::string cpu;
f >> cpu;
if (cpu != "cpu") return false;
f >> out.user >> out.nice >> out.system >> out.idle
>> out.iowait >> out.irq >> out.softirq >> out.steal;
return true;
}
static float diff_cpu_pct(CpuJiffies& prev, bool& have_prev) {
CpuJiffies cur{};
if (!parse_proc_stat(cur)) return 0.0f;
if (!have_prev) { prev = cur; have_prev = true; return 0.0f; }
auto total = [](const CpuJiffies& j) {
return j.user + j.nice + j.system + j.idle + j.iowait + j.irq + j.softirq + j.steal;
};
unsigned long long t0 = total(prev), t1 = total(cur);
unsigned long long i0 = prev.idle + prev.iowait, i1 = cur.idle + cur.iowait;
unsigned long long dt = t1 - t0, didle = i1 - i0;
prev = cur;
if (dt == 0) return 0.0f;
return (float)((double)(dt - didle) / (double)dt * 100.0);
}
std::vector<ProcInfo> fetch_processes_local() {
std::vector<ProcInfo> out;
DIR* d = opendir("/proc");
if (!d) return out;
long page_kb = sysconf(_SC_PAGESIZE) / 1024;
struct dirent* e;
while ((e = readdir(d)) != nullptr) {
if (e->d_type != DT_DIR) continue;
bool numeric = true;
for (const char* p = e->d_name; *p; ++p) if (*p < '0' || *p > '9') { numeric = false; break; }
if (!numeric) continue;
ProcInfo info;
info.pid = std::atoi(e->d_name);
std::string stat = read_file((std::string("/proc/") + e->d_name + "/stat").c_str());
if (!stat.empty()) {
auto open_paren = stat.find('(');
auto close_paren = stat.rfind(')');
if (open_paren != std::string::npos && close_paren != std::string::npos) {
info.name = stat.substr(open_paren + 1, close_paren - open_paren - 1);
std::istringstream iss(stat.substr(close_paren + 2));
char state;
int ppid;
iss >> state >> ppid;
info.state.assign(1, state);
info.ppid = ppid;
}
}
std::string statm = read_file((std::string("/proc/") + e->d_name + "/statm").c_str());
if (!statm.empty()) {
long size_pages, resident_pages;
if (std::sscanf(statm.c_str(), "%ld %ld", &size_pages, &resident_pages) == 2) {
info.ram_mb = (float)(resident_pages * page_kb) / 1024.0f;
}
}
std::string cmdline = read_file((std::string("/proc/") + e->d_name + "/cmdline").c_str());
for (auto& c : cmdline) if (c == '\0') c = ' ';
info.cmdline = std::move(cmdline);
out.push_back(std::move(info));
}
closedir(d);
return out;
}
StatsSnapshot fetch_stats_local() {
StatsSnapshot s;
s.unix_ts = (int64_t)std::time(nullptr);
struct sysinfo si{};
if (sysinfo(&si) == 0) {
s.ram_total_mb = (float)(si.totalram * si.mem_unit) / (1024.0f * 1024.0f);
s.ram_used_mb = (float)((si.totalram - si.freeram) * si.mem_unit) / (1024.0f * 1024.0f);
}
static CpuJiffies prev{}; static bool have_prev = false;
s.cpu_pct = diff_cpu_pct(prev, have_prev);
return s;
}
// En Linux el "wsl" no aplica: alias del local.
std::vector<ProcInfo> fetch_processes_wsl() { return fetch_processes_local(); }
StatsSnapshot fetch_stats_wsl() { return fetch_stats_local(); }
#endif // __linux__
// =========================================================================
// WINDOWS local (psapi + GetSystemTimes)
// =========================================================================
#ifdef _WIN32
static uint64_t ft_to_u64(const FILETIME& ft) {
ULARGE_INTEGER u{}; u.LowPart = ft.dwLowDateTime; u.HighPart = ft.dwHighDateTime;
return u.QuadPart;
}
StatsSnapshot fetch_stats_local() {
StatsSnapshot s;
s.unix_ts = (int64_t)std::time(nullptr);
MEMORYSTATUSEX ms{}; ms.dwLength = sizeof(ms);
if (GlobalMemoryStatusEx(&ms)) {
s.ram_total_mb = (float)((double)ms.ullTotalPhys / (1024.0 * 1024.0));
s.ram_used_mb = (float)((double)(ms.ullTotalPhys - ms.ullAvailPhys) / (1024.0 * 1024.0));
}
static FILETIME prev_idle{}, prev_kernel{}, prev_user{};
static bool have_prev = false;
FILETIME idle, kernel, user;
if (GetSystemTimes(&idle, &kernel, &user)) {
if (have_prev) {
uint64_t didle = ft_to_u64(idle) - ft_to_u64(prev_idle);
uint64_t dkern = ft_to_u64(kernel) - ft_to_u64(prev_kernel);
uint64_t duser = ft_to_u64(user) - ft_to_u64(prev_user);
uint64_t total = dkern + duser; // kernel-time on Windows includes idle
if (total > 0) {
s.cpu_pct = (float)((double)(total - didle) / (double)total * 100.0);
}
}
prev_idle = idle; prev_kernel = kernel; prev_user = user;
have_prev = true;
}
return s;
}
std::vector<ProcInfo> fetch_processes_local() {
std::vector<ProcInfo> out;
DWORD pids[4096], cb_needed = 0;
if (!EnumProcesses(pids, sizeof(pids), &cb_needed)) return out;
int n = (int)(cb_needed / sizeof(DWORD));
for (int i = 0; i < n; ++i) {
if (pids[i] == 0) continue;
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
FALSE, pids[i]);
if (!h) continue;
ProcInfo p;
p.pid = (int32_t)pids[i];
char path[MAX_PATH] = {};
if (GetModuleFileNameExA(h, NULL, path, sizeof(path))) {
const char* base = std::strrchr(path, '\\');
p.name = base ? (base + 1) : path;
p.cmdline = path;
} else {
p.name = "?";
}
PROCESS_MEMORY_COUNTERS pmc{};
if (GetProcessMemoryInfo(h, &pmc, sizeof(pmc))) {
p.ram_mb = (float)((double)pmc.WorkingSetSize / (1024.0 * 1024.0));
}
p.state = "R";
out.push_back(std::move(p));
CloseHandle(h);
}
return out;
}
// -------------------------------------------------------------------------
// WSL trampoline (binario Windows ejecuta `wsl.exe -e ...`)
// -------------------------------------------------------------------------
// Ejecuta un comando y captura stdout SIN abrir ventana de consola.
// _popen() spawns cmd.exe con WshShell -> flash de consola visible en cada
// llamada. CreateProcessA + CREATE_NO_WINDOW + STARTF_USESHOWWINDOW(SW_HIDE)
// la oculta. Pipe anonimo para leer stdout sincronamente.
static std::string run_capture(const char* cmd) {
std::string out;
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE rd = NULL, wr = NULL;
if (!CreatePipe(&rd, &wr, &sa, 0)) return out;
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOA si{};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
si.hStdOutput = wr;
si.hStdError = wr;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION pi{};
// CreateProcessA muta lpCommandLine — copiar a buffer escribible.
std::string mutable_cmd = cmd;
if (!CreateProcessA(NULL, mutable_cmd.data(), NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
CloseHandle(rd); CloseHandle(wr);
return out;
}
CloseHandle(wr); // padre cierra su extremo write para que ReadFile vea EOF
char buf[4096];
DWORD n = 0;
while (ReadFile(rd, buf, sizeof(buf), &n, NULL) && n > 0) {
out.append(buf, n);
}
CloseHandle(rd);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return out;
}
struct WslCpuJiffies { unsigned long long user, nice, system, idle, iowait, irq, softirq, steal; };
static bool parse_wsl_proc_stat(WslCpuJiffies& out) {
std::string txt = run_capture("wsl.exe -e sh -c \"head -n1 /proc/stat\"");
if (txt.empty()) return false;
std::istringstream iss(txt);
std::string cpu; iss >> cpu;
if (cpu != "cpu") return false;
iss >> out.user >> out.nice >> out.system >> out.idle
>> out.iowait >> out.irq >> out.softirq >> out.steal;
return true;
}
StatsSnapshot fetch_stats_wsl() {
StatsSnapshot s;
s.unix_ts = (int64_t)std::time(nullptr);
// RAM via /proc/meminfo
std::string mem = run_capture("wsl.exe -e sh -c \"cat /proc/meminfo\"");
if (mem.empty()) return s;
long mem_total_kb = 0, mem_available_kb = 0;
std::istringstream ms(mem);
std::string line;
while (std::getline(ms, line)) {
long v;
if (std::sscanf(line.c_str(), "MemTotal: %ld kB", &v) == 1) mem_total_kb = v;
else if (std::sscanf(line.c_str(), "MemAvailable: %ld kB", &v) == 1) mem_available_kb = v;
}
if (mem_total_kb > 0) {
s.ram_total_mb = (float)mem_total_kb / 1024.0f;
s.ram_used_mb = (float)(mem_total_kb - mem_available_kb) / 1024.0f;
}
// CPU% diff
static WslCpuJiffies prev{}; static bool have_prev = false;
WslCpuJiffies cur{};
if (parse_wsl_proc_stat(cur)) {
if (have_prev) {
auto total = [](const WslCpuJiffies& j) {
return j.user + j.nice + j.system + j.idle + j.iowait + j.irq + j.softirq + j.steal;
};
unsigned long long t0 = total(prev), t1 = total(cur);
unsigned long long i0 = prev.idle + prev.iowait, i1 = cur.idle + cur.iowait;
unsigned long long dt = t1 - t0, didle = i1 - i0;
if (dt > 0) s.cpu_pct = (float)((double)(dt - didle) / (double)dt * 100.0);
}
prev = cur;
have_prev = true;
}
return s;
}
std::vector<ProcInfo> fetch_processes_wsl() {
std::vector<ProcInfo> out;
// ps -eo pid,stat,rss,comm — sin header.
std::string txt = run_capture("wsl.exe -e ps -eo pid,stat,rss,comm --no-headers");
if (txt.empty()) return out;
std::istringstream iss(txt);
std::string line;
while (std::getline(iss, line)) {
int pid = 0, rss_kb = 0;
char stat[8] = {}, comm[256] = {};
if (std::sscanf(line.c_str(), "%d %7s %d %255s", &pid, stat, &rss_kb, comm) == 4) {
ProcInfo p;
p.pid = pid;
p.state = stat;
p.ram_mb = (float)rss_kb / 1024.0f;
p.name = comm;
out.push_back(std::move(p));
}
}
return out;
}
#endif // _WIN32
// =========================================================================
// Parsers compartidos para Network/Devices/Services (Linux + WSL output)
// =========================================================================
static std::vector<NetConn> parse_ss_output(const std::string& text) {
std::vector<NetConn> out;
std::istringstream iss(text);
std::string line;
while (std::getline(iss, line)) {
if (line.empty()) continue;
std::istringstream ls(line);
NetConn c;
std::string send_q, recv_q;
ls >> c.proto >> c.state >> send_q >> recv_q >> c.local >> c.remote;
std::string rest; std::getline(ls, rest);
auto pid_pos = rest.find("pid=");
if (pid_pos != std::string::npos) c.pid = std::atoi(rest.c_str() + pid_pos + 4);
if (!c.proto.empty()) out.push_back(c);
}
return out;
}
static std::vector<DeviceInfo> parse_lsblk_output(const std::string& text) {
std::vector<DeviceInfo> out;
std::istringstream iss(text);
std::string line;
while (std::getline(iss, line)) {
if (line.empty()) continue;
std::istringstream ls(line);
std::string name, size_bytes, mount, fstype;
ls >> name >> size_bytes >> mount >> fstype;
if (name.empty()) continue;
DeviceInfo d;
d.kind = "disk";
d.name = name;
double sz = (double)std::atoll(size_bytes.c_str()) / (1024.0*1024.0*1024.0);
char buf[128];
std::snprintf(buf, sizeof(buf), "%.1f GB %s %s",
sz,
fstype.empty() ? "" : fstype.c_str(),
mount.empty() ? "" : mount.c_str());
d.detail = buf;
d.usage_pct = -1.0f;
out.push_back(d);
}
return out;
}
static std::vector<ServiceInfo> parse_systemctl_output(const std::string& text) {
std::vector<ServiceInfo> out;
std::istringstream iss(text);
std::string line;
while (std::getline(iss, line)) {
if (line.empty()) continue;
std::istringstream ls(line);
std::string unit, load, active, sub;
ls >> unit >> load >> active >> sub;
if (unit.empty()) continue;
ServiceInfo s;
s.name = unit;
s.state = active + "/" + sub;
s.runtime = "systemd";
std::string desc; std::getline(ls, desc);
auto p0 = desc.find_first_not_of(' ');
if (p0 != std::string::npos) s.description = desc.substr(p0);
out.push_back(s);
}
return out;
}
// =========================================================================
// LINUX impls (Network/Devices/Services local)
// =========================================================================
#ifdef __linux__
static std::string popen_capture_linux(const char* cmd) {
std::string out;
FILE* fp = popen(cmd, "r");
if (!fp) return out;
char buf[4096];
while (fgets(buf, sizeof(buf), fp)) out += buf;
pclose(fp);
return out;
}
std::vector<NetConn> fetch_netconns_local() {
return parse_ss_output(popen_capture_linux("ss -tunaH -p 2>/dev/null"));
}
std::vector<DeviceInfo> fetch_devices_local() {
return parse_lsblk_output(popen_capture_linux(
"lsblk -bno NAME,SIZE,MOUNTPOINT,FSTYPE 2>/dev/null"));
}
std::vector<ServiceInfo> fetch_services_local() {
return parse_systemctl_output(popen_capture_linux(
"systemctl list-units --type=service --all --no-legend --plain --no-pager 2>/dev/null"));
}
// En Linux la "version WSL" = local (no aplica).
std::vector<NetConn> fetch_netconns_wsl() { return fetch_netconns_local(); }
std::vector<DeviceInfo> fetch_devices_wsl() { return fetch_devices_local(); }
std::vector<ServiceInfo> fetch_services_wsl() { return fetch_services_local(); }
#endif // __linux__
// =========================================================================
// WINDOWS impls (Network/Devices/Services local + WSL trampoline)
// =========================================================================
#ifdef _WIN32
static std::vector<NetConn> parse_netstat_windows(const std::string& text) {
std::vector<NetConn> out;
std::istringstream iss(text);
std::string line;
while (std::getline(iss, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
size_t p0 = line.find_first_not_of(' ');
if (p0 == std::string::npos) continue;
std::istringstream ls(line.substr(p0));
NetConn c;
ls >> c.proto;
if (c.proto != "TCP" && c.proto != "UDP") continue;
ls >> c.local >> c.remote;
std::string pid_str;
if (c.proto == "TCP") { ls >> c.state >> pid_str; }
else { ls >> pid_str; c.state = ""; }
c.pid = std::atoi(pid_str.c_str());
out.push_back(c);
}
return out;
}
std::vector<NetConn> fetch_netconns_local() {
return parse_netstat_windows(run_capture("netstat -ano"));
}
std::vector<DeviceInfo> fetch_devices_local() {
std::vector<DeviceInfo> out;
char drives[256] = {};
DWORD n = GetLogicalDriveStringsA(sizeof(drives), drives);
for (char* p = drives; *p && (p - drives) < (int)n; ) {
DeviceInfo d;
d.kind = "disk";
d.name = p;
ULARGE_INTEGER avail{}, total{}, freeb{};
if (GetDiskFreeSpaceExA(p, &avail, &total, &freeb)) {
char detail[128];
double total_gb = (double)total.QuadPart / (1024.0*1024.0*1024.0);
double free_gb = (double)freeb.QuadPart / (1024.0*1024.0*1024.0);
std::snprintf(detail, sizeof(detail),
"%.1f GB free / %.1f GB total", free_gb, total_gb);
d.detail = detail;
d.usage_pct = total.QuadPart
? (float)(100.0 * (double)(total.QuadPart - freeb.QuadPart) / (double)total.QuadPart)
: -1.0f;
} else {
d.detail = "(unavailable)";
d.usage_pct = -1.0f;
}
out.push_back(d);
p += std::strlen(p) + 1;
}
return out;
}
static std::vector<ServiceInfo> parse_sc_query_output(const std::string& text) {
std::vector<ServiceInfo> out;
std::istringstream iss(text);
std::string line;
ServiceInfo cur;
bool active = false;
auto trim = [](std::string s) {
size_t a = s.find_first_not_of(' ');
if (a == std::string::npos) return std::string();
size_t b = s.find_last_not_of(" \r\n\t");
return s.substr(a, b - a + 1);
};
while (std::getline(iss, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
size_t p0 = line.find_first_not_of(' ');
if (p0 == std::string::npos) continue;
std::string l = line.substr(p0);
if (l.rfind("SERVICE_NAME:", 0) == 0) {
if (active) out.push_back(cur);
cur = {};
cur.runtime = "sc";
cur.name = trim(l.substr(13));
active = true;
} else if (l.rfind("DISPLAY_NAME:", 0) == 0) {
cur.description = trim(l.substr(13));
} else if (l.rfind("STATE", 0) == 0) {
auto colon = l.find(':');
if (colon != std::string::npos) {
std::istringstream ss2(l.substr(colon + 1));
int code; std::string name;
ss2 >> code >> name;
cur.state = name;
}
}
}
if (active) out.push_back(cur);
return out;
}
std::vector<ServiceInfo> fetch_services_local() {
return parse_sc_query_output(run_capture("sc query state= all type= service"));
}
// WSL trampolines (binario Windows ejecuta wsl.exe). run_capture es hidden.
std::vector<NetConn> fetch_netconns_wsl() {
return parse_ss_output(run_capture(
"wsl.exe -e sh -c \"ss -tunaH -p 2>/dev/null\""));
}
std::vector<DeviceInfo> fetch_devices_wsl() {
return parse_lsblk_output(run_capture(
"wsl.exe -e sh -c \"lsblk -bno NAME,SIZE,MOUNTPOINT,FSTYPE 2>/dev/null\""));
}
std::vector<ServiceInfo> fetch_services_wsl() {
return parse_systemctl_output(run_capture(
"wsl.exe -e sh -c \"systemctl list-units --type=service --all --no-legend --plain --no-pager 2>/dev/null\""));
}
#endif // _WIN32
} // namespace pex
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace pex {
struct ProcInfo {
int32_t pid = 0;
int32_t ppid = 0;
std::string name;
std::string user;
float cpu_pct = 0.0f;
float ram_mb = 0.0f;
float disk_read_kb_s = 0.0f;
float disk_write_kb_s = 0.0f;
int32_t threads = 0;
std::string state; // R/S/D/Z/T (Linux) o running/suspended (Win)
std::string cmdline;
};
struct StatsSnapshot {
int64_t unix_ts = 0;
float cpu_pct = 0.0f;
float ram_used_mb = 0.0f;
float ram_total_mb = 0.0f;
float disk_read_mb_s = 0.0f;
float disk_write_mb_s = 0.0f;
float net_rx_mb_s = 0.0f;
float net_tx_mb_s = 0.0f;
float gpu_pct = 0.0f;
};
struct DeviceInfo {
std::string kind; // disk | gpu | usb | sensor
std::string name;
std::string detail;
float usage_pct = 0.0f; // -1 si no aplica
};
struct ServiceInfo {
std::string name;
std::string state; // running | stopped | failed
std::string description;
std::string runtime; // systemd | sc | docker | other
};
struct NetConn {
int32_t pid = 0;
std::string proto; // tcp | udp
std::string local; // ip:port
std::string remote;
std::string state; // ESTABLISHED, LISTEN, ...
};
// Stubs: parsean JSON del agente o devuelven vacios si no esta disponible.
// Implementacion real cuando el agente Go exista (issue 0111).
std::vector<ProcInfo> fetch_processes(const std::string& base_url, const std::string& token);
StatsSnapshot fetch_stats (const std::string& base_url, const std::string& token);
std::vector<DeviceInfo> fetch_devices (const std::string& base_url, const std::string& token);
std::vector<ServiceInfo> fetch_services (const std::string& base_url, const std::string& token);
std::vector<NetConn> fetch_netconns (const std::string& base_url, const std::string& token);
// Local del host donde corre el binario.
// - Linux: lee /proc.
// - Windows: WinAPI (psapi, GetSystemTimes, GlobalMemoryStatusEx).
std::vector<ProcInfo> fetch_processes_local();
StatsSnapshot fetch_stats_local();
// WSL desde un binario Windows — trampoline via `wsl.exe -e ...`.
// En Linux son aliases a fetch_*_local.
std::vector<ProcInfo> fetch_processes_wsl();
StatsSnapshot fetch_stats_wsl();
// Network/Devices/Services para el host LOCAL (binario corriendo aqui).
std::vector<NetConn> fetch_netconns_local();
std::vector<DeviceInfo> fetch_devices_local();
std::vector<ServiceInfo> fetch_services_local();
// Idem WSL — trampoline desde un binario Windows. En Linux son aliases local.
std::vector<NetConn> fetch_netconns_wsl();
std::vector<DeviceInfo> fetch_devices_wsl();
std::vector<ServiceInfo> fetch_services_wsl();
} // namespace pex
+95
View File
@@ -0,0 +1,95 @@
---
name: process_explorer
lang: cpp
domain: tools
version: 0.1.0
description: "Process explorer cross-PC: CPU/RAM/disk/net/GPU + procesos en tiempo real"
tags: [monitor, dashboard, cross-pc, imgui]
icon:
phosphor: cpu
accent: "#0d9488"
uses_functions:
- line_plot_cpp_viz
- kpi_card_cpp_viz
- data_table_cpp_viz
- viz_render_cpp_viz
- compute_stage_cpp_core
- compute_pipeline_cpp_core
- compute_column_stats_cpp_core
- auto_detect_type_cpp_core
- tql_emit_cpp_core
- tql_apply_cpp_core
- lua_engine_cpp_core
- join_tables_cpp_core
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "apps/process_explorer"
repo_url: "https://gitea.organic-machine.com/dataforge/process_explorer"
e2e_checks:
- id: build
cmd: "cmake --build cpp/build --target process_explorer -j"
timeout_s: 300
- id: self_test
cmd: "./cpp/build/apps/process_explorer/process_explorer --self-test"
timeout_s: 30
severity: warning
---
# process_explorer
Process explorer cross-PC: CPU/RAM/disk/net/GPU + procesos en tiempo real.
## Arquitectura
- **Explorer (esta app)**: ImGui C++. UI con 5 panels (Overview, Processes, Network, Devices, Services).
- **Agente HTTP por PC** (issue 0111): daemon Go ligero instalable en cada PC (Win+Linux) que expone:
- `GET /api/processes` — lista de procesos con CPU%/RAM/PID/usuario.
- `GET /api/stats` — snapshot global CPU/RAM/Disk/Net/GPU.
- `GET /api/devices` — discos, GPUs, USB, sensores.
- `GET /api/services` — systemctl (Linux) / sc query (Win).
- Auth: `Authorization: Bearer <token>` con token por PC.
- **Local Linux**: lectura directa de `/proc/*` cuando el host es local (sin red).
- **Remote**: SIEMPRE via HTTP agent + token.
## Panels
1. **Overview** (Ctrl+1) — KPIs globales con `kpi_card`: CPU%, RAM, Disk I/O, Net I/O, GPU. Sparklines historicos via `line_plot`.
2. **Processes** (Ctrl+2) — Tabla TQL (`data_table::render`) con todos los procesos. Filtros, sort, group-by.
3. **Network** (Ctrl+3) — Tabla de sockets/conexiones con PID, local, remote, estado.
4. **Devices** (Ctrl+4) — Enumeracion de discos, GPUs, USB, sensores.
5. **Services** (Ctrl+5) — systemctl (Linux) + `sc query` (Win). Diferente de `services_monitor` (que monitoriza apps DECLARADAS del registry); aqui se ven TODOS.
## Persistencia (local_files/)
- `hosts.db` — inventario PCs (url, token, os, last_seen).
- `operations.db` — entities=hosts, executions=samples (bucle reactivo).
- `process_samples.db` — time-series CPU/RAM/IO para graficar ventanas >1h.
## Build
```bash
cd cpp && cmake --build build --target process_explorer -j
```
## Run
```bash
./cpp/build/apps/process_explorer/process_explorer
```
## Notas de implementacion
- HTTP cliente: hoy stub local en `http_client.cpp`. Migrar a `fn_http::request` cuando cierre el issue 0110.
- Agente Go: issue 0111 (separado).
- Self-test: `--self-test` valida que `hosts.db` + `samples.db` se crean en `local_files/` y que el GL loader carga.
## Capability growth log
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
- `patch`: bugfix sin cambio observable.
- v0.1.0 (2026-05-18) — baseline.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

+102
View File
@@ -0,0 +1,102 @@
#include "hosts_db.h"
#include <sqlite3.h>
#include <cstring>
namespace pex {
static const char* k_schema = R"SQL(
CREATE TABLE IF NOT EXISTS hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
url TEXT NOT NULL,
token TEXT NOT NULL DEFAULT '',
os TEXT NOT NULL DEFAULT '',
last_seen_unix INTEGER NOT NULL DEFAULT 0,
is_local INTEGER NOT NULL DEFAULT 0
);
)SQL";
HostsDb::HostsDb() = default;
HostsDb::~HostsDb() { close(); }
bool HostsDb::open(const std::string& path) {
if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK) {
db_ = nullptr;
return false;
}
char* err = nullptr;
if (sqlite3_exec(db_, k_schema, nullptr, nullptr, &err) != SQLITE_OK) {
sqlite3_free(err);
close();
return false;
}
return true;
}
void HostsDb::close() {
if (db_) { sqlite3_close(db_); db_ = nullptr; }
}
std::vector<Host> HostsDb::list() {
std::vector<Host> out;
if (!db_) return out;
const char* sql = "SELECT id, name, url, token, os, last_seen_unix, is_local FROM hosts ORDER BY name";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return out;
while (sqlite3_step(st) == SQLITE_ROW) {
Host h;
h.id = sqlite3_column_int64(st, 0);
h.name = reinterpret_cast<const char*>(sqlite3_column_text(st, 1));
h.url = reinterpret_cast<const char*>(sqlite3_column_text(st, 2));
h.token = reinterpret_cast<const char*>(sqlite3_column_text(st, 3));
h.os = reinterpret_cast<const char*>(sqlite3_column_text(st, 4));
h.last_seen_unix = sqlite3_column_int64(st, 5);
h.is_local = sqlite3_column_int(st, 6) != 0;
out.push_back(std::move(h));
}
sqlite3_finalize(st);
return out;
}
int64_t HostsDb::upsert(const Host& h) {
if (!db_) return -1;
const char* sql =
"INSERT INTO hosts(name,url,token,os,last_seen_unix,is_local) VALUES(?,?,?,?,?,?) "
"ON CONFLICT(name) DO UPDATE SET url=excluded.url, token=excluded.token, "
"os=excluded.os, last_seen_unix=excluded.last_seen_unix, is_local=excluded.is_local";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return -1;
sqlite3_bind_text(st, 1, h.name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, h.url.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, h.token.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 4, h.os.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(st, 5, h.last_seen_unix);
sqlite3_bind_int(st, 6, h.is_local ? 1 : 0);
int rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE) return -1;
return sqlite3_last_insert_rowid(db_);
}
bool HostsDb::remove(int64_t id) {
if (!db_) return false;
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, "DELETE FROM hosts WHERE id=?", -1, &st, nullptr) != SQLITE_OK) return false;
sqlite3_bind_int64(st, 1, id);
bool ok = sqlite3_step(st) == SQLITE_DONE;
sqlite3_finalize(st);
return ok;
}
bool HostsDb::touch_last_seen(int64_t id, int64_t unix_ts) {
if (!db_) return false;
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, "UPDATE hosts SET last_seen_unix=? WHERE id=?", -1, &st, nullptr) != SQLITE_OK) return false;
sqlite3_bind_int64(st, 1, unix_ts);
sqlite3_bind_int64(st, 2, id);
bool ok = sqlite3_step(st) == SQLITE_DONE;
sqlite3_finalize(st);
return ok;
}
} // namespace pex
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
struct sqlite3;
namespace pex {
struct Host {
int64_t id = 0;
std::string name;
std::string url;
std::string token;
std::string os; // "linux" | "windows" | "wsl"
int64_t last_seen_unix = 0;
bool is_local = false;
};
class HostsDb {
public:
HostsDb();
~HostsDb();
// path debe ser absoluto (fn::local_path("hosts.db")).
bool open(const std::string& path);
void close();
std::vector<Host> list();
int64_t upsert(const Host& h); // devuelve id; -1 si falla
bool remove(int64_t id);
bool touch_last_seen(int64_t id, int64_t unix_ts);
private:
sqlite3* db_ = nullptr;
};
} // namespace pex
+44
View File
@@ -0,0 +1,44 @@
#include "http_client.h"
#include <cstdio>
#include <string>
namespace pex_http {
// Stub: usa curl via popen igual que llm_anthropic. Reemplazar por
// fn_http::request cuando el helper del registry exista (issue 0110).
Response request(const Request& req) {
Response resp;
if (req.url.empty()) {
resp.error = "empty url";
return resp;
}
std::string cmd = "curl -sS --max-time " + std::to_string(req.timeout_ms / 1000)
+ " -X " + (req.method.empty() ? "GET" : req.method);
if (!req.bearer_token.empty()) {
cmd += " -H \"Authorization: Bearer " + req.bearer_token + "\"";
}
for (const auto& kv : req.headers) {
cmd += " -H \"" + kv.first + ": " + kv.second + "\"";
}
if (!req.body.empty()) {
cmd += " --data-binary @-";
}
cmd += " -w \"\\n__STATUS__%{http_code}\" \"" + req.url + "\"";
FILE* fp = popen(cmd.c_str(), "r");
if (!fp) { resp.error = "popen failed"; return resp; }
char buf[4096];
while (fgets(buf, sizeof(buf), fp)) resp.body += buf;
pclose(fp);
auto pos = resp.body.rfind("\n__STATUS__");
if (pos != std::string::npos) {
try { resp.status = std::stoi(resp.body.substr(pos + 11)); } catch (...) {}
resp.body.resize(pos);
}
return resp;
}
} // namespace pex_http
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <vector>
#include <utility>
namespace pex_http {
struct Request {
std::string method;
std::string url;
std::vector<std::pair<std::string,std::string>> headers;
std::string body;
std::string bearer_token;
int timeout_ms = 5000;
};
struct Response {
int status = 0;
std::string body;
std::string error;
int64_t duration_ms = 0;
};
// Stub local — sera reemplazado por fn_http::request (issue 0110).
Response request(const Request& req);
inline Response get(const std::string& url, const std::string& bearer = "", int timeout_ms = 5000) {
Request r;
r.method = "GET";
r.url = url;
r.bearer_token = bearer;
r.timeout_ms = timeout_ms;
return request(r);
}
} // namespace pex_http
+655
View File
@@ -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;
}
BIN
View File
Binary file not shown.
+97
View File
@@ -0,0 +1,97 @@
#include "samples_db.h"
#include <sqlite3.h>
namespace pex {
static const char* k_schema = R"SQL(
CREATE TABLE IF NOT EXISTS samples (
host_id INTEGER NOT NULL,
unix_ts INTEGER NOT NULL,
cpu_pct REAL NOT NULL,
ram_used_mb REAL NOT NULL,
ram_total_mb REAL NOT NULL,
disk_read_mb_s REAL NOT NULL,
disk_write_mb_s REAL NOT NULL,
net_rx_mb_s REAL NOT NULL,
net_tx_mb_s REAL NOT NULL,
gpu_pct REAL NOT NULL,
PRIMARY KEY(host_id, unix_ts)
);
CREATE INDEX IF NOT EXISTS idx_samples_host_ts ON samples(host_id, unix_ts);
)SQL";
SamplesDb::SamplesDb() = default;
SamplesDb::~SamplesDb() { close(); }
bool SamplesDb::open(const std::string& path) {
if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK) {
db_ = nullptr;
return false;
}
char* err = nullptr;
if (sqlite3_exec(db_, k_schema, nullptr, nullptr, &err) != SQLITE_OK) {
sqlite3_free(err);
close();
return false;
}
return true;
}
void SamplesDb::close() {
if (db_) { sqlite3_close(db_); db_ = nullptr; }
}
bool SamplesDb::insert(const Sample& s) {
if (!db_) return false;
const char* sql =
"INSERT OR REPLACE INTO samples(host_id,unix_ts,cpu_pct,ram_used_mb,ram_total_mb,"
"disk_read_mb_s,disk_write_mb_s,net_rx_mb_s,net_tx_mb_s,gpu_pct) "
"VALUES(?,?,?,?,?,?,?,?,?,?)";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return false;
sqlite3_bind_int64(st, 1, s.host_id);
sqlite3_bind_int64(st, 2, s.unix_ts);
sqlite3_bind_double(st, 3, s.cpu_pct);
sqlite3_bind_double(st, 4, s.ram_used_mb);
sqlite3_bind_double(st, 5, s.ram_total_mb);
sqlite3_bind_double(st, 6, s.disk_read_mb_s);
sqlite3_bind_double(st, 7, s.disk_write_mb_s);
sqlite3_bind_double(st, 8, s.net_rx_mb_s);
sqlite3_bind_double(st, 9, s.net_tx_mb_s);
sqlite3_bind_double(st, 10, s.gpu_pct);
bool ok = sqlite3_step(st) == SQLITE_DONE;
sqlite3_finalize(st);
return ok;
}
std::vector<Sample> SamplesDb::query_range(int64_t host_id, int64_t unix_from, int64_t unix_to) {
std::vector<Sample> out;
if (!db_) return out;
const char* sql =
"SELECT host_id,unix_ts,cpu_pct,ram_used_mb,ram_total_mb,"
"disk_read_mb_s,disk_write_mb_s,net_rx_mb_s,net_tx_mb_s,gpu_pct "
"FROM samples WHERE host_id=? AND unix_ts BETWEEN ? AND ? ORDER BY unix_ts ASC";
sqlite3_stmt* st = nullptr;
if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return out;
sqlite3_bind_int64(st, 1, host_id);
sqlite3_bind_int64(st, 2, unix_from);
sqlite3_bind_int64(st, 3, unix_to);
while (sqlite3_step(st) == SQLITE_ROW) {
Sample s;
s.host_id = sqlite3_column_int64(st, 0);
s.unix_ts = sqlite3_column_int64(st, 1);
s.cpu_pct = (float)sqlite3_column_double(st, 2);
s.ram_used_mb = (float)sqlite3_column_double(st, 3);
s.ram_total_mb = (float)sqlite3_column_double(st, 4);
s.disk_read_mb_s = (float)sqlite3_column_double(st, 5);
s.disk_write_mb_s = (float)sqlite3_column_double(st, 6);
s.net_rx_mb_s = (float)sqlite3_column_double(st, 7);
s.net_tx_mb_s = (float)sqlite3_column_double(st, 8);
s.gpu_pct = (float)sqlite3_column_double(st, 9);
out.push_back(s);
}
sqlite3_finalize(st);
return out;
}
} // namespace pex
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
struct sqlite3;
namespace pex {
struct Sample {
int64_t host_id = 0;
int64_t unix_ts = 0;
float cpu_pct = 0.0f;
float ram_used_mb = 0.0f;
float ram_total_mb = 0.0f;
float disk_read_mb_s = 0.0f;
float disk_write_mb_s = 0.0f;
float net_rx_mb_s = 0.0f;
float net_tx_mb_s = 0.0f;
float gpu_pct = 0.0f;
};
class SamplesDb {
public:
SamplesDb();
~SamplesDb();
bool open(const std::string& path);
void close();
bool insert(const Sample& s);
// Devuelve muestras del host en ventana [unix_from, unix_to], ordenadas asc.
std::vector<Sample> query_range(int64_t host_id, int64_t unix_from, int64_t unix_to);
private:
sqlite3* db_ = nullptr;
};
} // namespace pex