fix(macos): avoid scene deadlock on app reactivation (#11055)
* fix(macos): avoid redundant focus on app activation * test(macos): cover passive app activation --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
parent
d548641f1d
commit
380034edf9
|
|
@ -165,6 +165,7 @@ import {
|
|||
setTrayAttention,
|
||||
type SystemTrayOptions
|
||||
} from './tray/system-tray'
|
||||
import { createMacAppActivationHandler } from './window/macos-app-activation'
|
||||
import { focusExistingMainWindow } from './window/focus-existing-window'
|
||||
import { notifyMainWindowBecameVisible } from './window/main-window-visibility'
|
||||
import { CodexAccountService } from './codex-accounts/service'
|
||||
|
|
@ -557,6 +558,11 @@ function requestDesktopActivation(): void {
|
|||
desktopActivationGate.requestActivation()
|
||||
}
|
||||
|
||||
const handleMacAppActivation = createMacAppActivationHandler({
|
||||
getWindow: () => mainWindow,
|
||||
requestActivation: requestDesktopActivation
|
||||
})
|
||||
|
||||
function getDesktopWindowStatus(): RuntimeDesktopWindowStatus {
|
||||
const state = desktopActivationGate.getState()
|
||||
return state === 'ready' ? 'openable' : state
|
||||
|
|
@ -1843,7 +1849,7 @@ function shouldSuppressCodexAutoApprovalSyntheticTitleFromHook(args: {
|
|||
)
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
void app.whenReady().then(async () => {
|
||||
logStartupMilestone('app-ready')
|
||||
// Why: install certificate decisions before any webview or headless window issues its first TLS request.
|
||||
app.on(
|
||||
|
|
@ -2602,7 +2608,7 @@ app.whenReady().then(async () => {
|
|||
})
|
||||
|
||||
startTerminalRuntimeStartupServices()
|
||||
app.on('activate', requestDesktopActivation)
|
||||
app.on('activate', handleMacAppActivation)
|
||||
|
||||
if (serveOptions) {
|
||||
// Why: give managed WSL launchers a brief chance to migrate before headless PTYs go live, without slow repairs withholding all RPC readiness.
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import { describe, expect, it } from 'vitest'
|
|||
describe('serve desktop activation wiring', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
|
||||
it('routes second-instance and app activation through one safety gate', () => {
|
||||
it('routes second-instance and windowless app activation through one safety gate', () => {
|
||||
expect(source).toContain('createServeDesktopActivationGate({')
|
||||
expect(source).toContain('acquireSingleInstanceLock(app, requestDesktopActivation)')
|
||||
expect(source).toContain("app.on('activate', requestDesktopActivation)")
|
||||
expect(source).toContain('createMacAppActivationHandler({')
|
||||
expect(source).toContain("app.on('activate', handleMacAppActivation)")
|
||||
expect(source).toContain('getDesktopWindowStatus: getDesktopWindowStatus')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -110,19 +110,21 @@ describe('focusExistingMainWindow', () => {
|
|||
|
||||
it('restores minimized windows before focusing them', () => {
|
||||
const window = makeFakeWindow({ minimized: true })
|
||||
const timer = makeTimer()
|
||||
|
||||
focusExistingMainWindow({
|
||||
app: makeFakeApp(),
|
||||
getWindow: () => window,
|
||||
openWindow: vi.fn(),
|
||||
platform: 'darwin',
|
||||
setTimeout: makeTimer().setTimeout
|
||||
setTimeout: timer.setTimeout
|
||||
})
|
||||
|
||||
expect(window.calls.restore).toHaveBeenCalledTimes(1)
|
||||
expect(window.calls.show).toHaveBeenCalledTimes(1)
|
||||
expect(window.calls.focus).toHaveBeenCalledTimes(1)
|
||||
expect(window.calls.moveTop).not.toHaveBeenCalled()
|
||||
expect(timer.scheduledMs()).toEqual([])
|
||||
})
|
||||
|
||||
it('waits for normal startup when no window exists before app readiness', () => {
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ function activateWindow(
|
|||
// Older Electron versions or destroyed windows may reject this; focus retry remains.
|
||||
}
|
||||
pulseAlwaysOnTop(window, setTimer)
|
||||
retryFocus(window, app, setTimer)
|
||||
}
|
||||
retryFocus(window, app, setTimer)
|
||||
}
|
||||
|
||||
// Why: a second-instance/activate reopen can race transient startup pressure
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import type { BrowserWindow } from 'electron'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMacAppActivationHandler } from './macos-app-activation'
|
||||
|
||||
function makeWindow(destroyed = false): BrowserWindow {
|
||||
return {
|
||||
isDestroyed: vi.fn(() => destroyed)
|
||||
} as unknown as BrowserWindow
|
||||
}
|
||||
|
||||
describe('createMacAppActivationHandler', () => {
|
||||
it('leaves an existing window to native macOS activation', () => {
|
||||
const requestActivation = vi.fn()
|
||||
const handler = createMacAppActivationHandler({
|
||||
getWindow: () => makeWindow(),
|
||||
requestActivation
|
||||
})
|
||||
|
||||
handler()
|
||||
|
||||
expect(requestActivation).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([null, makeWindow(true)])(
|
||||
'requests desktop activation for a missing or destroyed window',
|
||||
(window) => {
|
||||
const requestActivation = vi.fn()
|
||||
const handler = createMacAppActivationHandler({
|
||||
getWindow: () => window,
|
||||
requestActivation
|
||||
})
|
||||
|
||||
handler()
|
||||
|
||||
expect(requestActivation).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
export function createMacAppActivationHandler(options: {
|
||||
getWindow: () => BrowserWindow | null
|
||||
requestActivation: () => void
|
||||
}): () => void {
|
||||
return () => {
|
||||
const window = options.getWindow()
|
||||
// Why: re-focusing an existing macOS window can race its scene-backed Space transition.
|
||||
if (!window || window.isDestroyed()) {
|
||||
options.requestActivation()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue