From 3228e6cdd7aa7222d8888e789db57af1200cd74f Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 3 Mar 2026 14:46:59 -0800 Subject: [PATCH] fix: Docker status never updates after Docker starts Replace OnceLock with Mutex> in the Rust backend so failed Docker connections are retried instead of cached permanently. Add frontend polling (every 5s) when Docker is initially unavailable, stopping once detected. Co-Authored-By: Claude Opus 4.6 --- app/src-tauri/src/docker/client.rs | 27 +++++++++++++--------- app/src/App.tsx | 10 +++++++-- app/src/hooks/useDocker.ts | 36 +++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/app/src-tauri/src/docker/client.rs b/app/src-tauri/src/docker/client.rs index d477c08..49c4e5b 100644 --- a/app/src-tauri/src/docker/client.rs +++ b/app/src-tauri/src/docker/client.rs @@ -1,23 +1,28 @@ use bollard::Docker; -use std::sync::OnceLock; +use std::sync::Mutex; -static DOCKER: OnceLock> = OnceLock::new(); +static DOCKER: Mutex> = Mutex::new(None); -pub fn get_docker() -> Result<&'static Docker, String> { - let result = DOCKER.get_or_init(|| { - Docker::connect_with_local_defaults() - .map_err(|e| format!("Failed to connect to Docker daemon: {}", e)) - }); - match result { - Ok(docker) => Ok(docker), - Err(e) => Err(e.clone()), +pub fn get_docker() -> Result { + let mut guard = DOCKER.lock().map_err(|e| format!("Lock poisoned: {}", e))?; + if let Some(docker) = guard.as_ref() { + return Ok(docker.clone()); } + let docker = Docker::connect_with_local_defaults() + .map_err(|e| format!("Failed to connect to Docker daemon: {}", e))?; + guard.replace(docker.clone()); + Ok(docker) } pub async fn check_docker_available() -> Result { let docker = get_docker()?; match docker.ping().await { Ok(_) => Ok(true), - Err(e) => Err(format!("Docker daemon not responding: {}", e)), + Err(_) => { + // Connection object exists but daemon not responding — clear cache + let mut guard = DOCKER.lock().map_err(|e| format!("Lock poisoned: {}", e))?; + *guard = None; + Ok(false) + } } } diff --git a/app/src/App.tsx b/app/src/App.tsx index 7e9a9fc..127236a 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -11,7 +11,7 @@ import { useUpdates } from "./hooks/useUpdates"; import { useAppState } from "./store/appState"; export default function App() { - const { checkDocker, checkImage } = useDocker(); + const { checkDocker, checkImage, startDockerPolling } = useDocker(); const { loadSettings } = useSettings(); const { refresh } = useProjects(); const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates(); @@ -22,8 +22,13 @@ export default function App() { // Initialize on mount useEffect(() => { loadSettings(); + let stopPolling: (() => void) | undefined; checkDocker().then((available) => { - if (available) checkImage(); + if (available) { + checkImage(); + } else { + stopPolling = startDockerPolling(); + } }); refresh(); @@ -34,6 +39,7 @@ export default function App() { return () => { clearTimeout(updateTimer); cleanup?.(); + stopPolling?.(); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/app/src/hooks/useDocker.ts b/app/src/hooks/useDocker.ts index be49d33..bb948fb 100644 --- a/app/src/hooks/useDocker.ts +++ b/app/src/hooks/useDocker.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useRef } from "react"; import { useShallow } from "zustand/react/shallow"; import { listen } from "@tauri-apps/api/event"; import { useAppState } from "../store/appState"; @@ -59,6 +59,39 @@ export function useDocker() { [setImageExists], ); + const pollingRef = useRef | null>(null); + + const startDockerPolling = useCallback(() => { + // Don't start if already polling + if (pollingRef.current) return () => {}; + + const interval = setInterval(async () => { + try { + const available = await commands.checkDocker(); + if (available) { + clearInterval(interval); + pollingRef.current = null; + setDockerAvailable(true); + // Also check image once Docker is available + try { + const exists = await commands.checkImageExists(); + setImageExists(exists); + } catch { + setImageExists(false); + } + } + } catch { + // Still not available, keep polling + } + }, 5000); + + pollingRef.current = interval; + return () => { + clearInterval(interval); + pollingRef.current = null; + }; + }, [setDockerAvailable, setImageExists]); + const pullImage = useCallback( async (imageName: string, onProgress?: (msg: string) => void) => { const unlisten = onProgress @@ -84,5 +117,6 @@ export function useDocker() { checkImage, buildImage, pullImage, + startDockerPolling, }; }