feat: habilitar provisionamento desktop e rotas CORS
This commit is contained in:
parent
7569986ffc
commit
152550a9a0
19 changed files with 1806 additions and 211 deletions
333
apps/desktop/src-tauri/src/agent.rs
Normal file
333
apps/desktop/src-tauri/src/agent.rs
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use sysinfo::{Networks, System};
|
||||
use tauri::async_runtime::{self, JoinHandle};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AgentError {
|
||||
#[error("Falha ao obter hostname da máquina")]
|
||||
Hostname,
|
||||
#[error("Nenhum identificador de hardware disponível (MAC/serial)")]
|
||||
MissingIdentifiers,
|
||||
#[error("URL de API inválida")]
|
||||
InvalidApiUrl,
|
||||
#[error("Falha HTTP: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MachineOs {
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
pub architecture: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MachineMetrics {
|
||||
pub collected_at: DateTime<Utc>,
|
||||
pub cpu_logical_cores: usize,
|
||||
pub cpu_physical_cores: Option<usize>,
|
||||
pub cpu_usage_percent: f32,
|
||||
pub load_average_one: Option<f64>,
|
||||
pub load_average_five: Option<f64>,
|
||||
pub load_average_fifteen: Option<f64>,
|
||||
pub memory_total_bytes: u64,
|
||||
pub memory_used_bytes: u64,
|
||||
pub memory_used_percent: f32,
|
||||
pub uptime_seconds: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MachineInventory {
|
||||
pub cpu_brand: Option<String>,
|
||||
pub host_identifier: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MachineProfile {
|
||||
pub hostname: String,
|
||||
pub os: MachineOs,
|
||||
pub mac_addresses: Vec<String>,
|
||||
pub serial_numbers: Vec<String>,
|
||||
pub inventory: MachineInventory,
|
||||
pub metrics: MachineMetrics,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HeartbeatPayload {
|
||||
machine_token: String,
|
||||
status: Option<String>,
|
||||
hostname: Option<String>,
|
||||
os: Option<MachineOs>,
|
||||
metrics: Option<MachineMetrics>,
|
||||
metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn collect_mac_addresses() -> Vec<String> {
|
||||
let mut macs = Vec::new();
|
||||
let mut networks = Networks::new();
|
||||
networks.refresh_list();
|
||||
networks.refresh();
|
||||
|
||||
for (_, data) in networks.iter() {
|
||||
let bytes = data.mac_address().0;
|
||||
if bytes.iter().all(|byte| *byte == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let formatted = bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{:02x}", byte))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":");
|
||||
|
||||
if !macs.contains(&formatted) {
|
||||
macs.push(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
macs
|
||||
}
|
||||
|
||||
fn collect_system() -> System {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
system
|
||||
}
|
||||
|
||||
fn collect_metrics(system: &System) -> MachineMetrics {
|
||||
let collected_at = Utc::now();
|
||||
let total_memory = system.total_memory();
|
||||
let used_memory = system.used_memory();
|
||||
let memory_total_bytes = total_memory.saturating_mul(1024);
|
||||
let memory_used_bytes = used_memory.saturating_mul(1024);
|
||||
let memory_used_percent = if total_memory > 0 {
|
||||
(used_memory as f32 / total_memory as f32) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let load = System::load_average();
|
||||
let cpu_usage_percent = system.global_cpu_usage();
|
||||
let cpu_logical_cores = system.cpus().len();
|
||||
let cpu_physical_cores = system.physical_core_count();
|
||||
|
||||
MachineMetrics {
|
||||
collected_at,
|
||||
cpu_logical_cores,
|
||||
cpu_physical_cores,
|
||||
cpu_usage_percent,
|
||||
load_average_one: Some(load.one),
|
||||
load_average_five: Some(load.five),
|
||||
load_average_fifteen: Some(load.fifteen),
|
||||
memory_total_bytes,
|
||||
memory_used_bytes,
|
||||
memory_used_percent,
|
||||
uptime_seconds: System::uptime(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_profile() -> Result<MachineProfile, AgentError> {
|
||||
let hostname = hostname::get()
|
||||
.map_err(|_| AgentError::Hostname)?
|
||||
.to_string_lossy()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let system = collect_system();
|
||||
|
||||
let os_name = System::name()
|
||||
.or_else(|| System::long_os_version())
|
||||
.unwrap_or_else(|| "desconhecido".to_string());
|
||||
let os_version = System::os_version();
|
||||
let architecture = std::env::consts::ARCH.to_string();
|
||||
|
||||
let mac_addresses = collect_mac_addresses();
|
||||
let serials: Vec<String> = Vec::new();
|
||||
|
||||
if mac_addresses.is_empty() && serials.is_empty() {
|
||||
return Err(AgentError::MissingIdentifiers);
|
||||
}
|
||||
|
||||
let metrics = collect_metrics(&system);
|
||||
let cpu_brand = system
|
||||
.cpus()
|
||||
.first()
|
||||
.map(|cpu| cpu.brand().to_string())
|
||||
.filter(|brand| !brand.trim().is_empty());
|
||||
|
||||
let inventory = MachineInventory {
|
||||
cpu_brand,
|
||||
host_identifier: serials.first().cloned(),
|
||||
};
|
||||
|
||||
Ok(MachineProfile {
|
||||
hostname,
|
||||
os: MachineOs {
|
||||
name: os_name,
|
||||
version: os_version,
|
||||
architecture: Some(architecture),
|
||||
},
|
||||
mac_addresses,
|
||||
serial_numbers: serials,
|
||||
inventory,
|
||||
metrics,
|
||||
})
|
||||
}
|
||||
|
||||
static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent("sistema-de-chamados-agent/1.0")
|
||||
.timeout(Duration::from_secs(20))
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.expect("failed to build http client")
|
||||
});
|
||||
|
||||
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()
|
||||
.map_err(|_| AgentError::Hostname)?
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let os = MachineOs {
|
||||
name: System::name()
|
||||
.or_else(|| System::long_os_version())
|
||||
.unwrap_or_else(|| "desconhecido".to_string()),
|
||||
version: System::os_version(),
|
||||
architecture: Some(std::env::consts::ARCH.to_string()),
|
||||
};
|
||||
|
||||
let payload = HeartbeatPayload {
|
||||
machine_token: token.to_string(),
|
||||
status,
|
||||
hostname: Some(hostname),
|
||||
os: Some(os),
|
||||
metrics: Some(metrics),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let url = format!("{}/api/machines/heartbeat", base_url);
|
||||
HTTP_CLIENT.post(url).json(&payload).send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct HeartbeatHandle {
|
||||
token: String,
|
||||
base_url: String,
|
||||
status: Option<String>,
|
||||
stop_signal: Arc<Notify>,
|
||||
join_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl HeartbeatHandle {
|
||||
fn stop(self) {
|
||||
self.stop_signal.notify_waiters();
|
||||
self.join_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AgentRuntime {
|
||||
inner: Mutex<Option<HeartbeatHandle>>,
|
||||
}
|
||||
|
||||
fn sanitize_base_url(input: &str) -> Result<String, AgentError> {
|
||||
let trimmed = input.trim().trim_end_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
return Err(AgentError::InvalidApiUrl);
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
impl AgentRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_heartbeat(
|
||||
&self,
|
||||
base_url: String,
|
||||
token: String,
|
||||
status: Option<String>,
|
||||
interval_seconds: Option<u64>,
|
||||
) -> Result<(), AgentError> {
|
||||
let sanitized_base = sanitize_base_url(&base_url)?;
|
||||
let interval = interval_seconds.unwrap_or(300).max(60);
|
||||
|
||||
{
|
||||
let mut guard = self.inner.lock();
|
||||
if let Some(handle) = guard.take() {
|
||||
if handle.token == token && handle.base_url == sanitized_base {
|
||||
// Reuse existing heartbeat; keep running.
|
||||
*guard = Some(handle);
|
||||
return Ok(());
|
||||
}
|
||||
handle.stop();
|
||||
}
|
||||
}
|
||||
|
||||
let stop_signal = Arc::new(Notify::new());
|
||||
let stop_signal_clone = stop_signal.clone();
|
||||
let token_clone = token.clone();
|
||||
let base_clone = sanitized_base.clone();
|
||||
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 {
|
||||
eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
|
||||
}
|
||||
|
||||
let mut ticker = tokio::time::interval(Duration::from_secs(interval));
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
// Wait interval
|
||||
tokio::select! {
|
||||
_ = stop_signal_clone.notified() => {
|
||||
break;
|
||||
}
|
||||
_ = ticker.tick() => {}
|
||||
}
|
||||
|
||||
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
|
||||
eprintln!("[agent] Falha ao enviar heartbeat: {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let handle = HeartbeatHandle {
|
||||
token,
|
||||
base_url: sanitized_base,
|
||||
status,
|
||||
stop_signal,
|
||||
join_handle,
|
||||
};
|
||||
|
||||
let mut guard = self.inner.lock();
|
||||
*guard = Some(handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
let mut guard = self.inner.lock();
|
||||
if let Some(handle) = guard.take() {
|
||||
handle.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue