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";
+ }
+ }
+}