Redesenho da UI de dispositivos e correcao de VRAM
- Reorganiza layout da tela de dispositivos admin - Renomeia secao "Controles do dispositivo" para "Atalhos" - Adiciona botao de Tickets com badge de quantidade - Simplifica textos de botoes (Acesso, Resetar) - Remove email da maquina do cabecalho - Move empresa e status para mesma linha - Remove chip de Build do resumo - Corrige deteccao de VRAM para GPUs >4GB usando nvidia-smi - Adiciona prefixo "VRAM" na exibicao de memoria da GPU - Documenta sincronizacao RustDesk 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c5150fee8f
commit
23e7cf58ae
11 changed files with 863 additions and 441 deletions
|
|
@ -931,7 +931,44 @@ fn collect_windows_extended() -> serde_json::Value {
|
|||
.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!([]));
|
||||
// Coleta de GPU com VRAM correta (nvidia-smi para NVIDIA, registro como fallback para >4GB)
|
||||
let video = ps(r#"
|
||||
$gpus = @()
|
||||
$wmiGpus = Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID
|
||||
foreach ($gpu in $wmiGpus) {
|
||||
$vram = $gpu.AdapterRAM
|
||||
# Tenta nvidia-smi para GPUs NVIDIA (retorna valor correto para >4GB)
|
||||
if ($gpu.Name -match 'NVIDIA') {
|
||||
try {
|
||||
$nvidiaSmi = & 'nvidia-smi' '--query-gpu=memory.total' '--format=csv,noheader,nounits' 2>$null
|
||||
if ($nvidiaSmi) {
|
||||
$vramMB = [int64]($nvidiaSmi.Trim())
|
||||
$vram = $vramMB * 1024 * 1024
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
# Fallback: tenta registro do Windows (qwMemorySize é uint64)
|
||||
if ($vram -le 4294967296 -and $vram -gt 0) {
|
||||
try {
|
||||
$regPath = 'HKLM:\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0*'
|
||||
$regGpus = Get-ItemProperty $regPath -ErrorAction SilentlyContinue
|
||||
foreach ($reg in $regGpus) {
|
||||
if ($reg.DriverDesc -eq $gpu.Name -and $reg.'HardwareInformation.qwMemorySize') {
|
||||
$vram = [int64]$reg.'HardwareInformation.qwMemorySize'
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
$gpus += [PSCustomObject]@{
|
||||
Name = $gpu.Name
|
||||
AdapterRAM = $vram
|
||||
DriverVersion = $gpu.DriverVersion
|
||||
PNPDeviceID = $gpu.PNPDeviceID
|
||||
}
|
||||
}
|
||||
@($gpus)
|
||||
"#).unwrap_or_else(|| json!([]));
|
||||
let disks = ps("@(Get-CimInstance Win32_DiskDrive | Select-Object Model,SerialNumber,Size,InterfaceType,MediaType)").unwrap_or_else(|| json!([]));
|
||||
|
||||
json!({
|
||||
|
|
@ -1305,8 +1342,10 @@ impl AgentRuntime {
|
|||
// Verifica politica USB apos heartbeat inicial
|
||||
check_and_apply_usb_policy(&base_clone, &token_clone).await;
|
||||
|
||||
let mut ticker = tokio::time::interval(Duration::from_secs(interval));
|
||||
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(60));
|
||||
usb_ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
// Wait interval
|
||||
|
|
@ -1314,7 +1353,11 @@ impl AgentRuntime {
|
|||
_ = stop_signal_clone.notified() => {
|
||||
break;
|
||||
}
|
||||
_ = ticker.tick() => {}
|
||||
_ = heartbeat_ticker.tick() => {}
|
||||
_ = usb_ticker.tick() => {
|
||||
check_and_apply_usb_policy(&base_clone, &token_clone).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) =
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ mod rustdesk;
|
|||
mod usb_control;
|
||||
|
||||
use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile};
|
||||
use chrono::Local;
|
||||
use usb_control::{UsbPolicy, UsbPolicyResult};
|
||||
use tauri::{Emitter, Manager, WindowEvent};
|
||||
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;
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
@ -59,6 +63,38 @@ fn open_devtools(window: tauri::WebviewWindow) -> Result<(), String> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn log_app_event(message: String) -> Result<(), String> {
|
||||
append_app_log(&message)
|
||||
}
|
||||
|
||||
fn append_app_log(message: &str) -> Result<(), String> {
|
||||
let Some(dir) = logs_directory() else {
|
||||
return Err("LOCALAPPDATA indisponivel para gravar logs".to_string());
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&dir)
|
||||
.map_err(|error| format!("Falha ao criar pasta de logs: {error}"))?;
|
||||
|
||||
let path = dir.join("app.log");
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|error| format!("Falha ao abrir app.log: {error}"))?;
|
||||
|
||||
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
writeln!(file, "[{timestamp}] {message}")
|
||||
.map_err(|error| format!("Falha ao escrever log: {error}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn logs_directory() -> Option<PathBuf> {
|
||||
let base = std::env::var("LOCALAPPDATA").ok()?;
|
||||
Some(Path::new(&base).join("br.com.esdrasrenan.sistemadechamados").join("logs"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn ensure_rustdesk_and_emit(
|
||||
app: tauri::AppHandle,
|
||||
|
|
@ -150,6 +186,7 @@ pub fn run() {
|
|||
start_machine_agent,
|
||||
stop_machine_agent,
|
||||
open_devtools,
|
||||
log_app_event,
|
||||
ensure_rustdesk_and_emit,
|
||||
apply_usb_policy,
|
||||
get_usb_policy,
|
||||
|
|
|
|||
|
|
@ -282,6 +282,29 @@ pub fn ensure_rustdesk(
|
|||
log_event("Dados do RustDesk salvos no machine-agent.json");
|
||||
}
|
||||
|
||||
// Sincroniza com o backend imediatamente apos provisionar
|
||||
// O Rust faz o HTTP direto, sem passar pelo CSP do webview
|
||||
if let Err(error) = sync_remote_access_with_backend(&result) {
|
||||
log_event(&format!("Aviso: falha ao sincronizar com backend: {error}"));
|
||||
} else {
|
||||
log_event("Acesso remoto sincronizado com backend");
|
||||
// Atualiza lastSyncedAt no store
|
||||
let synced_data = serde_json::json!({
|
||||
"id": final_id,
|
||||
"password": password,
|
||||
"installedVersion": version,
|
||||
"updated": freshly_installed,
|
||||
"lastProvisionedAt": last_provisioned_at,
|
||||
"lastSyncedAt": Utc::now().timestamp_millis(),
|
||||
"lastError": serde_json::Value::Null
|
||||
});
|
||||
if let Err(e) = upsert_machine_store_value("rustdesk", synced_data) {
|
||||
log_event(&format!("Aviso: falha ao atualizar lastSyncedAt: {e}"));
|
||||
} else {
|
||||
log_event("lastSyncedAt atualizado com sucesso");
|
||||
}
|
||||
}
|
||||
|
||||
log_event(&format!("Provisionamento concluído. ID final: {final_id}. Versão: {:?}", version));
|
||||
|
||||
Ok(result)
|
||||
|
|
@ -1459,3 +1482,78 @@ fn mark_acl_unlock_flag() {
|
|||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_machine_store_path() -> Result<PathBuf, RustdeskError> {
|
||||
let base = env::var("LOCALAPPDATA")
|
||||
.map_err(|_| RustdeskError::MissingId)?;
|
||||
Ok(Path::new(&base)
|
||||
.join(APP_IDENTIFIER)
|
||||
.join(MACHINE_STORE_FILENAME))
|
||||
}
|
||||
|
||||
fn sync_remote_access_with_backend(result: &crate::RustdeskProvisioningResult) -> Result<(), RustdeskError> {
|
||||
log_event("Iniciando sincronizacao com backend...");
|
||||
|
||||
// Le token e config do store
|
||||
let store_path = get_machine_store_path()?;
|
||||
let store_content = fs::read_to_string(&store_path)
|
||||
.map_err(RustdeskError::Io)?;
|
||||
let store: serde_json::Value = serde_json::from_str(&store_content)
|
||||
.map_err(|_| RustdeskError::MissingId)?;
|
||||
|
||||
let token = store.get("token")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or(RustdeskError::MissingId)?;
|
||||
|
||||
let config = store.get("config")
|
||||
.ok_or(RustdeskError::MissingId)?;
|
||||
|
||||
let machine_id = config.get("machineId")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or(RustdeskError::MissingId)?;
|
||||
|
||||
let api_base_url = config.get("apiBaseUrl")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("https://tickets.esdrasrenan.com.br");
|
||||
|
||||
log_event(&format!("Sincronizando com backend: {} (machineId: {})", api_base_url, machine_id));
|
||||
|
||||
// Monta payload conforme schema esperado pelo backend
|
||||
// Schema: { machineToken, provider, identifier, password?, url?, username?, notes? }
|
||||
let payload = serde_json::json!({
|
||||
"machineToken": token,
|
||||
"provider": "RustDesk",
|
||||
"identifier": result.id,
|
||||
"password": result.password,
|
||||
"notes": format!("Versao: {}. Provisionado em: {}",
|
||||
result.installed_version.as_deref().unwrap_or("desconhecida"),
|
||||
result.last_provisioned_at)
|
||||
});
|
||||
|
||||
// Faz POST para /api/machines/remote-access
|
||||
let client = Client::builder()
|
||||
.user_agent(USER_AGENT)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()?;
|
||||
|
||||
let url = format!("{}/api/machines/remote-access", api_base_url);
|
||||
let response = client.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Idempotency-Key", format!("{}:RustDesk:{}", machine_id, result.id))
|
||||
.body(payload.to_string())
|
||||
.send()?;
|
||||
|
||||
if response.status().is_success() {
|
||||
log_event(&format!("Sync com backend OK: status {}", response.status()));
|
||||
Ok(())
|
||||
} else {
|
||||
let status = response.status();
|
||||
let body = response.text().unwrap_or_default();
|
||||
let body_preview = if body.len() > 200 { &body[..200] } else { &body };
|
||||
log_event(&format!("Sync com backend falhou: {} - {}", status, body_preview));
|
||||
Err(RustdeskError::CommandFailed {
|
||||
command: "sync_remote_access".to_string(),
|
||||
status: Some(status.as_u16() as i32)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ pub enum UsbControlError {
|
|||
#[cfg(target_os = "windows")]
|
||||
mod windows_impl {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
|
|
@ -80,6 +83,39 @@ mod windows_impl {
|
|||
pub fn apply_usb_policy(policy: UsbPolicy) -> Result<UsbPolicyResult, UsbControlError> {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
|
||||
let direct_result = try_apply_policy_direct(policy);
|
||||
|
||||
match direct_result {
|
||||
Ok(()) => Ok(UsbPolicyResult {
|
||||
success: true,
|
||||
policy: policy.as_str().to_string(),
|
||||
error: None,
|
||||
applied_at: Some(now),
|
||||
}),
|
||||
Err(err) => {
|
||||
// Tenta elevação se faltou permissão
|
||||
if is_permission_error(&err) {
|
||||
if let Err(elevated_err) = apply_policy_with_elevation(policy) {
|
||||
return Err(elevated_err);
|
||||
}
|
||||
// Revalida a policy após elevação
|
||||
let current = get_current_policy()?;
|
||||
if current != policy {
|
||||
return Err(UsbControlError::PermissionDenied);
|
||||
}
|
||||
return Ok(UsbPolicyResult {
|
||||
success: true,
|
||||
policy: policy.as_str().to_string(),
|
||||
error: None,
|
||||
applied_at: Some(now),
|
||||
});
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_apply_policy_direct(policy: UsbPolicy) -> Result<(), UsbControlError> {
|
||||
// 1. Aplicar Removable Storage Access Policy
|
||||
apply_removable_storage_policy(policy)?;
|
||||
|
||||
|
|
@ -93,12 +129,7 @@ mod windows_impl {
|
|||
apply_write_protect(false)?;
|
||||
}
|
||||
|
||||
Ok(UsbPolicyResult {
|
||||
success: true,
|
||||
policy: policy.as_str().to_string(),
|
||||
error: None,
|
||||
applied_at: Some(now),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> {
|
||||
|
|
@ -120,27 +151,27 @@ mod windows_impl {
|
|||
UsbPolicy::BlockAll => {
|
||||
let (key, _) = hklm
|
||||
.create_subkey(&full_path)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
key.set_value("Deny_Read", &1u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Write", &1u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Execute", &1u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
UsbPolicy::Readonly => {
|
||||
let (key, _) = hklm
|
||||
.create_subkey(&full_path)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
// Permite leitura, bloqueia escrita
|
||||
key.set_value("Deny_Read", &0u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Write", &1u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
key.set_value("Deny_Execute", &0u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,13 +183,13 @@ mod windows_impl {
|
|||
|
||||
let key = hklm
|
||||
.open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
match policy {
|
||||
UsbPolicy::Allow => {
|
||||
// Start = 3 habilita o driver
|
||||
key.set_value("Start", &3u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
UsbPolicy::BlockAll | UsbPolicy::Readonly => {
|
||||
// Start = 4 desabilita o driver
|
||||
|
|
@ -166,11 +197,11 @@ mod windows_impl {
|
|||
// Porem, como fallback de seguranca, desabilitamos para BlockAll
|
||||
if policy == UsbPolicy::BlockAll {
|
||||
key.set_value("Start", &4u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
} else {
|
||||
// Readonly mantem driver ativo
|
||||
key.set_value("Start", &3u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,10 +215,10 @@ mod windows_impl {
|
|||
if enable {
|
||||
let (key, _) = hklm
|
||||
.create_subkey(STORAGE_POLICY_PATH)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
|
||||
key.set_value("WriteProtect", &1u32)
|
||||
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
|
||||
.map_err(map_winreg_error)?;
|
||||
} else {
|
||||
if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) {
|
||||
let _ = key.set_value("WriteProtect", &0u32);
|
||||
|
|
@ -227,6 +258,99 @@ mod windows_impl {
|
|||
Ok(UsbPolicy::Allow)
|
||||
}
|
||||
|
||||
fn is_permission_error(error: &UsbControlError) -> bool {
|
||||
match error {
|
||||
UsbControlError::PermissionDenied => true,
|
||||
UsbControlError::RegistryError(msg) => {
|
||||
let lower = msg.to_lowercase();
|
||||
lower.contains("access is denied") || lower.contains("acesso negado") || lower.contains("5")
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_policy_with_elevation(policy: UsbPolicy) -> Result<(), UsbControlError> {
|
||||
// Cria script temporário para aplicar as chaves via PowerShell elevado
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_path: PathBuf = temp_dir.join("raven_usb_policy.ps1");
|
||||
|
||||
let policy_str = policy.as_str();
|
||||
let script = format!(
|
||||
r#"$ErrorActionPreference = 'Stop'
|
||||
$guid = '{guid}'
|
||||
$policy = '{policy}'
|
||||
|
||||
function Set-Allow {{
|
||||
reg delete 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f 2>$null
|
||||
reg delete 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f 2>$null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 3 /f | Out-Null
|
||||
}}
|
||||
|
||||
function Set-BlockAll {{
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f | Out-Null
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Read /t REG_DWORD /d 1 /f | Out-Null
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Write /t REG_DWORD /d 1 /f | Out-Null
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Execute /t REG_DWORD /d 1 /f | Out-Null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 4 /f | Out-Null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f | Out-Null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /v WriteProtect /t REG_DWORD /d 0 /f | Out-Null
|
||||
}}
|
||||
|
||||
function Set-Readonly {{
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /f | Out-Null
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Read /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Write /t REG_DWORD /d 1 /f | Out-Null
|
||||
reg add 'HKLM\Software\Policies\Microsoft\Windows\RemovableStorageDevices\{guid}' /v Deny_Execute /t REG_DWORD /d 0 /f | Out-Null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR' /v Start /t REG_DWORD /d 3 /f | Out-Null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /f | Out-Null
|
||||
reg add 'HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies' /v WriteProtect /t REG_DWORD /d 1 /f | Out-Null
|
||||
}}
|
||||
|
||||
switch ($policy) {{
|
||||
'ALLOW' {{ Set-Allow }}
|
||||
'BLOCK_ALL' {{ Set-BlockAll }}
|
||||
'READONLY' {{ Set-Readonly }}
|
||||
default {{ throw 'Politica invalida' }}
|
||||
}}
|
||||
|
||||
try {{
|
||||
gpupdate /target:computer /force | Out-Null
|
||||
}} catch {{}}
|
||||
"#,
|
||||
guid = REMOVABLE_STORAGE_GUID,
|
||||
policy = policy_str
|
||||
);
|
||||
|
||||
fs::write(&script_path, script).map_err(|e| UsbControlError::Io(e))?;
|
||||
|
||||
// Start-Process com RunAs para acionar UAC
|
||||
let arg = format!(
|
||||
"Start-Process -WindowStyle Hidden -FilePath powershell -Verb RunAs -Wait -ArgumentList '-ExecutionPolicy Bypass -File \"{}\"'",
|
||||
script_path.display()
|
||||
);
|
||||
|
||||
let status = Command::new("powershell")
|
||||
.arg("-Command")
|
||||
.arg(arg)
|
||||
.status()
|
||||
.map_err(|e| UsbControlError::Io(e))?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(UsbControlError::PermissionDenied);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_winreg_error(error: io::Error) -> UsbControlError {
|
||||
if let Some(code) = error.raw_os_error() {
|
||||
if code == 5 {
|
||||
return UsbControlError::PermissionDenied;
|
||||
}
|
||||
}
|
||||
UsbControlError::RegistryError(error.to_string())
|
||||
}
|
||||
|
||||
pub fn refresh_group_policy() -> Result<(), UsbControlError> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ const apiBaseUrl = normalizeUrl(import.meta.env.VITE_API_BASE_URL, appUrl)
|
|||
const RUSTDESK_CONFIG_STRING = import.meta.env.VITE_RUSTDESK_CONFIG_STRING?.trim() || null
|
||||
const RUSTDESK_DEFAULT_PASSWORD = import.meta.env.VITE_RUSTDESK_DEFAULT_PASSWORD?.trim() || null
|
||||
|
||||
const RUSTDESK_SYNC_INTERVAL_MS = 60 * 60 * 1000 // 1h
|
||||
const TOKEN_SELF_HEAL_DEBOUNCE_MS = 30 * 1000
|
||||
|
||||
function sanitizeEmail(value: string | null | undefined) {
|
||||
|
|
@ -244,7 +243,10 @@ async function writeRustdeskInfo(store: Store, info: RustdeskInfo): Promise<void
|
|||
|
||||
function logDesktop(message: string, data?: Record<string, unknown>) {
|
||||
const enriched = data ? `${message} ${JSON.stringify(data)}` : message
|
||||
console.log(`[raven] ${enriched}`)
|
||||
const line = `[raven] ${enriched}`
|
||||
console.log(line)
|
||||
// Persiste em arquivo local para facilitar debugging fora do console
|
||||
invoke("log_app_event", { message: line }).catch(() => {})
|
||||
}
|
||||
|
||||
function bytes(n?: number) {
|
||||
|
|
@ -782,99 +784,68 @@ const resolvedAppUrl = useMemo(() => {
|
|||
return normalized
|
||||
}, [config?.appUrl])
|
||||
|
||||
const syncRemoteAccessNow = useCallback(
|
||||
async (info: RustdeskInfo, allowRetry = true) => {
|
||||
if (!store) return
|
||||
if (!config?.machineId) {
|
||||
logDesktop("remoteAccess:sync:skipped", { reason: "unregistered" })
|
||||
return
|
||||
}
|
||||
const payload = buildRemoteAccessPayload(info)
|
||||
if (!payload) return
|
||||
// Funcao simplificada de sync - sempre le do disco para evitar race conditions
|
||||
const syncRemoteAccessDirect = useCallback(
|
||||
async (info: RustdeskInfo, allowRetry = true): Promise<boolean> => {
|
||||
try {
|
||||
// Sempre le do disco para evitar race conditions com state React
|
||||
const freshStore = await loadStore()
|
||||
const freshConfig = await readConfig(freshStore)
|
||||
const freshToken = await readToken(freshStore)
|
||||
|
||||
const resolveToken = async (allowHeal: boolean): Promise<string | null> => {
|
||||
let currentToken = token
|
||||
if (!currentToken) {
|
||||
currentToken = (await readToken(store)) ?? null
|
||||
if (currentToken) {
|
||||
setToken(currentToken)
|
||||
}
|
||||
if (!freshConfig?.machineId || !freshToken) {
|
||||
logDesktop("remoteAccess:sync:skip", {
|
||||
hasMachineId: !!freshConfig?.machineId,
|
||||
hasToken: !!freshToken,
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (!currentToken && allowHeal) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
currentToken = (await readToken(store)) ?? null
|
||||
if (currentToken) {
|
||||
setToken(currentToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentToken
|
||||
}
|
||||
|
||||
const sendRequest = async (machineToken: string, retryAllowed: boolean): Promise<void> => {
|
||||
const payload = buildRemoteAccessPayload(info)
|
||||
if (!payload) return false
|
||||
|
||||
logDesktop("remoteAccess:sync:start", { id: info.id })
|
||||
|
||||
const response = await fetch(`${apiBaseUrl}/api/machines/remote-access`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": `${config?.machineId ?? "unknown"}:RustDesk:${info.id}`,
|
||||
"Idempotency-Key": `${freshConfig.machineId}:RustDesk:${info.id}`,
|
||||
},
|
||||
body: JSON.stringify({ machineToken, ...payload }),
|
||||
body: JSON.stringify({ machineToken: freshToken, ...payload }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logDesktop("remoteAccess:sync:error", { status: response.status })
|
||||
const text = await response.text()
|
||||
if (retryAllowed && (response.status === 401 || isTokenRevokedMessage(text))) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
const refreshedToken = await resolveToken(false)
|
||||
if (refreshedToken) {
|
||||
return sendRequest(refreshedToken, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(text.slice(0, 300) || "Falha ao registrar acesso remoto")
|
||||
if (response.ok) {
|
||||
const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null }
|
||||
await writeRustdeskInfo(freshStore, nextInfo)
|
||||
setRustdeskInfo(nextInfo)
|
||||
logDesktop("remoteAccess:sync:success", { id: info.id })
|
||||
return true
|
||||
}
|
||||
|
||||
const nextInfo: RustdeskInfo = { ...info, lastSyncedAt: Date.now(), lastError: null }
|
||||
await writeRustdeskInfo(store, nextInfo)
|
||||
setRustdeskInfo(nextInfo)
|
||||
logDesktop("remoteAccess:sync:success", { id: info.id })
|
||||
}
|
||||
const errorText = await response.text()
|
||||
logDesktop("remoteAccess:sync:error", { status: response.status, error: errorText.slice(0, 200) })
|
||||
|
||||
try {
|
||||
const machineToken = await resolveToken(true)
|
||||
if (!machineToken) {
|
||||
const failedInfo: RustdeskInfo = {
|
||||
...info,
|
||||
lastError: "Token indisponível para sincronizar acesso remoto",
|
||||
}
|
||||
await writeRustdeskInfo(store, failedInfo)
|
||||
setRustdeskInfo(failedInfo)
|
||||
logDesktop("remoteAccess:sync:skipped", { reason: "missing-token" })
|
||||
return
|
||||
}
|
||||
await sendRequest(machineToken, allowRetry)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error("Falha ao sincronizar acesso remoto com a plataforma", error)
|
||||
const failedInfo: RustdeskInfo = { ...info, lastError: message }
|
||||
await writeRustdeskInfo(store, failedInfo)
|
||||
setRustdeskInfo(failedInfo)
|
||||
if (allowRetry && isTokenRevokedMessage(message)) {
|
||||
// Se token invalido, tenta self-heal uma vez
|
||||
if (allowRetry && (response.status === 401 || isTokenRevokedMessage(errorText))) {
|
||||
const healed = await attemptSelfHeal("remote-access")
|
||||
if (healed) {
|
||||
const refreshedToken = await resolveToken(false)
|
||||
if (refreshedToken) {
|
||||
return syncRemoteAccessNow(failedInfo, false)
|
||||
}
|
||||
return syncRemoteAccessDirect(info, false)
|
||||
}
|
||||
}
|
||||
logDesktop("remoteAccess:sync:failed", { id: info.id, error: message })
|
||||
|
||||
// Salva erro no store
|
||||
const failedInfo: RustdeskInfo = { ...info, lastError: errorText.slice(0, 200) }
|
||||
await writeRustdeskInfo(freshStore, failedInfo)
|
||||
setRustdeskInfo(failedInfo)
|
||||
return false
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logDesktop("remoteAccess:sync:exception", { error: message })
|
||||
return false
|
||||
}
|
||||
},
|
||||
[store, token, config?.machineId, attemptSelfHeal, setToken]
|
||||
[attemptSelfHeal]
|
||||
)
|
||||
|
||||
const handleRustdeskProvision = useCallback(
|
||||
|
|
@ -1007,23 +978,58 @@ const resolvedAppUrl = useMemo(() => {
|
|||
}
|
||||
}, [store, handleRustdeskProvision])
|
||||
|
||||
// Bootstrap do RustDesk + retry simplificado (60s)
|
||||
useEffect(() => {
|
||||
if (!store || !config?.machineId) return
|
||||
if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) {
|
||||
rustdeskBootstrapRef.current = true
|
||||
ensureRustdesk().finally(() => {
|
||||
rustdeskBootstrapRef.current = false
|
||||
})
|
||||
return
|
||||
}
|
||||
if (rustdeskInfo && !isRustdeskProvisioning) {
|
||||
const lastSync = rustdeskInfo.lastSyncedAt ?? 0
|
||||
const needsSync = Date.now() - lastSync > RUSTDESK_SYNC_INTERVAL_MS
|
||||
if (needsSync) {
|
||||
syncRemoteAccessNow(rustdeskInfo)
|
||||
|
||||
let disposed = false
|
||||
|
||||
async function bootstrap() {
|
||||
// Se nao tem rustdeskInfo, provisiona primeiro
|
||||
if (!rustdeskInfo && !isRustdeskProvisioning && !rustdeskBootstrapRef.current) {
|
||||
rustdeskBootstrapRef.current = true
|
||||
try {
|
||||
await ensureRustdesk()
|
||||
} finally {
|
||||
rustdeskBootstrapRef.current = false
|
||||
}
|
||||
return // handleRustdeskProvision fara o sync
|
||||
}
|
||||
|
||||
// Se ja tem rustdeskInfo mas nunca sincronizou, tenta sync
|
||||
if (rustdeskInfo && !rustdeskInfo.lastSyncedAt) {
|
||||
logDesktop("remoteAccess:sync:bootstrap", { id: rustdeskInfo.id })
|
||||
await syncRemoteAccessDirect(rustdeskInfo)
|
||||
}
|
||||
}
|
||||
}, [store, config?.machineId, rustdeskInfo, ensureRustdesk, syncRemoteAccessNow, isRustdeskProvisioning])
|
||||
|
||||
bootstrap()
|
||||
|
||||
// Retry a cada 30s se nunca sincronizou (o Rust faz o sync automaticamente)
|
||||
const interval = setInterval(async () => {
|
||||
if (disposed) return
|
||||
try {
|
||||
const freshStore = await loadStore()
|
||||
const freshRustdesk = await readRustdeskInfo(freshStore)
|
||||
if (freshRustdesk && !freshRustdesk.lastSyncedAt) {
|
||||
logDesktop("remoteAccess:sync:retry:fallback", { id: freshRustdesk.id })
|
||||
// Re-invoca o Rust para tentar sync novamente
|
||||
await invoke("ensure_rustdesk_and_emit", {
|
||||
configString: RUSTDESK_CONFIG_STRING || null,
|
||||
password: RUSTDESK_DEFAULT_PASSWORD || null,
|
||||
machineId: config?.machineId,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logDesktop("remoteAccess:sync:retry:error", { error: String(err) })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [store, config?.machineId, rustdeskInfo, isRustdeskProvisioning, ensureRustdesk, syncRemoteAccessDirect])
|
||||
|
||||
async function register() {
|
||||
if (!profile) return
|
||||
|
|
@ -1100,10 +1106,23 @@ const resolvedAppUrl = useMemo(() => {
|
|||
},
|
||||
})
|
||||
|
||||
await ensureRustdesk()
|
||||
logDesktop("register:rustdesk:done", { machineId: data.machineId })
|
||||
// Provisiona RustDesk em background (fire-and-forget)
|
||||
// O Rust faz o sync com o backend automaticamente, sem passar pelo CSP do webview
|
||||
logDesktop("register:rustdesk:start", { machineId: data.machineId })
|
||||
invoke<RustdeskProvisioningResult>("ensure_rustdesk_and_emit", {
|
||||
configString: RUSTDESK_CONFIG_STRING || null,
|
||||
password: RUSTDESK_DEFAULT_PASSWORD || null,
|
||||
machineId: data.machineId,
|
||||
}).then((result) => {
|
||||
logDesktop("register:rustdesk:done", { machineId: data.machineId, id: result.id })
|
||||
}).catch((err) => {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
if (!msg.toLowerCase().includes("apenas no windows")) {
|
||||
logDesktop("register:rustdesk:error", { error: msg })
|
||||
}
|
||||
})
|
||||
|
||||
// Abre o sistema imediatamente após registrar (evita ficar com token inválido no fluxo antigo)
|
||||
// Redireciona imediatamente (nao espera RustDesk)
|
||||
try {
|
||||
await fetch(`${apiBaseUrl}/api/machines/sessions`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue