From 2a4d8c0ea4e11ff33fb88ca67a65c00ce0b2fe2a Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Tue, 1 Jul 2025 17:42:16 +0100 Subject: [PATCH] Starts backend from react and monitors health --- frontend/src-tauri/src/lib.rs | 178 ++---------------- frontend/src/App.tsx | 39 ++-- .../src/components/BackendHealthIndicator.tsx | 28 +++ frontend/src/hooks/useBackendHealth.ts | 70 +++++++ frontend/src/services/backendService.ts | 27 +-- frontend/src/utils/api.ts | 18 +- 6 files changed, 149 insertions(+), 211 deletions(-) create mode 100644 frontend/src/components/BackendHealthIndicator.tsx create mode 100644 frontend/src/hooks/useBackendHealth.ts diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index c1ae161fb..9a9a6faa6 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -20,12 +20,6 @@ fn add_log(message: String) { println!("{}", message); // Also print to console } -// Command to get backend logs -#[tauri::command] -async fn get_backend_logs() -> Result, String> { - let logs = BACKEND_LOGS.lock().unwrap(); - Ok(logs.iter().cloned().collect()) -} // Command to start the backend with bundled JRE #[tauri::command] @@ -253,7 +247,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result { // Wait for the backend to start println!("⏳ Waiting for backend startup..."); - tokio::time::sleep(std::time::Duration::from_millis(5000)).await; + tokio::time::sleep(std::time::Duration::from_millis(10000)).await; Ok("Backend startup initiated successfully with bundled JRE".to_string()) } @@ -261,20 +255,18 @@ async fn start_backend(app: tauri::AppHandle) -> Result { // Command to check if backend is healthy #[tauri::command] async fn check_backend_health() -> Result { - println!("🔍 Checking backend health..."); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; - match client.get("http://localhost:8080/actuator/health").send().await { + match client.get("http://localhost:8080/api/v1/info/status").send().await { Ok(response) => { let status = response.status(); println!("💓 Health check response status: {}", status); if status.is_success() { match response.text().await { Ok(body) => { - println!("💓 Health check response: {}", body); Ok(true) } Err(e) => { @@ -294,41 +286,7 @@ async fn check_backend_health() -> Result { } } -// Command to get backend process status -#[tauri::command] -async fn get_backend_status() -> Result { - let process_guard = BACKEND_PROCESS.lock().unwrap(); - match process_guard.as_ref() { - Some(child) => { - // Try to check if process is still alive - let pid = child.pid(); - println!("🔍 Checking backend process status, PID: {}", pid); - Ok(format!("Backend process is running with bundled JRE (PID: {})", pid)) - }, - None => Ok("Backend process is not running".to_string()), - } -} -// Command to check if backend port is accessible -#[tauri::command] -async fn check_backend_port() -> Result { - println!("🔍 Checking if port 8080 is accessible..."); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(3)) - .build() - .map_err(|e| format!("Failed to create HTTP client: {}", e))?; - - match client.head("http://localhost:8080/").send().await { - Ok(response) => { - println!("✅ Port 8080 responded with status: {}", response.status()); - Ok(true) - } - Err(e) => { - println!("❌ Port 8080 not accessible: {}", e); - Ok(false) - } - } -} // Command to check bundled runtime and JAR #[tauri::command] @@ -387,99 +345,8 @@ async fn check_jar_exists(app: tauri::AppHandle) -> Result { } } -// Command to test sidecar binary directly -#[tauri::command] -async fn test_sidecar_binary(app: tauri::AppHandle) -> Result { - println!("🔍 Testing sidecar binary availability..."); - - // Test if we can create the sidecar command (this validates the binary exists) - match app.shell().sidecar("stirling-pdf-backend") { - Ok(_) => { - println!("✅ Sidecar binary 'stirling-pdf-backend' is available"); - Ok("Sidecar binary 'stirling-pdf-backend' is available and can be executed".to_string()) - } - Err(e) => { - println!("❌ Failed to access sidecar binary: {}", e); - Ok(format!("Sidecar binary not available: {}. Make sure the binary exists in the binaries/ directory with correct permissions.", e)) - } - } -} -// Command to stop the backend process -#[tauri::command] -async fn stop_backend() -> Result { - 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 { - println!("🔍 Checking bundled Java environment..."); - - if let Ok(resource_dir) = app.path().resource_dir() { - let jre_dir = resource_dir.join("runtime").join("jre"); - let java_executable = if cfg!(windows) { - jre_dir.join("bin").join("java.exe") - } else { - jre_dir.join("bin").join("java") - }; - - if java_executable.exists() { - let output = std::process::Command::new(&java_executable) - .arg("--version") - .output(); - - match output { - Ok(output) => { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let version_info = if !stdout.is_empty() { stdout } else { stderr }; - println!("✅ Bundled Java found: {}", version_info); - Ok(format!("Bundled Java available: {}", version_info.trim())) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("❌ Bundled Java command failed: {}", stderr); - Ok(format!("Bundled Java command failed: {}", stderr)) - } - } - Err(e) => { - println!("❌ Failed to execute bundled Java: {}", e); - Ok(format!("Failed to execute bundled Java: {}", e)) - } - } - } else { - Ok("❌ Bundled JRE not found".to_string()) - } - } else { - Ok("❌ Could not access bundled resources".to_string()) - } -} // Cleanup function to stop backend on app exit fn cleanup_backend() { @@ -505,34 +372,25 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .setup(|app| { - // Disable file logging in debug mode to prevent dev server restart loops - // if cfg!(debug_assertions) { - // app.handle().plugin( - // tauri_plugin_log::Builder::default() - // .level(log::LevelFilter::Info) - // .build(), - // )?; - // } - + // Automatically start the backend when Tauri starts - let app_handle = app.handle().clone(); - tauri::async_runtime::spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; // Small delay to ensure app is ready - add_log("🔄 Tauri app ready, starting backend...".to_string()); - - match start_backend(app_handle).await { - Ok(result) => { - add_log(format!("🚀 Auto-started backend on Tauri startup: {}", result)); - } - Err(error) => { - add_log(format!("❌ Failed to auto-start backend: {}", error)); - } - } - }); + // let app_handle = app.handle().clone(); + // tauri::async_runtime::spawn( + // async move { + // match start_backend(app_handle).await { + // Ok(result) => { + // add_log(format!("🚀 Auto-started backend on Tauri startup: {}", result)); + // } + // Err(error) => { + // add_log(format!("❌ Failed to auto-start backend: {}", error)); + // } + // } + // }); Ok(()) - }) - .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]) + } + ) + .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, check_jar_exists]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6d6a5739..ca85eb228 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,26 +1,37 @@ import './index.css'; -import React, { useState } from 'react'; +import React, { useEffect } from 'react'; import HomePage from './pages/HomePage'; -import { SidecarTest } from './components/SidecarTest'; +import { BackendHealthIndicator } from './components/BackendHealthIndicator'; +import { backendService } from './services/backendService'; export default function App() { - const [showTests, setShowTests] = useState(false); // Start with app visible - + useEffect(() => { + // Only start backend if running in Tauri + const initializeBackend = async () => { + try { + // Check if we're running in Tauri environment + if (typeof window !== 'undefined' && window.__TAURI__) { + console.log('Running in Tauri - Starting backend on React app startup...'); + await backendService.startBackend(); + console.log('Backend started successfully'); + } + } catch (error) { + console.error('Failed to start backend on app startup:', error); + } + }; + + initializeBackend(); + }, []); return (
-
-
-

Stirling PDF - Tauri Integration

- +
+ +
+

Stirling PDF

- {showTests ? : } +
); } diff --git a/frontend/src/components/BackendHealthIndicator.tsx b/frontend/src/components/BackendHealthIndicator.tsx new file mode 100644 index 000000000..4cf3a3d3a --- /dev/null +++ b/frontend/src/components/BackendHealthIndicator.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import '../index.css'; +import { useBackendHealth } from '../hooks/useBackendHealth'; + +interface BackendHealthIndicatorProps { + className?: string; +} + +export const BackendHealthIndicator: React.FC = ({ + className = '' +}) => { + const { isHealthy, isChecking, error, checkHealth } = useBackendHealth(); + + let statusColor = 'bg-red-500'; // offline + if (isChecking || (!isHealthy && error === 'Backend starting up...')) { + statusColor = 'bg-yellow-500'; // starting/checking + } else if (isHealthy) { + statusColor = 'bg-green-500'; // online + } + + return ( +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/hooks/useBackendHealth.ts b/frontend/src/hooks/useBackendHealth.ts new file mode 100644 index 000000000..842175382 --- /dev/null +++ b/frontend/src/hooks/useBackendHealth.ts @@ -0,0 +1,70 @@ +import { useState, useEffect, useCallback } from 'react'; +import { backendService } from '../services/backendService'; + +export interface BackendHealthState { + isHealthy: boolean; + isChecking: boolean; + lastChecked: Date | null; + error: string | null; +} + +export const useBackendHealth = (checkInterval: number = 2000) => { + const [healthState, setHealthState] = useState({ + isHealthy: false, + isChecking: false, + lastChecked: null, + error: null, + }); + + const [startupTime] = useState(new Date()); + const [attemptCount, setAttemptCount] = useState(0); + + const checkHealth = useCallback(async () => { + setHealthState(prev => ({ ...prev, isChecking: true, error: null })); + setAttemptCount(prev => prev + 1); + + try { + const isHealthy = await backendService.checkHealth(); + setHealthState({ + isHealthy, + isChecking: false, + lastChecked: new Date(), + error: null, + }); + if (isHealthy) { + setAttemptCount(0); // Reset attempt count on success + } + } catch (error) { + const now = new Date(); + const timeSinceStartup = now.getTime() - startupTime.getTime(); + const isWithinStartupPeriod = timeSinceStartup < 60000; // 60 seconds + + // Don't show error during initial startup period + const errorMessage = isWithinStartupPeriod + ? 'Backend starting up...' + : (error instanceof Error ? error.message : 'Health check failed'); + + setHealthState({ + isHealthy: false, + isChecking: false, + lastChecked: new Date(), + error: errorMessage, + }); + } + }, [startupTime]); + + useEffect(() => { + // Initial health check + checkHealth(); + + // Set up periodic health checks + const interval = setInterval(checkHealth, checkInterval); + + return () => clearInterval(interval); + }, [checkHealth, checkInterval]); + + return { + ...healthState, + checkHealth, + }; +}; \ No newline at end of file diff --git a/frontend/src/services/backendService.ts b/frontend/src/services/backendService.ts index beff78a2d..d696cfce7 100644 --- a/frontend/src/services/backendService.ts +++ b/frontend/src/services/backendService.ts @@ -1,5 +1,4 @@ import { invoke } from '@tauri-apps/api/core'; -import { getApiBaseUrl } from '../utils/api'; export class BackendService { private static instance: BackendService; @@ -31,6 +30,9 @@ export class BackendService { } async checkHealth(): Promise { + if (!this.backendStarted) { + return false; + } try { return await invoke('check_backend_health'); } catch (error) { @@ -39,7 +41,7 @@ export class BackendService { } } - private async waitForHealthy(maxAttempts = 30): Promise { + private async waitForHealthy(maxAttempts = 60): Promise { for (let i = 0; i < maxAttempts; i++) { const isHealthy = await this.checkHealth(); if (isHealthy) { @@ -48,26 +50,7 @@ export class BackendService { } await new Promise(resolve => setTimeout(resolve, 1000)); } - throw new Error('Backend failed to become healthy after 30 seconds'); - } - - getBackendUrl(): string { - return getApiBaseUrl() || 'http://localhost:8080'; - } - - async makeApiCall(endpoint: string, options?: RequestInit): Promise { - if (!this.backendStarted) { - await this.startBackend(); - } - - const url = `${this.getBackendUrl()}${endpoint}`; - return fetch(url, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers, - }, - }); + throw new Error('Backend failed to become healthy after 60 seconds'); } } diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 16333f8f6..3038dbb14 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,20 +1,8 @@ -export const getApiBaseUrl = (): string => { - const envUrl = import.meta.env.VITE_API_BASE_URL; - - // In development, use empty string to leverage Vite proxy - // In production/Tauri, use localhost:8080 directly - // if (envUrl !== undefined) { - // console.log(`Using API base URL from environment: ${envUrl}`); - // return envUrl; - // } - - // Fallback for development - console.log('Using default API base URL: http://localhost:8080'); - return 'http://localhost:8080'; -}; +const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; + export const makeApiUrl = (endpoint: string): string => { - const baseUrl = getApiBaseUrl(); + const baseUrl = apiBaseUrl; // If baseUrl is empty (development), return endpoint as-is for proxy if (!baseUrl) {