orca/src/main/updater-mac-install.ts

130 lines
4.0 KiB
TypeScript

import { app } from 'electron'
import type { UpdateStatus } from '../shared/types'
const MAC_INSTALL_READY_TIMEOUT_MS = 15000
/** 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
* staged update ready to apply. Without this handoff, quitting during the
* localhost-proxy phase exits back into the old app and the update is lost. */
let installRequestedAfterSquirrelReady = false
/** Prevents the updater-specific before-quit guard from re-blocking the
* quitAndInstall-triggered shutdown that is supposed to apply the update. */
let quitAndInstallInFlight = false
/** Lets a timed-out quit attempt proceed exactly once so the app never gets
* trapped open if Squirrel.Mac stops short of the native ready signal. */
let bypassMacInstallGuardOnce = false
let pendingInstallTimeout: ReturnType<typeof setTimeout> | null = null
function clearPendingInstallTimeout(): void {
if (pendingInstallTimeout) {
clearTimeout(pendingInstallTimeout)
pendingInstallTimeout = null
}
}
export function resetMacInstallState(): void {
installRequestedAfterSquirrelReady = false
quitAndInstallInFlight = false
bypassMacInstallGuardOnce = false
clearPendingInstallTimeout()
}
export function beginMacUpdateDownload(): void {
resetMacInstallState()
squirrelReady = false
}
export function markMacQuitAndInstallInFlight(): void {
installRequestedAfterSquirrelReady = false
quitAndInstallInFlight = true
bypassMacInstallGuardOnce = false
clearPendingInstallTimeout()
}
export function consumeMacInstallGuardBypass(): boolean {
if (!bypassMacInstallGuardOnce) {
return false
}
bypassMacInstallGuardOnce = false
return true
}
export function isMacQuitAndInstallInFlight(): boolean {
return quitAndInstallInFlight
}
export function isMacInstallerReady(): boolean {
return squirrelReady
}
export function isWaitingForMacInstallerReadiness(
currentStatus: UpdateStatus,
hasNewerDownloadedVersion: boolean
): boolean {
if (process.platform !== 'darwin' || squirrelReady || !hasNewerDownloadedVersion) {
return false
}
// electron-updater fires 'update-downloaded' before Squirrel.Mac has staged
// the update. Once we show 100% downloaded, treat quits as "install this as
// soon as ShipIt is ready" instead of exiting back into the old version.
return currentStatus.state === 'downloading' && currentStatus.percent === 100
}
export function deferMacQuitUntilInstallerReady(
currentStatus: UpdateStatus,
hasNewerDownloadedVersion: boolean,
getPendingInstallVersion: () => string,
sendStatus: (status: UpdateStatus) => void
): boolean {
if (!isWaitingForMacInstallerReadiness(currentStatus, hasNewerDownloadedVersion)) {
return false
}
installRequestedAfterSquirrelReady = true
sendStatus({ state: 'downloading', percent: 100, version: getPendingInstallVersion() })
if (pendingInstallTimeout) {
return true
}
pendingInstallTimeout = setTimeout(() => {
pendingInstallTimeout = null
if (!installRequestedAfterSquirrelReady || quitAndInstallInFlight) {
return
}
console.warn(
`[updater] macOS installer was not ready after ${MAC_INSTALL_READY_TIMEOUT_MS}ms; allowing quit without install`
)
installRequestedAfterSquirrelReady = false
// This is a safety valve. The updater path should wait for ShipIt so the
// staged update can apply, but if the native ready signal never arrives we
// must let the app close instead of trapping the user in a blocked quit.
bypassMacInstallGuardOnce = true
app.quit()
}, MAC_INSTALL_READY_TIMEOUT_MS)
return true
}
export function handleMacInstallerReady(
hasNewerDownloadedVersion: boolean,
onReadyToInstall: () => void,
onReadyToReportDownloaded: () => void
): void {
squirrelReady = true
clearPendingInstallTimeout()
if (installRequestedAfterSquirrelReady && hasNewerDownloadedVersion) {
onReadyToInstall()
return
}
if (hasNewerDownloadedVersion) {
onReadyToReportDownloaded()
}
}