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,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()
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
+102
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user