// Prevents an extra console window on Windows in release builds. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use guara_core::service::Engine; use std::sync::Arc; struct State(Arc); /// Single bridge between the UI and the core (same methods as `guara ui` mode). #[tauri::command] async fn rpc( state: tauri::State<'_, State>, method: String, params: serde_json::Value, ) -> Result { let eng = state.0.clone(); tauri::async_runtime::spawn_blocking(move || { eng.rpc(&method, params).map_err(|e| e.to_string()) }) .await .map_err(|e| e.to_string())? } #[tauri::command] fn update_check(state: tauri::State<'_, State>) -> Result { let status = state.0.update_check().map_err(|e| e.to_string())?; serde_json::to_value(status).map_err(|e| e.to_string()) } #[tauri::command] fn update_apply(state: tauri::State<'_, State>) -> Result { state.0.update_apply().map_err(|e| e.to_string()) } /// `true` when the process already has sufficient privileges to read raw disks. /// On Windows the manifest already requires administrator privileges before launch, /// so this is always `true` here. #[tauri::command] fn is_elevated() -> bool { #[cfg(unix)] { unsafe { libc::geteuid() == 0 } } #[cfg(windows)] { true } } /// Relaunches the app with administrator privileges (Linux/macOS). On Windows the /// manifest already enforces elevation. The new instance terminates this one on startup; if /// authorization fails, this window stays open and the UI displays the reason. #[tauri::command] fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> { #[cfg(any(target_os = "linux", target_os = "macos"))] { do_relaunch_elevated(app) } #[cfg(windows)] { let _ = app; Err("o Guará já roda como administrador no Windows".into()) } } #[cfg(any(target_os = "linux", target_os = "macos"))] fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> { use tauri::Emitter; let exe = elevated_exe()?; let cfg = guara_core::settings::config_dir(); let log = std::env::temp_dir().join(format!("guara-elevate-{}.log", std::process::id())); let _ = std::fs::write(&log, b""); let out = std::fs::OpenOptions::new() .append(true) .open(&log) .map_err(|e| e.to_string())?; let err = out.try_clone().map_err(|e| e.to_string())?; let pid = std::process::id().to_string(); #[cfg(target_os = "linux")] let mut cmd = { let mut c = std::process::Command::new("pkexec"); c.arg("/usr/bin/env"); for k in [ "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND", ] { if let Ok(v) = std::env::var(k) { c.arg(format!("{k}={v}")); } } c.arg(format!("GUARA_CONFIG_DIR={}", cfg.display())); c.arg(format!("GUARA_REPLACE_PID={pid}")); c.arg(&exe); c }; #[cfg(target_os = "macos")] let mut cmd = { let shell = format!( "GUARA_CONFIG_DIR='{}' GUARA_REPLACE_PID={} '{}' >/dev/null 2>&1 &", cfg.display(), pid, exe.display() ); let script = format!( "do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\"") ); let mut c = std::process::Command::new("osascript"); c.args(["-e", &script]); c }; cmd.stdout(out).stderr(err); let mut child = cmd .spawn() .map_err(|e| format!("não foi possível pedir privilégios: {e}"))?; std::thread::spawn(move || { // On Linux, pkexec only exits with success when the elevated app closes (and the elevated // app will have already terminated this process). Any failure before that becomes a warning. let status = child.wait(); if matches!(&status, Ok(s) if s.success()) { let _ = std::fs::remove_file(&log); return; } let detail = std::fs::read_to_string(&log).unwrap_or_default(); let msg = match status.ok().and_then(|s| s.code()) { Some(126) => "Autorização cancelada.".to_string(), Some(127) => "Não foi possível autenticar como administrador.".to_string(), _ => format!( "Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim() ), }; let _ = app.emit("elevate-failed", msg); let _ = std::fs::remove_file(&log); }); Ok(()) } /// Executable path accessible to root. Inside an AppImage, `current_exe` points /// inside a user FUSE mount that root cannot access: use the /// `.AppImage` file itself (`APPIMAGE` environment variable). #[cfg(any(target_os = "linux", target_os = "macos"))] fn elevated_exe() -> Result { if let Some(p) = std::env::var_os("APPIMAGE") .map(std::path::PathBuf::from) .filter(|p| p.is_file()) { return Ok(p); } std::env::current_exe().map_err(|e| e.to_string()) } /// Serves recovered files for preview (`guara://localhost/file//`), /// with Range support for video/audio/PDF, reading directly from the source. fn serve_file(eng: &Engine, req: &tauri::http::Request>) -> tauri::http::Response> { use tauri::http::Response; if let Some(rest) = req.uri().path().strip_prefix("/thumb/") { let mut it = rest.split('/'); let (job, id) = ( it.next().unwrap_or(""), it.next().and_then(|s| s.parse::().ok()), ); let size = req .uri() .query() .and_then(|q| q.split('&').find_map(|kv| kv.strip_prefix("s="))) .and_then(|v| v.parse::().ok()) .unwrap_or(256); let err = |code: u16| { Response::builder() .status(code) .header("Access-Control-Allow-Origin", "*") .body(Vec::new()) .unwrap() }; let Some(id) = id else { return err(400) }; return match eng.thumb(job, id, size) { Ok(b) => Response::builder() .status(200) .header("Content-Type", "image/jpeg") .header("Cache-Control", "private, max-age=3600") .header("Access-Control-Allow-Origin", "*") .body(b.as_ref().clone()) .unwrap_or_else(|_| err(500)), Err(code) => err(code), }; } const CAP: u64 = 8 * 1024 * 1024; let mut it = req.uri().path().trim_start_matches("/file/").split('/'); let (job, id) = ( it.next().unwrap_or(""), it.next().and_then(|s| s.parse::().ok()), ); let err = |code: u16| { Response::builder() .status(code) .header("Access-Control-Allow-Origin", "*") .body(Vec::new()) .unwrap() }; let Some(id) = id else { return err(400) }; let range = req.headers().get("range").and_then(|v| v.to_str().ok()); let slice = match eng.file_slice(job, id, range) { Ok(s) => s, Err(code) => return err(code), }; let total = slice.total; let start = slice.start; let want = slice.len(); let mut headers = slice.headers(); let partial = slice.partial || want > CAP; let Ok(data) = slice.read_all(CAP) else { return err(500); }; let mut b = Response::builder().status(if partial { 206 } else { 200 }); headers.retain(|(k, _)| *k != "Content-Range"); for (k, v) in &headers { b = b.header(*k, v); } if partial && !data.is_empty() { b = b.header( "Content-Range", format!( "bytes {}-{}/{}", start, start + data.len() as u64 - 1, total ), ); } b.body(data).unwrap_or_else(|_| err(500)) } fn main() { let engine = Arc::new(Engine::new()); let for_protocol = engine.clone(); tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .manage(State(engine)) .register_asynchronous_uri_scheme_protocol("guara", move |_ctx, request, responder| { let eng = for_protocol.clone(); std::thread::spawn(move || responder.respond(serve_file(&eng, &request))); }) .setup(|app| { use tauri::{Listener, Manager}; // When launched elevated by the unprivileged instance, terminate the old process. #[cfg(unix)] if let Some(pid) = std::env::var("GUARA_REPLACE_PID") .ok() .and_then(|p| p.parse::().ok()) { std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_millis(1200)); // SAFETY: only sends SIGTERM to the previous process owned by the invoking user. unsafe { if libc::geteuid() == 0 { libc::kill(pid, libc::SIGTERM); } } }); } // The window starts hidden (tauri.conf.json: "visible": false) so the // unprivileged instance never flashes on screen before being replaced. // Reading raw disks requires root: request elevation as soon as the app // launches, without waiting for the user to look for a button, and only // show a window once we know which one it is — the elevated relaunch's, // or this same one if elevation turned out not to be needed/possible. #[cfg(any(target_os = "linux", target_os = "macos"))] { if unsafe { libc::geteuid() != 0 } && std::env::var("GUARA_REPLACE_PID").is_err() { use tauri::Emitter; let handle = app.handle().clone(); let handle_for_failure = app.handle().clone(); std::thread::spawn(move || { // do_relaunch_elevated only emits "elevate-failed" itself once the // child process has actually spawned and later exits non-zero. If it // fails earlier (pkexec/osascript missing, exe path unresolved, log // file unwritable), it returns Err directly without emitting anything // — emit it here too, or the hidden window would never reappear. if let Err(e) = do_relaunch_elevated(handle) { let _ = handle_for_failure.emit("elevate-failed", e); } }); // Whichever path emits it, make sure the window comes back so the // user sees the warning instead of a silently stuck, invisible app. if let Some(win) = app.get_webview_window("main") { app.handle().listen("elevate-failed", move |_| { let _ = win.show(); }); } } else if let Some(win) = app.get_webview_window("main") { let _ = win.show(); } } #[cfg(not(any(target_os = "linux", target_os = "macos")))] if let Some(win) = app.get_webview_window("main") { let _ = win.show(); } Ok(()) }) .invoke_handler(tauri::generate_handler![ rpc, is_elevated, relaunch_elevated, update_check, update_apply ]) .run(tauri::generate_context!()) .expect("erro ao iniciar o Guará"); }