diff --git a/apps/desktop/src-tauri/src/agent.rs b/apps/desktop/src-tauri/src/agent.rs index e54f567..b663005 100644 --- a/apps/desktop/src-tauri/src/agent.rs +++ b/apps/desktop/src-tauri/src/agent.rs @@ -1185,12 +1185,17 @@ struct UsbPolicyStatusReport { } async fn check_and_apply_usb_policy(base_url: &str, token: &str) { + crate::log_info!("Verificando politica USB pendente..."); + let url = format!("{}/api/machines/usb-policy?machineToken={}", base_url, token); let response = match HTTP_CLIENT.get(&url).send().await { - Ok(resp) => resp, + Ok(resp) => { + crate::log_info!("Resposta da verificacao de politica USB: status={}", resp.status()); + resp + } Err(e) => { - eprintln!("[agent] Falha ao verificar politica USB: {e}"); + crate::log_error!("Falha ao verificar politica USB: {e}"); return; } }; @@ -1198,23 +1203,26 @@ async fn check_and_apply_usb_policy(base_url: &str, token: &str) { let policy_response: UsbPolicyResponse = match response.json().await { Ok(data) => data, Err(e) => { - eprintln!("[agent] Falha ao parsear resposta de politica USB: {e}"); + crate::log_error!("Falha ao parsear resposta de politica USB: {e}"); return; } }; if !policy_response.pending { + crate::log_info!("Nenhuma politica USB pendente"); return; } let policy_str = match policy_response.policy { Some(p) => p, None => { - eprintln!("[agent] Politica USB pendente mas sem valor de policy"); + crate::log_warn!("Politica USB pendente mas sem valor de policy"); return; } }; + crate::log_info!("Politica USB pendente encontrada: {}", policy_str); + #[cfg(target_os = "windows")] { use crate::usb_control::{apply_usb_policy, get_current_policy, UsbPolicy}; @@ -1222,7 +1230,7 @@ async fn check_and_apply_usb_policy(base_url: &str, token: &str) { let policy = match UsbPolicy::from_str(&policy_str) { Some(p) => p, None => { - eprintln!("[agent] Politica USB invalida: {}", policy_str); + crate::log_error!("Politica USB invalida: {}", policy_str); report_usb_policy_status(base_url, token, "FAILED", Some(format!("Politica invalida: {}", policy_str)), None).await; return; } @@ -1231,30 +1239,44 @@ async fn check_and_apply_usb_policy(base_url: &str, token: &str) { // Verifica se a politica ja esta aplicada localmente match get_current_policy() { Ok(current) if current == policy => { - eprintln!("[agent] Politica USB ja esta aplicada localmente: {}", policy_str); - report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str)).await; + crate::log_info!("Politica USB ja esta aplicada localmente: {}", policy_str); + let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await; + if !reported { + crate::log_error!("Falha ao reportar politica ja aplicada"); + } return; } Ok(current) => { - eprintln!("[agent] Politica atual: {:?}, esperada: {:?}", current, policy); + crate::log_info!("Politica atual: {:?}, esperada: {:?}", current, policy); } Err(e) => { - eprintln!("[agent] Nao foi possivel ler politica atual: {e}"); + crate::log_warn!("Nao foi possivel ler politica atual: {e}"); } } - eprintln!("[agent] Aplicando politica USB: {}", policy_str); + crate::log_info!("Aplicando politica USB: {}", policy_str); // Reporta APPLYING para progress bar real no frontend - report_usb_policy_status(base_url, token, "APPLYING", None, None).await; + let _ = report_usb_policy_status(base_url, token, "APPLYING", None, None).await; match apply_usb_policy(policy) { Ok(result) => { - eprintln!("[agent] Politica USB aplicada com sucesso: {:?}", result); - report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str)).await; + crate::log_info!("Politica USB aplicada com sucesso: {:?}", result); + let reported = report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str.clone())).await; + if !reported { + crate::log_error!("CRITICO: Politica aplicada mas falha ao reportar ao servidor!"); + // Agenda retry em background + let base_url = base_url.to_string(); + let token = token.to_string(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(60)).await; + crate::log_info!("Retry agendado: reportando politica USB..."); + let _ = report_usb_policy_status(&base_url, &token, "APPLIED", None, Some(policy_str)).await; + }); + } } Err(e) => { - eprintln!("[agent] Falha ao aplicar politica USB: {e}"); + crate::log_error!("Falha ao aplicar politica USB: {e}"); report_usb_policy_status(base_url, token, "FAILED", Some(e.to_string()), None).await; } } @@ -1262,7 +1284,7 @@ async fn check_and_apply_usb_policy(base_url: &str, token: &str) { #[cfg(not(target_os = "windows"))] { - eprintln!("[agent] Controle de USB nao suportado neste sistema operacional"); + crate::log_warn!("Controle de USB nao suportado neste sistema operacional"); report_usb_policy_status(base_url, token, "FAILED", Some("Sistema operacional nao suportado".to_string()), None).await; } } @@ -1273,7 +1295,7 @@ async fn report_usb_policy_status( status: &str, error: Option, current_policy: Option, -) { +) -> bool { let url = format!("{}/api/machines/usb-policy", base_url); let report = UsbPolicyStatusReport { @@ -1283,39 +1305,56 @@ async fn report_usb_policy_status( current_policy, }; - // Retry com backoff exponencial: 2s, 4s, 8s - let delays = [2, 4, 8]; + crate::log_info!("Reportando status de politica USB: status={}", status); + + // Retry simples: 1 tentativa imediata + 1 retry após 2s + let delays = [2]; let mut last_error = None; for (attempt, delay_secs) in delays.iter().enumerate() { match HTTP_CLIENT.post(&url).json(&report).send().await { - Ok(response) if response.status().is_success() => { - if attempt > 0 { - eprintln!("[agent] Report de politica USB enviado na tentativa {}", attempt + 1); - } - return; - } Ok(response) => { - last_error = Some(format!("HTTP {}", response.status())); + let status_code = response.status(); + if status_code.is_success() { + crate::log_info!( + "Report de politica USB enviado com sucesso na tentativa {}", + attempt + 1 + ); + return true; + } else { + let body = response.text().await.unwrap_or_default(); + last_error = Some(format!("HTTP {} - {}", status_code, body)); + crate::log_warn!( + "Report de politica USB falhou (tentativa {}): HTTP {}", + attempt + 1, + status_code + ); + } } Err(e) => { last_error = Some(e.to_string()); + crate::log_warn!( + "Report de politica USB falhou (tentativa {}): {}", + attempt + 1, + e + ); } } if attempt < delays.len() - 1 { - eprintln!( - "[agent] Falha ao reportar politica USB (tentativa {}), retentando em {}s...", - attempt + 1, - delay_secs - ); + crate::log_info!("Retentando report de politica USB em {}s...", delay_secs); tokio::time::sleep(Duration::from_secs(*delay_secs)).await; } } if let Some(err) = last_error { - eprintln!("[agent] Falha ao reportar status de politica USB apos 3 tentativas: {err}"); + crate::log_error!( + "Falha ao reportar status de politica USB apos {} tentativas: {err}", + delays.len() + ); } + + false } struct HeartbeatHandle { @@ -1332,9 +1371,9 @@ impl HeartbeatHandle { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct AgentRuntime { - inner: Mutex>, + inner: Arc>>, } fn sanitize_base_url(input: &str) -> Result { @@ -1348,7 +1387,7 @@ fn sanitize_base_url(input: &str) -> Result { impl AgentRuntime { pub fn new() -> Self { Self { - inner: Mutex::new(None), + inner: Arc::new(Mutex::new(None)), } } @@ -1381,24 +1420,29 @@ impl AgentRuntime { let status_clone = status.clone(); let join_handle = async_runtime::spawn(async move { + crate::log_info!("Loop de agente iniciado"); + if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await { - eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}"); + crate::log_error!("Falha inicial ao enviar heartbeat: {error}"); + } else { + crate::log_info!("Heartbeat inicial enviado com sucesso"); } // Verifica politica USB apos heartbeat inicial check_and_apply_usb_policy(&base_clone, &token_clone).await; - let mut heartbeat_ticker = tokio::time::interval(Duration::from_secs(interval)); - heartbeat_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); - let mut usb_ticker = tokio::time::interval(Duration::from_secs(15)); - usb_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + let mut heartbeat_ticker = tokio::time::interval(Duration::from_secs(interval)); + heartbeat_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + let mut usb_ticker = tokio::time::interval(Duration::from_secs(15)); + usb_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { // Wait interval tokio::select! { _ = stop_signal_clone.notified() => { + crate::log_info!("Loop de agente encerrado por sinal de parada"); break; } _ = heartbeat_ticker.tick() => {} @@ -1411,7 +1455,7 @@ impl AgentRuntime { if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await { - eprintln!("[agent] Falha ao enviar heartbeat: {error}"); + crate::log_error!("Falha ao enviar heartbeat: {error}"); } // Verifica politica USB apos cada heartbeat diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 0364074..76f8ac0 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -11,12 +11,76 @@ use tauri_plugin_store::Builder as StorePluginBuilder; use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; -#[cfg(target_os = "windows")] -use std::process::Command; +use std::sync::OnceLock; #[cfg(target_os = "windows")] use tauri::menu::{MenuBuilder, MenuItemBuilder}; #[cfg(target_os = "windows")] use tauri::tray::TrayIconBuilder; +#[cfg(target_os = "windows")] +use winreg::enums::*; +#[cfg(target_os = "windows")] +use winreg::RegKey; + +// ============================================================================ +// Sistema de Logging para Agente +// ============================================================================ + +static AGENT_LOG_FILE: OnceLock> = OnceLock::new(); + +pub fn init_agent_logging() -> Result<(), String> { + let dir = logs_directory() + .ok_or("LOCALAPPDATA indisponivel para logging")?; + + std::fs::create_dir_all(&dir) + .map_err(|e| format!("Falha ao criar diretorio de logs: {e}"))?; + + let path = dir.join("raven-agent.log"); + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .map_err(|e| format!("Falha ao abrir raven-agent.log: {e}"))?; + + let _ = AGENT_LOG_FILE.set(std::sync::Mutex::new(file)); + Ok(()) +} + +pub fn log_agent(level: &str, message: &str) { + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let line = format!("[{timestamp}] [{level}] {message}\n"); + + // Escreve para stderr (util em dev/debug) + eprint!("{line}"); + + // Escreve para arquivo + if let Some(mutex) = AGENT_LOG_FILE.get() { + if let Ok(mut file) = mutex.lock() { + let _ = file.write_all(line.as_bytes()); + let _ = file.flush(); + } + } +} + +#[macro_export] +macro_rules! log_info { + ($($arg:tt)*) => { + $crate::log_agent("INFO", &format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! log_error { + ($($arg:tt)*) => { + $crate::log_agent("ERROR", &format!($($arg)*)) + }; +} + +#[macro_export] +macro_rules! log_warn { + ($($arg:tt)*) => { + $crate::log_agent("WARN", &format!($($arg)*)) + }; +} #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] @@ -173,10 +237,28 @@ pub fn run() { } }) .setup(|app| { + // Inicializa sistema de logging primeiro + if let Err(e) = init_agent_logging() { + eprintln!("[raven] Falha ao inicializar logging: {e}"); + } + + log_info!("Raven iniciando..."); + #[cfg(target_os = "windows")] { setup_raven_autostart(); setup_tray(&app.handle())?; + + // Tenta iniciar o agente em background se houver credenciais salvas + let app_handle = app.handle().clone(); + let runtime = app.state::().inner().clone(); + tauri::async_runtime::spawn(async move { + // Aguarda um pouco para o app estabilizar + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + if let Err(e) = try_start_background_agent(&app_handle, runtime).await { + log_warn!("Agente nao iniciado em background: {e}"); + } + }); } Ok(()) }) @@ -198,22 +280,53 @@ pub fn run() { #[cfg(target_os = "windows")] fn setup_raven_autostart() { - if let Ok(exe) = std::env::current_exe() { - let path = exe.display().to_string(); - let quoted = format!("\"{}\"", path); - let _ = Command::new("cmd") - .args([ - "/C", - "reg", - "add", - r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", - "/v", - "Raven", - "/d", - "ed, - "/f", - ]) - .status(); + let exe_path = match std::env::current_exe() { + Ok(p) => p, + Err(e) => { + log_error!("Falha ao obter caminho do executavel: {e}"); + return; + } + }; + + let path_str = exe_path.display().to_string(); + // Adiciona flag --background para indicar inicio automatico + let value = format!("\"{}\" --background", path_str); + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + + match hkcu.open_subkey_with_flags( + r"Software\Microsoft\Windows\CurrentVersion\Run", + KEY_SET_VALUE | KEY_READ, + ) { + Ok(key) => { + match key.set_value("Raven", &value) { + Ok(()) => { + log_info!("Auto-start configurado: {value}"); + + // Valida que foi salvo corretamente + match key.get_value::("Raven") { + Ok(saved) => { + if saved == value { + log_info!("Auto-start validado: entrada existe no registro"); + } else { + log_warn!( + "Auto-start: valor difere. Esperado: {value}, Salvo: {saved}" + ); + } + } + Err(e) => { + log_warn!("Auto-start: nao foi possivel validar entrada: {e}"); + } + } + } + Err(e) => { + log_error!("Falha ao definir valor de auto-start no registro: {e}"); + } + } + } + Err(e) => { + log_error!("Falha ao abrir chave de registro Run: {e}"); + } } } @@ -259,3 +372,65 @@ fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> { builder.build(app)?; Ok(()) } + +#[cfg(target_os = "windows")] +async fn try_start_background_agent( + app: &tauri::AppHandle, + runtime: AgentRuntime, +) -> Result<(), String> { + log_info!("Verificando credenciais salvas para iniciar agente..."); + + let app_data = app + .path() + .app_local_data_dir() + .map_err(|e| format!("Falha ao obter diretorio de dados: {e}"))?; + + let store_path = app_data.join("machine-agent.json"); + + if !store_path.exists() { + return Err("Nenhuma configuracao encontrada".to_string()); + } + + // Ler arquivo JSON diretamente + let content = std::fs::read_to_string(&store_path) + .map_err(|e| format!("Falha ao ler machine-agent.json: {e}"))?; + + let data: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| format!("Falha ao parsear machine-agent.json: {e}"))?; + + let token = data + .get("token") + .and_then(|v| v.as_str()) + .filter(|t| !t.is_empty()) + .ok_or("Token nao encontrado ou vazio")?; + + let config = data.get("config"); + + let api_base_url = config + .and_then(|c| c.get("apiBaseUrl")) + .and_then(|v| v.as_str()) + .unwrap_or("https://tickets.esdrasrenan.com.br"); + + let interval = config + .and_then(|c| c.get("heartbeatIntervalSec")) + .and_then(|v| v.as_u64()) + .unwrap_or(300); + + log_info!( + "Iniciando agente em background: url={}, interval={}s", + api_base_url, + interval + ); + + runtime + .start_heartbeat( + api_base_url.to_string(), + token.to_string(), + Some("online".to_string()), + Some(interval), + ) + .map_err(|e| format!("Falha ao iniciar heartbeat: {e}"))?; + + log_info!("Agente iniciado com sucesso em background"); + Ok(()) +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 1b2eaad..ea5aeaf 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Raven", - "version": "0.1.9", + "version": "0.2.0", "identifier": "br.com.esdrasrenan.sistemadechamados", "build": { "beforeDevCommand": "bun run dev", diff --git a/convex/machines.ts b/convex/machines.ts index 785fd7e..37dc681 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -2318,10 +2318,19 @@ async function upsertRemoteAccessSnapshotFromHeartbeat( if (!identifier) return const existingEntries = normalizeRemoteAccessList(machine.remoteAccess) - const idx = existingEntries.findIndex( + + // Busca primeiro por provider + identifier (atualização exata) + let idx = existingEntries.findIndex( (entry) => entry.provider.toLowerCase() === provider.toLowerCase() && entry.identifier.toLowerCase() === identifier.toLowerCase() ) + // Se não encontrou, busca apenas por provider (substituição - ex: RustDesk ID mudou) + if (idx < 0) { + idx = existingEntries.findIndex( + (entry) => entry.provider.toLowerCase() === provider.toLowerCase() + ) + } + const entryId = idx >= 0 ? existingEntries[idx].id : createRemoteAccessId() const metadata = { ...(normalized.metadata ?? {}), diff --git a/src/app/api/machines/usb-policy/route.ts b/src/app/api/machines/usb-policy/route.ts index 814a257..2f9b7e6 100644 --- a/src/app/api/machines/usb-policy/route.ts +++ b/src/app/api/machines/usb-policy/route.ts @@ -11,8 +11,8 @@ const getPolicySchema = z.object({ const reportStatusSchema = z.object({ machineToken: z.string().min(1), status: z.enum(["PENDING", "APPLYING", "APPLIED", "FAILED"]), - error: z.string().optional(), - currentPolicy: z.string().optional(), + error: z.string().nullable().optional(), + currentPolicy: z.string().nullable().optional(), }) const CORS_METHODS = "GET, POST, OPTIONS"