use std::sync::Arc; use eframe::egui; use tracing::info; use tracing_subscriber::EnvFilter; use std::fs::{File, OpenOptions, TryLockError}; use doctate_desktop::app::DoctateApp; use doctate_desktop::paths::{cases_dir, lock_file_path, pending_dir, snapshot_cache_path}; /// Unwrap a startup `Result` or print `context: {err}` to stderr and /// `exit(1)`. Used for unrecoverable bootstrap failures before the /// egui event loop runs — there is no UI to show an error in. fn or_die(result: Result, context: &str) -> T { match result { Ok(v) => v, Err(e) => { eprintln!("{context}: {e}"); std::process::exit(1); } } } fn main() -> eframe::Result { tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("doctate_desktop=info")), ) .init(); info!("starting doctate-desktop"); let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build() .expect("build tokio runtime"), ); let pending = or_die(pending_dir(), "cannot determine pending directory"); // Single-instance guard. The lock is held by the `_lock_file` binding // for the entire `main` lifetime — OS releases it automatically on // process exit or crash, so a stale lock file after a crash is // harmless (the next launch re-acquires). let _lock_file = acquire_single_instance_lock(); let cases = or_die(cases_dir(), "cannot determine cases directory"); let cache_path = or_die( snapshot_cache_path(), "cannot determine snapshot cache path", ); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([400.0, 560.0]) .with_min_inner_size([320.0, 400.0]) .with_resizable(true), ..Default::default() }; eframe::run_native( "doctate", options, Box::new(move |_cc| { let app = DoctateApp::new(runtime, pending, cases, cache_path); Ok(Box::new(app) as Box) }), ) } /// Open and exclusively lock a per-user `client.lock`. Returns the /// handle; dropping it (process exit) releases the lock. If another /// instance already holds it, prints a friendly message and exits. fn acquire_single_instance_lock() -> File { let path = or_die(lock_file_path(), "cannot determine lock file path"); if let Some(parent) = path.parent() && let Err(e) = std::fs::create_dir_all(parent) { eprintln!("cannot create data directory {}: {e}", parent.display()); std::process::exit(1); } let file = match OpenOptions::new() .create(true) .read(true) .write(true) .truncate(false) .open(&path) { Ok(f) => f, Err(e) => { eprintln!("cannot open lock file {}: {e}", path.display()); std::process::exit(1); } }; match file.try_lock() { Ok(()) => file, Err(TryLockError::WouldBlock) => { eprintln!( "doctate läuft bereits (Lock hält auf {}). Nur eine Instanz pro User zulässig.", path.display() ); std::process::exit(1); } Err(TryLockError::Error(e)) => { eprintln!("lock acquire failed on {}: {e}", path.display()); std::process::exit(1); } } }