mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
fix(electron): recover from incompatible data directory
This commit is contained in:
1 parent
e482a675a0
commit
dfabc4f087
10 files changed
+325
-44
No files matched your search
@@ -20,7 +20,8 @@ import {
|
||||
} from '../paths/legacy-migration'
|
||||
import { ensureAppDirs, getSystemDataDir, getAiDataDir, getTempDir } from '../paths/locations'
|
||||
import { getPathProvider } from '../paths/provider'
|
||||
import { assertDesktopDataDirCompatible, getDesktopAppVersion } from '../runtime/compat'
|
||||
import { assertDesktopDataDirCompatible, getDesktopAppVersion, getDesktopUpdateRequirement } from '../runtime/compat'
|
||||
import { recoverRequiredDesktopUpdate } from '../update/manager'
|
||||
|
||||
export async function prepareDesktopRuntime(isTestMode: boolean): Promise<boolean> {
|
||||
logNativeParserStatus()
|
||||
@@ -42,6 +43,14 @@ export async function prepareDesktopRuntime(isTestMode: boolean): Promise<boolea
|
||||
runtime = assertDesktopDataDirCompatible(getPathProvider(), getDesktopAppVersion(app.getVersion()))
|
||||
} catch (error) {
|
||||
console.error('[Main] Data directory compatibility check failed:', error)
|
||||
const updateRequirement = getDesktopUpdateRequirement(error)
|
||||
if (updateRequirement && !isTestMode) {
|
||||
await app.whenReady()
|
||||
await initLocale()
|
||||
await recoverRequiredDesktopUpdate(updateRequirement)
|
||||
return false
|
||||
}
|
||||
|
||||
dialog.showErrorBox(
|
||||
'ChatLab Data Directory Incompatible',
|
||||
`ChatLab cannot open this data directory with the current desktop version.\n\n${
|
||||
|
||||
@@ -30,6 +30,18 @@ export default {
|
||||
remindLater: 'Remind Later',
|
||||
installOnQuit: 'Later (auto-install on quit)',
|
||||
upToDate: 'You are up to date',
|
||||
requiredTitle: 'ChatLab Update Required',
|
||||
requiredMessage: 'Version {{currentVersion}} cannot open this data directory',
|
||||
requiredDetail:
|
||||
'This data directory requires ChatLab {{minRuntimeVersion}} or newer. To protect your data, ChatLab will not open the database with the current version.\n\nAfter you choose Update Now, ChatLab downloads in the background and starts the installer when ready; the main window stays closed during the download. You can also open the official download page.\nData directory: {{userDataDir}}',
|
||||
updateNow: 'Update Now',
|
||||
openDownloadPage: 'Open Download Page',
|
||||
quit: 'Quit',
|
||||
requiredUpdateFailedTitle: 'Automatic Update Failed',
|
||||
requiredUpdateFailedMessage: 'ChatLab could not update to the required version',
|
||||
requiredUpdateFailedDetail:
|
||||
'Install ChatLab {{minRuntimeVersion}} or newer from the official download page, then reopen the app.',
|
||||
openDownloadFailed: 'Could not open the download page. Open this address manually:',
|
||||
},
|
||||
|
||||
// ===== P0: File/directory dialogs =====
|
||||
|
||||
@@ -30,6 +30,18 @@ export default {
|
||||
remindLater: '後で通知',
|
||||
installOnQuit: '後で(アプリ終了時に自動インストール)',
|
||||
upToDate: '最新バージョンです',
|
||||
requiredTitle: 'ChatLab の更新が必要です',
|
||||
requiredMessage: '現在のバージョン {{currentVersion}} では、このデータディレクトリを開けません',
|
||||
requiredDetail:
|
||||
'このデータディレクトリには ChatLab {{minRuntimeVersion}} 以降が必要です。データを保護するため、現在のバージョンではデータベースを開きません。\n\n「今すぐ更新」を選ぶとバックグラウンドでダウンロードし、完了後にインストーラーを起動します。ダウンロード中はメイン画面を開きません。公式ダウンロードページを開くこともできます。\nデータディレクトリ:{{userDataDir}}',
|
||||
updateNow: '今すぐ更新',
|
||||
openDownloadPage: 'ダウンロードページを開く',
|
||||
quit: '終了',
|
||||
requiredUpdateFailedTitle: '自動更新に失敗しました',
|
||||
requiredUpdateFailedMessage: '必要なバージョンに更新できませんでした',
|
||||
requiredUpdateFailedDetail:
|
||||
'公式ダウンロードページから ChatLab {{minRuntimeVersion}} 以降をインストールして、アプリを再度開いてください。',
|
||||
openDownloadFailed: 'ダウンロードページを開けませんでした。次の URL を手動で開いてください:',
|
||||
},
|
||||
|
||||
// ===== P0: ファイル/ディレクトリダイアログ =====
|
||||
|
||||
@@ -29,6 +29,17 @@ export default {
|
||||
remindLater: '之后提醒',
|
||||
installOnQuit: '稍后(应用退出后自动安装)',
|
||||
upToDate: '已是最新版本',
|
||||
requiredTitle: '需要更新 ChatLab',
|
||||
requiredMessage: '当前版本 {{currentVersion}} 无法打开此数据目录',
|
||||
requiredDetail:
|
||||
'此数据目录需要 ChatLab {{minRuntimeVersion}} 或更高版本。为保护数据,ChatLab 不会使用当前版本打开数据库。\n\n选择立即更新后,应用将在后台下载,完成后自动启动安装程序;下载期间主页面不会打开。你也可以前往官方下载页面。\n数据目录:{{userDataDir}}',
|
||||
updateNow: '立即更新',
|
||||
openDownloadPage: '打开下载页面',
|
||||
quit: '退出',
|
||||
requiredUpdateFailedTitle: '自动更新失败',
|
||||
requiredUpdateFailedMessage: '无法自动升级到所需版本',
|
||||
requiredUpdateFailedDetail: '请从官方下载页面安装 ChatLab {{minRuntimeVersion}} 或更高版本,然后重新打开应用。',
|
||||
openDownloadFailed: '无法打开下载页面,请手动访问:',
|
||||
},
|
||||
|
||||
// ===== P0: 文件/目录对话框 =====
|
||||
|
||||
@@ -30,6 +30,17 @@ export default {
|
||||
remindLater: '稍後提醒',
|
||||
installOnQuit: '稍後(應用退出後自動安裝)',
|
||||
upToDate: '已是最新版本',
|
||||
requiredTitle: '需要更新 ChatLab',
|
||||
requiredMessage: '目前版本 {{currentVersion}} 無法開啟此資料目錄',
|
||||
requiredDetail:
|
||||
'此資料目錄需要 ChatLab {{minRuntimeVersion}} 或更新版本。為了保護資料,ChatLab 不會使用目前版本開啟資料庫。\n\n選擇立即更新後,應用程式會在背景下載,完成後自動啟動安裝程式;下載期間不會開啟主畫面。你也可以前往官方下載頁面。\n資料目錄:{{userDataDir}}',
|
||||
updateNow: '立即更新',
|
||||
openDownloadPage: '開啟下載頁面',
|
||||
quit: '結束',
|
||||
requiredUpdateFailedTitle: '自動更新失敗',
|
||||
requiredUpdateFailedMessage: '無法自動升級到所需版本',
|
||||
requiredUpdateFailedDetail: '請從官方下載頁面安裝 ChatLab {{minRuntimeVersion}} 或更新版本,然後重新開啟應用程式。',
|
||||
openDownloadFailed: '無法開啟下載頁面,請手動前往:',
|
||||
},
|
||||
|
||||
// ===== P0: 檔案/目錄對話框 =====
|
||||
|
||||
@@ -4,7 +4,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import { assertDesktopDataDirCompatible, resolveDesktopAppVersion } from './compat'
|
||||
import { assertDesktopDataDirCompatible, getDesktopUpdateRequirement, resolveDesktopAppVersion } from './compat'
|
||||
|
||||
function makeTempDir(): string {
|
||||
const baseDir = process.env.CHATLAB_TEST_TMPDIR ?? (fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir())
|
||||
@@ -94,6 +94,18 @@ test('assertDesktopDataDirCompatible formats a startup error for older desktop r
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
let startupError: unknown
|
||||
try {
|
||||
assertDesktopDataDirCompatible(makePathProvider(userDataDir), '0.25.0')
|
||||
} catch (error) {
|
||||
startupError = error
|
||||
}
|
||||
assert.deepEqual(getDesktopUpdateRequirement(startupError), {
|
||||
currentVersion: '0.25.0',
|
||||
minRuntimeVersion: '999.0.0',
|
||||
userDataDir,
|
||||
})
|
||||
})
|
||||
|
||||
test('assertDesktopDataDirCompatible shows original prerelease version when blocked', () => {
|
||||
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
type RuntimeIdentity,
|
||||
} from '@openchatlab/node-runtime/data-dir-compat'
|
||||
|
||||
export interface DesktopUpdateRequirement {
|
||||
currentVersion: string
|
||||
minRuntimeVersion: string
|
||||
userDataDir: string
|
||||
}
|
||||
|
||||
export function resolveDesktopAppVersion(electronVersion: string | null | undefined, bundledVersion?: string): string {
|
||||
const normalizedElectronVersion = normalizeVersion(electronVersion)
|
||||
if (normalizedElectronVersion && normalizedElectronVersion !== '0.0.0') return normalizedElectronVersion
|
||||
@@ -43,6 +49,24 @@ export function assertDesktopDataDirCompatible(pathProvider: PathProvider, versi
|
||||
return runtime
|
||||
}
|
||||
|
||||
export function getDesktopUpdateRequirement(error: unknown): DesktopUpdateRequirement | null {
|
||||
const cause = error instanceof Error ? error.cause : undefined
|
||||
if (
|
||||
!(cause instanceof DataDirCompatibilityError) ||
|
||||
cause.code !== 'DATA_DIR_REQUIRES_NEWER_RUNTIME' ||
|
||||
!cause.currentVersion ||
|
||||
!cause.minRuntimeVersion
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
currentVersion: cause.currentVersion,
|
||||
minRuntimeVersion: cause.minRuntimeVersion,
|
||||
userDataDir: cause.userDataDir,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeVersion(version: string | null | undefined): string {
|
||||
return typeof version === 'string' ? version.trim() : ''
|
||||
}
|
||||
|
||||
@@ -9,13 +9,22 @@ class FakeAutoUpdater extends EventEmitter {
|
||||
checkCalls = 0
|
||||
quitInstallCalls = 0
|
||||
feedUrls: unknown[] = []
|
||||
checkResults: unknown[] = []
|
||||
|
||||
setFeedURL(url: unknown): void {
|
||||
this.feedUrls.push(url)
|
||||
}
|
||||
|
||||
async checkForUpdates(): Promise<void> {
|
||||
async checkForUpdates(): Promise<unknown> {
|
||||
this.checkCalls++
|
||||
const result = this.checkResults.length
|
||||
? this.checkResults.shift()
|
||||
: { isUpdateAvailable: false, updateInfo: { version: '0.36.0' } }
|
||||
if (result instanceof Error) {
|
||||
this.emit('error', result)
|
||||
throw result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async downloadUpdate(): Promise<void> {
|
||||
@@ -60,12 +69,18 @@ class FakeUpdateWindow extends EventEmitter {
|
||||
async function loadUpdaterModule() {
|
||||
const autoUpdater = new FakeAutoUpdater()
|
||||
const dialogCalls: DialogCall[] = []
|
||||
const dialogResponses: number[] = []
|
||||
const externalUrls: string[] = []
|
||||
const logMessages: string[] = []
|
||||
let quitCalls = 0
|
||||
|
||||
await mock.module('electron', {
|
||||
namedExports: {
|
||||
app: {
|
||||
isPackaged: true,
|
||||
quit() {
|
||||
quitCalls++
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
async showMessageBox(parentOrOptions: unknown, maybeOptions?: DialogCall) {
|
||||
@@ -74,7 +89,15 @@ async function loadUpdaterModule() {
|
||||
...options,
|
||||
parent: maybeOptions ? parentOrOptions : undefined,
|
||||
})
|
||||
return { response: 1 }
|
||||
return { response: dialogResponses.shift() ?? 1 }
|
||||
},
|
||||
showErrorBox() {
|
||||
logMessages.push('showErrorBox')
|
||||
},
|
||||
},
|
||||
shell: {
|
||||
async openExternal(url: string) {
|
||||
externalUrls.push(url)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,6 +114,9 @@ async function loadUpdaterModule() {
|
||||
info(message: string) {
|
||||
logMessages.push(message)
|
||||
},
|
||||
warn(message: string) {
|
||||
logMessages.push(message)
|
||||
},
|
||||
error(message: string) {
|
||||
logMessages.push(message)
|
||||
},
|
||||
@@ -117,50 +143,100 @@ async function loadUpdaterModule() {
|
||||
return {
|
||||
autoUpdater,
|
||||
dialogCalls,
|
||||
dialogResponses,
|
||||
externalUrls,
|
||||
getQuitCalls: () => quitCalls,
|
||||
checkUpdate: mod.checkUpdate as unknown as (win: FakeUpdateWindow) => void,
|
||||
manualCheckForUpdates: mod.manualCheckForUpdates as () => void,
|
||||
recoverRequiredDesktopUpdate: mod.recoverRequiredDesktopUpdate as (requirement: {
|
||||
currentVersion: string
|
||||
minRuntimeVersion: string
|
||||
userDataDir: string
|
||||
}) => Promise<void>,
|
||||
}
|
||||
}
|
||||
|
||||
test('automatic stable updates defer the install prompt until the app window is active', async () => {
|
||||
const { autoUpdater, dialogCalls, checkUpdate, manualCheckForUpdates } = await loadUpdaterModule()
|
||||
const sent: unknown[][] = []
|
||||
const win = new FakeUpdateWindow({
|
||||
send: (...args: unknown[]) => sent.push(args),
|
||||
test('desktop updater workflows', async (t) => {
|
||||
const harness = await loadUpdaterModule()
|
||||
|
||||
await t.test('required update retries with GitHub, downloads, and installs without opening the app', async () => {
|
||||
const networkError = Object.assign(new Error('network timeout'), { code: 'ETIMEDOUT' })
|
||||
harness.autoUpdater.checkResults.push(networkError, {
|
||||
isUpdateAvailable: true,
|
||||
updateInfo: { version: '0.36.0' },
|
||||
cancellationToken: {},
|
||||
})
|
||||
harness.dialogResponses.push(0)
|
||||
|
||||
await harness.recoverRequiredDesktopUpdate({
|
||||
currentVersion: '0.35.0',
|
||||
minRuntimeVersion: '0.35.1',
|
||||
userDataDir: '/tmp/chatlab-data',
|
||||
})
|
||||
|
||||
assert.equal(harness.autoUpdater.checkCalls, 2)
|
||||
assert.equal(harness.autoUpdater.downloadCalls, 1)
|
||||
assert.equal(harness.autoUpdater.quitInstallCalls, 1)
|
||||
assert.equal(harness.getQuitCalls(), 0)
|
||||
assert.deepEqual(harness.autoUpdater.feedUrls.at(-1), {
|
||||
provider: 'github',
|
||||
owner: 'ChatLab',
|
||||
repo: 'ChatLab',
|
||||
})
|
||||
})
|
||||
win.focused = false
|
||||
|
||||
checkUpdate(win)
|
||||
await t.test('required update failure offers the official download page and quits safely', async () => {
|
||||
harness.autoUpdater.checkResults.push(null)
|
||||
harness.dialogResponses.push(0, 0)
|
||||
|
||||
autoUpdater.emit('update-available', { version: '0.28.2' })
|
||||
await Promise.resolve()
|
||||
await harness.recoverRequiredDesktopUpdate({
|
||||
currentVersion: '0.35.0',
|
||||
minRuntimeVersion: '0.35.1',
|
||||
userDataDir: '/tmp/chatlab-data',
|
||||
})
|
||||
|
||||
assert.equal(autoUpdater.downloadCalls, 1)
|
||||
assert.equal(dialogCalls.length, 0)
|
||||
assert.equal(autoUpdater.autoDownload, false)
|
||||
assert.equal(autoUpdater.autoInstallOnAppQuit, false)
|
||||
assert.equal(harness.dialogCalls.at(-1)?.title, 'update.requiredUpdateFailedTitle')
|
||||
assert.deepEqual(harness.externalUrls, ['https://github.com/ChatLab/ChatLab/releases'])
|
||||
assert.equal(harness.getQuitCalls(), 1)
|
||||
})
|
||||
|
||||
autoUpdater.emit('update-downloaded', { version: '0.28.2' })
|
||||
await Promise.resolve()
|
||||
await t.test('automatic stable updates defer the install prompt until the app window is active', async () => {
|
||||
const sent: unknown[][] = []
|
||||
const downloadsBefore = harness.autoUpdater.downloadCalls
|
||||
const win = new FakeUpdateWindow({
|
||||
send: (...args: unknown[]) => sent.push(args),
|
||||
})
|
||||
win.focused = false
|
||||
|
||||
assert.equal(dialogCalls.length, 0)
|
||||
harness.checkUpdate(win)
|
||||
|
||||
win.focused = true
|
||||
win.emit('focus')
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
harness.autoUpdater.emit('update-available', { version: '0.36.1' })
|
||||
await Promise.resolve()
|
||||
|
||||
assert.equal(dialogCalls.length, 1)
|
||||
assert.equal(dialogCalls[0]?.title, 'update.downloadComplete')
|
||||
assert.equal(dialogCalls[0]?.message, 'update.readyToInstall')
|
||||
assert.equal(dialogCalls[0]?.parent, win)
|
||||
assert.equal(harness.autoUpdater.downloadCalls, downloadsBefore + 1)
|
||||
assert.equal(harness.autoUpdater.autoDownload, false)
|
||||
assert.equal(harness.autoUpdater.autoInstallOnAppQuit, false)
|
||||
|
||||
manualCheckForUpdates()
|
||||
autoUpdater.emit('update-available', { version: '0.28.3' })
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
harness.autoUpdater.emit('update-downloaded', { version: '0.36.1' })
|
||||
await Promise.resolve()
|
||||
|
||||
assert.equal(dialogCalls.length, 2)
|
||||
assert.equal(dialogCalls[1]?.title, 'update.newVersionTitle:0.28.3')
|
||||
assert.equal(dialogCalls[1]?.message, 'update.newVersionMessage:0.28.3')
|
||||
assert.equal(dialogCalls[1]?.parent, win)
|
||||
assert.equal(autoUpdater.downloadCalls, 1)
|
||||
assert.notEqual(harness.dialogCalls.at(-1)?.title, 'update.downloadComplete')
|
||||
|
||||
win.focused = true
|
||||
win.emit('focus')
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
|
||||
assert.equal(harness.dialogCalls.at(-1)?.title, 'update.downloadComplete')
|
||||
assert.equal(harness.dialogCalls.at(-1)?.message, 'update.readyToInstall')
|
||||
assert.equal(harness.dialogCalls.at(-1)?.parent, win)
|
||||
|
||||
harness.manualCheckForUpdates()
|
||||
harness.autoUpdater.emit('update-available', { version: '0.36.2' })
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
|
||||
assert.equal(harness.dialogCalls.at(-1)?.title, 'update.newVersionTitle:0.36.2')
|
||||
assert.equal(harness.dialogCalls.at(-1)?.message, 'update.newVersionMessage:0.36.2')
|
||||
assert.equal(harness.dialogCalls.at(-1)?.parent, win)
|
||||
assert.equal(harness.autoUpdater.downloadCalls, downloadsBefore + 1)
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,23 @@
|
||||
import { dialog, app, type BrowserWindow } from 'electron'
|
||||
import { dialog, app, shell, type BrowserWindow } from 'electron'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { platform } from '@electron-toolkit/utils'
|
||||
import { isRuntimeVersionAtLeast } from '@openchatlab/node-runtime/data-dir-compat'
|
||||
import { logger } from '../logger'
|
||||
import { closeWorkerAsync } from '../worker/workerManager'
|
||||
import { t } from '../i18n'
|
||||
import { configureUpdateProxy, handleUpdateError, resetToDefaultSource } from './source'
|
||||
import type { DesktopUpdateRequirement } from '../runtime/compat'
|
||||
import {
|
||||
configureUpdateProxy,
|
||||
handleUpdateError,
|
||||
isUpdateNetworkError,
|
||||
resetToDefaultSource,
|
||||
switchToGitHubUpdateSource,
|
||||
} from './source'
|
||||
|
||||
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
|
||||
|
||||
const appWithQuitFlag = app as AppWithQuitFlag
|
||||
const OFFICIAL_RELEASES_URL = 'https://github.com/ChatLab/ChatLab/releases'
|
||||
|
||||
let isFirstShow = true
|
||||
let isManualCheck = false
|
||||
@@ -31,11 +40,67 @@ function runWhenWindowActive(win: BrowserWindow, callback: () => void): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function recoverRequiredDesktopUpdate(requirement: DesktopUpdateRequirement): Promise<void> {
|
||||
configureUpdater()
|
||||
configureUpdateProxy()
|
||||
resetToDefaultSource()
|
||||
|
||||
const prompt = await dialog.showMessageBox({
|
||||
title: t('update.requiredTitle'),
|
||||
message: t('update.requiredMessage', {
|
||||
currentVersion: requirement.currentVersion,
|
||||
minRuntimeVersion: requirement.minRuntimeVersion,
|
||||
}),
|
||||
detail: t('update.requiredDetail', {
|
||||
minRuntimeVersion: requirement.minRuntimeVersion,
|
||||
userDataDir: requirement.userDataDir,
|
||||
}),
|
||||
buttons: [t('update.updateNow'), t('update.openDownloadPage'), t('update.quit')],
|
||||
defaultId: 0,
|
||||
cancelId: 2,
|
||||
type: 'warning',
|
||||
noLink: true,
|
||||
})
|
||||
|
||||
if (prompt.response === 1) {
|
||||
await openOfficialReleasesAndQuit()
|
||||
return
|
||||
}
|
||||
if (prompt.response !== 0) {
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
const logUpdaterError = (error: Error): void => {
|
||||
logger.error('[Update] Required desktop update error', error)
|
||||
}
|
||||
autoUpdater.on('error', logUpdaterError)
|
||||
|
||||
try {
|
||||
const update = await checkRequiredUpdateWithFallback()
|
||||
if (
|
||||
!update?.isUpdateAvailable ||
|
||||
!isRuntimeVersionAtLeast(update.updateInfo.version, requirement.minRuntimeVersion)
|
||||
) {
|
||||
throw new Error(
|
||||
`No compatible desktop update is available (required ${requirement.minRuntimeVersion}, received ${update?.updateInfo.version ?? 'none'}).`
|
||||
)
|
||||
}
|
||||
|
||||
await autoUpdater.downloadUpdate(update.cancellationToken)
|
||||
appWithQuitFlag.isQuiting = true
|
||||
autoUpdater.quitAndInstall(true, true)
|
||||
} catch (error) {
|
||||
logger.error('[Update] Failed to recover from an incompatible data directory', error)
|
||||
await showRequiredUpdateFailure(requirement.minRuntimeVersion)
|
||||
} finally {
|
||||
autoUpdater.off('error', logUpdaterError)
|
||||
}
|
||||
}
|
||||
|
||||
export function checkUpdate(win: BrowserWindow): void {
|
||||
configureUpdateProxy()
|
||||
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = false
|
||||
configureUpdater()
|
||||
|
||||
let showUpdateMessageBox = false
|
||||
let showInstallMessageBox = false
|
||||
@@ -196,3 +261,52 @@ export function simulateUpdateDialog(win: BrowserWindow): void {
|
||||
function isPreReleaseVersion(version: string): boolean {
|
||||
return /-/.test(version)
|
||||
}
|
||||
|
||||
function configureUpdater(): void {
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = false
|
||||
}
|
||||
|
||||
async function checkRequiredUpdateWithFallback() {
|
||||
try {
|
||||
return await autoUpdater.checkForUpdates()
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error) || !isUpdateNetworkError(error)) throw error
|
||||
logger.warn('[Update] Required update check failed on R2 mirror; retrying with GitHub')
|
||||
switchToGitHubUpdateSource()
|
||||
return await autoUpdater.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
async function showRequiredUpdateFailure(minRuntimeVersion: string): Promise<void> {
|
||||
const result = await dialog.showMessageBox({
|
||||
title: t('update.requiredUpdateFailedTitle'),
|
||||
message: t('update.requiredUpdateFailedMessage'),
|
||||
detail: t('update.requiredUpdateFailedDetail', { minRuntimeVersion }),
|
||||
buttons: [t('update.openDownloadPage'), t('update.quit')],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
type: 'error',
|
||||
noLink: true,
|
||||
})
|
||||
|
||||
if (result.response === 0) {
|
||||
await openOfficialReleasesAndQuit()
|
||||
return
|
||||
}
|
||||
app.quit()
|
||||
}
|
||||
|
||||
async function openOfficialReleasesAndQuit(): Promise<void> {
|
||||
try {
|
||||
await shell.openExternal(OFFICIAL_RELEASES_URL)
|
||||
} catch (error) {
|
||||
logger.error('[Update] Failed to open the official releases page', error)
|
||||
dialog.showErrorBox(
|
||||
t('update.requiredUpdateFailedTitle'),
|
||||
`${t('update.openDownloadFailed')}\n\n${OFFICIAL_RELEASES_URL}`
|
||||
)
|
||||
} finally {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,11 @@ export function resetToDefaultSource(): void {
|
||||
export function handleUpdateError(error: Error): void {
|
||||
logger.error(`[Update] Update error (${currentSource}): ${error.message || error}`)
|
||||
|
||||
if (currentSource !== 'r2' || hasTriedFallback || !isNetworkError(error)) return
|
||||
if (currentSource !== 'r2' || hasTriedFallback || !isUpdateNetworkError(error)) return
|
||||
|
||||
hasTriedFallback = true
|
||||
logger.info('[Update] R2 mirror failed, trying GitHub fallback...')
|
||||
switchToGitHub()
|
||||
switchToGitHubUpdateSource()
|
||||
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates().catch((retryError) => {
|
||||
@@ -51,7 +51,7 @@ function switchToR2Mirror(): void {
|
||||
})
|
||||
}
|
||||
|
||||
function switchToGitHub(): void {
|
||||
export function switchToGitHubUpdateSource(): void {
|
||||
currentSource = 'github'
|
||||
autoUpdater.setFeedURL({
|
||||
provider: 'github',
|
||||
@@ -61,7 +61,7 @@ function switchToGitHub(): void {
|
||||
logger.info('[Update] Switched to GitHub fallback source')
|
||||
}
|
||||
|
||||
function isNetworkError(error: Error): boolean {
|
||||
export function isUpdateNetworkError(error: Error): boolean {
|
||||
const networkErrorKeywords = [
|
||||
'ECONNREFUSED',
|
||||
'ENOTFOUND',
|
||||
|
||||
Reference in new issue
Block a user