Fix Tauri app to start server before window, use WebviewUrl::External

- Start FastAPI server before Tauri initializes
- Wait for health check to pass before creating window
- Create window programmatically with External URL
- Try uv first, fall back to python3
- Remove window config from tauri.conf.json (now in code)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 13:51:49 -05:00
parent a776c0c73c
commit 06382e83a0
+74 -88
View File
@@ -1,102 +1,78 @@
// Prevents additional console window on Windows in release
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Child, Command};
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use std::time::Duration;
use tauri::{AppHandle, Manager, RunEvent};
use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder};
struct ServerProcess(Mutex<Option<Child>>);
fn find_python_executable(app: &AppHandle) -> Option<String> {
// In development, use system Python
if cfg!(debug_assertions) {
return Some("python3".to_string());
}
const SERVER_URL: &str = "http://127.0.0.1:31102";
// In production, look for bundled Python sidecar
let resource_path = app.path().resource_dir().ok()?;
#[cfg(target_os = "macos")]
{
let sidecar_path = resource_path.join("sidecar").join("kjvstudy-server");
if sidecar_path.exists() {
return Some(sidecar_path.to_string_lossy().to_string());
}
}
#[cfg(target_os = "windows")]
{
let sidecar_path = resource_path.join("sidecar").join("kjvstudy-server.exe");
if sidecar_path.exists() {
return Some(sidecar_path.to_string_lossy().to_string());
}
}
// Fallback to system Python
Some("python3".to_string())
}
fn start_server(app: &AppHandle) -> Option<Child> {
let python = find_python_executable(app)?;
// Get the resource directory for data files
let resource_dir = app.path().resource_dir().ok()?;
// For development, use the project directory
let working_dir = if cfg!(debug_assertions) {
std::env::current_dir().ok()?
} else {
resource_dir.clone()
};
fn start_server() -> Option<Child> {
// Get the current working directory (project root in dev)
let working_dir = std::env::current_dir().ok()?;
println!("Starting KJV Study server...");
println!("Python executable: {}", python);
println!("Working directory: {:?}", working_dir);
let child = if python.ends_with("kjvstudy-server") || python.ends_with("kjvstudy-server.exe") {
// Running bundled executable
Command::new(&python)
.current_dir(&working_dir)
.env("KJVSTUDY_PORT", "31102")
.spawn()
.ok()?
} else {
// Running with Python interpreter (development)
Command::new(&python)
.args([
"-m", "uvicorn",
"kjvstudy_org.server:app",
"--host", "127.0.0.1",
"--port", "31102",
"--log-level", "warning"
])
.current_dir(&working_dir)
.spawn()
.ok()?
};
// Try uv first (preferred), then fall back to python3
let child = Command::new("uv")
.args([
"run", "uvicorn",
"kjvstudy_org.server:app",
"--host", "127.0.0.1",
"--port", "31102",
"--log-level", "warning"
])
.current_dir(&working_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.or_else(|_| {
println!("uv not found, trying python3...");
Command::new("python3")
.args([
"-m", "uvicorn",
"kjvstudy_org.server:app",
"--host", "127.0.0.1",
"--port", "31102",
"--log-level", "warning"
])
.current_dir(&working_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
})
.ok()?;
println!("Server process started with PID: {}", child.id());
Some(child)
}
fn wait_for_server(max_attempts: u32) -> bool {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(1))
.timeout(Duration::from_secs(2))
.build()
.unwrap();
for attempt in 1..=max_attempts {
println!("Waiting for server... (attempt {}/{})", attempt, max_attempts);
match client.get("http://127.0.0.1:31102/api/health").send() {
match client.get(format!("{}/api/health", SERVER_URL)).send() {
Ok(response) if response.status().is_success() => {
println!("Server is ready!");
return true;
}
_ => {
std::thread::sleep(Duration::from_millis(500));
Ok(response) => {
println!("Server responded with status: {}", response.status());
}
Err(e) => {
println!("Connection error: {}", e);
}
}
std::thread::sleep(Duration::from_millis(500));
}
println!("Server failed to start after {} attempts", max_attempts);
@@ -104,30 +80,39 @@ fn wait_for_server(max_attempts: u32) -> bool {
}
fn main() {
// Start server BEFORE Tauri
let server_child = start_server();
if server_child.is_none() {
eprintln!("ERROR: Failed to start server process!");
eprintln!("Make sure you're running from the project directory with uv or python3 available.");
std::process::exit(1);
}
// Wait for server to be ready
if !wait_for_server(30) {
eprintln!("ERROR: Server failed to become ready!");
std::process::exit(1);
}
// Now start Tauri
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.manage(ServerProcess(Mutex::new(None)))
.manage(ServerProcess(Mutex::new(server_child)))
.setup(|app| {
// Start the Python server
let handle = app.handle().clone();
if let Some(child) = start_server(&handle) {
let state = app.state::<ServerProcess>();
*state.0.lock().unwrap() = Some(child);
// Wait for server to be ready
if !wait_for_server(30) {
eprintln!("Warning: Server may not be fully ready");
}
} else {
eprintln!("Failed to start server process");
}
// Navigate to the local server
if let Some(window) = app.get_webview_window("main") {
let _ = window.eval("window.location.href = 'http://127.0.0.1:31102'");
}
// Create window pointing to our server
let _window = WebviewWindowBuilder::new(
app,
"main",
WebviewUrl::External(SERVER_URL.parse().unwrap())
)
.title("KJV Study")
.inner_size(1200.0, 800.0)
.min_inner_size(800.0, 600.0)
.center()
.build()?;
println!("Window created, loading {}", SERVER_URL);
Ok(())
})
.build(tauri::generate_context!())
@@ -140,6 +125,7 @@ fn main() {
println!("Shutting down server...");
let _ = child.kill();
let _ = child.wait();
println!("Server stopped.");
};
}
});