mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 00:02:05 +00:00
fix(updater): preserve Linux recovery across stale events
This commit is contained in:
@@ -1,16 +1,48 @@
|
||||
import type { UpdateStatus } from '../shared/update-status-types'
|
||||
import {
|
||||
captureLinuxPackageArtifact,
|
||||
clearTrackedLinuxPackageArtifact
|
||||
clearTrackedLinuxPackageArtifact,
|
||||
getTrackedLinuxPackageArtifact
|
||||
} from './linux-package-update-recovery'
|
||||
import {
|
||||
getLinuxPackageType,
|
||||
LINUX_PACKAGE_MARKER_UNUSABLE_MESSAGE
|
||||
} from './linux-update-package-type'
|
||||
import type { LinuxPackageArtifact } from './linux-package-update-recovery'
|
||||
|
||||
export const LINUX_PACKAGE_MANUAL_INSTALL_MESSAGE =
|
||||
'Quit Orca before running the system package install command.'
|
||||
const PACKAGE_METADATA_UNUSABLE_MESSAGE =
|
||||
'The downloaded package metadata could not be verified. Quit Orca before downloading and installing the update from the official release page.'
|
||||
|
||||
export function createLinuxPackageManualInstallStatus(
|
||||
artifact: Pick<LinuxPackageArtifact, 'packageType' | 'version'>
|
||||
): UpdateStatus {
|
||||
return {
|
||||
state: 'error',
|
||||
message: LINUX_PACKAGE_MANUAL_INSTALL_MESSAGE,
|
||||
recovery: {
|
||||
kind: 'linux-package-install',
|
||||
packageType: artifact.packageType,
|
||||
reason: 'manual-install-required',
|
||||
version: artifact.version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getRetainedLinuxPackageManualInstallStatus(): UpdateStatus | null {
|
||||
const artifact = getTrackedLinuxPackageArtifact()
|
||||
return artifact ? createLinuxPackageManualInstallStatus(artifact) : null
|
||||
}
|
||||
|
||||
export function shouldIgnoreDownloadedUpdateEvent(
|
||||
status: UpdateStatus,
|
||||
infoVersion: string,
|
||||
pendingVersion: string
|
||||
): boolean {
|
||||
return status.state === 'checking' || (pendingVersion !== '' && infoVersion !== pendingVersion)
|
||||
}
|
||||
|
||||
export function resolveLinuxPackageDownloadedStatus(info: {
|
||||
version: string
|
||||
}): UpdateStatus | null {
|
||||
@@ -36,14 +68,5 @@ export function resolveLinuxPackageDownloadedStatus(info: {
|
||||
retryable: false
|
||||
}
|
||||
}
|
||||
return {
|
||||
state: 'error',
|
||||
message: 'Quit Orca before running the system package install command.',
|
||||
recovery: {
|
||||
kind: 'linux-package-install',
|
||||
packageType: artifact.packageType,
|
||||
reason: 'manual-install-required',
|
||||
version: artifact.version
|
||||
}
|
||||
}
|
||||
return createLinuxPackageManualInstallStatus(artifact)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function createContext(overrides?: Partial<HandlerContext>): HandlerContext {
|
||||
consumeMissingManifestPrereleaseFallbackResult: vi.fn(() => null),
|
||||
getPublishingWindowLastGoodCheck: vi.fn(() => null),
|
||||
getMissingManifestPrereleaseFallbackUserInitiated: vi.fn(() => null),
|
||||
getCurrentStatus: vi.fn(() => ({ state: 'checking' }) as never),
|
||||
getCurrentStatus: vi.fn(() => ({ state: 'idle' }) as never),
|
||||
getActiveUpdateCheckEventAttemptId: vi.fn(() => 1),
|
||||
getKnownReleaseUrl: vi.fn(() => undefined),
|
||||
getPendingInstallVersion: vi.fn(() => '1.0.61'),
|
||||
@@ -249,13 +249,37 @@ describe('registerAutoUpdaterHandlers linux package artifact tracking', () => {
|
||||
expect(context.sendErrorStatus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('drops the artifact once the update resolves as not available', async () => {
|
||||
const { emit, getArtifact } = await register()
|
||||
it('keeps manual-install recovery when a later check finds no newer release', async () => {
|
||||
const { emit, context, getArtifact } = await register()
|
||||
emit('update-downloaded', downloadedEvent())
|
||||
|
||||
emit('update-not-available')
|
||||
|
||||
expect(getArtifact()).toEqual(expect.objectContaining({ version: '1.0.61' }))
|
||||
expect(context.sendStatus).toHaveBeenLastCalledWith({
|
||||
state: 'error',
|
||||
message: 'Quit Orca before running the system package install command.',
|
||||
recovery: {
|
||||
kind: 'linux-package-install',
|
||||
packageType: 'deb',
|
||||
reason: 'manual-install-required',
|
||||
version: '1.0.61'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('clears recovery when a newer update takes over before no-update settles', async () => {
|
||||
const { emit, context, getArtifact } = await register()
|
||||
emit('update-downloaded', downloadedEvent())
|
||||
|
||||
emit('update-available', { version: '1.0.62' })
|
||||
emit('update-not-available')
|
||||
|
||||
expect(getArtifact()).toBeNull()
|
||||
expect(context.sendStatus).toHaveBeenLastCalledWith({
|
||||
state: 'not-available',
|
||||
userInitiated: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('drops the artifact when another version takes over the cycle', async () => {
|
||||
@@ -267,6 +291,18 @@ describe('registerAutoUpdaterHandlers linux package artifact tracking', () => {
|
||||
expect(getArtifact()).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores a downloaded event for an older target', async () => {
|
||||
const { emit, context, getArtifact } = await register({
|
||||
getCurrentStatus: vi.fn(() => ({ state: 'available', version: '1.0.62' }) as never),
|
||||
getPendingInstallVersion: vi.fn(() => '1.0.62')
|
||||
})
|
||||
|
||||
emit('update-downloaded', downloadedEvent())
|
||||
|
||||
expect(getArtifact()).toBeNull()
|
||||
expect(context.sendStatus).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('drops the artifact when progress reports a different pending version', async () => {
|
||||
const { emit, getArtifact } = await register({
|
||||
getPendingInstallVersion: vi.fn(() => '1.0.62')
|
||||
|
||||
+31
-49
@@ -1,18 +1,19 @@
|
||||
import { app, autoUpdater as nativeUpdater } from 'electron'
|
||||
import { app } from 'electron'
|
||||
import type { UpdateStatus } from '../shared/update-status-types'
|
||||
import {
|
||||
consumeMacInstallGuardBypass,
|
||||
deferMacQuitUntilInstallerReady,
|
||||
handleMacInstallerReady,
|
||||
isMacInstallerReady,
|
||||
isMacQuitAndInstallInFlight,
|
||||
registerMacUpdaterEvents,
|
||||
resetMacInstallState
|
||||
} from './updater-mac-install'
|
||||
import { compareVersions } from './updater-fallback'
|
||||
import { fetchChangelog } from './updater-changelog'
|
||||
import type { ElectronAutoUpdater } from './electron-updater-loader'
|
||||
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
|
||||
import { resolveLinuxPackageDownloadedStatus } from './linux-package-downloaded-status'
|
||||
import {
|
||||
getRetainedLinuxPackageManualInstallStatus,
|
||||
resolveLinuxPackageDownloadedStatus,
|
||||
shouldIgnoreDownloadedUpdateEvent
|
||||
} from './linux-package-downloaded-status'
|
||||
import * as linuxPackageRecovery from './linux-package-update-recovery'
|
||||
|
||||
const AUTO_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
||||
@@ -98,47 +99,14 @@ export function registerAutoUpdaterHandlers({
|
||||
setAvailableVersion,
|
||||
setUserInitiatedCheck
|
||||
}: UpdaterHandlerContext): void {
|
||||
// Why: electron-updater fires 'update-downloaded' before Squirrel.Mac finishes; track readiness to avoid a premature "ready".
|
||||
if (process.platform === 'darwin') {
|
||||
nativeUpdater.on('update-downloaded', () => {
|
||||
const hasInstallableVersion = hasInstallableDownloadedVersion()
|
||||
handleMacInstallerReady(hasInstallableVersion, performQuitAndInstall, () => {
|
||||
// Send the held status only while its staged build is still installable.
|
||||
sendStatus({
|
||||
state: 'downloaded',
|
||||
version: getPendingInstallVersion(),
|
||||
releaseUrl: getKnownReleaseUrl()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
app.on('before-quit', (event) => {
|
||||
if (!shouldDeferMacQuitForInstall()) {
|
||||
return
|
||||
}
|
||||
if (consumeMacInstallGuardBypass()) {
|
||||
recordUpdaterLifecycle('macos_before_quit_guard_bypassed')
|
||||
return
|
||||
}
|
||||
if (isMacQuitAndInstallInFlight()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Why: quitting before Squirrel.Mac finishes staging leaves nothing to install; hold the quit until it's ready.
|
||||
if (
|
||||
deferMacQuitUntilInstallerReady(
|
||||
getCurrentStatus(),
|
||||
hasInstallableDownloadedVersion(),
|
||||
getPendingInstallVersion,
|
||||
sendStatus
|
||||
)
|
||||
) {
|
||||
recordUpdaterLifecycle('macos_before_quit_deferred', {
|
||||
version: getPendingInstallVersion()
|
||||
})
|
||||
event.preventDefault()
|
||||
}
|
||||
registerMacUpdaterEvents({
|
||||
getCurrentStatus,
|
||||
hasInstallableDownloadedVersion,
|
||||
getPendingInstallVersion,
|
||||
getKnownReleaseUrl,
|
||||
performQuitAndInstall,
|
||||
shouldDeferMacQuitForInstall,
|
||||
sendStatus
|
||||
})
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
@@ -238,7 +206,7 @@ export function registerAutoUpdaterHandlers({
|
||||
}
|
||||
clearBackgroundCheckLaunchPending()
|
||||
resetMacInstallState()
|
||||
linuxPackageRecovery.clearTrackedLinuxPackageArtifact()
|
||||
const retainedStatus = getRetainedLinuxPackageManualInstallStatus()
|
||||
const missingManifestFallback = consumeMissingManifestPrereleaseFallbackResult()
|
||||
const publishingWindowLastGoodCheck = getPublishingWindowLastGoodCheck()
|
||||
const wasUserInitiated = missingManifestFallback?.userInitiated ?? getUserInitiatedCheck()
|
||||
@@ -259,7 +227,11 @@ export function registerAutoUpdaterHandlers({
|
||||
}
|
||||
}
|
||||
}
|
||||
sendStatus({ state: 'not-available', userInitiated: wasUserInitiated || undefined })
|
||||
// Why: a later check can report no newer release while a verified deb/rpm is still waiting for
|
||||
// the user to install it outside Orca. Keep both the artifact and its recovery card reachable.
|
||||
sendStatus(
|
||||
retainedStatus ?? { state: 'not-available', userInitiated: wasUserInitiated || undefined }
|
||||
)
|
||||
if (localBuildCheck || pinnedBuildCheck) {
|
||||
restoreReleaseUpdateSource()
|
||||
}
|
||||
@@ -277,6 +249,16 @@ export function registerAutoUpdaterHandlers({
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
// Why: an earlier download can finish after a newer target replaced it; uncached pre-staged events have no target to compare.
|
||||
if (
|
||||
shouldIgnoreDownloadedUpdateEvent(
|
||||
getCurrentStatus(),
|
||||
info.version,
|
||||
getPendingInstallVersion()
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
clearBackgroundCheckLaunchPending()
|
||||
// Release downloads remain newer-only; the local source was validated before checking, and a pinned jump is explicit.
|
||||
if (
|
||||
|
||||
@@ -1,9 +1,66 @@
|
||||
import { app } from 'electron'
|
||||
import { app, autoUpdater as nativeUpdater } from 'electron'
|
||||
import type { UpdateStatus } from '../shared/update-status-types'
|
||||
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
|
||||
|
||||
const MAC_INSTALL_READY_TIMEOUT_MS = 15000
|
||||
|
||||
export function registerMacUpdaterEvents({
|
||||
getCurrentStatus,
|
||||
hasInstallableDownloadedVersion,
|
||||
getPendingInstallVersion,
|
||||
getKnownReleaseUrl,
|
||||
performQuitAndInstall,
|
||||
shouldDeferMacQuitForInstall,
|
||||
sendStatus
|
||||
}: {
|
||||
getCurrentStatus: () => UpdateStatus
|
||||
hasInstallableDownloadedVersion: () => boolean
|
||||
getPendingInstallVersion: () => string
|
||||
getKnownReleaseUrl: () => string | undefined
|
||||
performQuitAndInstall: () => void | Promise<void>
|
||||
shouldDeferMacQuitForInstall: () => boolean
|
||||
sendStatus: (status: UpdateStatus) => void
|
||||
}): void {
|
||||
if (process.platform === 'darwin') {
|
||||
nativeUpdater.on('update-downloaded', () => {
|
||||
const hasInstallableVersion = hasInstallableDownloadedVersion()
|
||||
handleMacInstallerReady(hasInstallableVersion, performQuitAndInstall, () => {
|
||||
sendStatus({
|
||||
state: 'downloaded',
|
||||
version: getPendingInstallVersion(),
|
||||
releaseUrl: getKnownReleaseUrl()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
app.on('before-quit', (event) => {
|
||||
if (!shouldDeferMacQuitForInstall()) {
|
||||
return
|
||||
}
|
||||
if (consumeMacInstallGuardBypass()) {
|
||||
recordUpdaterLifecycle('macos_before_quit_guard_bypassed')
|
||||
return
|
||||
}
|
||||
if (isMacQuitAndInstallInFlight()) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
deferMacQuitUntilInstallerReady(
|
||||
getCurrentStatus(),
|
||||
hasInstallableDownloadedVersion(),
|
||||
getPendingInstallVersion,
|
||||
sendStatus
|
||||
)
|
||||
) {
|
||||
recordUpdaterLifecycle('macos_before_quit_deferred', {
|
||||
version: getPendingInstallVersion()
|
||||
})
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Whether Squirrel.Mac has finished downloading the update from the localhost proxy. */
|
||||
let squirrelReady = false
|
||||
/** Remembers a user/app quit request that arrived before Squirrel.Mac had a
|
||||
|
||||
Reference in New Issue
Block a user