feat: melhorar inventário e gestão de máquinas
This commit is contained in:
parent
b1d334045d
commit
3f0702d80b
5 changed files with 584 additions and 59 deletions
|
|
@ -6,8 +6,8 @@ use once_cell::sync::Lazy;
|
|||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use sysinfo::{Networks, System};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{DiskExt, Networks, System, SystemExt};
|
||||
use tauri::async_runtime::{self, JoinHandle};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
|
|
@ -166,27 +166,90 @@ fn collect_network_addrs() -> Vec<serde_json::Value> {
|
|||
entries
|
||||
}
|
||||
|
||||
fn collect_disks(_system: &System) -> Vec<serde_json::Value> {
|
||||
// API de discos mudou no sysinfo 0.31: usamos Disks diretamente
|
||||
fn collect_disks(system: &System) -> Vec<serde_json::Value> {
|
||||
let mut out = Vec::new();
|
||||
let disks = sysinfo::Disks::new_with_refreshed_list();
|
||||
for d in disks.list() {
|
||||
let name = d.name().to_string_lossy().to_string();
|
||||
let mount = d.mount_point().to_string_lossy().to_string();
|
||||
let fs = d.file_system().to_string_lossy().to_string();
|
||||
let total = d.total_space();
|
||||
let avail = d.available_space();
|
||||
for disk in system.disks() {
|
||||
let name = disk.name().to_string_lossy().to_string();
|
||||
let mount = disk.mount_point().to_string_lossy().to_string();
|
||||
let fs = String::from_utf8_lossy(disk.file_system()).to_string();
|
||||
let total = disk.total_space();
|
||||
let avail = disk.available_space();
|
||||
out.push(json!({
|
||||
"name": name,
|
||||
"name": if name.is_empty() { mount.clone() } else { name },
|
||||
"mountPoint": mount,
|
||||
"fs": fs,
|
||||
"totalBytes": total,
|
||||
"availableBytes": avail,
|
||||
}));
|
||||
}
|
||||
|
||||
if out.is_empty() {
|
||||
let disks = sysinfo::Disks::new_with_refreshed_list();
|
||||
for d in disks.list() {
|
||||
let name = d.name().to_string_lossy().to_string();
|
||||
let mount = d.mount_point().to_string_lossy().to_string();
|
||||
let fs = String::from_utf8_lossy(d.file_system()).to_string();
|
||||
let total = d.total_space();
|
||||
let avail = d.available_space();
|
||||
out.push(json!({
|
||||
"name": if name.is_empty() { mount.clone() } else { name },
|
||||
"mountPoint": mount,
|
||||
"fs": fs,
|
||||
"totalBytes": total,
|
||||
"availableBytes": avail,
|
||||
}));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_u64(value: &serde_json::Value) -> Option<u64> {
|
||||
if let Some(num) = value.as_u64() {
|
||||
return Some(num);
|
||||
}
|
||||
if let Some(num) = value.as_f64() {
|
||||
if num.is_finite() && num >= 0.0 {
|
||||
return Some(num as u64);
|
||||
}
|
||||
}
|
||||
if let Some(text) = value.as_str() {
|
||||
if let Ok(parsed) = text.trim().parse::<u64>() {
|
||||
return Some(parsed);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn push_gpu(
|
||||
list: &mut Vec<serde_json::Value>,
|
||||
name: Option<&str>,
|
||||
memory: Option<u64>,
|
||||
vendor: Option<&str>,
|
||||
driver: Option<&str>,
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
if name.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert("name".into(), json!(name.trim()));
|
||||
if let Some(memory) = memory {
|
||||
obj.insert("memoryBytes".into(), json!(memory));
|
||||
}
|
||||
if let Some(vendor) = vendor {
|
||||
if !vendor.trim().is_empty() {
|
||||
obj.insert("vendor".into(), json!(vendor.trim()));
|
||||
}
|
||||
}
|
||||
if let Some(driver) = driver {
|
||||
if !driver.trim().is_empty() {
|
||||
obj.insert("driver".into(), json!(driver.trim()));
|
||||
}
|
||||
}
|
||||
list.push(serde_json::Value::Object(obj));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_inventory_metadata(system: &System) -> serde_json::Value {
|
||||
let cpu_brand = system
|
||||
.cpus()
|
||||
|
|
@ -204,6 +267,26 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
|
|||
"disks": disks,
|
||||
});
|
||||
|
||||
if let Some(obj) = inventory.as_object_mut() {
|
||||
let mut hardware = serde_json::Map::new();
|
||||
if let Some(brand) = cpu_brand.clone() {
|
||||
if !brand.trim().is_empty() {
|
||||
hardware.insert("cpuType".into(), json!(brand.trim()));
|
||||
}
|
||||
}
|
||||
if let Some(physical) = system.physical_core_count() {
|
||||
hardware.insert("physicalCores".into(), json!(physical));
|
||||
}
|
||||
hardware.insert("logicalCores".into(), json!(system.cpus().len()));
|
||||
if mem_total_bytes > 0 {
|
||||
hardware.insert("memoryBytes".into(), json!(mem_total_bytes));
|
||||
hardware.insert("memory".into(), json!(mem_total_bytes));
|
||||
}
|
||||
if !hardware.is_empty() {
|
||||
obj.insert("hardware".into(), serde_json::Value::Object(hardware));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Softwares instalados (dpkg ou rpm)
|
||||
|
|
@ -253,10 +336,19 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
|
|||
if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
|
||||
if let Some(ws) = win.get("software").and_then(|v| v.as_array()) {
|
||||
for item in ws {
|
||||
let name = item.get("DisplayName").or_else(|| item.get("name")).cloned().unwrap_or(json!(null));
|
||||
let version = item.get("DisplayVersion").or_else(|| item.get("version")).cloned().unwrap_or(json!(null));
|
||||
let name = item
|
||||
.get("DisplayName")
|
||||
.or_else(|| item.get("name"))
|
||||
.cloned()
|
||||
.unwrap_or(json!(null));
|
||||
let version = item
|
||||
.get("DisplayVersion")
|
||||
.or_else(|| item.get("version"))
|
||||
.cloned()
|
||||
.unwrap_or(json!(null));
|
||||
let publisher = item.get("Publisher").cloned().unwrap_or(json!(null));
|
||||
software.push(json!({ "name": name, "version": version, "source": publisher }));
|
||||
software
|
||||
.push(json!({ "name": name, "version": version, "source": publisher }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -285,7 +377,9 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
|
|||
let name = s.get("Name").cloned().unwrap_or(json!(null));
|
||||
let status = s.get("Status").cloned().unwrap_or(json!(null));
|
||||
let display = s.get("DisplayName").cloned().unwrap_or(json!(null));
|
||||
services.push(json!({ "name": name, "status": status, "displayName": display }));
|
||||
services.push(
|
||||
json!({ "name": name, "status": status, "displayName": display }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -293,6 +387,106 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
|
|||
if !services.is_empty() {
|
||||
obj.insert("services".into(), json!(services));
|
||||
}
|
||||
|
||||
let mut gpus: Vec<serde_json::Value> = Vec::new();
|
||||
if let Some(ext) = obj.get("extended").and_then(|v| v.as_object()) {
|
||||
if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
|
||||
if let Some(video) = win.get("videoControllers").and_then(|v| v.as_array()) {
|
||||
for controller in video {
|
||||
let name = controller.get("Name").and_then(|v| v.as_str());
|
||||
let memory = controller.get("AdapterRAM").and_then(parse_u64);
|
||||
let driver = controller.get("DriverVersion").and_then(|v| v.as_str());
|
||||
push_gpu(&mut gpus, name, memory, None, driver);
|
||||
}
|
||||
}
|
||||
if obj
|
||||
.get("disks")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.is_empty())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
if let Some(raw) = win.get("disks").and_then(|v| v.as_array()) {
|
||||
let mapped = raw
|
||||
.iter()
|
||||
.map(|disk| {
|
||||
let model = disk.get("Model").and_then(|v| v.as_str()).unwrap_or_default();
|
||||
let serial = disk.get("SerialNumber").and_then(|v| v.as_str()).unwrap_or_default();
|
||||
let size = parse_u64(disk.get("Size").unwrap_or(&serde_json::Value::Null)).unwrap_or(0);
|
||||
let interface = disk.get("InterfaceType").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let media = disk.get("MediaType").and_then(|v| v.as_str()).unwrap_or("");
|
||||
json!({
|
||||
"name": if !model.is_empty() { model } else { serial },
|
||||
"mountPoint": "",
|
||||
"fs": if !media.is_empty() { media } else { "—" },
|
||||
"interface": if !interface.is_empty() { interface } else { serde_json::Value::Null },
|
||||
"serial": if !serial.is_empty() { serial } else { serde_json::Value::Null },
|
||||
"totalBytes": size,
|
||||
"availableBytes": serde_json::Value::Null,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !mapped.is_empty() {
|
||||
obj.insert("disks".into(), json!(mapped));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(linux) = ext.get("linux").and_then(|v| v.as_object()) {
|
||||
if let Some(pci) = linux.get("pciList").and_then(|v| v.as_array()) {
|
||||
for entry in pci {
|
||||
if let Some(text) = entry.get("text").and_then(|v| v.as_str()) {
|
||||
let lower = text.to_lowercase();
|
||||
if lower.contains(" vga ")
|
||||
|| lower.contains(" 3d controller")
|
||||
|| lower.contains("display controller")
|
||||
{
|
||||
push_gpu(&mut gpus, Some(text), None, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(macos) = ext.get("macos").and_then(|v| v.as_object()) {
|
||||
if let Some(profiler) = macos.get("systemProfiler").and_then(|v| v.as_object()) {
|
||||
if let Some(displays) = profiler
|
||||
.get("SPDisplaysDataType")
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
for display in displays {
|
||||
if let Some(d) = display.as_object() {
|
||||
let name = d.get("_name").and_then(|v| v.as_str());
|
||||
let vram = d
|
||||
.get("spdisplays_vram")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| {
|
||||
let digits = s.split_whitespace().next().unwrap_or("");
|
||||
digits.parse::<u64>().ok().map(|n| {
|
||||
if s.to_lowercase().contains("gb") {
|
||||
n * 1024 * 1024 * 1024
|
||||
} else if s.to_lowercase().contains("mb") {
|
||||
n * 1024 * 1024
|
||||
} else {
|
||||
n
|
||||
}
|
||||
})
|
||||
});
|
||||
push_gpu(&mut gpus, name, vram, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !gpus.is_empty() {
|
||||
let entry = obj.entry("hardware").or_insert_with(|| json!({}));
|
||||
if let Some(hardware) = entry.as_object_mut() {
|
||||
hardware.insert("gpus".into(), json!(gpus.clone()));
|
||||
if let Some(primary) = gpus.first() {
|
||||
hardware.insert("primaryGpu".into(), primary.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json!({ "inventory": inventory })
|
||||
|
|
@ -369,7 +563,9 @@ fn collect_services_linux() -> serde_json::Value {
|
|||
// Typical format: UNIT LOAD ACTIVE SUB DESCRIPTION
|
||||
// We take UNIT and ACTIVE
|
||||
let cols: Vec<&str> = line.split_whitespace().collect();
|
||||
if cols.is_empty() { continue; }
|
||||
if cols.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let unit = cols.get(0).unwrap_or(&"");
|
||||
let active = cols.get(2).copied().unwrap_or("");
|
||||
if !unit.is_empty() {
|
||||
|
|
@ -391,7 +587,13 @@ fn collect_linux_extended() -> serde_json::Value {
|
|||
.arg("lsblk -J -b 2>/dev/null || true")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|out| if out.status.success() { Some(out.stdout) } else { Some(out.stdout) })
|
||||
.and_then(|out| {
|
||||
if out.status.success() {
|
||||
Some(out.stdout)
|
||||
} else {
|
||||
Some(out.stdout)
|
||||
}
|
||||
})
|
||||
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
|
||||
.unwrap_or_else(|| json!({}));
|
||||
|
||||
|
|
@ -427,12 +629,10 @@ fn collect_linux_extended() -> serde_json::Value {
|
|||
if let Ok(out) = std::process::Command::new("sh")
|
||||
.arg("-lc")
|
||||
.arg("command -v smartctl >/dev/null 2>&1 && echo yes || echo no")
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
if out.status.success() && String::from_utf8_lossy(&out.stdout).contains("yes") {
|
||||
if let Some(devices) = block_json
|
||||
.get("blockdevices")
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
if let Some(devices) = block_json.get("blockdevices").and_then(|v| v.as_array()) {
|
||||
for dev in devices {
|
||||
let t = dev.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let name = dev.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
|
@ -441,9 +641,12 @@ fn collect_linux_extended() -> serde_json::Value {
|
|||
if let Ok(out) = Command::new("sh")
|
||||
.arg("-lc")
|
||||
.arg(format!("smartctl -H -j {} 2>/dev/null || true", path))
|
||||
.output() {
|
||||
.output()
|
||||
{
|
||||
if out.status.success() || !out.stdout.is_empty() {
|
||||
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(&out.stdout) {
|
||||
if let Ok(val) =
|
||||
serde_json::from_slice::<serde_json::Value>(&out.stdout)
|
||||
{
|
||||
smart.push(val);
|
||||
}
|
||||
}
|
||||
|
|
@ -468,8 +671,8 @@ fn collect_linux_extended() -> serde_json::Value {
|
|||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn collect_windows_extended() -> serde_json::Value {
|
||||
use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
fn ps(cmd: &str) -> Option<serde_json::Value> {
|
||||
let ps_cmd = format!(
|
||||
|
|
@ -479,19 +682,23 @@ fn collect_windows_extended() -> serde_json::Value {
|
|||
let out = Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("-NoProfile")
|
||||
.arg("-WindowStyle").arg("Hidden")
|
||||
.arg("-WindowStyle")
|
||||
.arg("Hidden")
|
||||
.arg("-NoLogo")
|
||||
.arg("-Command")
|
||||
.arg(ps_cmd)
|
||||
.output()
|
||||
.ok()?;
|
||||
if out.stdout.is_empty() { return None; }
|
||||
if out.stdout.is_empty() {
|
||||
return None;
|
||||
}
|
||||
serde_json::from_slice::<serde_json::Value>(&out.stdout).ok()
|
||||
}
|
||||
|
||||
let software = ps(r#"@(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher"#)
|
||||
.unwrap_or_else(|| json!([]));
|
||||
let services = ps("@(Get-Service | Select-Object Name,Status,DisplayName)").unwrap_or_else(|| json!([]));
|
||||
let services =
|
||||
ps("@(Get-Service | Select-Object Name,Status,DisplayName)").unwrap_or_else(|| json!([]));
|
||||
let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({}));
|
||||
let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([]));
|
||||
|
||||
|
|
@ -514,7 +721,10 @@ fn collect_windows_extended() -> serde_json::Value {
|
|||
|
||||
// Hardware detalhado (CPU/Board/BIOS/Memória/Vídeo/Discos)
|
||||
let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({}));
|
||||
let baseboard = ps("Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version").unwrap_or_else(|| json!({}));
|
||||
let baseboard = ps(
|
||||
"Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version",
|
||||
)
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({}));
|
||||
let memory = ps("@(Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage)").unwrap_or_else(|| json!([]));
|
||||
let video = ps("@(Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID)").unwrap_or_else(|| json!([]));
|
||||
|
|
@ -543,7 +753,7 @@ fn collect_macos_extended() -> serde_json::Value {
|
|||
// system_profiler em JSON (pode ser pesado; limitar a alguns tipos)
|
||||
let profiler = Command::new("sh")
|
||||
.arg("-lc")
|
||||
.arg("system_profiler -json SPHardwareDataType SPSoftwareDataType SPNetworkDataType SPStorageDataType 2>/dev/null || true")
|
||||
.arg("system_profiler -json SPHardwareDataType SPSoftwareDataType SPNetworkDataType SPStorageDataType SPDisplaysDataType 2>/dev/null || true")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|out| serde_json::from_slice::<serde_json::Value>(&out.stdout).ok())
|
||||
|
|
@ -553,7 +763,13 @@ fn collect_macos_extended() -> serde_json::Value {
|
|||
.arg("pkgutil --pkgs 2>/dev/null || true")
|
||||
.output()
|
||||
.ok()
|
||||
.map(|out| String::from_utf8_lossy(&out.stdout).lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<Vec<_>>())
|
||||
.map(|out| {
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let services_text = Command::new("sh")
|
||||
.arg("-lc")
|
||||
|
|
@ -668,7 +884,11 @@ static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
|||
.expect("failed to build http client")
|
||||
});
|
||||
|
||||
async fn post_heartbeat(base_url: &str, token: &str, status: Option<String>) -> Result<(), AgentError> {
|
||||
async fn post_heartbeat(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
status: Option<String>,
|
||||
) -> Result<(), AgentError> {
|
||||
let system = collect_system();
|
||||
let metrics = collect_metrics(&system);
|
||||
let hostname = hostname::get()
|
||||
|
|
@ -760,7 +980,9 @@ impl AgentRuntime {
|
|||
let status_clone = status.clone();
|
||||
|
||||
let join_handle = async_runtime::spawn(async move {
|
||||
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
|
||||
if let Err(error) =
|
||||
post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
|
||||
{
|
||||
eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
|
||||
}
|
||||
|
||||
|
|
@ -776,7 +998,9 @@ impl AgentRuntime {
|
|||
_ = ticker.tick() => {}
|
||||
}
|
||||
|
||||
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
|
||||
if let Err(error) =
|
||||
post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
|
||||
{
|
||||
eprintln!("[agent] Falha ao enviar heartbeat: {error}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue