use tauri_plugin_shell::ShellExt; use tauri::Manager; use tauri::{RunEvent, WindowEvent}; // Store backend process handle and logs globally use std::sync::Mutex; use std::collections::VecDeque; static BACKEND_PROCESS: Mutex> = Mutex::new(None); static BACKEND_LOGS: Mutex> = Mutex::new(VecDeque::new()); static BACKEND_STARTING: Mutex = Mutex::new(false); // Helper function to add log entry fn add_log(message: String) { let mut logs = BACKEND_LOGS.lock().unwrap(); logs.push_back(format!("{}: {}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), message)); // Keep only last 100 log entries if logs.len() > 100 { logs.pop_front(); } println!("{}", message); // Also print to console } // Helper function to reset starting flag fn reset_starting_flag() { let mut starting_guard = BACKEND_STARTING.lock().unwrap(); *starting_guard = false; } // Command to start the backend with bundled JRE #[tauri::command] async fn start_backend(app: tauri::AppHandle) -> Result { add_log("๐Ÿš€ start_backend() called - Attempting to start backend with bundled JRE...".to_string()); // Check if backend is already running or starting { let process_guard = BACKEND_PROCESS.lock().unwrap(); if process_guard.is_some() { add_log("โš ๏ธ Backend process already running, skipping start".to_string()); return Ok("Backend already running".to_string()); } } // Check and set starting flag to prevent multiple simultaneous starts { let mut starting_guard = BACKEND_STARTING.lock().unwrap(); if *starting_guard { add_log("โš ๏ธ Backend already starting, skipping duplicate start".to_string()); return Ok("Backend startup already in progress".to_string()); } *starting_guard = true; } // Use Tauri's resource API to find the bundled JRE and JAR let resource_dir = app.path().resource_dir().map_err(|e| { let error_msg = format!("โŒ Failed to get resource directory: {}", e); add_log(error_msg.clone()); reset_starting_flag(); error_msg })?; add_log(format!("๐Ÿ” Resource directory: {:?}", resource_dir)); // Find the bundled JRE let jre_dir = resource_dir.join("runtime").join("jre"); let java_executable = if cfg!(windows) { jre_dir.join("bin").join("java.exe") } else { jre_dir.join("bin").join("java") }; if !java_executable.exists() { let error_msg = format!("โŒ Bundled JRE not found at: {:?}", java_executable); add_log(error_msg.clone()); reset_starting_flag(); return Err(error_msg); } add_log(format!("โœ… Found bundled JRE: {:?}", java_executable)); // Find the Stirling-PDF JAR let libs_dir = resource_dir.join("libs"); let mut jar_files: Vec<_> = std::fs::read_dir(&libs_dir) .map_err(|e| { let error_msg = format!("Failed to read libs directory: {}. Make sure the JAR is copied to libs/", e); add_log(error_msg.clone()); reset_starting_flag(); error_msg })? .filter_map(|entry| entry.ok()) .filter(|entry| { let path = entry.path(); // Match any .jar file containing "stirling-pdf" (case-insensitive) path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false) && path.file_name() .and_then(|f| f.to_str()) .map(|name| name.to_ascii_lowercase().contains("stirling-pdf")) .unwrap_or(false) }) .collect(); if jar_files.is_empty() { let error_msg = "No Stirling-PDF JAR found in libs directory.".to_string(); add_log(error_msg.clone()); reset_starting_flag(); return Err(error_msg); } // Sort by filename to get the latest version (case-insensitive) jar_files.sort_by(|a, b| { let name_a = a.file_name().to_string_lossy().to_ascii_lowercase(); let name_b = b.file_name().to_string_lossy().to_ascii_lowercase(); name_b.cmp(&name_a) // Reverse order to get latest first }); let jar_path = jar_files[0].path(); add_log(format!("๐Ÿ“‹ Selected JAR: {:?}", jar_path.file_name().unwrap())); // Normalize the paths to remove Windows UNC prefix \\?\ let normalized_java_path = if cfg!(windows) { let path_str = java_executable.to_string_lossy(); if path_str.starts_with(r"\\?\") { std::path::PathBuf::from(&path_str[4..]) // Remove \\?\ prefix } else { java_executable.clone() } } else { java_executable.clone() }; let normalized_jar_path = if cfg!(windows) { let path_str = jar_path.to_string_lossy(); if path_str.starts_with(r"\\?\") { std::path::PathBuf::from(&path_str[4..]) // Remove \\?\ prefix } else { jar_path.clone() } } else { jar_path.clone() }; add_log(format!("๐Ÿ“ฆ Found JAR file: {:?}", jar_path)); add_log(format!("๐Ÿ“ฆ Normalized JAR path: {:?}", normalized_jar_path)); add_log(format!("๐Ÿ“ฆ Normalized Java path: {:?}", normalized_java_path)); // Log the equivalent command for external testing let java_command = format!( "\"{}\" -Xmx2g -DBROWSER_OPEN=false -DSTIRLING_PDF_DESKTOP_UI=false -jar \"{}\"", normalized_java_path.display(), normalized_jar_path.display() ); add_log(format!("๐Ÿ”ง Equivalent command: {}", java_command)); // Create Java command with bundled JRE using normalized paths // Configure logging to write outside src-tauri to prevent dev server restarts let temp_dir = std::env::temp_dir(); let log_dir = temp_dir.join("stirling-pdf-logs"); std::fs::create_dir_all(&log_dir).ok(); // Create log directory if it doesn't exist let sidecar_command = app .shell() .command(normalized_java_path.to_str().unwrap()) .args([ "-Xmx2g", "-DBROWSER_OPEN=false", "-DSTIRLING_PDF_DESKTOP_UI=false", "-DSTIRLING_PDF_TAURI_MODE=true", &format!("-Dlogging.file.path={}", log_dir.display()), "-Dlogging.file.name=stirling-pdf.log", "-jar", normalized_jar_path.to_str().unwrap() ]) .env("TAURI_PARENT_PID", std::process::id().to_string()); add_log("โš™๏ธ Starting backend with bundled JRE...".to_string()); let (mut rx, child) = sidecar_command .spawn() .map_err(|e| { let error_msg = format!("โŒ Failed to spawn sidecar: {}", e); add_log(error_msg.clone()); reset_starting_flag(); error_msg })?; // Store the process handle { let mut process_guard = BACKEND_PROCESS.lock().unwrap(); *process_guard = Some(child); } add_log("โœ… Backend started with bundled JRE, monitoring output...".to_string()); // Listen to sidecar output for debugging tokio::spawn(async move { let mut _startup_detected = false; let mut error_count = 0; while let Some(event) = rx.recv().await { match event { tauri_plugin_shell::process::CommandEvent::Stdout(output) => { let output_str = String::from_utf8_lossy(&output); add_log(format!("๐Ÿ“ค Backend stdout: {}", output_str)); // Look for startup indicators if output_str.contains("Started SPDFApplication") || output_str.contains("Navigate to "){ _startup_detected = true; add_log(format!("๐ŸŽ‰ Backend startup detected: {}", output_str)); } // Look for port binding if output_str.contains("8080") { add_log(format!("๐Ÿ”Œ Port 8080 related output: {}", output_str)); } } tauri_plugin_shell::process::CommandEvent::Stderr(output) => { let output_str = String::from_utf8_lossy(&output); add_log(format!("๐Ÿ“ฅ Backend stderr: {}", output_str)); // Look for error indicators if output_str.contains("ERROR") || output_str.contains("Exception") || output_str.contains("FATAL") { error_count += 1; add_log(format!("โš ๏ธ Backend error #{}: {}", error_count, output_str)); } // Look for specific common issues if output_str.contains("Address already in use") { add_log("๐Ÿšจ CRITICAL: Port 8080 is already in use by another process!".to_string()); } if output_str.contains("java.lang.ClassNotFoundException") { add_log("๐Ÿšจ CRITICAL: Missing Java dependencies!".to_string()); } if output_str.contains("java.io.FileNotFoundException") { add_log("๐Ÿšจ CRITICAL: Required file not found!".to_string()); } } tauri_plugin_shell::process::CommandEvent::Error(error) => { add_log(format!("โŒ Backend process error: {}", error)); } tauri_plugin_shell::process::CommandEvent::Terminated(payload) => { add_log(format!("๐Ÿ’€ Backend terminated with code: {:?}", payload.code)); if let Some(code) = payload.code { match code { 0 => println!("โœ… Process terminated normally"), 1 => println!("โŒ Process terminated with generic error"), 2 => println!("โŒ Process terminated due to misuse"), 126 => println!("โŒ Command invoked cannot execute"), 127 => println!("โŒ Command not found"), 128 => println!("โŒ Invalid exit argument"), 130 => println!("โŒ Process terminated by Ctrl+C"), _ => println!("โŒ Process terminated with code: {}", code), } } // Clear the stored process handle let mut process_guard = BACKEND_PROCESS.lock().unwrap(); *process_guard = None; } _ => { println!("๐Ÿ” Unknown command event: {:?}", event); } } } if error_count > 0 { println!("โš ๏ธ Backend process ended with {} errors detected", error_count); } }); // Wait for the backend to start println!("โณ Waiting for backend startup..."); tokio::time::sleep(std::time::Duration::from_millis(10000)).await; // Reset the starting flag since startup is complete reset_starting_flag(); add_log("โœ… Backend startup sequence completed, starting flag cleared".to_string()); Ok("Backend startup initiated successfully with bundled JRE".to_string()) } // Command to check if backend is healthy #[tauri::command] async fn check_backend_health() -> Result { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; match client.get("http://localhost:8080/api/v1/info/status").send().await { Ok(response) => { let status = response.status(); println!("๐Ÿ’“ Health check response status: {}", status); if status.is_success() { match response.text().await { Ok(_body) => { Ok(true) } Err(e) => { println!("โš ๏ธ Failed to read health response: {}", e); Ok(false) } } } else { println!("โš ๏ธ Health check failed with status: {}", status); Ok(false) } } Err(e) => { println!("โŒ Health check error: {}", e); Ok(false) } } } // Command to get opened file path (if app was launched with a file) #[tauri::command] async fn get_opened_file() -> Result, String> { // Get command line arguments let args: Vec = std::env::args().collect(); // Look for a PDF file argument (skip the first arg which is the executable) for arg in args.iter().skip(1) { if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() { add_log(format!("๐Ÿ“‚ PDF file opened: {}", arg)); return Ok(Some(arg.clone())); } } Ok(None) } // Command to check bundled runtime and JAR #[tauri::command] async fn check_jar_exists(app: tauri::AppHandle) -> Result { println!("๐Ÿ” Checking for bundled JRE and JAR files..."); if let Ok(resource_dir) = app.path().resource_dir() { let mut status_parts = Vec::new(); // Check bundled JRE let jre_dir = resource_dir.join("runtime").join("jre"); let java_executable = if cfg!(windows) { jre_dir.join("bin").join("java.exe") } else { jre_dir.join("bin").join("java") }; if java_executable.exists() { status_parts.push("โœ… Bundled JRE found".to_string()); } else { status_parts.push("โŒ Bundled JRE not found".to_string()); } // Check JAR files let libs_dir = resource_dir.join("libs"); if libs_dir.exists() { match std::fs::read_dir(&libs_dir) { Ok(entries) => { let jar_files: Vec = entries .filter_map(|entry| entry.ok()) .filter(|entry| { let path = entry.path(); // Match any .jar file containing "stirling-pdf" (case-insensitive) path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false) && path.file_name() .and_then(|f| f.to_str()) .map(|name| name.to_ascii_lowercase().contains("stirling-pdf")) .unwrap_or(false) }) .map(|entry| entry.file_name().to_string_lossy().to_string()) .collect(); if !jar_files.is_empty() { status_parts.push(format!("โœ… Found JAR files: {:?}", jar_files)); } else { status_parts.push("โŒ No Stirling-PDF JAR files found".to_string()); } } Err(e) => { status_parts.push(format!("โŒ Failed to read libs directory: {}", e)); } } } else { status_parts.push("โŒ Libs directory not found".to_string()); } Ok(status_parts.join("\n")) } else { Ok("โŒ Could not access bundled resources".to_string()) } } // Cleanup function to stop backend on app exit fn cleanup_backend() { let mut process_guard = BACKEND_PROCESS.lock().unwrap(); if let Some(child) = process_guard.take() { let pid = child.pid(); add_log(format!("๐Ÿงน App shutting down, cleaning up backend process (PID: {})", pid)); match child.kill() { Ok(_) => { add_log(format!("โœ… Backend process (PID: {}) terminated during cleanup", pid)); } Err(e) => { add_log(format!("โŒ Failed to terminate backend process during cleanup: {}", e)); println!("โŒ Failed to terminate backend process during cleanup: {}", e); } } } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_fs::init()) .setup(|_app| {Ok(())}) .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, check_jar_exists, get_opened_file]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { match event { RunEvent::ExitRequested { .. } => { add_log("๐Ÿ”„ App exit requested, cleaning up...".to_string()); cleanup_backend(); // Use Tauri's built-in cleanup app_handle.cleanup_before_exit(); } RunEvent::WindowEvent { event: WindowEvent::CloseRequested {.. }, .. } => { add_log("๐Ÿ”„ Window close requested, cleaning up...".to_string()); cleanup_backend(); // Allow the window to close } _ => {} } }); }