mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 07:05:14 -07:00
fix: GUI startup with external server + data refresh on server switch
Two fixes for issue #312: 1. GUI stuck on loading screen when backend is already running externally (e.g. via python/uvicorn/Docker): - Rust: add HTTP health check fallback when the process on the port doesn't have 'voicebox' in its name. If /health responds with a valid Voicebox response, reuse the server instead of erroring. - Frontend: when startServer() fails, fall back to polling the health endpoint every 2s instead of permanently blocking. 2. No data refresh when switching server URLs in settings: - serverStore.setServerUrl() now invalidates all React Query caches when the URL actually changes, so profiles/history/models/stories are re-fetched from the new server. - Export queryClient from main.tsx for store-level cache invalidation. Fixes #312
This commit is contained in:
@@ -4,6 +4,7 @@ import voiceboxLogo from '@/assets/voicebox-logo.png';
|
|||||||
import ShinyText from '@/components/ShinyText';
|
import ShinyText from '@/components/ShinyText';
|
||||||
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
|
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
|
||||||
import { useAutoUpdater } from '@/hooks/useAutoUpdater';
|
import { useAutoUpdater } from '@/hooks/useAutoUpdater';
|
||||||
|
import { apiClient } from '@/lib/api/client';
|
||||||
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
import { usePlatform } from '@/platform/PlatformContext';
|
import { usePlatform } from '@/platform/PlatformContext';
|
||||||
@@ -122,6 +123,24 @@ function App() {
|
|||||||
serverStartingRef.current = false;
|
serverStartingRef.current = false;
|
||||||
// @ts-expect-error - adding property to window
|
// @ts-expect-error - adding property to window
|
||||||
window.__voiceboxServerStartedByApp = false;
|
window.__voiceboxServerStartedByApp = false;
|
||||||
|
|
||||||
|
// Fall back to polling: the server may already be running externally
|
||||||
|
// (e.g. started via python/uvicorn/Docker). Poll the health endpoint
|
||||||
|
// until it responds, then transition to the main UI.
|
||||||
|
console.log('Falling back to health-check polling...');
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.getHealth();
|
||||||
|
console.log('External server detected via health check');
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
setServerReady(true);
|
||||||
|
} catch {
|
||||||
|
// Server not ready yet, keep polling
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Stop polling after 2 minutes to avoid polling forever
|
||||||
|
setTimeout(() => clearInterval(pollInterval), 120_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup: stop server on actual unmount (not StrictMode remount)
|
// Cleanup: stop server on actual unmount (not StrictMode remount)
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
|||||||
@@ -30,11 +30,29 @@ interface ServerStore {
|
|||||||
setCustomModelsDir: (dir: string | null) => void;
|
setCustomModelsDir: (dir: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all React Query caches and reset UI selection state.
|
||||||
|
* Called when the server URL changes so stale data from the previous
|
||||||
|
* server is not shown.
|
||||||
|
*/
|
||||||
|
function invalidateAllServerData() {
|
||||||
|
// Lazy import to avoid circular dependency (main.tsx -> serverStore -> main.tsx)
|
||||||
|
import('@/main').then(({ queryClient }) => {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const useServerStore = create<ServerStore>()(
|
export const useServerStore = create<ServerStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set, get) => ({
|
||||||
serverUrl: 'http://127.0.0.1:17493',
|
serverUrl: 'http://127.0.0.1:17493',
|
||||||
setServerUrl: (url) => set({ serverUrl: url }),
|
setServerUrl: (url) => {
|
||||||
|
const prev = get().serverUrl;
|
||||||
|
set({ serverUrl: url });
|
||||||
|
if (url !== prev) {
|
||||||
|
invalidateAllServerData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
setIsConnected: (connected) => set({ isConnected: connected }),
|
setIsConnected: (connected) => set({ isConnected: connected }),
|
||||||
|
|||||||
@@ -53,6 +53,35 @@ fn find_voicebox_pid_on_port(port: u16) -> Option<u32> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a Voicebox server is responding on the given port.
|
||||||
|
///
|
||||||
|
/// Sends an HTTP GET to `/health` and returns `true` if the response
|
||||||
|
/// contains the expected JSON field (`"status"`), confirming it's
|
||||||
|
/// a Voicebox backend rather than an unrelated service.
|
||||||
|
#[allow(dead_code)] // Used in platform-specific cfg blocks
|
||||||
|
fn check_health(port: u16) -> bool {
|
||||||
|
let url = format!("http://127.0.0.1:{}/health", port);
|
||||||
|
match reqwest::blocking::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(3))
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(client) => match client.get(&url).send() {
|
||||||
|
Ok(resp) => {
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Verify the body looks like a Voicebox health response
|
||||||
|
match resp.text() {
|
||||||
|
Ok(body) => body.contains("status"),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
},
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ServerState {
|
struct ServerState {
|
||||||
child: Mutex<Option<tauri_plugin_shell::process::CommandChild>>,
|
child: Mutex<Option<tauri_plugin_shell::process::CommandChild>>,
|
||||||
server_pid: Mutex<Option<u32>>,
|
server_pid: Mutex<Option<u32>>,
|
||||||
@@ -80,7 +109,8 @@ async fn start_server(
|
|||||||
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a voicebox server is already running on our port (from previous session with keep_running=true)
|
// Check if a voicebox server is already running on our port (from previous session with keep_running=true,
|
||||||
|
// or an externally started server e.g. via `python`, `uvicorn`, Docker, etc.)
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -101,6 +131,20 @@ async fn start_server(
|
|||||||
*state.server_pid.lock().unwrap() = Some(pid);
|
*state.server_pid.lock().unwrap() = Some(pid);
|
||||||
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Process name doesn't contain "voicebox" — could be an external
|
||||||
|
// Python/uvicorn/Docker server. Verify via HTTP health check.
|
||||||
|
println!("Port {} in use by '{}' (PID: {}), checking if it's a Voicebox server...", SERVER_PORT, command, pid_str);
|
||||||
|
if check_health(SERVER_PORT) {
|
||||||
|
println!("Health check passed — reusing external server on port {}", SERVER_PORT);
|
||||||
|
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
||||||
|
}
|
||||||
|
println!("Health check failed — port is occupied by a non-Voicebox process");
|
||||||
|
return Err(format!(
|
||||||
|
"Port {} is already in use by another application ({}). \
|
||||||
|
Close it or change the Voicebox server port.",
|
||||||
|
SERVER_PORT, command
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,18 +158,24 @@ async fn start_server(
|
|||||||
&format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(),
|
&format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(),
|
||||||
std::time::Duration::from_secs(1),
|
std::time::Duration::from_secs(1),
|
||||||
).is_ok() {
|
).is_ok() {
|
||||||
// Port is in use — check if it's a voicebox process
|
// Port is in use — check if it's a voicebox process by name first
|
||||||
if let Some(pid) = find_voicebox_pid_on_port(SERVER_PORT) {
|
if let Some(pid) = find_voicebox_pid_on_port(SERVER_PORT) {
|
||||||
println!("Found existing voicebox-server on port {} (PID: {}), reusing it", SERVER_PORT, pid);
|
println!("Found existing voicebox-server on port {} (PID: {}), reusing it", SERVER_PORT, pid);
|
||||||
*state.server_pid.lock().unwrap() = Some(pid);
|
*state.server_pid.lock().unwrap() = Some(pid);
|
||||||
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
||||||
} else {
|
|
||||||
return Err(format!(
|
|
||||||
"Port {} is already in use by another application. \
|
|
||||||
Close the other application or change the Voicebox port.",
|
|
||||||
SERVER_PORT
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
// Process name doesn't match — could be an external Python/Docker server.
|
||||||
|
// Verify via HTTP health check before giving up.
|
||||||
|
println!("Port {} in use by unknown process, checking if it's a Voicebox server...", SERVER_PORT);
|
||||||
|
if check_health(SERVER_PORT) {
|
||||||
|
println!("Health check passed — reusing external server on port {}", SERVER_PORT);
|
||||||
|
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
|
||||||
|
}
|
||||||
|
return Err(format!(
|
||||||
|
"Port {} is already in use by another application. \
|
||||||
|
Close the other application or change the Voicebox port.",
|
||||||
|
SERVER_PORT
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user