From bc3a85daeab67c38065e1a765bfad70fc45b21b2 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Tue, 24 Jun 2025 15:18:27 +0100 Subject: [PATCH] Cleanup backend if orphaned in tauri mode --- frontend/src-tauri/src/lib.rs | 85 +++++++++- .../software/SPDF/SPDFApplication.java | 8 + .../SPDF/config/TauriProcessMonitor.java | 157 ++++++++++++++++++ 3 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 2228de4a7..e5745399f 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,12 +1,13 @@ 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::sync::Arc; use std::collections::VecDeque; -static BACKEND_PROCESS: Mutex>> = Mutex::new(None); +static BACKEND_PROCESS: Mutex> = Mutex::new(None); static BACKEND_LOGS: Mutex> = Mutex::new(VecDeque::new()); // Helper function to add log entry @@ -146,11 +147,13 @@ async fn start_backend(app: tauri::AppHandle) -> Result { "-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()); @@ -165,14 +168,14 @@ async fn start_backend(app: tauri::AppHandle) -> Result { // Store the process handle { let mut process_guard = BACKEND_PROCESS.lock().unwrap(); - *process_guard = Some(Arc::new(child)); + *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 _startup_detected = false; let mut error_count = 0; while let Some(event) = rx.recv().await { @@ -187,7 +190,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result { output_str.contains("Started on port") || output_str.contains("Netty started") || output_str.contains("Started StirlingPDF") { - startup_detected = true; + _startup_detected = true; add_log(format!("๐ŸŽ‰ Backend startup detected: {}", output_str)); } @@ -403,6 +406,37 @@ async fn test_sidecar_binary(app: tauri::AppHandle) -> Result { } } +// Command to stop the backend process +#[tauri::command] +async fn stop_backend() -> Result { + add_log("๐Ÿ›‘ stop_backend() called - Attempting to stop backend...".to_string()); + + let mut process_guard = BACKEND_PROCESS.lock().unwrap(); + match process_guard.take() { + Some(child) => { + let pid = child.pid(); + add_log(format!("๐Ÿ”„ Terminating backend process (PID: {})", pid)); + + // Kill the process + match child.kill() { + Ok(_) => { + add_log(format!("โœ… Backend process (PID: {}) terminated successfully", pid)); + Ok(format!("Backend process (PID: {}) terminated successfully", pid)) + } + Err(e) => { + let error_msg = format!("โŒ Failed to terminate backend process: {}", e); + add_log(error_msg.clone()); + Err(error_msg) + } + } + } + None => { + add_log("โš ๏ธ No backend process running to stop".to_string()); + Ok("No backend process running".to_string()) + } + } +} + // Command to check Java environment (bundled version) #[tauri::command] async fn check_java_environment(app: tauri::AppHandle) -> Result { @@ -448,6 +482,25 @@ async fn check_java_environment(app: tauri::AppHandle) -> Result } } +// 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() @@ -480,7 +533,23 @@ pub fn run() { Ok(()) }) - .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, check_jar_exists, test_sidecar_binary, get_backend_status, check_backend_port, check_java_environment, get_backend_logs]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .invoke_handler(tauri::generate_handler![start_backend, stop_backend, check_backend_health, check_jar_exists, test_sidecar_binary, get_backend_status, check_backend_port, check_java_environment, get_backend_logs]) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app_handle, event| { + match event { + RunEvent::ExitRequested { api, .. } => { + 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 { api, .. }, .. } => { + add_log("๐Ÿ”„ Window close requested, cleaning up...".to_string()); + cleanup_backend(); + // Allow the window to close + } + _ => {} + } + }); } diff --git a/src/main/java/stirling/software/SPDF/SPDFApplication.java b/src/main/java/stirling/software/SPDF/SPDFApplication.java index 3cf89a657..449a67e1f 100644 --- a/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -144,6 +144,14 @@ public class SPDFApplication { baseUrlStatic = this.baseUrl; contextPathStatic = this.contextPath; String url = baseUrl + ":" + getStaticPort() + contextPath; + + // Log Tauri mode information + if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) { + String parentPid = System.getenv("TAURI_PARENT_PID"); + log.info( + "Running in Tauri mode. Parent process PID: {}", + parentPid != null ? parentPid : "not set"); + } if (webBrowser != null && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { webBrowser.initWebUI(url); diff --git a/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java b/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java new file mode 100644 index 000000000..a3feded74 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java @@ -0,0 +1,157 @@ +package stirling.software.SPDF.config; + +import java.lang.management.ManagementFactory; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +/** + * Monitor for Tauri parent process to detect orphaned Java backend processes. When running in Tauri + * mode, this component periodically checks if the parent Tauri process is still alive. If the + * parent process terminates unexpectedly, this will trigger a graceful shutdown of the Java backend + * to prevent orphaned processes. + */ +@Component +@ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true") +public class TauriProcessMonitor { + + private static final Logger logger = LoggerFactory.getLogger(TauriProcessMonitor.class); + + private final ApplicationContext applicationContext; + private String parentProcessId; + private ScheduledExecutorService scheduler; + private volatile boolean monitoring = false; + + public TauriProcessMonitor(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @PostConstruct + public void init() { + parentProcessId = System.getenv("TAURI_PARENT_PID"); + + if (parentProcessId != null && !parentProcessId.trim().isEmpty()) { + logger.info("Tauri mode detected. Parent process ID: {}", parentProcessId); + startMonitoring(); + } else { + logger.warn( + "TAURI_PARENT_PID environment variable not found. Tauri process monitoring disabled."); + } + } + + private void startMonitoring() { + scheduler = + Executors.newSingleThreadScheduledExecutor( + r -> { + Thread t = new Thread(r, "tauri-process-monitor"); + t.setDaemon(true); + return t; + }); + + monitoring = true; + + // Check every 5 seconds + scheduler.scheduleAtFixedRate(this::checkParentProcess, 5, 5, TimeUnit.SECONDS); + + logger.info("Started monitoring parent Tauri process (PID: {})", parentProcessId); + } + + private void checkParentProcess() { + if (!monitoring) { + return; + } + + try { + if (!isProcessAlive(parentProcessId)) { + logger.warn( + "Parent Tauri process (PID: {}) is no longer alive. Initiating graceful shutdown...", + parentProcessId); + initiateGracefulShutdown(); + } + } catch (Exception e) { + logger.error("Error checking parent process status", e); + } + } + + private boolean isProcessAlive(String pid) { + try { + long processId = Long.parseLong(pid); + + // Check if process exists using ProcessHandle (Java 9+) + return ProcessHandle.of(processId).isPresent(); + + } catch (NumberFormatException e) { + logger.error("Invalid parent process ID format: {}", pid); + return false; + } catch (Exception e) { + logger.error("Error checking if process {} is alive", pid, e); + return false; + } + } + + private void initiateGracefulShutdown() { + monitoring = false; + + logger.info("Orphaned Java backend detected. Shutting down gracefully..."); + + // Shutdown asynchronously to avoid blocking the monitor thread + CompletableFuture.runAsync( + () -> { + try { + // Give a small delay to ensure logging completes + Thread.sleep(1000); + + if (applicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) applicationContext).close(); + } else { + // Fallback to system exit + logger.warn( + "Unable to shutdown Spring context gracefully, using System.exit"); + System.exit(0); + } + } catch (Exception e) { + logger.error("Error during graceful shutdown", e); + System.exit(1); + } + }); + } + + @PreDestroy + public void cleanup() { + monitoring = false; + + if (scheduler != null && !scheduler.isShutdown()) { + logger.info("Shutting down Tauri process monitor"); + scheduler.shutdown(); + + try { + if (!scheduler.awaitTermination(2, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** Get the current Java process ID for logging/debugging purposes */ + public static String getCurrentProcessId() { + try { + return ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + } catch (Exception e) { + return "unknown"; + } + } +}