Cleanup backend if orphaned in tauri mode

This commit is contained in:
Connor Yoh 2025-06-24 15:18:27 +01:00
parent 6a75ecc6ae
commit bc3a85daea
3 changed files with 242 additions and 8 deletions

View File

@ -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<Option<Arc<tauri_plugin_shell::process::CommandChild>>> = Mutex::new(None);
static BACKEND_PROCESS: Mutex<Option<tauri_plugin_shell::process::CommandChild>> = Mutex::new(None);
static BACKEND_LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
// Helper function to add log entry
@ -146,11 +147,13 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
"-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<String, String> {
// 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<String, String> {
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<String, String> {
}
}
// Command to stop the backend process
#[tauri::command]
async fn stop_backend() -> Result<String, String> {
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<String, String> {
@ -448,6 +482,25 @@ async fn check_java_environment(app: tauri::AppHandle) -> Result<String, 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()
@ -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
}
_ => {}
}
});
}

View File

@ -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);

View File

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