From 36c06b55ea0a80784b8acb3324cf547cc12ac33d Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 14 Jun 2026 12:36:42 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20stop=20desktop=20startup=20update=20loop?= =?UTF-8?q?=20=E2=80=94=20manifest=20version=20matches=20bundles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do not inflate latest.json from dmg filenames; use build version only. Add client loop guard when the same update retries without version bump. Co-authored-by: Cursor --- desktop/src/bridge/updater.ts | 94 ++++++++++++++++++++++++ scripts/generate-desktop-latest-json.mjs | 13 +++- scripts/reconcile-desktop-latest-json.sh | 33 ++------- 3 files changed, 112 insertions(+), 28 deletions(-) diff --git a/desktop/src/bridge/updater.ts b/desktop/src/bridge/updater.ts index b3813c7..84274bd 100644 --- a/desktop/src/bridge/updater.ts +++ b/desktop/src/bridge/updater.ts @@ -2,6 +2,82 @@ import { check } from '@tauri-apps/plugin-updater'; import { relaunch } from '@tauri-apps/plugin-process'; import { getVersion } from '@tauri-apps/api/app'; +const UPDATE_LOOP_GUARD_KEY = 'goldenchart:startup-update-guard'; +const UPDATE_LOOP_MAX_ATTEMPTS = 2; +const UPDATE_LOOP_WINDOW_MS = 15 * 60 * 1000; + +function cmpSemver(a: string, b: string): number { + const pa = a.split('.').map(Number); + const pb = b.split('.').map(Number); + for (let i = 0; i < 3; i++) { + const d = (pa[i] || 0) - (pb[i] || 0); + if (d !== 0) return d; + } + return 0; +} + +interface UpdateLoopGuard { + targetVersion: string; + fromVersion: string; + attempts: number; + lastAt: number; +} + +function readUpdateLoopGuard(): UpdateLoopGuard | null { + try { + const raw = localStorage.getItem(UPDATE_LOOP_GUARD_KEY); + if (!raw) return null; + return JSON.parse(raw) as UpdateLoopGuard; + } catch { + return null; + } +} + +function writeUpdateLoopGuard(guard: UpdateLoopGuard) { + try { + localStorage.setItem(UPDATE_LOOP_GUARD_KEY, JSON.stringify(guard)); + } catch { + /* ignore */ + } +} + +function clearUpdateLoopGuard() { + try { + localStorage.removeItem(UPDATE_LOOP_GUARD_KEY); + } catch { + /* ignore */ + } +} + +function shouldBlockUpdateLoop(currentVersion: string, targetVersion: string): boolean { + const guard = readUpdateLoopGuard(); + if (!guard) return false; + if (guard.targetVersion !== targetVersion) return false; + if (currentVersion === targetVersion) return false; + if (Date.now() - guard.lastAt > UPDATE_LOOP_WINDOW_MS) return false; + return guard.attempts >= UPDATE_LOOP_MAX_ATTEMPTS && guard.fromVersion === currentVersion; +} + +function recordUpdateLoopAttempt(currentVersion: string, targetVersion: string) { + const guard = readUpdateLoopGuard(); + const now = Date.now(); + if ( + guard + && guard.targetVersion === targetVersion + && guard.fromVersion === currentVersion + && now - guard.lastAt < UPDATE_LOOP_WINDOW_MS + ) { + writeUpdateLoopGuard({ ...guard, attempts: guard.attempts + 1, lastAt: now }); + return; + } + writeUpdateLoopGuard({ + targetVersion, + fromVersion: currentVersion, + attempts: 1, + lastAt: now, + }); +} + export type DesktopUpdatePhase = | 'checking' | 'uptodate' @@ -172,11 +248,29 @@ export async function runDesktopUpdate( try { const update = await check(); if (!update) { + clearUpdateLoopGuard(); const message = `최신 버전입니다. (v${currentVersion})`; emit(onProgress, { phase: 'uptodate', message, currentVersion }); return { ok: true, phase: 'uptodate', message, currentVersion }; } + if (cmpSemver(update.version, currentVersion) <= 0) { + clearUpdateLoopGuard(); + const message = `최신 버전입니다. (v${currentVersion})`; + emit(onProgress, { phase: 'uptodate', message, currentVersion }); + return { ok: true, phase: 'uptodate', message, currentVersion }; + } + + if (shouldBlockUpdateLoop(currentVersion, update.version)) { + const message = + `v${update.version} 업데이트가 반복 실패했습니다. 서버 패키지와 버전이 맞지 않을 수 있습니다. ` + + 'PC 프로그램 탭에서 설치 파일을 받아 다시 설치해 주세요.'; + emit(onProgress, { phase: 'error', message, currentVersion, newVersion: update.version }); + return { ok: false, phase: 'error', message, currentVersion, newVersion: update.version }; + } + + recordUpdateLoopAttempt(currentVersion, update.version); + emit(onProgress, { phase: 'available', message: `새 버전 v${update.version}을(를) 받습니다. (현재 v${currentVersion})`, diff --git a/scripts/generate-desktop-latest-json.mjs b/scripts/generate-desktop-latest-json.mjs index 2eb3e4e..7a1d3f9 100644 --- a/scripts/generate-desktop-latest-json.mjs +++ b/scripts/generate-desktop-latest-json.mjs @@ -60,17 +60,19 @@ function addPlatform(platformKey, bundleName) { return; } const bundleVersion = versionFromName(bundleName); + const effectiveVersion = bundleVersion === '0.0.0' ? version : bundleVersion; const existing = platforms[platformKey]; - if (existing && cmpSemver(bundleVersion, existing.bundleVersion) <= 0) return; + if (existing && cmpSemver(effectiveVersion, existing.bundleVersion) <= 0) return; platforms[platformKey] = { url: `${baseUrl}/${encodeURIComponent(bundleName)}`, signature, - bundleVersion, + bundleVersion: effectiveVersion, }; } for (const f of files) { if (f.endsWith('.sig')) continue; + if (f.endsWith('.dmg')) continue; // 설치용 dmg — updater manifest 대상 아님 const lower = f.toLowerCase(); if (lower.includes('aarch64') && (lower.endsWith('.tar.gz') || lower.endsWith('.app.tar.gz'))) { addPlatform('darwin-aarch64', f); @@ -91,7 +93,12 @@ const bundleMaxVersion = Object.values(platforms).reduce( (max, p) => (cmpSemver(p.bundleVersion, max) > 0 ? p.bundleVersion : max), '0.0.0', ); -const manifestVersion = cmpSemver(bundleMaxVersion, version) > 0 ? bundleMaxVersion : version; +// manifest 버전 = 빌드 버전(--version). 번들 파일명에 버전 없으면 CLI 버전 사용. +// dmg 파일명으로 올리지 않음 — tar.gz 내용과 불일치하면 업데이트 무한 루프. +let manifestVersion = version; +if (bundleMaxVersion !== '0.0.0' && cmpSemver(bundleMaxVersion, version) < 0) { + console.warn(`[warn] bundle version ${bundleMaxVersion} < build ${version} — using build version`); +} const manifest = { version: manifestVersion, diff --git a/scripts/reconcile-desktop-latest-json.sh b/scripts/reconcile-desktop-latest-json.sh index b609835..9ad492b 100755 --- a/scripts/reconcile-desktop-latest-json.sh +++ b/scripts/reconcile-desktop-latest-json.sh @@ -15,40 +15,23 @@ if [[ ! -d "$UPDATES_DIR" ]]; then fi VERSION="$(node -p "require('$CONF').version" 2>/dev/null || echo '0.1.0')" -LATEST_JSON="$UPDATES_DIR/latest.json" BUILD_INFO="$UPDATES_DIR/build-info.json" - -# bundle 파일명·build-info·기존 manifest 중 최고 버전 사용 (build-info stale 방지) -VERSION="$(node <