Integrate backend open-webui wrapper with tauri

This commit is contained in:
I. A. Naval 2025-06-28 15:50:11 -04:00
parent 3218c10063
commit 3794459323
Signed by: potato
GPG Key ID: D22B0F9008C43F2B
9 changed files with 359 additions and 6 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
backend/.webui_secret_key .webui_secret_key
.venv .venv
backend/build backend/build
backend/dist backend/dist

View File

@ -1,5 +1,22 @@
import argparse
import uvicorn
from open_webui import app 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__": if __name__ == "__main__":
app() main()

75
src-tauri/Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -838,6 +838,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" 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]] [[package]]
name = "endi" name = "endi"
version = "1.1.0" version = "1.1.0"
@ -1336,6 +1345,7 @@ dependencies = [
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-shell",
] ]
[[package]] [[package]]
@ -2384,6 +2394,16 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@ -3247,12 +3267,44 @@ dependencies = [
"digest", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.5" version = "1.4.5"
@ -3667,6 +3719,27 @@ dependencies = [
"zbus", "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]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.7.0" version = "2.7.0"

View File

@ -20,6 +20,6 @@ tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View File

@ -5,6 +5,8 @@
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default" "opener:default",
"shell:allow-open",
"shell:allow-execute"
] ]
} }

View File

@ -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<Mutex<Option<u16>>>,
backend_process: Arc<Mutex<Option<tauri_plugin_shell::process::CommandChild>>>,
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command] #[tauri::command]
fn greet(name: &str) -> String { fn greet(name: &str) -> String {
@ -13,11 +24,140 @@ fn ollama_status() -> Result<String, String> {
.map(|o| String::from_utf8_lossy(&o.stdout).to_string()) .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
} }
#[tauri::command]
async fn start_backend(app: tauri::AppHandle) -> Result<u16, String> {
let state = app.state::<AppState>();
// 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::<AppState>() {
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<u16, String> {
let state = app.state::<AppState>();
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::<AppState>();
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<u16, Box<dyn std::error::Error>> {
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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init()) .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!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -30,6 +30,14 @@
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
],
"externalBin": [
"../backend/dist/glowpath-backend"
] ]
},
"plugins": {
"shell": {
"open": true
}
} }
} }

94
src/lib/backend.ts Normal file
View File

@ -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<void> {
try {
// Get or start the backend and get its port
this.port = await invoke<number>("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<void> {
try {
this.port = await invoke<number>("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<void> {
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<void> {
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<Response> {
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<any> {
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();

View File

@ -1,20 +1,39 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte";
import ModelsPane from "$lib/components/ModelsPane.svelte"; import ModelsPane from "$lib/components/ModelsPane.svelte";
import { backendService } from "$lib/backend";
let name = $state(""); let name = $state("");
let greetMsg = $state(""); let greetMsg = $state("");
let backendStatus = $state("Initializing...");
async function greet(event: Event) { async function greet(event: Event) {
event.preventDefault(); event.preventDefault();
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
greetMsg = await invoke("greet", { name }); greetMsg = await invoke("greet", { name });
} }
onMount(async () => {
try {
backendStatus = "Starting backend...";
await backendService.initialize();
backendStatus = `Backend running at ${backendService.getBaseUrl()}`;
} catch (error) {
console.error("Failed to initialize backend:", error);
backendStatus = "Backend failed to start";
}
});
</script> </script>
<main class="container"> <main class="container">
<h1>Welcome to Tauri + Svelte</h1> <h1>Welcome to Tauri + Svelte</h1>
<div class="backend-status">
<h2>Backend Status</h2>
<p>{backendStatus}</p>
</div>
<h1 class="text-2xl font-bold mb-4">Installed Ollama models</h1> <h1 class="text-2xl font-bold mb-4">Installed Ollama models</h1>
<ModelsPane /> <ModelsPane />