1→//! Main tray application logic 2→ 3→use anyhow::{Context, Result}; 4→use std::sync::Arc; 5→use tokio::sync::{mpsc, RwLock}; 6→use tray_icon::{ 7→ menu::MenuEvent, 8→ Icon, TrayIcon, TrayIconBuilder, 9→}; 10→use tracing::{debug, error, info, warn}; 11→use winit::event_loop::{ControlFlow, EventLoop}; 12→ 13→use crate::ipc::{AgentStatus, ConnectionState, IpcClient, IpcRequest, TrayPolicy}; 14→use crate::menu::{self, MenuAction}; 15→ 16→/// Icon state 17→#[derive(Debug, Clone, Copy, PartialEq, Eq)] 18→pub enum IconState { 19→ Connected, 20→ Reconnecting, 21→ Error, 22→ Disabled, 23→} 24→ 25→/// Embedded icon data (will be replaced with actual icons) 26→mod icons { 27→ // Placeholder: 16x16 RGBA icons 28→ // In production, these would be loaded from the assets folder 29→ 30→ pub fn connected() -> Vec { 31→ // Green circle placeholder (16x16 RGBA) 32→ create_circle_icon(0x22, 0xc5, 0x5e, 0xff) 33→ } 34→ 35→ pub fn reconnecting() -> Vec { 36→ // Yellow circle placeholder 37→ create_circle_icon(0xea, 0xb3, 0x08, 0xff) 38→ } 39→ 40→ pub fn error() -> Vec { 41→ // Red circle placeholder 42→ create_circle_icon(0xef, 0x44, 0x44, 0xff) 43→ } 44→ 45→ pub fn disabled() -> Vec { 46→ // Gray circle placeholder 47→ create_circle_icon(0x6b, 0x72, 0x80, 0xff) 48→ } 49→ 50→ fn create_circle_icon(r: u8, g: u8, b: u8, a: u8) -> Vec { 51→ let size = 32; 52→ let mut data = vec![0u8; size * size * 4]; 53→ let center = size as f32 / 2.0; 54→ let radius = center - 2.0; 55→ 56→ for y in 0..size { 57→ for x in 0..size { 58→ let dx = x as f32 - center; 59→ let dy = y as f32 - center; 60→ let dist = (dx * dx + dy * dy).sqrt(); 61→ 62→ let idx = (y * size + x) * 4; 63→ if dist <= radius { 64→ data[idx] = r; 65→ data[idx + 1] = g; 66→ data[idx + 2] = b; 67→ data[idx + 3] = a; 68→ } else if dist <= radius + 1.0 { 69→ // Anti-aliased edge 70→ let alpha = ((radius + 1.0 - dist) * a as f32) as u8; 71→ data[idx] = r; 72→ data[idx + 1] = g; 73→ data[idx + 2] = b; 74→ data[idx + 3] = alpha; 75→ } 76→ } 77→ } 78→ 79→ data 80→ } 81→} 82→ 83→/// Main tray application 84→pub struct TrayApp { 85→ /// Tokio runtime for async operations 86→ runtime: tokio::runtime::Runtime, 87→ 88→ /// IPC client state 89→ connection_state: Arc>, 90→ status: Arc>, 91→ policy: Arc>, 92→ 93→ /// Request sender 94→ request_tx: mpsc::Sender, 95→} 96→ 97→impl TrayApp { 98→ /// Create a new tray application 99→ pub fn new() -> Result { 100→ let runtime = tokio::runtime::Builder::new_multi_thread() 101→ .enable_all() 102→ .build() 103→ .context("Failed to create tokio runtime")?; 104→ 105→ let connection_state = Arc::new(RwLock::new(ConnectionState::Disconnected)); 106→ let status = Arc::new(RwLock::new(AgentStatus::default())); 107→ let policy = Arc::new(RwLock::new(TrayPolicy::default_permissive())); 108→ 109→ let (request_tx, request_rx) = mpsc::channel(32); 110→ let (update_tx, _update_rx) = mpsc::channel(32); 111→ 112→ // Spawn IPC connection task 113→ let conn_state = Arc::clone(&connection_state); 114→ let conn_status = Arc::clone(&status); 115→ let conn_policy = Arc::clone(&policy); 116→ 117→ runtime.spawn(async move { 118→ crate::ipc::connection::run_connection( 119→ conn_state, 120→ conn_status, 121→ conn_policy, 122→ request_rx, 123→ update_tx, 124→ ) 125→ .await; 126→ }); 127→ 128→ Ok(Self { 129→ runtime, 130→ connection_state, 131→ status, 132→ policy, 133→ request_tx, 134→ }) 135→ } 136→ 137→ /// Run the tray application (blocking) 138→ pub fn run(self) -> Result<()> { 139→ let event_loop = EventLoop::new().context("Failed to create event loop")?; 140→ 141→ // Create initial icon 142→ let icon = create_icon(IconState::Reconnecting)?; 143→ 144→ // Get initial status and policy for menu 145→ let (status, policy) = self.runtime.block_on(async { 146→ let s = self.status.read().await.clone(); 147→ let p = self.policy.read().await.clone(); 148→ (s, p) 149→ }); 150→ 151→ // Build initial menu 152→ let menu = menu::build_menu(&status, &policy); 153→ 154→ // Create tray icon 155→ let tooltip = policy 156→ .tooltip_text 157→ .clone() 158→ .unwrap_or_else(|| "GuruRMM Agent".to_string()); 159→ 160→ let tray_icon = TrayIconBuilder::new() 161→ .with_menu(Box::new(menu)) 162→ .with_tooltip(&tooltip) 163→ .with_icon(icon) 164→ .build() 165→ .context("Failed to create tray icon")?; 166→ 167→ info!("Tray icon created"); 168→ 169→ // Menu event receiver 170→ let menu_channel = MenuEvent::receiver(); 171→ 172→ // Track last known state for icon updates 173→ let mut last_icon_state = IconState::Reconnecting; 174→ let mut last_connected = false; 175→ 176→ // Run event loop 177→ event_loop.run(move |_event, event_loop| { 178→ event_loop.set_control_flow(ControlFlow::Wait); 179→ 180→ // Check for menu events (non-blocking) 181→ if let Ok(event) = menu_channel.try_recv() { 182→ let action = menu::handle_menu_event(event); 183→ debug!("Menu action: {:?}", action); 184→ 185→ match action { 186→ MenuAction::ForceCheckin => { 187→ let tx = self.request_tx.clone(); 188→ self.runtime.spawn(async move { 189→ if let Err(e) = tx.send(IpcRequest::ForceCheckin).await { 190→ error!("Failed to send force checkin: {}", e); 191→ } 192→ }); 193→ } 194→ MenuAction::ViewLogs => { 195→ // Open log file location 196→ #[cfg(windows)] 197→ { 198→ let _ = std::process::Command::new("explorer") 199→ .arg(r"C:\ProgramData\GuruRMM\logs") 200→ .spawn(); 201→ } 202→ } 203→ MenuAction::OpenDashboard => { 204→ let policy = self.runtime.block_on(async { 205→ self.policy.read().await.clone() 206→ }); 207→ if let Some(url) = policy.dashboard_url { 208→ let _ = open::that(&url); 209→ } 210→ } 211→ MenuAction::StopAgent => { 212→ // Show confirmation dialog before stopping 213→ // For now, just send the request 214→ let tx = self.request_tx.clone(); 215→ self.runtime.spawn(async move { 216→ if let Err(e) = tx.send(IpcRequest::StopAgent).await { 217→ error!("Failed to send stop agent: {}", e); 218→ } 219→ }); 220→ } 221→ MenuAction::ExitTray => { 222→ info!("Exit requested"); 223→ event_loop.exit(); 224→ } 225→ MenuAction::Unknown(id) => { 226→ warn!("Unknown menu action: {}", id); 227→ } 228→ } 229→ } 230→ 231→ // Periodically update icon and menu based on state 232→ let (conn_state, status, policy) = self.runtime.block_on(async { 233→ let c = *self.connection_state.read().await; 234→ let s = self.status.read().await.clone(); 235→ let p = self.policy.read().await.clone(); 236→ (c, s, p) 237→ }); 238→ 239→ // Determine icon state 240→ let icon_state = match conn_state { 241→ ConnectionState::Connected if status.connected => IconState::Connected, 242→ ConnectionState::Connected => IconState::Error, 243→ ConnectionState::Connecting => IconState::Reconnecting, 244→ ConnectionState::Disconnected => IconState::Disabled, 245→ }; 246→ 247→ // Update icon if state changed 248→ if icon_state != last_icon_state { 249→ last_icon_state = icon_state; 250→ if let Ok(new_icon) = create_icon(icon_state) { 251→ if let Err(e) = tray_icon.set_icon(Some(new_icon)) { 252→ warn!("Failed to update icon: {}", e); 253→ } 254→ } 255→ 256→ // Update tooltip 257→ let tooltip = match icon_state { 258→ IconState::Connected => format!("GuruRMM - Connected to {}", 259→ status.server_url.split('/').nth(2).unwrap_or(&status.server_url)), 260→ IconState::Reconnecting => "GuruRMM - Reconnecting...".to_string(), 261→ IconState::Error => format!("GuruRMM - Error: {}", 262→ status.error.as_deref().unwrap_or("Unknown")), 263→ IconState::Disabled => "GuruRMM - Disabled".to_string(), 264→ }; 265→ let _ = tray_icon.set_tooltip(Some(&tooltip)); 266→ } 267→ 268→ // Rebuild menu if connection state changed 269→ if status.connected != last_connected { 270→ last_connected = status.connected; 271→ let new_menu = menu::build_menu(&status, &policy); 272→ tray_icon.set_menu(Some(Box::new(new_menu))); 273→ } 274→ })?; 275→ 276→ Ok(()) 277→ } 278→} 279→ 280→/// Create an icon for the given state 281→fn create_icon(state: IconState) -> Result { 282→ let data = match state { 283→ IconState::Connected => icons::connected(), 284→ IconState::Reconnecting => icons::reconnecting(), 285→ IconState::Error => icons::error(), 286→ IconState::Disabled => icons::disabled(), 287→ }; 288→ 289→ Icon::from_rgba(data, 32, 32).context("Failed to create icon") 290→} 291→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.