#include "agent_protocol.h" #include "http_client.h" #include #include #include #include #include #include #include #ifdef __linux__ #include #include #include #endif #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #include #endif namespace pex { // ========================================================================= // HTTP-agent placeholders (issue 0111) // ========================================================================= std::vector 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 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 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 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 fetch_processes_local() { std::vector 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 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 fetch_processes_local() { std::vector 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 fetch_processes_wsl() { std::vector 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 parse_ss_output(const std::string& text) { std::vector 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 parse_lsblk_output(const std::string& text) { std::vector 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 parse_systemctl_output(const std::string& text) { std::vector 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 fetch_netconns_local() { return parse_ss_output(popen_capture_linux("ss -tunaH -p 2>/dev/null")); } std::vector fetch_devices_local() { return parse_lsblk_output(popen_capture_linux( "lsblk -bno NAME,SIZE,MOUNTPOINT,FSTYPE 2>/dev/null")); } std::vector 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 fetch_netconns_wsl() { return fetch_netconns_local(); } std::vector fetch_devices_wsl() { return fetch_devices_local(); } std::vector fetch_services_wsl() { return fetch_services_local(); } #endif // __linux__ // ========================================================================= // WINDOWS impls (Network/Devices/Services local + WSL trampoline) // ========================================================================= #ifdef _WIN32 static std::vector parse_netstat_windows(const std::string& text) { std::vector 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 fetch_netconns_local() { return parse_netstat_windows(run_capture("netstat -ano")); } std::vector fetch_devices_local() { std::vector 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 parse_sc_query_output(const std::string& text) { std::vector 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 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 fetch_netconns_wsl() { return parse_ss_output(run_capture( "wsl.exe -e sh -c \"ss -tunaH -p 2>/dev/null\"")); } std::vector 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 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