Add updater diagnostics and refine daemon teardown policy on quit (#7544)

* Add updater diagnostics and refine daemon teardown policy on quit

Introduce detailed logging, crash breadcrumbs, and telemetry tracing
events for the update quit-and-install lifecycle. This improves
visibility into macOS Squirrel.Mac state transitions, install guard
timeouts, and pre-quit cleanup failures.

Also, formalize and unit test the daemon teardown policy on quit to
explicitly keep the daemon alive (via disconnect) during updater and
normal quits so that daemon-backed PTYs are preserved for warm reattach.

* Remove updater lifecycle logging

Remove calls to `recordUpdaterLifecycle` across the auto-updater modules. This cleans up unused or redundant telemetry events during macOS update download, ready, and pre-quit installation phases.

* Remove redundant quit daemon teardown policy helper

Inline the daemon teardown check directly into the `will-quit` event
handler in the main process. The deleted `shouldShutdownDaemonForQuit`
helper simply returned `isDevParentShutdownRequested`, so using that
check directly simplifies the codebase and allows removing the redundant
policy and test files.

* Clarify PTY cleanup targets only local in-process PTYs

Update doc comments, test descriptions, and telemetry events to make
it explicit that only local, in-process PTYs are killed on quit.
Daemon-backed PTYs are preserved via daemon disconnect.

* Recover quit-for-update state on autoUpdater error events

- Reset update flags when the installer fails asynchronously via the
  autoUpdater 'error' event (such as "no staged update" errors).
- Prevents the app from getting stuck in a half-transitioned state
  where PTYs are killed and windows refuse to reopen.
- Migrate legacy updater warning logs and crash breadcrumbs to use
  the structured recordUpdaterLifecycle diagnostic system.

* Simplify duplicate isMacInstallerReady() call in update-downloaded handl

Cache the result in a local variable so the deferred-status check reuses
the value from the log call instead of recomputing it.

* fix: tighten quit-and-install failure recovery

Only recover stuck quit-for-update flags after native quitAndInstall is
invoked and before install is committed. Defer PTY kill and close-listener
removal until after a successful native handoff so sync install failures keep
the session intact, and ignore post-commit updater errors for UI/mac state.

* fix: typecheck login-shell args check in shell-ready test

Avoid args.includes('-l') on a const tuple union that only admits '-i'
on one branch; compare args[0] instead so merge-base typecheck passes.
This commit is contained in:
Jinjing 2026-07-09 23:00:01 -07:00 committed by GitHub
parent d43dcc7f2a
commit 5d5e2c4b95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 352 additions and 25 deletions

View File

@ -51,6 +51,7 @@ import {
rebuildAppMenu
} from './menu/register-app-menu'
import { checkForUpdatesFromMenu, isQuittingForUpdate } from './updater'
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
import {
configureElectronNetworkCompatibility,
configureDevUserDataPath,
@ -2210,6 +2211,11 @@ app.whenReady().then(async () => {
})
app.on('before-quit', () => {
if (isQuittingForUpdate()) {
recordUpdaterLifecycle('before_quit_allowed', undefined, {
message: 'before-quit allowed for update install'
})
}
isQuitting = true
unsubscribeSystemResumeBroadcast?.()
unsubscribeSystemResumeBroadcast = null
@ -2231,6 +2237,14 @@ app.on('before-quit', () => {
// async work and let Electron exit.
let daemonDisconnectDone = false
app.on('will-quit', (e) => {
const updateQuitInProgress = isQuittingForUpdate()
if (updateQuitInProgress) {
recordUpdaterLifecycle(
'will_quit_cleanup_started',
{ daemonTeardown: 'disconnect' },
{ message: 'will-quit cleanup for update install; daemonTeardown=disconnect' }
)
}
// Why: before-quit can still be aborted by renderer beforeunload; wait until
// the committed quit path before removing the Windows notification icon.
destroySystemTray()

View File

@ -3928,7 +3928,7 @@ export function registerHeadlessPtyRuntime(
}
/**
* Kill all PTY processes. Call on app quit.
* Kill in-process local PTYs. Daemon-backed PTYs are preserved by daemon disconnect.
*/
export function killAllPty(): void {
if (localProvider instanceof LocalPtyProvider) {

View File

@ -1044,7 +1044,7 @@ export class LocalPtyProvider implements IPtyProvider {
return ptyProcesses.get(id)
}
/** Kill all PTYs. Call on app quit. */
/** Kill all in-process local PTYs. Call on app quit. */
killAll(): void {
for (const [id, proc] of ptyProcesses) {
safeKillAndClean(id, proc)

View File

@ -883,7 +883,9 @@ path=(/custom/bin $path)
expect(result.status, `zsh ${args.join(' ')} failed: ${result.stderr}`).toBe(0)
expect(result.stdout).toContain('USER_ZSHRC_LOADED=yes')
expect(result.stdout).toContain(`FINAL_ZDOTDIR=${testHome}`)
expect(result.stdout).toContain(args.includes('-l') ? 'IS_LOGIN=yes' : 'IS_LOGIN=no')
// Why: `as const` makes .includes('-l') reject the union of tuple
// element types; check the login flag by position instead.
expect(result.stdout).toContain(args[0] === '-l' ? 'IS_LOGIN=yes' : 'IS_LOGIN=no')
}
} finally {
rmSync(movedUserData, { recursive: true, force: true })

View File

@ -11,6 +11,7 @@ import {
import { compareVersions } from './updater-fallback'
import { fetchChangelog } from './updater-changelog'
import type { ElectronAutoUpdater } from './electron-updater-loader'
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
const AUTO_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
const AUTO_UPDATE_RETRY_INTERVAL_MS = 60 * 60 * 1000
@ -27,6 +28,8 @@ type UpdaterHandlerContext = {
getKnownReleaseUrl: () => string | undefined
getPendingInstallVersion: () => string
getUserInitiatedCheck: () => boolean
handleQuitAndInstallFailure: () => boolean
isQuitAndInstallHandoffActive: () => boolean
hasNewerDownloadedVersion: () => boolean
shouldHandleUpdaterErrorEvent: () => boolean
clearUpdateAvailableEventPending: (attemptId: number | null) => void
@ -64,6 +67,8 @@ export function registerAutoUpdaterHandlers({
getKnownReleaseUrl,
getPendingInstallVersion,
getUserInitiatedCheck,
handleQuitAndInstallFailure,
isQuitAndInstallHandoffActive,
hasNewerDownloadedVersion,
shouldHandleUpdaterErrorEvent,
clearUpdateAvailableEventPending,
@ -89,7 +94,8 @@ export function registerAutoUpdaterHandlers({
// Track Squirrel readiness so we don't show "ready to install" prematurely.
if (process.platform === 'darwin') {
nativeUpdater.on('update-downloaded', () => {
handleMacInstallerReady(hasNewerDownloadedVersion(), performQuitAndInstall, () => {
const hasNewerVersion = hasNewerDownloadedVersion()
handleMacInstallerReady(hasNewerVersion, performQuitAndInstall, () => {
// If we were holding the 'downloaded' status, send it now — but only
// when the staged version is actually newer than what's running.
sendStatus({
@ -102,7 +108,11 @@ export function registerAutoUpdaterHandlers({
}
app.on('before-quit', (event) => {
if (consumeMacInstallGuardBypass() || isMacQuitAndInstallInFlight()) {
if (consumeMacInstallGuardBypass()) {
recordUpdaterLifecycle('macos_before_quit_guard_bypassed')
return
}
if (isMacQuitAndInstallInFlight()) {
return
}
@ -119,6 +129,9 @@ export function registerAutoUpdaterHandlers({
sendStatus
)
) {
recordUpdaterLifecycle('macos_before_quit_deferred', {
version: getPendingInstallVersion()
})
event.preventDefault()
}
})
@ -255,12 +268,15 @@ export function registerAutoUpdaterHandlers({
sendStatus({ state: 'not-available' })
return
}
const macInstallerReady = process.platform === 'darwin' ? isMacInstallerReady() : true
recordUpdaterLifecycle('update_downloaded', { version: info.version, macInstallerReady })
// On macOS, defer the 'downloaded' status until Squirrel.Mac has finished
// processing the update via the localhost proxy. On other platforms,
// the update is ready immediately after electron-updater downloads it.
if (process.platform === 'darwin' && !isMacInstallerReady()) {
if (process.platform === 'darwin' && !macInstallerReady) {
// Squirrel is still processing. Keep the UI at 100% downloaded so the
// user sees the handoff instead of a misleading "ready to install".
recordUpdaterLifecycle('macos_waiting_for_squirrel', { version: info.version })
sendStatus({ state: 'downloading', percent: 100, version: info.version })
return
}
@ -269,6 +285,18 @@ export function registerAutoUpdaterHandlers({
autoUpdater.on('error', (err) => {
const message = err?.message ?? 'Unknown error'
// Why: quitAndInstall reports the common "no staged update" failure through
// this event (often sync on Win/Linux, async on macOS/spawn). Recover
// quit-for-update flags before any suppression guard can early-return, but
// only after native invoke and only when install is not yet committed.
if (handleQuitAndInstallFailure()) {
return
}
// Why: handoff still owns the process (cleanup, native in-flight, or
// post-commit). Do not treat as check/download error or reset mac install.
if (isQuitAndInstallHandoffActive()) {
return
}
// Why: primary/fallback promise handlers may already own this failure; do
// not let their delayed paired error event consume fallback context.
if (shouldSuppressMissingManifestPrereleaseFallbackEvent(message, err)) {

View File

@ -0,0 +1,12 @@
import type { CrashReportBreadcrumbData } from '../shared/crash-reporting'
import { recordCrashBreadcrumb } from './crash-reporting/crash-breadcrumb-store'
export function recordUpdaterLifecycle(
event: string,
data?: CrashReportBreadcrumbData,
options?: { level?: 'info' | 'warn' | 'error'; message?: string }
): void {
recordCrashBreadcrumb(`updater_${event}`, data)
const suffix = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : ''
console[options?.level ?? 'info'](`[updater] ${options?.message ?? event}${suffix}`)
}

View File

@ -1,5 +1,6 @@
import { app } from 'electron'
import type { UpdateStatus } from '../shared/types'
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
const MAC_INSTALL_READY_TIMEOUT_MS = 15000
@ -96,8 +97,13 @@ export function deferMacQuitUntilInstallerReady(
return
}
console.warn(
`[updater] macOS installer was not ready after ${MAC_INSTALL_READY_TIMEOUT_MS}ms; allowing quit without install`
recordUpdaterLifecycle(
'macos_install_guard_timeout',
{ timeoutMs: MAC_INSTALL_READY_TIMEOUT_MS },
{
level: 'warn',
message: `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
@ -117,14 +123,19 @@ export function handleMacInstallerReady(
): void {
squirrelReady = true
clearPendingInstallTimeout()
recordUpdaterLifecycle('macos_installer_ready', {
deferredInstallRequested: installRequestedAfterSquirrelReady,
hasNewerDownloadedVersion
})
if (installRequestedAfterSquirrelReady && hasNewerDownloadedVersion) {
void Promise.resolve()
.then(() => onReadyToInstall())
.catch((error) => {
console.warn(
'[updater] Deferred macOS install handoff failed:',
error instanceof Error ? error.name : typeof error
recordUpdaterLifecycle(
'macos_deferred_install_handoff_failed',
{ errorType: error instanceof Error ? error.name : typeof error },
{ level: 'warn', message: 'Deferred macOS install handoff failed' }
)
})
return

View File

@ -258,9 +258,9 @@ describe('updater mac install handoff', () => {
expect(reportDownloaded).not.toHaveBeenCalled()
await vi.waitFor(() => {
// Why: recordUpdaterLifecycle packs metadata into one console line.
expect(warn).toHaveBeenCalledWith(
'[updater] Deferred macOS install handoff failed:',
'Error'
'[updater] Deferred macOS install handoff failed {"errorType":"Error"}'
)
})
expect(JSON.stringify(warn.mock.calls)).not.toContain('handoff-secret')

View File

@ -1177,7 +1177,7 @@ describe('updater', () => {
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(false, true)
})
it('runs pre-quit cleanup before killing PTYs during update install', async () => {
it('runs pre-quit cleanup before local PTY cleanup during update install', async () => {
vi.useFakeTimers()
const onBeforeQuit = vi.fn()
@ -1240,6 +1240,145 @@ describe('updater', () => {
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
})
it('recovers quit-for-update state on sync quitAndInstall error event without killing PTYs', async () => {
vi.useFakeTimers()
autoUpdaterMock.quitAndInstall.mockImplementation(() => {
// Why: BaseUpdater dispatches 'error' synchronously inside install() for
// the common "no staged update filepath" path.
autoUpdaterMock.emit(
'error',
new Error("No update filepath provided, can't quit and install")
)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, quitAndInstall, isQuittingForUpdate } = await import('./updater')
setupAutoUpdater(mainWindow as never)
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
expect(isQuittingForUpdate()).toBe(false)
// Why: destructive prep runs only after quitAndInstall returns still in
// progress; sync recovery clears flags first so PTYs stay alive.
expect(killAllPtyMock).not.toHaveBeenCalled()
expect(sendMock).toHaveBeenCalledWith(
'updater:status',
expect.objectContaining({
state: 'error',
message: 'Could not restart to install the update. Quit and reopen Orca, then try again.'
})
)
})
it('does not recover quit-for-update state from late errors after install commit', async () => {
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
fetchNewerReleaseTagsMock.mockResolvedValue({ tags: ['v1.0.61'], state: 'ready' })
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
autoUpdaterMock.emit('checking-for-update')
queueMicrotask(() => {
autoUpdaterMock.emit('update-available', { version: '1.0.61' })
})
return Promise.resolve(undefined)
})
const { setupAutoUpdater, checkForUpdatesFromMenu, quitAndInstall, isQuittingForUpdate } =
await import('./updater')
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
// Why: put status in downloaded so a naive error handler would otherwise
// treat a late post-commit error as a download/install UI failure.
await vi.waitFor(() => {
expect(sendMock).toHaveBeenCalledWith('updater:status', {
state: 'available',
version: '1.0.61',
changelog: null
})
})
autoUpdaterMock.emit('update-downloaded', { version: '1.0.61' })
// Why: on macOS install is only "committed" once Squirrel is ready; mark
// it ready so this test covers the post-commit path on all platforms and
// the UI can leave the "waiting for Squirrel" downloading state.
if (process.platform === 'darwin') {
const nativeDownloadedHandler = nativeUpdaterMock.on.mock.calls.find(
([eventName]) => eventName === 'update-downloaded'
)?.[1] as (() => void) | undefined
expect(nativeDownloadedHandler).toBeTypeOf('function')
nativeDownloadedHandler?.()
}
await vi.waitFor(() => {
expect(sendMock).toHaveBeenCalledWith(
'updater:status',
expect.objectContaining({ state: 'downloaded', version: '1.0.61' })
)
})
quitAndInstall()
await vi.waitFor(() => {
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
})
expect(killAllPtyMock).toHaveBeenCalledTimes(1)
expect(isQuittingForUpdate()).toBe(true)
sendMock.mockClear()
autoUpdaterMock.emit('error', new Error('late post-commit install error'))
expect(isQuittingForUpdate()).toBe(true)
// Why: handoff still owns the process after commit — no recovery message
// and no general check/download error status either.
expect(sendMock).not.toHaveBeenCalled()
})
it('does not treat pre-native autoUpdater errors as quitAndInstall recovery', async () => {
vi.useFakeTimers()
let finishCleanup!: () => void
const onBeforeQuit = vi.fn(
() =>
new Promise<void>((resolve) => {
finishCleanup = resolve
})
)
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, quitAndInstall, isQuittingForUpdate } = await import('./updater')
setupAutoUpdater(mainWindow as never, {
onBeforeQuit,
getLastUpdateCheckAt: () => Date.now()
})
quitAndInstall()
await vi.advanceTimersByTimeAsync(100)
expect(onBeforeQuit).toHaveBeenCalledTimes(1)
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled()
expect(isQuittingForUpdate()).toBe(true)
sendMock.mockClear()
// Why: an unrelated error during pre-quit cleanup must not clear
// quittingForUpdate or emit the install-recovery status (native not invoked).
autoUpdaterMock.emit('error', new Error('pre-native concurrent error'))
expect(isQuittingForUpdate()).toBe(true)
expect(sendMock).not.toHaveBeenCalled()
finishCleanup()
await vi.advanceTimersByTimeAsync(0)
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledTimes(1)
expect(isQuittingForUpdate()).toBe(true)
})
it('runs a startup check immediately when the last background check is stale', async () => {
const mainWindow = { webContents: { send: vi.fn() } }
const setLastUpdateCheckAt = vi.fn()

View File

@ -9,9 +9,12 @@ import { writeMainThreadDiagnosticMarker } from './diagnostics/main-thread-churn
import {
beginMacUpdateDownload,
deferMacQuitUntilInstallerReady,
markMacQuitAndInstallInFlight
isMacInstallerReady,
markMacQuitAndInstallInFlight,
resetMacInstallState
} from './updater-mac-install'
import { registerAutoUpdaterHandlers } from './updater-events'
import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics'
import {
compareVersions,
isBenignCheckFailure,
@ -64,6 +67,14 @@ let autoUpdateCheckTimer: ReturnType<typeof setTimeout> | null = null
let nudgeCheckTimer: ReturnType<typeof setTimeout> | null = null
let pendingQuitAndInstallTimer: ReturnType<typeof setTimeout> | null = null
let quitAndInstallInProgress = false
// Why: once quitAndInstall has committed (Win/Linux install, or macOS with
// Squirrel ready), late autoUpdater 'error' events must not clear
// quittingForUpdate — that would re-enable dock activate mid-installer.
let updateInstallCommitted = false
// Why: quit-and-install recovery must only run after the native
// quitAndInstall call. Pre-native cleanup-time autoUpdater errors must not
// clear quittingForUpdate or look like install recovery.
let quitAndInstallNativeInvoked = false
let persistLastUpdateCheckAt: ((timestamp: number) => void) | null = null
let _getLastUpdateCheckAt: (() => number | null) | null = null
let backgroundCheckLaunchPending = false
@ -547,6 +558,7 @@ function clearPrereleaseFallbackContextIfSettled(): void {
async function performQuitAndInstall(): Promise<void> {
if (quitAndInstallInProgress) {
recordUpdaterLifecycle('quit_and_install_ignored', { reason: 'already-in-progress' })
return
}
quitAndInstallInProgress = true
@ -566,14 +578,112 @@ async function performQuitAndInstall(): Promise<void> {
// either can't replace it or the user ends up on the old version.
quittingForUpdate = true
await runBeforeUpdateQuitCleanup()
killAllPty()
const pendingVersion = getPendingInstallVersion()
try {
await withUpdaterSpan({ stage: 'install' }, async (span) => {
span.setAttribute('updater.version', pendingVersion || 'unknown')
span.setAttribute('updater.platform', process.platform)
span.setAttribute(
'updater.macosInstallerReady',
process.platform === 'darwin' ? isMacInstallerReady() : true
)
recordUpdaterLifecycle('quit_and_install_started', {
version: pendingVersion || null,
macInstallerReady: process.platform === 'darwin' ? isMacInstallerReady() : true
})
span.addEvent('pre_quit_cleanup_start')
await runBeforeUpdateQuitCleanup()
span.addEvent('pre_quit_cleanup_done')
for (const win of BrowserWindow.getAllWindows()) {
win.removeAllListeners('close')
recordUpdaterLifecycle('quit_and_install_invoking_native', {
version: pendingVersion || null
})
// Why: defensive — state should stay in-progress until native invoke, but
// never call quitAndInstall if recovery/reset already cleared the handoff.
if (!quitAndInstallInProgress) {
return
}
// Why: mark before the call so a sync 'error' during quitAndInstall can
// recover; pre-native errors must not look like install failure.
quitAndInstallNativeInvoked = true
// Why: invoke quitAndInstall before killAllPty/remove close listeners so a
// sync 'error' (common "no filepath" path) recovers while windows and
// local PTYs are still intact.
getAutoUpdater().quitAndInstall(false, true)
span.addEvent('native_quit_and_install_invoked')
// Why: handleQuitAndInstallFailure may clear quitAndInstallInProgress
// synchronously during quitAndInstall (Win/Linux dispatchError). Skip
// destructive prep if recovery already ran.
if (!quitAndInstallInProgress) {
return
}
killAllPty()
span.addEvent('local_pty_kill_all')
for (const win of BrowserWindow.getAllWindows()) {
win.removeAllListeners('close')
}
span.addEvent('window_close_listeners_removed', {
windowCount: BrowserWindow.getAllWindows().length
})
// Why: committed installs must keep quittingForUpdate true so dock
// activate cannot reopen the old process mid-ShipIt/installer. macOS
// without Squirrel ready stays uncommitted so late native errors can
// still recover flags (PTYs may already be dead — residual OK).
if (process.platform !== 'darwin' || isMacInstallerReady()) {
updateInstallCommitted = true
}
})
} catch (error) {
resetQuitForUpdateState()
recordUpdaterLifecycle(
'quit_and_install_failed',
{ errorType: error instanceof Error ? error.name : typeof error },
{
level: 'warn',
message: 'Could not start update install'
}
)
sendErrorStatus(
'Could not restart to install the update. Quit and reopen Orca, then try again.'
)
}
}
getAutoUpdater().quitAndInstall(false, true)
function resetQuitForUpdateState(): void {
quitAndInstallInProgress = false
quittingForUpdate = false
updateInstallCommitted = false
quitAndInstallNativeInvoked = false
resetMacInstallState()
}
// Why: electron-updater often reports quitAndInstall failures via the 'error'
// event. On Win/Linux this is frequently synchronous (dispatchError inside
// install()); on macOS/spawn it can be async. Recover only after native invoke
// and only when install has not been committed — after commit, clearing
// quittingForUpdate would allow dock activate to reopen the old process
// mid-installer.
function handleQuitAndInstallFailure(): boolean {
if (!quitAndInstallInProgress || !quitAndInstallNativeInvoked || updateInstallCommitted) {
return false
}
resetQuitForUpdateState()
recordUpdaterLifecycle('quit_and_install_failed_via_event', undefined, {
level: 'warn',
message: 'Update install could not start; recovered app state'
})
sendErrorStatus('Could not restart to install the update. Quit and reopen Orca, then try again.')
return true
}
// Why: while quit-and-install owns the process (pre-native cleanup through
// post-commit handoff), general check/download error UI must not run.
function isQuitAndInstallHandoffActive(): boolean {
return quitAndInstallInProgress
}
async function runBeforeUpdateQuitCleanup(): Promise<void> {
@ -585,9 +695,13 @@ async function runBeforeUpdateQuitCleanup(): Promise<void> {
const cleanup = Promise.resolve()
.then(() => onBeforeQuitCleanup?.())
.catch((error) => {
console.warn(
'[updater] Pre-quit cleanup failed; continuing update install:',
error instanceof Error ? error.name : typeof error
recordUpdaterLifecycle(
'pre_quit_cleanup_failed',
{ errorType: error instanceof Error ? error.name : typeof error },
{
level: 'warn',
message: 'Pre-quit cleanup failed; continuing update install'
}
)
})
const timeoutResult = new Promise<'timeout'>((resolve) => {
@ -596,8 +710,13 @@ async function runBeforeUpdateQuitCleanup(): Promise<void> {
const result = await Promise.race([cleanup.then(() => 'done' as const), timeoutResult])
if (result === 'timeout') {
console.warn(
`[updater] Pre-quit cleanup exceeded ${PRE_QUIT_CLEANUP_TIMEOUT_MS}ms; continuing update install`
recordUpdaterLifecycle(
'pre_quit_cleanup_timeout',
{ timeoutMs: PRE_QUIT_CLEANUP_TIMEOUT_MS },
{
level: 'warn',
message: `Pre-quit cleanup exceeded ${PRE_QUIT_CLEANUP_TIMEOUT_MS}ms; continuing update install`
}
)
return
}
@ -1304,6 +1423,8 @@ export function setupAutoUpdater(
getKnownReleaseUrl,
getPendingInstallVersion,
getUserInitiatedCheck: () => userInitiatedCheck,
handleQuitAndInstallFailure,
isQuitAndInstallHandoffActive,
hasNewerDownloadedVersion,
shouldHandleUpdaterErrorEvent,
performQuitAndInstall,