mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
V2 Tauri integration (#3854)
# Description of Changes Please provide a summary of the changes, including: ## Add PDF File Association Support for Tauri App ### 🎯 **Features Added** - PDF file association configuration in Tauri - Command line argument detection for opened files - Automatic file loading when app is launched via "Open with" - Cross-platform support (Windows/macOS) ### 🔧 **Technical Changes** - Added `fileAssociations` in `tauri.conf.json` for PDF files - New `get_opened_file` Tauri command to detect file arguments - `fileOpenService` with Tauri fs plugin integration - `useOpenedFile` hook for React integration - Improved backend health logging during startup (reduced noise) ### 🧪 **Testing** See * https://v2.tauri.app/start/prerequisites/ * [DesktopApplicationDevelopmentGuide.md](DesktopApplicationDevelopmentGuide.md) ```bash # Test file association during development: cd frontend npm install cargo tauri dev --no-watch -- -- "path/to/file.pdf" ``` For production testing: 1. Build: npm run tauri build 2. Install the built app 3. Right-click PDF → "Open with" → Stirling-PDF 🚀 User Experience - Users can now double-click PDF files to open them directly in Stirling-PDF - Files automatically load in the viewer when opened via file association - Seamless integration with OS file handling --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: James Brunton <james@stirlingpdf.com> Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
@@ -41,7 +41,6 @@ dependencies {
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|
||||
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
|
||||
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
|
||||
implementation 'me.friwi:jcefmaven:132.3.1'
|
||||
implementation 'org.openjfx:javafx-controls:21'
|
||||
implementation 'org.openjfx:javafx-swing:21'
|
||||
}
|
||||
|
||||
@@ -144,6 +144,13 @@ public class SPDFApplication {
|
||||
serverPortStatic = serverPort;
|
||||
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");
|
||||
}
|
||||
// Desktop UI initialization removed - webBrowser dependency eliminated
|
||||
// Keep backwards compatibility for STIRLING_PDF_DESKTOP_UI system property
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
@@ -16,6 +18,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
private final EndpointInterceptor endpointInterceptor;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebMvcConfig.class);
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
@@ -23,10 +27,34 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
// Only configure CORS if allowed origins are specified
|
||||
if (applicationProperties.getSystem() != null
|
||||
&& applicationProperties.getSystem().getCorsAllowedOrigins() != null
|
||||
&& !applicationProperties.getSystem().getCorsAllowedOrigins().isEmpty()) {
|
||||
// Check if running in Tauri mode
|
||||
boolean isTauriMode =
|
||||
Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"));
|
||||
|
||||
// Check if user has configured custom origins
|
||||
boolean hasConfiguredOrigins =
|
||||
applicationProperties.getSystem() != null
|
||||
&& applicationProperties.getSystem().getCorsAllowedOrigins() != null
|
||||
&& !applicationProperties.getSystem().getCorsAllowedOrigins().isEmpty();
|
||||
|
||||
if (isTauriMode) {
|
||||
// Automatically enable CORS for Tauri desktop app
|
||||
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
|
||||
logger.info("Tauri mode detected - enabling CORS for Tauri protocols (v1 and v2)");
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
"https://tauri.localhost")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
} else if (hasConfiguredOrigins) {
|
||||
// Use user-configured origins
|
||||
logger.info(
|
||||
"Configuring CORS with allowed origins: {}",
|
||||
applicationProperties.getSystem().getCorsAllowedOrigins());
|
||||
|
||||
String[] allowedOrigins =
|
||||
applicationProperties
|
||||
@@ -41,15 +69,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
// If no origins are configured, CORS is not enabled (secure by default)
|
||||
// If no origins are configured and not in Tauri mode, CORS is not enabled (secure by
|
||||
// default)
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// // Handler for external static resources - DISABLED in backend-only mode
|
||||
// registry.addResourceHandler("/**")
|
||||
// .addResourceLocations(
|
||||
// "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// // .setCachePeriod(0); // Optional: disable caching
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user