mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 07:35:22 +00:00
Cleanup backend if orphaned in tauri mode
This commit is contained in:
parent
6a75ecc6ae
commit
bc3a85daea
@ -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
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user