mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 07:35:22 +00:00
Starts backend from react and monitors health
This commit is contained in:
parent
125c223b2d
commit
2a4d8c0ea4
@ -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<Vec<String>, 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<String, String> {
|
||||
|
||||
// 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<String, String> {
|
||||
// Command to check if backend is healthy
|
||||
#[tauri::command]
|
||||
async fn check_backend_health() -> Result<bool, String> {
|
||||
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<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Command to get backend process status
|
||||
#[tauri::command]
|
||||
async fn get_backend_status() -> Result<String, String> {
|
||||
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<bool, String> {
|
||||
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<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Command to test sidecar binary directly
|
||||
#[tauri::command]
|
||||
async fn test_sidecar_binary(app: tauri::AppHandle) -> Result<String, String> {
|
||||
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<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> {
|
||||
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| {
|
||||
|
@ -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 (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<div className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-4xl mx-auto px-4 py-3 flex justify-between items-center">
|
||||
<h1 className="text-xl font-bold">Stirling PDF - Tauri Integration</h1>
|
||||
<button
|
||||
onClick={() => setShowTests(!showTests)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
{showTests ? 'Show App' : 'Show Tests'}
|
||||
</button>
|
||||
<div className="bg-white shadow-sm border-b relative">
|
||||
<BackendHealthIndicator className="absolute top-3 left-3 z-10" />
|
||||
<div className="max-w-4xl mx-auto px-4 py-3">
|
||||
<h1 className="text-xl font-bold">Stirling PDF</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showTests ? <SidecarTest /> : <HomePage />}
|
||||
<HomePage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
28
frontend/src/components/BackendHealthIndicator.tsx
Normal file
28
frontend/src/components/BackendHealthIndicator.tsx
Normal file
@ -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<BackendHealthIndicatorProps> = ({
|
||||
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 (
|
||||
<div
|
||||
className={`w-2xs h-2xs ${statusColor} rounded-full cursor-pointer ${isChecking ? 'animate-pulse' : ''} ${className}`}
|
||||
onClick={checkHealth}
|
||||
title={isHealthy ? 'Backend Online' : isChecking ? 'Checking...' : 'Backend Offline'}
|
||||
/>
|
||||
);
|
||||
};
|
70
frontend/src/hooks/useBackendHealth.ts
Normal file
70
frontend/src/hooks/useBackendHealth.ts
Normal file
@ -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<BackendHealthState>({
|
||||
isHealthy: false,
|
||||
isChecking: false,
|
||||
lastChecked: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const [startupTime] = useState<Date>(new Date());
|
||||
const [attemptCount, setAttemptCount] = useState<number>(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,
|
||||
};
|
||||
};
|
@ -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<boolean> {
|
||||
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<void> {
|
||||
private async waitForHealthy(maxAttempts = 60): Promise<void> {
|
||||
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<Response> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user