From 379445932326c2df92337b2c76f4f46c314f6b7a Mon Sep 17 00:00:00 2001 From: "I. A. Naval" <790279+ianonavy@users.noreply.github.com> Date: Sat, 28 Jun 2025 15:50:11 -0400 Subject: [PATCH] Integrate backend open-webui wrapper with tauri --- .gitignore | 2 +- backend/main.py | 19 +++- src-tauri/Cargo.lock | 75 ++++++++++++++- src-tauri/Cargo.toml | 2 +- src-tauri/capabilities/default.json | 4 +- src-tauri/src/lib.rs | 142 +++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 8 ++ src/lib/backend.ts | 94 ++++++++++++++++++ src/routes/+page.svelte | 19 ++++ 9 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 src/lib/backend.ts diff --git a/.gitignore b/.gitignore index 943f472..b6eb5ad 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* -backend/.webui_secret_key +.webui_secret_key .venv backend/build backend/dist diff --git a/backend/main.py b/backend/main.py index bb97758..a47affc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,22 @@ +import argparse +import uvicorn from open_webui import app +def main(): + parser = argparse.ArgumentParser(description="GlowPath Backend Server") + parser.add_argument( + "--port", type=int, default=8080, help="Port to run the server on" + ) + parser.add_argument( + "--host", type=str, default="127.0.0.1", help="Host to bind the server to" + ) + + args = parser.parse_args() + + # Run the FastAPI app using uvicorn + uvicorn.run(app, host=args.host, port=args.port, log_level="info") + + if __name__ == "__main__": - app() + main() diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4aabb8a..a077813 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -838,6 +838,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -1336,6 +1345,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "tauri-plugin-shell", ] [[package]] @@ -2384,6 +2394,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "pango" version = "0.18.3" @@ -3247,12 +3267,44 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2778001df1384cf20b6dc5a5a90f48da35539885edaaefd887f8d744e939c0b" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1219ef50fc0fdb04fcc243e6aa27f855553434ffafe4fa26554efb78b5b4bf89" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.5" @@ -3667,6 +3719,27 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-shell" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "tokio", +] + [[package]] name = "tauri-runtime" version = "2.7.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 21c01f3..96b072c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,6 +20,6 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" +tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" - diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49..8e81843 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,8 @@ "windows": ["main"], "permissions": [ "core:default", - "opener:default" + "opener:default", + "shell:allow-open", + "shell:allow-execute" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5a6e4bc..e466fec 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,14 @@ +use std::sync::{Arc, Mutex}; +use tauri::Manager; +use tauri_plugin_shell::{process::CommandEvent, ShellExt}; + +// Global state to track backend process and port +#[derive(Default)] +struct AppState { + backend_port: Arc>>, + backend_process: Arc>>, +} + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { @@ -13,11 +24,140 @@ fn ollama_status() -> Result { .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) } +#[tauri::command] +async fn start_backend(app: tauri::AppHandle) -> Result { + let state = app.state::(); + + // Check if backend is already running + if let Ok(port_guard) = state.backend_port.lock() { + if let Some(port) = *port_guard { + return Ok(port); + } + } + + // Find an available port + let port = find_available_port().map_err(|e| e.to_string())?; + + // Start the backend sidecar + let sidecar_command = app + .shell() + .sidecar("glowpath-backend") + .map_err(|e| format!("Failed to create sidecar command: {}", e))? + .args(&["serve", "--port", &port.to_string()]); + + let (mut rx, child) = sidecar_command + .spawn() + .map_err(|e| format!("Failed to spawn backend process: {}", e))?; + + // Store the process and port + if let Ok(mut process_guard) = state.backend_process.lock() { + *process_guard = Some(child); + } + + if let Ok(mut port_guard) = state.backend_port.lock() { + *port_guard = Some(port); + } + + // Handle process events in the background + let app_handle = app.clone(); + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(data) => { + println!("Backend stdout: {}", String::from_utf8_lossy(&data)); + } + CommandEvent::Stderr(data) => { + eprintln!("Backend stderr: {}", String::from_utf8_lossy(&data)); + } + CommandEvent::Error(error) => { + eprintln!("Backend error: {}", error); + } + CommandEvent::Terminated(payload) => { + println!("Backend terminated with code: {:?}", payload.code); + // Reset state + if let Some(state) = app_handle.try_state::() { + if let Ok(mut port_guard) = state.backend_port.lock() { + *port_guard = None; + } + if let Ok(mut process_guard) = state.backend_process.lock() { + *process_guard = None; + } + } + break; + } + _ => {} // Handle any other events + } + } + }); + + Ok(port) +} + +#[tauri::command] +async fn get_backend_port(app: tauri::AppHandle) -> Result { + let state = app.state::(); + + if let Ok(port_guard) = state.backend_port.lock() { + if let Some(port) = *port_guard { + return Ok(port); + } + } + + // If no port is set, start the backend + start_backend(app).await +} + +#[tauri::command] +async fn stop_backend(app: tauri::AppHandle) -> Result<(), String> { + let state = app.state::(); + + if let Ok(mut process_guard) = state.backend_process.lock() { + if let Some(child) = process_guard.take() { + child + .kill() + .map_err(|e| format!("Failed to kill backend process: {}", e))?; + } + } + + if let Ok(mut port_guard) = state.backend_port.lock() { + *port_guard = None; + } + + Ok(()) +} + +fn find_available_port() -> Result> { + use std::net::TcpListener; + + // Try to bind to port 0 to get an available port + let listener = TcpListener::bind("127.0.0.1:0")?; + let addr = listener.local_addr()?; + Ok(addr.port()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![greet, ollama_status]) + .plugin(tauri_plugin_shell::init()) + .manage(AppState::default()) + .invoke_handler(tauri::generate_handler![ + greet, + ollama_status, + start_backend, + get_backend_port, + stop_backend + ]) + .setup(|app| { + // Optionally start backend on app startup + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + if let Err(e) = start_backend(app_handle).await { + eprintln!("Failed to start backend on startup: {}", e); + } + }); + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3821fec..0dc4dd3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,6 +30,14 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" + ], + "externalBin": [ + "../backend/dist/glowpath-backend" ] + }, + "plugins": { + "shell": { + "open": true + } } } diff --git a/src/lib/backend.ts b/src/lib/backend.ts new file mode 100644 index 0000000..f220e7d --- /dev/null +++ b/src/lib/backend.ts @@ -0,0 +1,94 @@ +import { invoke } from "@tauri-apps/api/core"; + +class BackendService { + private port: number | null = null; + private baseUrl: string | null = null; + + async initialize(): Promise { + try { + // Get or start the backend and get its port + this.port = await invoke("get_backend_port"); + this.baseUrl = `http://127.0.0.1:${this.port}`; + + // Wait for backend to be ready + await this.waitForBackend(); + } catch (error) { + console.error("Failed to initialize backend service:", error); + throw error; + } + } + + async start(): Promise { + try { + this.port = await invoke("start_backend"); + this.baseUrl = `http://127.0.0.1:${this.port}`; + await this.waitForBackend(); + } catch (error) { + console.error("Failed to start backend:", error); + throw error; + } + } + + async stop(): Promise { + try { + await invoke("stop_backend"); + this.port = null; + this.baseUrl = null; + } catch (error) { + console.error("Failed to stop backend:", error); + throw error; + } + } + + private async waitForBackend(maxAttempts = 30): Promise { + for (let i = 0; i < maxAttempts; i++) { + try { + const response = await fetch(this.baseUrl + "/"); + if (response.ok) { + return; + } + } catch (error) { + // Backend not ready yet + } + + await new Promise(resolve => setTimeout(resolve, 30000)); + } + + throw new Error("Backend failed to start within timeout period"); + } + + getBaseUrl(): string { + if (!this.baseUrl) { + throw new Error("Backend service not initialized"); + } + return this.baseUrl; + } + + async fetch(path: string, options?: RequestInit): Promise { + if (!this.baseUrl) { + throw new Error("Backend service not initialized"); + } + + const url = `${this.baseUrl}${path}`; + return fetch(url, options); + } + + // Proxy method for API calls to the backend + async api(path: string, options?: RequestInit): Promise { + const response = await this.fetch(`/api${path}`, { + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`); + } + + return response.json(); + } +} + +export const backendService = new BackendService(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7c4f9ea..ea619ed 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,20 +1,39 @@

Welcome to Tauri + Svelte

+
+

Backend Status

+

{backendStatus}

+
+

Installed Ollama models