main.rs (13077B)
1 use std::{ 2 error::Error, 3 io, 4 sync::{Mutex, OnceLock}, 5 }; 6 7 use tauri::{AppHandle, State, WindowEvent}; 8 use tauri_plugin_shell::{ShellExt, process::CommandChild}; 9 use tauri_plugin_updater::{Update, UpdaterExt}; 10 11 #[cfg(any(target_os = "macos", target_os = "windows"))] 12 use tauri::{ 13 Manager, 14 menu::{Menu, MenuItem}, 15 tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, 16 }; 17 18 struct IunaSidecar(Mutex<Option<CommandChild>>); 19 struct IunaSleepInhibitor(Mutex<Option<SleepInhibitor>>); 20 struct PendingDesktopUpdate(Mutex<Option<Update>>); 21 22 static SIDECAR: OnceLock<IunaSidecar> = OnceLock::new(); 23 static SLEEP_INHIBITOR: OnceLock<IunaSleepInhibitor> = OnceLock::new(); 24 25 fn main() { 26 let updater = tauri_plugin_updater::Builder::new() 27 .pubkey(include_str!("../../config/update-signing.key.pub").trim()); 28 tauri::Builder::default() 29 .plugin(tauri_plugin_shell::init()) 30 .plugin(updater.build()) 31 .manage(PendingDesktopUpdate(Mutex::new(None))) 32 .invoke_handler(tauri::generate_handler![ 33 check_desktop_update, 34 install_desktop_update 35 ]) 36 .setup(|app| { 37 #[cfg(any(target_os = "macos", target_os = "windows"))] 38 setup_desktop_tray(app)?; 39 40 let (mut events, child) = app 41 .shell() 42 .sidecar("iuna-sidecar") 43 .map_err(setup_error)? 44 .spawn() 45 .map_err(setup_error)?; 46 47 keep_system_awake_while_node_runs(); 48 49 tauri::async_runtime::spawn(async move { while events.recv().await.is_some() {} }); 50 51 let sidecar = SIDECAR.get_or_init(|| IunaSidecar(Mutex::new(None))); 52 *sidecar.0.lock().expect("sidecar mutex poisoned") = Some(child); 53 54 Ok(()) 55 }) 56 .on_window_event(|_window, event| { 57 #[cfg(any(target_os = "macos", target_os = "windows"))] 58 if let WindowEvent::CloseRequested { api, .. } = event { 59 api.prevent_close(); 60 if let Err(error) = _window.hide() { 61 eprintln!("iuna desktop could not hide its window: {error}"); 62 } 63 } 64 65 #[cfg(not(any(target_os = "macos", target_os = "windows")))] 66 if matches!(event, WindowEvent::CloseRequested { .. }) { 67 stop_sidecar(); 68 } 69 }) 70 .build(tauri::generate_context!()) 71 .expect("error while building iuna desktop") 72 .run(|_app, event| { 73 #[cfg(target_os = "macos")] 74 if matches!( 75 &event, 76 tauri::RunEvent::Reopen { 77 has_visible_windows: false, 78 .. 79 } 80 ) { 81 show_main_window(_app); 82 } 83 84 if matches!( 85 event, 86 tauri::RunEvent::Exit | tauri::RunEvent::ExitRequested { .. } 87 ) { 88 stop_sidecar(); 89 } 90 }); 91 } 92 93 #[tauri::command] 94 async fn check_desktop_update( 95 app: AppHandle, 96 pending: State<'_, PendingDesktopUpdate>, 97 ) -> Result<Option<String>, String> { 98 let update = app 99 .updater() 100 .map_err(|error| error.to_string())? 101 .check() 102 .await 103 .map_err(|error| error.to_string())?; 104 let version = update.as_ref().map(|update| update.version.clone()); 105 *pending.0.lock().map_err(|_| "updater mutex poisoned")? = update; 106 Ok(version) 107 } 108 109 #[tauri::command] 110 async fn install_desktop_update( 111 app: AppHandle, 112 pending: State<'_, PendingDesktopUpdate>, 113 ) -> Result<(), String> { 114 let update = pending 115 .0 116 .lock() 117 .map_err(|_| "updater mutex poisoned")? 118 .take() 119 .ok_or_else(|| "no verified desktop update is ready".to_string())?; 120 let bytes = update 121 .download(|_, _| {}, || {}) 122 .await 123 .map_err(|error| error.to_string())?; 124 125 stop_sidecar(); 126 update.install(bytes).map_err(|error| error.to_string())?; 127 128 #[cfg(not(target_os = "windows"))] 129 app.restart(); 130 #[cfg(target_os = "windows")] 131 Ok(()) 132 } 133 134 #[cfg(any(target_os = "macos", target_os = "windows"))] 135 fn setup_desktop_tray(app: &tauri::App) -> tauri::Result<()> { 136 const OPEN_MENU_ID: &str = "open-iuna"; 137 const QUIT_MENU_ID: &str = "quit-iuna"; 138 139 let open = MenuItem::with_id(app, OPEN_MENU_ID, "Open iuna", true, None::<&str>)?; 140 let quit = MenuItem::with_id(app, QUIT_MENU_ID, "Quit iuna", true, None::<&str>)?; 141 let menu = Menu::with_items(app, &[&open, &quit])?; 142 143 let mut tray = TrayIconBuilder::new() 144 .menu(&menu) 145 .tooltip("iuna — node running") 146 .show_menu_on_left_click(cfg!(target_os = "macos")) 147 .on_menu_event(|app, event| match event.id().as_ref() { 148 OPEN_MENU_ID => show_main_window(app), 149 QUIT_MENU_ID => app.exit(0), 150 _ => {} 151 }) 152 .on_tray_icon_event(|tray, event| { 153 if cfg!(target_os = "windows") 154 && matches!( 155 event, 156 TrayIconEvent::Click { 157 button: MouseButton::Left, 158 button_state: MouseButtonState::Up, 159 .. 160 } 161 ) 162 { 163 show_main_window(tray.app_handle()); 164 } 165 }); 166 167 tray = tray 168 .icon(tauri::include_image!("./icons/tray-icon.png")) 169 .icon_as_template(cfg!(target_os = "macos")); 170 171 tray.build(app)?; 172 Ok(()) 173 } 174 175 #[cfg(any(target_os = "macos", target_os = "windows"))] 176 fn show_main_window(app: &tauri::AppHandle) { 177 let Some(window) = app.get_webview_window("main") else { 178 eprintln!("iuna desktop could not find its main window"); 179 return; 180 }; 181 182 if let Err(error) = window 183 .show() 184 .and_then(|_| window.unminimize()) 185 .and_then(|_| window.set_focus()) 186 { 187 eprintln!("iuna desktop could not show its window: {error}"); 188 } 189 } 190 191 fn stop_sidecar() { 192 if let Some(sidecar) = SIDECAR.get() { 193 if let Some(child) = sidecar.0.lock().expect("sidecar mutex poisoned").take() { 194 let _ = child.kill(); 195 } 196 } 197 release_system_awake_inhibitor(); 198 } 199 200 fn setup_error(error: impl Error + Send + Sync + 'static) -> Box<dyn Error> { 201 Box::new(io::Error::other(error)) 202 } 203 204 fn keep_system_awake_while_node_runs() { 205 match SleepInhibitor::acquire() { 206 Ok(inhibitor) => { 207 let store = SLEEP_INHIBITOR.get_or_init(|| IunaSleepInhibitor(Mutex::new(None))); 208 *store.0.lock().expect("sleep inhibitor mutex poisoned") = Some(inhibitor); 209 } 210 Err(error) => { 211 eprintln!("iuna desktop could not prevent system sleep: {error}"); 212 } 213 } 214 } 215 216 fn release_system_awake_inhibitor() { 217 let Some(store) = SLEEP_INHIBITOR.get() else { 218 return; 219 }; 220 let _ = store 221 .0 222 .lock() 223 .expect("sleep inhibitor mutex poisoned") 224 .take(); 225 } 226 227 #[cfg(target_os = "macos")] 228 struct SleepInhibitor { 229 assertion_id: u32, 230 } 231 232 #[cfg(target_os = "macos")] 233 impl SleepInhibitor { 234 fn acquire() -> io::Result<Self> { 235 macos_sleep::prevent_idle_system_sleep("iuna node is running") 236 .map(|assertion_id| Self { assertion_id }) 237 } 238 } 239 240 #[cfg(target_os = "macos")] 241 impl Drop for SleepInhibitor { 242 fn drop(&mut self) { 243 let _ = macos_sleep::release_assertion(self.assertion_id); 244 } 245 } 246 247 #[cfg(target_os = "macos")] 248 mod macos_sleep { 249 use std::{ 250 ffi::{CString, c_char, c_void}, 251 io, ptr, 252 }; 253 254 type CFStringRef = *const c_void; 255 type IOReturn = i32; 256 257 const K_CF_STRING_ENCODING_UTF8: u32 = 0x0800_0100; 258 const K_IOPM_ASSERTION_LEVEL_ON: u32 = 255; 259 const K_IO_RETURN_SUCCESS: IOReturn = 0; 260 261 #[link(name = "CoreFoundation", kind = "framework")] 262 unsafe extern "C" { 263 fn CFStringCreateWithCString( 264 alloc: *const c_void, 265 c_str: *const c_char, 266 encoding: u32, 267 ) -> CFStringRef; 268 fn CFRelease(cf: *const c_void); 269 } 270 271 #[link(name = "IOKit", kind = "framework")] 272 unsafe extern "C" { 273 fn IOPMAssertionCreateWithName( 274 assertion_type: CFStringRef, 275 assertion_level: u32, 276 assertion_name: CFStringRef, 277 assertion_id: *mut u32, 278 ) -> IOReturn; 279 fn IOPMAssertionRelease(assertion_id: u32) -> IOReturn; 280 } 281 282 pub fn prevent_idle_system_sleep(reason: &str) -> io::Result<u32> { 283 let assertion_type = cf_string("PreventUserIdleSystemSleep")?; 284 let assertion_name = cf_string(reason)?; 285 let mut assertion_id = 0_u32; 286 let result = unsafe { 287 IOPMAssertionCreateWithName( 288 assertion_type, 289 K_IOPM_ASSERTION_LEVEL_ON, 290 assertion_name, 291 &mut assertion_id, 292 ) 293 }; 294 unsafe { 295 CFRelease(assertion_type); 296 CFRelease(assertion_name); 297 } 298 if result == K_IO_RETURN_SUCCESS { 299 Ok(assertion_id) 300 } else { 301 Err(io::Error::other(format!( 302 "IOPMAssertionCreateWithName failed with status {result}" 303 ))) 304 } 305 } 306 307 pub fn release_assertion(assertion_id: u32) -> io::Result<()> { 308 let result = unsafe { IOPMAssertionRelease(assertion_id) }; 309 if result == K_IO_RETURN_SUCCESS { 310 Ok(()) 311 } else { 312 Err(io::Error::other(format!( 313 "IOPMAssertionRelease failed with status {result}" 314 ))) 315 } 316 } 317 318 fn cf_string(value: &str) -> io::Result<CFStringRef> { 319 let value = CString::new(value) 320 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; 321 let cf_string = unsafe { 322 CFStringCreateWithCString(ptr::null(), value.as_ptr(), K_CF_STRING_ENCODING_UTF8) 323 }; 324 if cf_string.is_null() { 325 Err(io::Error::other("CFStringCreateWithCString returned null")) 326 } else { 327 Ok(cf_string) 328 } 329 } 330 } 331 332 #[cfg(target_os = "windows")] 333 struct SleepInhibitor; 334 335 #[cfg(target_os = "windows")] 336 impl SleepInhibitor { 337 fn acquire() -> io::Result<Self> { 338 windows_sleep::prevent_idle_system_sleep()?; 339 Ok(Self) 340 } 341 } 342 343 #[cfg(target_os = "windows")] 344 impl Drop for SleepInhibitor { 345 fn drop(&mut self) { 346 let _ = windows_sleep::clear_sleep_requirement(); 347 } 348 } 349 350 #[cfg(target_os = "windows")] 351 mod windows_sleep { 352 use std::io; 353 354 const ES_CONTINUOUS: u32 = 0x8000_0000; 355 const ES_SYSTEM_REQUIRED: u32 = 0x0000_0001; 356 357 #[link(name = "kernel32")] 358 unsafe extern "system" { 359 fn SetThreadExecutionState(es_flags: u32) -> u32; 360 } 361 362 pub fn prevent_idle_system_sleep() -> io::Result<()> { 363 set_execution_state(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) 364 } 365 366 pub fn clear_sleep_requirement() -> io::Result<()> { 367 set_execution_state(ES_CONTINUOUS) 368 } 369 370 fn set_execution_state(flags: u32) -> io::Result<()> { 371 let previous = unsafe { SetThreadExecutionState(flags) }; 372 if previous == 0 { 373 Err(io::Error::last_os_error()) 374 } else { 375 Ok(()) 376 } 377 } 378 } 379 380 #[cfg(target_os = "linux")] 381 struct SleepInhibitor { 382 child: Option<std::process::Child>, 383 } 384 385 #[cfg(target_os = "linux")] 386 impl SleepInhibitor { 387 fn acquire() -> io::Result<Self> { 388 let parent_pid = std::process::id().to_string(); 389 let child = std::process::Command::new("systemd-inhibit") 390 .args([ 391 "--what=sleep", 392 "--mode=block", 393 "--why=iuna node is running", 394 "sh", 395 "-c", 396 "trap 'exit 0' TERM; while kill -0 \"$1\" 2>/dev/null; do sleep 30 & wait $!; done", 397 "iuna-sleep-inhibitor", 398 &parent_pid, 399 ]) 400 .stdin(std::process::Stdio::null()) 401 .stdout(std::process::Stdio::null()) 402 .stderr(std::process::Stdio::null()) 403 .spawn()?; 404 Ok(Self { child: Some(child) }) 405 } 406 } 407 408 #[cfg(target_os = "linux")] 409 impl Drop for SleepInhibitor { 410 fn drop(&mut self) { 411 if let Some(mut child) = self.child.take() { 412 let _ = child.kill(); 413 let _ = child.wait(); 414 } 415 } 416 } 417 418 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] 419 struct SleepInhibitor; 420 421 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] 422 impl SleepInhibitor { 423 fn acquire() -> io::Result<Self> { 424 Ok(Self) 425 } 426 } 427 428 #[cfg(test)] 429 mod tests { 430 #[test] 431 fn tray_icon_contains_rgba8_pixels_for_its_declared_dimensions() { 432 let icon = tauri::include_image!("./icons/tray-icon.png"); 433 let expected_bytes = icon.width() as usize * icon.height() as usize * 4; 434 435 assert_eq!(icon.rgba().len(), expected_bytes); 436 assert!(icon.rgba().chunks_exact(4).any(|pixel| pixel[3] > 0)); 437 } 438 }