feat: habilitar provisionamento desktop e rotas CORS

This commit is contained in:
Esdras Renan 2025-10-08 23:07:49 -03:00
parent 7569986ffc
commit 152550a9a0
19 changed files with 1806 additions and 211 deletions

View 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();
}
}
}

View file

@ -1,14 +1,43 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
mod agent;
use agent::{collect_profile, AgentRuntime, MachineProfile};
use tauri_plugin_store::Builder as StorePluginBuilder;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
fn collect_machine_profile() -> Result<MachineProfile, String> {
collect_profile().map_err(|error| error.to_string())
}
#[tauri::command]
fn start_machine_agent(
state: tauri::State<AgentRuntime>,
base_url: String,
token: String,
status: Option<String>,
interval_seconds: Option<u64>,
) -> Result<(), String> {
state
.start_heartbeat(base_url, token, status, interval_seconds)
.map_err(|error| error.to_string())
}
#[tauri::command]
fn stop_machine_agent(state: tauri::State<AgentRuntime>) -> Result<(), String> {
state.stop();
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(AgentRuntime::new())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.plugin(StorePluginBuilder::default().build())
.invoke_handler(tauri::generate_handler![
collect_machine_profile,
start_machine_agent,
stop_machine_agent
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}