iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }