Integrate backend open-webui wrapper with tauri
This commit is contained in:
parent
3218c10063
commit
3794459323
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
|
@ -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()
|
||||
|
75
src-tauri/Cargo.lock
generated
75
src-tauri/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"shell:allow-open",
|
||||
"shell:allow-execute"
|
||||
]
|
||||
}
|
||||
|
@ -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/
|
||||
#[tauri::command]
|
||||
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())
|
||||
}
|
||||
|
||||
#[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)]
|
||||
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");
|
||||
}
|
||||
|
@ -30,6 +30,14 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": [
|
||||
"../backend/dist/glowpath-backend"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
94
src/lib/backend.ts
Normal file
94
src/lib/backend.ts
Normal 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();
|
@ -1,20 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMount } from "svelte";
|
||||
import ModelsPane from "$lib/components/ModelsPane.svelte";
|
||||
import { backendService } from "$lib/backend";
|
||||
|
||||
let name = $state("");
|
||||
let greetMsg = $state("");
|
||||
let backendStatus = $state("Initializing...");
|
||||
|
||||
async function greet(event: Event) {
|
||||
event.preventDefault();
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
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>
|
||||
|
||||
<main class="container">
|
||||
<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>
|
||||
<ModelsPane />
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user