Compare commits

..

1 Commits

Author SHA1 Message Date
3228e6cdd7 fix: Docker status never updates after Docker starts
All checks were successful
Build App / build-macos (push) Successful in 2m22s
Build App / build-windows (push) Successful in 3m22s
Build App / build-linux (push) Successful in 5m56s
Sync Release to GitHub / sync-release (release) Successful in 1s
Replace OnceLock with Mutex<Option<Docker>> 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 <noreply@anthropic.com>
2026-03-03 14:46:59 -08:00
3 changed files with 59 additions and 14 deletions

View File

@@ -1,23 +1,28 @@
use bollard::Docker; use bollard::Docker;
use std::sync::OnceLock; use std::sync::Mutex;
static DOCKER: OnceLock<Result<Docker, String>> = OnceLock::new(); static DOCKER: Mutex<Option<Docker>> = Mutex::new(None);
pub fn get_docker() -> Result<&'static Docker, String> { pub fn get_docker() -> Result<Docker, String> {
let result = DOCKER.get_or_init(|| { let mut guard = DOCKER.lock().map_err(|e| format!("Lock poisoned: {}", e))?;
Docker::connect_with_local_defaults() if let Some(docker) = guard.as_ref() {
.map_err(|e| format!("Failed to connect to Docker daemon: {}", e)) return Ok(docker.clone());
});
match result {
Ok(docker) => Ok(docker),
Err(e) => Err(e.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<bool, String> { pub async fn check_docker_available() -> Result<bool, String> {
let docker = get_docker()?; let docker = get_docker()?;
match docker.ping().await { match docker.ping().await {
Ok(_) => Ok(true), 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)
}
} }
} }

View File

@@ -11,7 +11,7 @@ import { useUpdates } from "./hooks/useUpdates";
import { useAppState } from "./store/appState"; import { useAppState } from "./store/appState";
export default function App() { export default function App() {
const { checkDocker, checkImage } = useDocker(); const { checkDocker, checkImage, startDockerPolling } = useDocker();
const { loadSettings } = useSettings(); const { loadSettings } = useSettings();
const { refresh } = useProjects(); const { refresh } = useProjects();
const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates(); const { loadVersion, checkForUpdates, startPeriodicCheck } = useUpdates();
@@ -22,8 +22,13 @@ export default function App() {
// Initialize on mount // Initialize on mount
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
let stopPolling: (() => void) | undefined;
checkDocker().then((available) => { checkDocker().then((available) => {
if (available) checkImage(); if (available) {
checkImage();
} else {
stopPolling = startDockerPolling();
}
}); });
refresh(); refresh();
@@ -34,6 +39,7 @@ export default function App() {
return () => { return () => {
clearTimeout(updateTimer); clearTimeout(updateTimer);
cleanup?.(); cleanup?.();
stopPolling?.();
}; };
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps

View File

@@ -1,4 +1,4 @@
import { useCallback } from "react"; import { useCallback, useRef } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { useAppState } from "../store/appState"; import { useAppState } from "../store/appState";
@@ -59,6 +59,39 @@ export function useDocker() {
[setImageExists], [setImageExists],
); );
const pollingRef = useRef<ReturnType<typeof setInterval> | 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( const pullImage = useCallback(
async (imageName: string, onProgress?: (msg: string) => void) => { async (imageName: string, onProgress?: (msg: string) => void) => {
const unlisten = onProgress const unlisten = onProgress
@@ -84,5 +117,6 @@ export function useDocker() {
checkImage, checkImage,
buildImage, buildImage,
pullImage, pullImage,
startDockerPolling,
}; };
} }