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
+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