diff --git a/openflare_agent/internal/updater/restart_unix.go b/openflare_agent/internal/updater/restart_unix.go index 16350a7e..173af72b 100644 --- a/openflare_agent/internal/updater/restart_unix.go +++ b/openflare_agent/internal/updater/restart_unix.go @@ -11,30 +11,26 @@ import ( func replaceAndRestart(execPath string, tmpPath string) error { backupPath := execPath + ".bak" - err := os.Remove(backupPath) - if err != nil { - slog.Error("remove backup binary failed", "path", backupPath, "error", err) + if err := removeBackupBinary(backupPath); err != nil { return err } if err := os.Rename(execPath, backupPath); err != nil { - err := os.Remove(tmpPath) - if err != nil { + renameErr := err + if err := os.Remove(tmpPath); err != nil && !os.IsNotExist(err) { slog.Error("remove tmp binary failed", "path", tmpPath, "error", err) - return err + return fmt.Errorf("backup current binary: %w; remove tmp binary: %v", renameErr, err) } - return fmt.Errorf("backup current binary: %w", err) + return fmt.Errorf("backup current binary: %w", renameErr) } if err := os.Rename(tmpPath, execPath); err != nil { - err := os.Rename(backupPath, execPath) - if err != nil { + replaceErr := err + if err := os.Rename(backupPath, execPath); err != nil { slog.Error("restore backup binary failed", "path", backupPath, "error", err) - return err + return fmt.Errorf("replace binary: %w; restore backup binary: %v", replaceErr, err) } - return fmt.Errorf("replace binary: %w", err) + return fmt.Errorf("replace binary: %w", replaceErr) } - err = os.Remove(backupPath) - if err != nil { - slog.Error("remove backup binary failed", "path", backupPath, "error", err) + if err := removeBackupBinary(backupPath); err != nil { return err } if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil { @@ -42,3 +38,14 @@ func replaceAndRestart(execPath string, tmpPath string) error { } return fmt.Errorf("unreachable after exec") } + +func removeBackupBinary(path string) error { + if err := os.Remove(path); err != nil { + if os.IsNotExist(err) { + return nil + } + slog.Error("remove backup binary failed", "path", path, "error", err) + return err + } + return nil +} diff --git a/openflare_agent/internal/updater/restart_unix_test.go b/openflare_agent/internal/updater/restart_unix_test.go new file mode 100644 index 00000000..a984346a --- /dev/null +++ b/openflare_agent/internal/updater/restart_unix_test.go @@ -0,0 +1,15 @@ +//go:build !windows + +package updater + +import ( + "path/filepath" + "testing" +) + +func TestRemoveBackupBinaryIgnoresMissingFile(t *testing.T) { + backupPath := filepath.Join(t.TempDir(), "openflare-agent.bak") + if err := removeBackupBinary(backupPath); err != nil { + t.Fatalf("expected missing backup cleanup to be ignored: %v", err) + } +} diff --git a/openflare_server/web/components/layout/dashboard-topbar.tsx b/openflare_server/web/components/layout/dashboard-topbar.tsx index 8987308f..d05b95a6 100644 --- a/openflare_server/web/components/layout/dashboard-topbar.tsx +++ b/openflare_server/web/components/layout/dashboard-topbar.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/components/providers/auth-provider'; @@ -53,6 +53,9 @@ export function DashboardTopbar() { const [upgradeStream, setUpgradeStream] = useState(null); const menuRef = useRef(null); + const upgradeRefreshPendingRef = useRef(false); + const upgradeReloadStartedRef = useRef(false); + const upgradeReloadTimerRef = useRef(null); const isRoot = (user?.role ?? 0) >= 100; const upgradeStatusPollInterval = 3000; @@ -90,6 +93,7 @@ export function DashboardTopbar() { const upgradeMutation = useMutation({ mutationFn: (channel: ReleaseChannel) => upgradeServer(channel), onSuccess: (release) => { + upgradeRefreshPendingRef.current = true; setUploadedBinary(null); setManualUpgradeStatus(null); setManualUpgradeError(null); @@ -133,6 +137,7 @@ export function DashboardTopbar() { const confirmManualUpgradeMutation = useMutation({ mutationFn: confirmManualServerUpgrade, onSuccess: (candidate) => { + upgradeRefreshPendingRef.current = true; setVersionFeedback(null); setManualUpgradeError(null); setUploadedBinary(candidate); @@ -152,6 +157,40 @@ export function DashboardTopbar() { }, }); + const scheduleUpgradePageReload = useCallback(() => { + if (upgradeReloadStartedRef.current) { + return; + } + + upgradeReloadStartedRef.current = true; + setVersionFeedback('服务升级已进入重启阶段,页面将在服务恢复后自动刷新。'); + + const reloadWhenServerReady = async () => { + try { + await getPublicStatus(); + window.location.reload(); + } catch { + upgradeReloadTimerRef.current = window.setTimeout( + reloadWhenServerReady, + 1500, + ); + } + }; + + upgradeReloadTimerRef.current = window.setTimeout( + reloadWhenServerReady, + 1200, + ); + }, []); + + useEffect(() => { + return () => { + if (upgradeReloadTimerRef.current !== null) { + window.clearTimeout(upgradeReloadTimerRef.current); + } + }; + }, []); + useEffect(() => { if (!isVersionModalOpen || !isRoot) { setUpgradeStream(null); @@ -175,14 +214,28 @@ export function DashboardTopbar() { socket.onmessage = (event) => { const snapshot = parseUpgradeStreamSnapshot(String(event.data)); if (snapshot) { + if ( + snapshot.in_progress || + snapshot.upgrade_status === 'succeeded' + ) { + upgradeRefreshPendingRef.current = true; + } + if (snapshot.upgrade_status === 'failed') { + upgradeRefreshPendingRef.current = false; + } setUpgradeStream(snapshot); } }; socket.onclose = () => { - if (!closed) { - reconnectTimer = window.setTimeout(connect, 1500); + if (closed) { + return; } + if (upgradeRefreshPendingRef.current) { + scheduleUpgradePageReload(); + return; + } + reconnectTimer = window.setTimeout(connect, 1500); }; }; @@ -195,7 +248,7 @@ export function DashboardTopbar() { } socket?.close(); }; - }, [isRoot, isVersionModalOpen]); + }, [isRoot, isVersionModalOpen, scheduleUpgradePageReload]); useEffect(() => { if (!isUserMenuOpen) {