diff --git a/build-tauri-jlink.bat b/build-tauri-jlink.bat index bda181233..a790a4ae6 100644 --- a/build-tauri-jlink.bat +++ b/build-tauri-jlink.bat @@ -26,8 +26,10 @@ if errorlevel 1 ( exit /b 1 ) -REM Find the built JAR -for %%f in (build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f +REM Find the built JAR(s) +echo ▶ Listing all built JAR files in stirling-pdf\build\libs: +dir /b stirling-pdf\build\libs\Stirling-PDF-*.jar +for %%f in (stirling-pdf\build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f if not exist "%STIRLING_JAR%" ( echo ❌ No Stirling-PDF JAR found in build/libs/ exit /b 1 @@ -43,6 +45,10 @@ echo ▶ Copying JAR to Tauri libs directory... copy "%STIRLING_JAR%" "frontend\src-tauri\libs\" echo ✅ JAR copied to frontend\src-tauri\libs\ +REM Log out all JAR files now in the Tauri libs directory +echo ▶ Listing all JAR files in frontend\src-tauri\libs after copy: +dir /b frontend\src-tauri\libs\Stirling-PDF-*.jar + echo ▶ Creating custom JRE with jlink... if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre" diff --git a/build-tauri-jlink.sh b/build-tauri-jlink.sh index ae4390fa6..383171e7b 100644 --- a/build-tauri-jlink.sh +++ b/build-tauri-jlink.sh @@ -62,13 +62,13 @@ fi print_step "Building Stirling-PDF JAR..." ./gradlew clean bootJar --no-daemon -if [ ! -f "build/libs/Stirling-PDF-"*.jar ]; then +if [ ! -f "stirling-pdf/build/libs/Stirling-PDF-"*.jar ]; then print_error "Failed to build Stirling-PDF JAR" exit 1 fi # Find the built JAR -STIRLING_JAR=$(ls build/libs/Stirling-PDF-*.jar | head -n 1) +STIRLING_JAR=$(ls stirling-pdf/build/libs/Stirling-PDF-*.jar | head -n 1) print_success "Built JAR: $STIRLING_JAR" # Create directories for Tauri diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 9a9a6faa6..8b156b2d1 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -8,6 +8,7 @@ use std::collections::VecDeque; static BACKEND_PROCESS: Mutex> = Mutex::new(None); static BACKEND_LOGS: Mutex> = Mutex::new(VecDeque::new()); +static BACKEND_STARTING: Mutex = Mutex::new(false); // Helper function to add log entry fn add_log(message: String) { @@ -20,25 +21,42 @@ fn add_log(message: String) { println!("{}", message); // Also print to console } +// Helper function to reset starting flag +fn reset_starting_flag() { + let mut starting_guard = BACKEND_STARTING.lock().unwrap(); + *starting_guard = false; +} + // Command to start the backend with bundled JRE #[tauri::command] async fn start_backend(app: tauri::AppHandle) -> Result { add_log("🚀 start_backend() called - Attempting to start backend with bundled JRE...".to_string()); - // Check if backend is already running + // Check if backend is already running or starting { let process_guard = BACKEND_PROCESS.lock().unwrap(); if process_guard.is_some() { - add_log("⚠️ Backend already running, skipping start".to_string()); + add_log("⚠️ Backend process already running, skipping start".to_string()); return Ok("Backend already running".to_string()); } } + // Check and set starting flag to prevent multiple simultaneous starts + { + let mut starting_guard = BACKEND_STARTING.lock().unwrap(); + if *starting_guard { + add_log("⚠️ Backend already starting, skipping duplicate start".to_string()); + return Ok("Backend startup already in progress".to_string()); + } + *starting_guard = true; + } + // Use Tauri's resource API to find the bundled JRE and JAR let resource_dir = app.path().resource_dir().map_err(|e| { let error_msg = format!("❌ Failed to get resource directory: {}", e); add_log(error_msg.clone()); + reset_starting_flag(); error_msg })?; @@ -55,6 +73,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result { if !java_executable.exists() { let error_msg = format!("❌ Bundled JRE not found at: {:?}", java_executable); add_log(error_msg.clone()); + reset_starting_flag(); return Err(error_msg); } @@ -66,26 +85,32 @@ async fn start_backend(app: tauri::AppHandle) -> Result { .map_err(|e| { let error_msg = format!("Failed to read libs directory: {}. Make sure the JAR is copied to libs/", e); add_log(error_msg.clone()); + reset_starting_flag(); error_msg })? .filter_map(|entry| entry.ok()) .filter(|entry| { let path = entry.path(); - path.extension().and_then(|s| s.to_str()) == Some("jar") - && path.file_name().unwrap().to_string_lossy().contains("Stirling-PDF") + // Match any .jar file containing "stirling-pdf" (case-insensitive) + path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false) + && path.file_name() + .and_then(|f| f.to_str()) + .map(|name| name.to_ascii_lowercase().contains("stirling-pdf")) + .unwrap_or(false) }) .collect(); if jar_files.is_empty() { let error_msg = "No Stirling-PDF JAR found in libs directory.".to_string(); add_log(error_msg.clone()); + reset_starting_flag(); return Err(error_msg); } - // Sort by filename to get the latest version + // Sort by filename to get the latest version (case-insensitive) jar_files.sort_by(|a, b| { - let name_a = a.file_name().to_string_lossy().to_string(); - let name_b = b.file_name().to_string_lossy().to_string(); + let name_a = a.file_name().to_string_lossy().to_ascii_lowercase(); + let name_b = b.file_name().to_string_lossy().to_ascii_lowercase(); name_b.cmp(&name_a) // Reverse order to get latest first }); @@ -155,6 +180,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result { .map_err(|e| { let error_msg = format!("❌ Failed to spawn sidecar: {}", e); add_log(error_msg.clone()); + reset_starting_flag(); error_msg })?; @@ -249,6 +275,10 @@ async fn start_backend(app: tauri::AppHandle) -> Result { println!("⏳ Waiting for backend startup..."); tokio::time::sleep(std::time::Duration::from_millis(10000)).await; + // Reset the starting flag since startup is complete + reset_starting_flag(); + add_log("✅ Backend startup sequence completed, starting flag cleared".to_string()); + Ok("Backend startup initiated successfully with bundled JRE".to_string()) } @@ -319,8 +349,12 @@ async fn check_jar_exists(app: tauri::AppHandle) -> Result { .filter_map(|entry| entry.ok()) .filter(|entry| { let path = entry.path(); - path.extension().and_then(|s| s.to_str()) == Some("jar") - && path.file_name().unwrap().to_string_lossy().contains("Stirling-PDF") + // Match any .jar file containing "stirling-pdf" (case-insensitive) + path.extension().and_then(|s| s.to_str()).map(|ext| ext.eq_ignore_ascii_case("jar")).unwrap_or(false) + && path.file_name() + .and_then(|f| f.to_str()) + .map(|name| name.to_ascii_lowercase().contains("stirling-pdf")) + .unwrap_or(false) }) .map(|entry| entry.file_name().to_string_lossy().to_string()) .collect(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 94ee41fd0..39a35dd44 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider'; import React, { useEffect } from 'react'; +import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider'; import HomePage from './pages/HomePage'; // Import global styles @@ -15,11 +14,14 @@ export default function App() { const initializeBackend = async () => { try { // Check if we're running in Tauri environment - if (typeof window !== 'undefined' && window.__TAURI__) { + if (typeof window !== 'undefined' && (window.__TAURI__ || window.__TAURI_INTERNALS__)) { const { tauriBackendService } = await import('./services/tauriBackendService'); console.log('Running in Tauri - Starting backend on React app startup...'); await tauriBackendService.startBackend(); console.log('Backend started successfully'); + } + else { + console.warn('Not running in Tauri - Backend will not be started'); } } catch (error) { console.error('Failed to start backend on app startup:', error); diff --git a/frontend/src/hooks/useAppConfig.ts b/frontend/src/hooks/useAppConfig.ts index 899038f51..4e71152dd 100644 --- a/frontend/src/hooks/useAppConfig.ts +++ b/frontend/src/hooks/useAppConfig.ts @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { makeApiUrl } from '../utils/api'; export interface AppConfig { baseUrl?: string; @@ -46,7 +47,7 @@ export function useAppConfig(): UseAppConfigReturn { setLoading(true); setError(null); - const response = await fetch('/api/v1/config/app-config'); + const response = await fetch(makeApiUrl('/api/v1/config/app-config')); if (!response.ok) { throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`); diff --git a/frontend/src/hooks/useEndpointConfig.ts b/frontend/src/hooks/useEndpointConfig.ts index 13f13764b..51f283976 100644 --- a/frontend/src/hooks/useEndpointConfig.ts +++ b/frontend/src/hooks/useEndpointConfig.ts @@ -1,9 +1,11 @@ import { useState, useEffect } from 'react'; +import { makeApiUrl } from '../utils/api'; +import { useBackendHealth } from './useBackendHealth'; /** * Hook to check if a specific endpoint is enabled */ -export function useEndpointEnabled(endpoint: string): { +export function useEndpointEnabled(endpoint: string, backendHealthy?: boolean): { enabled: boolean | null; loading: boolean; error: string | null; @@ -24,7 +26,7 @@ export function useEndpointEnabled(endpoint: string): { setLoading(true); setError(null); - const response = await fetch(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`); + const response = await fetch(makeApiUrl(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`)); if (!response.ok) { throw new Error(`Failed to check endpoint: ${response.status} ${response.statusText}`); @@ -42,8 +44,16 @@ export function useEndpointEnabled(endpoint: string): { }; useEffect(() => { - fetchEndpointStatus(); - }, [endpoint]); + // Only fetch endpoint status if backend is healthy (or if backendHealthy is not provided) + if (backendHealthy === undefined || backendHealthy === true) { + fetchEndpointStatus(); + } else { + // Backend is not healthy, reset state + setEnabled(null); + setLoading(false); + setError('Backend not available'); + } + }, [endpoint, backendHealthy]); return { enabled, @@ -57,7 +67,7 @@ export function useEndpointEnabled(endpoint: string): { * Hook to check multiple endpoints at once using batch API * Returns a map of endpoint -> enabled status */ -export function useMultipleEndpointsEnabled(endpoints: string[]): { +export function useMultipleEndpointsEnabled(endpoints: string[], backendHealthy?: boolean): { endpointStatus: Record; loading: boolean; error: string | null; @@ -80,7 +90,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { // Use batch API for efficiency const endpointsParam = endpoints.join(','); - const response = await fetch(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`); + const response = await fetch(makeApiUrl(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`)); if (!response.ok) { throw new Error(`Failed to check endpoints: ${response.status} ${response.statusText}`); @@ -105,8 +115,16 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { }; useEffect(() => { - fetchAllEndpointStatuses(); - }, [endpoints.join(',')]); // Re-run when endpoints array changes + // Only fetch endpoint statuses if backend is healthy (or if backendHealthy is not provided) + if (backendHealthy === undefined || backendHealthy === true) { + fetchAllEndpointStatuses(); + } else { + // Backend is not healthy, reset state + setEndpointStatus({}); + setLoading(false); + setError('Backend not available'); + } + }, [endpoints.join(','), backendHealthy]); // Re-run when endpoints array changes or backend health changes return { endpointStatus, @@ -114,4 +132,50 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { error, refetch: fetchAllEndpointStatuses, }; +} + +/** + * Convenience hook that combines backend health checking with endpoint status checking + * Only checks endpoint status once backend is healthy + */ +export function useEndpointEnabledWithHealthCheck(endpoint: string): { + enabled: boolean | null; + loading: boolean; + error: string | null; + backendHealthy: boolean; + refetch: () => Promise; +} { + const { isHealthy: backendHealthy } = useBackendHealth(); + const { enabled, loading, error, refetch } = useEndpointEnabled(endpoint, backendHealthy); + + return { + enabled, + loading, + error, + backendHealthy, + refetch, + }; +} + +/** + * Convenience hook that combines backend health checking with multiple endpoint status checking + * Only checks endpoint statuses once backend is healthy + */ +export function useMultipleEndpointsEnabledWithHealthCheck(endpoints: string[]): { + endpointStatus: Record; + loading: boolean; + error: string | null; + backendHealthy: boolean; + refetch: () => Promise; +} { + const { isHealthy: backendHealthy } = useBackendHealth(); + const { endpointStatus, loading, error, refetch } = useMultipleEndpointsEnabled(endpoints, backendHealthy); + + return { + endpointStatus, + loading, + error, + backendHealthy, + refetch, + }; } \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index a6b5277f6..7fc1f0905 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -24,7 +24,7 @@ import CompressPdfPanel from "../tools/Compress"; import MergePdfPanel from "../tools/Merge"; import ToolRenderer from "../components/tools/ToolRenderer"; import QuickAccessBar from "../components/shared/QuickAccessBar"; -import { useMultipleEndpointsEnabled } from "../hooks/useEndpointConfig"; +import { useMultipleEndpointsEnabledWithHealthCheck } from "../hooks/useEndpointConfig"; type ToolRegistryEntry = { icon: React.ReactNode; @@ -79,7 +79,7 @@ export default function HomePage() { // Get all unique endpoints for batch checking const allEndpoints = Array.from(new Set(Object.values(toolEndpoints).flat())); - const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints); + const { endpointStatus, loading: endpointsLoading, backendHealthy } = useMultipleEndpointsEnabledWithHealthCheck(allEndpoints); // Persist active files across reloads useEffect(() => { diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index 6fbf3a06f..0e5a5c875 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -4,7 +4,7 @@ import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Loader, Alert import { FileWithUrl } from "../types/file"; import { fileStorage } from "../services/fileStorage"; import { makeApiUrl } from "../utils/api"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useEndpointEnabledWithHealthCheck } from "../hooks/useEndpointConfig"; export interface CompressProps { files?: FileWithUrl[]; @@ -37,7 +37,7 @@ const CompressPdfPanel: React.FC = ({ const [selected, setSelected] = useState(files.map(() => false)); const [localLoading, setLocalLoading] = useState(false); - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf"); + const { enabled: endpointEnabled, loading: endpointLoading, backendHealthy } = useEndpointEnabledWithHealthCheck("compress-pdf"); const { compressionLevel, diff --git a/frontend/src/tools/Merge.tsx b/frontend/src/tools/Merge.tsx index 8fd3454ea..9f3cb4219 100644 --- a/frontend/src/tools/Merge.tsx +++ b/frontend/src/tools/Merge.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { FileWithUrl } from "../types/file"; import { fileStorage } from "../services/fileStorage"; import { makeApiUrl } from "../utils/api"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useEndpointEnabledWithHealthCheck } from "../hooks/useEndpointConfig"; export interface MergePdfPanelProps { files: FileWithUrl[]; @@ -28,7 +28,7 @@ const MergePdfPanel: React.FC = ({ const [downloadUrl, setLocalDownloadUrl] = useState(null); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs"); + const { enabled: endpointEnabled, loading: endpointLoading, backendHealthy } = useEndpointEnabledWithHealthCheck("merge-pdfs"); useEffect(() => { setSelectedFiles(files.map(() => true)); diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index 19250be6c..c68c29da5 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -17,7 +17,7 @@ import { useTranslation } from "react-i18next"; import DownloadIcon from "@mui/icons-material/Download"; import { FileWithUrl } from "../types/file"; import { fileStorage } from "../services/fileStorage"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useEndpointEnabledWithHealthCheck } from "../hooks/useEndpointConfig"; export interface SplitPdfPanelProps { file: { file: FileWithUrl; url: string } | null; @@ -83,7 +83,7 @@ const SplitPdfPanel: React.FC = ({ } = params; - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(getEndpointName(mode)); + const { enabled: endpointEnabled, loading: endpointLoading, backendHealthy } = useEndpointEnabledWithHealthCheck(getEndpointName(mode)); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) {