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:
ConnorYoh
2025-11-05 11:44:59 +00:00
committed by GitHub
parent f3eed4428d
commit 4c0c9b28ef
120 changed files with 11005 additions and 1294 deletions

View File

@@ -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'
}

View File

@@ -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"))) {

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

View File

@@ -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
// }
}