mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
fix(updater): drive download/install/restart from backend to avoid hang
The 3.16/3.16.1 update flow was frontend-driven: downloadAndInstall() then relaunchApp(). relaunch() routed through AppHandle::restart(), and the old WebView had to keep running JS after the .app bundle was already swapped — an unstable window that could hang the update or leave the old version running until a manual restart. Move the whole download -> install -> cleanup -> restart chain into a new backend command install_update_and_restart, so it no longer depends on the old WebView running JS after the bundle is swapped. Platform-aware install ordering (install() behaves differently per OS): - Windows: install() launches the external installer and exits the process internally, so cleanup + single-instance destroy must run before install. Surface a recovery hint on failure since the proxy may already be stopped. - macOS/Linux: install() returns, so install first then cleanup — an install failure no longer wrongly stops the proxy / reverts takeover. Eliminate the restart vs single-instance race: restart_process() destroys the single-instance lock (remove socket on macOS, ReleaseMutex on Windows) before tauri::process::restart(), so the freshly spawned process can't connect to the old listener and exit itself. Also remove the now-dead frontend update plumbing (relaunchApp, UpdateHandle, mapUpdateHandle) and surface backend errors in the toast. Adapted from the original af4271f4 while rebasing onto #4069: the ExitRequested handler changes were dropped entirely — the classifier from #4069 already routes RESTART_EXIT_CODE to Tauri's default restart flow, and the original should_restart branch (prevent_exit + async cleanup) would have reintroduced the window-state deadlock that #4069 fixed. install_update_and_restart bypasses ExitRequested entirely, so the two fixes compose cleanly.
This commit is contained in:
@@ -32,7 +32,6 @@ import type {
|
||||
ToolInstallationReport,
|
||||
} from "@/lib/api/settings";
|
||||
import { useUpdate } from "@/contexts/UpdateContext";
|
||||
import { relaunchApp } from "@/lib/updater";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { motion } from "framer-motion";
|
||||
import appIcon from "@/assets/icons/app-icon.png";
|
||||
@@ -195,14 +194,8 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
);
|
||||
const [showInstallCommands, setShowInstallCommands] = useState(false);
|
||||
|
||||
const {
|
||||
hasUpdate,
|
||||
updateInfo,
|
||||
updateHandle,
|
||||
checkUpdate,
|
||||
resetDismiss,
|
||||
isChecking,
|
||||
} = useUpdate();
|
||||
const { hasUpdate, updateInfo, checkUpdate, resetDismiss, isChecking } =
|
||||
useUpdate();
|
||||
|
||||
const [wslShellByTool, setWslShellByTool] = useState<
|
||||
Record<string, WslShellPreference>
|
||||
@@ -392,7 +385,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
}, [t, updateInfo?.availableVersion, version]);
|
||||
|
||||
const handleCheckUpdate = useCallback(async () => {
|
||||
if (hasUpdate && updateHandle) {
|
||||
if (hasUpdate) {
|
||||
if (isPortable) {
|
||||
try {
|
||||
await settingsApi.checkUpdates();
|
||||
@@ -405,11 +398,16 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
resetDismiss();
|
||||
await updateHandle.downloadAndInstall();
|
||||
await relaunchApp();
|
||||
const installed = await settingsApi.installUpdateAndRestart();
|
||||
if (!installed) {
|
||||
toast.success(t("settings.upToDate"), { closeButton: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AboutSection] Update failed", error);
|
||||
toast.error(t("settings.updateFailed"));
|
||||
toast.error(t("settings.updateFailed"), {
|
||||
description: extractErrorMessage(error) || undefined,
|
||||
closeButton: true,
|
||||
});
|
||||
try {
|
||||
await settingsApi.checkUpdates();
|
||||
} catch (fallbackError) {
|
||||
@@ -433,7 +431,7 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
console.error("[AboutSection] Check update failed", error);
|
||||
toast.error(t("settings.checkUpdateFailed"));
|
||||
}
|
||||
}, [checkUpdate, hasUpdate, isPortable, resetDismiss, t, updateHandle]);
|
||||
}, [checkUpdate, hasUpdate, isPortable, resetDismiss, t]);
|
||||
|
||||
const handleCopyInstallCommands = useCallback(async () => {
|
||||
try {
|
||||
|
||||
@@ -6,14 +6,13 @@ import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from "react";
|
||||
import type { UpdateInfo, UpdateHandle } from "../lib/updater";
|
||||
import type { UpdateInfo } from "../lib/updater";
|
||||
import { checkForUpdate } from "../lib/updater";
|
||||
|
||||
interface UpdateContextValue {
|
||||
// 更新状态
|
||||
hasUpdate: boolean;
|
||||
updateInfo: UpdateInfo | null;
|
||||
updateHandle: UpdateHandle | null;
|
||||
isChecking: boolean;
|
||||
error: string | null;
|
||||
|
||||
@@ -34,7 +33,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||
const [updateHandle, setUpdateHandle] = useState<UpdateHandle | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
@@ -72,7 +70,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
if (result.status === "available") {
|
||||
setHasUpdate(true);
|
||||
setUpdateInfo(result.info);
|
||||
setUpdateHandle(result.update);
|
||||
|
||||
// 检查是否已经关闭过这个版本的提醒
|
||||
let dismissedVersion = localStorage.getItem(DISMISSED_VERSION_KEY);
|
||||
@@ -89,7 +86,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
} else {
|
||||
setHasUpdate(false);
|
||||
setUpdateInfo(null);
|
||||
setUpdateHandle(null);
|
||||
setIsDismissed(false);
|
||||
return false; // 已是最新
|
||||
}
|
||||
@@ -132,7 +128,6 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
|
||||
const value: UpdateContextValue = {
|
||||
hasUpdate,
|
||||
updateInfo,
|
||||
updateHandle,
|
||||
isChecking,
|
||||
error,
|
||||
isDismissed,
|
||||
|
||||
@@ -36,6 +36,10 @@ export const settingsApi = {
|
||||
return await invoke("restart_app");
|
||||
},
|
||||
|
||||
async installUpdateAndRestart(): Promise<boolean> {
|
||||
return await invoke("install_update_and_restart");
|
||||
},
|
||||
|
||||
async checkUpdates(): Promise<void> {
|
||||
await invoke("check_for_updates");
|
||||
},
|
||||
|
||||
+5
-83
@@ -1,22 +1,7 @@
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
|
||||
// 可选导入:在未注册插件或非 Tauri 环境下,调用时会抛错,外层需做兜底
|
||||
// 我们按需加载并在运行时捕获错误,避免构建期类型问题
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import type { Update } from "@tauri-apps/plugin-updater";
|
||||
|
||||
export type UpdateChannel = "stable" | "beta";
|
||||
|
||||
export type UpdaterPhase =
|
||||
| "idle"
|
||||
| "checking"
|
||||
| "available"
|
||||
| "downloading"
|
||||
| "installing"
|
||||
| "restarting"
|
||||
| "upToDate"
|
||||
| "error";
|
||||
|
||||
export interface UpdateInfo {
|
||||
currentVersion: string;
|
||||
availableVersion: string;
|
||||
@@ -24,64 +9,11 @@ export interface UpdateInfo {
|
||||
pubDate?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProgressEvent {
|
||||
event: "Started" | "Progress" | "Finished";
|
||||
total?: number;
|
||||
downloaded?: number;
|
||||
}
|
||||
|
||||
export interface UpdateHandle {
|
||||
version: string;
|
||||
notes?: string;
|
||||
date?: string;
|
||||
downloadAndInstall: (
|
||||
onProgress?: (e: UpdateProgressEvent) => void,
|
||||
) => Promise<void>;
|
||||
download?: () => Promise<void>;
|
||||
install?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface CheckOptions {
|
||||
timeout?: number;
|
||||
channel?: UpdateChannel;
|
||||
}
|
||||
|
||||
function mapUpdateHandle(raw: Update): UpdateHandle {
|
||||
return {
|
||||
version: (raw as any).version ?? "",
|
||||
notes: (raw as any).notes,
|
||||
date: (raw as any).date,
|
||||
async downloadAndInstall(onProgress?: (e: UpdateProgressEvent) => void) {
|
||||
await (raw as any).downloadAndInstall((evt: any) => {
|
||||
if (!onProgress) return;
|
||||
const mapped: UpdateProgressEvent = {
|
||||
event: evt?.event,
|
||||
};
|
||||
if (evt?.event === "Started") {
|
||||
mapped.total = evt?.data?.contentLength ?? 0;
|
||||
mapped.downloaded = 0;
|
||||
} else if (evt?.event === "Progress") {
|
||||
mapped.downloaded = evt?.data?.chunkLength ?? 0; // 累积由调用方完成
|
||||
}
|
||||
onProgress(mapped);
|
||||
});
|
||||
},
|
||||
// 透传可选 API(若插件版本支持)
|
||||
download: (raw as any).download
|
||||
? async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await (raw as any).download();
|
||||
}
|
||||
: undefined,
|
||||
install: (raw as any).install
|
||||
? async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
await (raw as any).install();
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentVersion(): Promise<string> {
|
||||
try {
|
||||
return await getVersion();
|
||||
@@ -93,8 +25,7 @@ export async function getCurrentVersion(): Promise<string> {
|
||||
export async function checkForUpdate(
|
||||
opts: CheckOptions = {},
|
||||
): Promise<
|
||||
| { status: "up-to-date" }
|
||||
| { status: "available"; info: UpdateInfo; update: UpdateHandle }
|
||||
{ status: "up-to-date" } | { status: "available"; info: UpdateInfo }
|
||||
> {
|
||||
// 动态引入,避免在未安装插件时导致打包期问题
|
||||
const { check } = await import("@tauri-apps/plugin-updater");
|
||||
@@ -106,21 +37,12 @@ export async function checkForUpdate(
|
||||
return { status: "up-to-date" };
|
||||
}
|
||||
|
||||
const mapped = mapUpdateHandle(update);
|
||||
const info: UpdateInfo = {
|
||||
currentVersion,
|
||||
availableVersion: mapped.version,
|
||||
notes: mapped.notes,
|
||||
pubDate: mapped.date,
|
||||
availableVersion: (update as any).version ?? "",
|
||||
notes: (update as any).notes,
|
||||
pubDate: (update as any).date,
|
||||
};
|
||||
|
||||
return { status: "available", info, update: mapped };
|
||||
return { status: "available", info };
|
||||
}
|
||||
|
||||
export async function relaunchApp(): Promise<void> {
|
||||
const { relaunch } = await import("@tauri-apps/plugin-process");
|
||||
await relaunch();
|
||||
}
|
||||
|
||||
// 旧的聚合更新流程已由调用方直接使用 updateHandle 取代
|
||||
// 如需单函数封装,可在需要时基于 checkForUpdate + updateHandle 复合调用
|
||||
|
||||
Reference in New Issue
Block a user