Files
geo/apps/desktop-client/src/main/bootstrap.ts
T

897 lines
25 KiB
TypeScript
Raw Normal View History

import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { DesktopAccountInfo, DesktopRuntimeSessionSyncRequest } from '@geo/shared-types'
import { shell } from 'electron'
import type {
BrowserWindow as ElectronBrowserWindow,
WebContents as ElectronWebContents,
IpcMainInvokeEvent,
} from 'electron/main'
import { BrowserWindow, Menu, app, ipcMain, nativeTheme } from 'electron/main'
import { bindPublishAccount, openPublishAccountConsole } from './account-binder'
import { initAccountHealth } from './account-health'
import {
applyDesktopHealthIndicatorFromSnapshot,
createDesktopHealthIcon,
startDesktopHealthIndicator,
syncDesktopHealthIndicator,
} from './app-issue-indicator'
import {
getDesktopAppSettings,
hasShownBackgroundBalloon,
initDesktopAppSettings,
markBackgroundBalloonShown,
setDesktopAppSetting,
type DesktopAppSettingKey,
} from './app-settings'
import { resolveDesktopClientID, resolveDesktopDeviceInfo } from './device-id'
import {
installObservedGlobalFetch,
registerObservedRequestRendererTarget,
} from './network-observer'
import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from './playwright-cdp'
import { initProcessMetricsSampler } from './process-metrics'
import { registerRendererDevtoolsProxyTarget } from './renderer-devtools-proxy'
import {
noteRuntimeAccountBound,
refreshRuntimeAccounts,
releaseRuntimeSession,
requestRuntimeAccountProbe,
syncRuntimeSession,
unbindRuntimeAccount,
} from './runtime-controller'
import { onRuntimeInvalidated } from './runtime-events'
import { createRuntimeAccountSnapshot, createRuntimeSnapshot } from './runtime-snapshot'
import { initScheduler } from './scheduler'
import { initSessionRegistry } from './session-registry'
import { initSingleInstance } from './single-instance'
import {
initTransport,
listDesktopPublishTasks,
retryDesktopPublishTask,
} from './transport/api-client'
import { initTray, showTrayBalloon } from './tray'
import { STANDARD_USER_AGENT } from './user-agent'
import { startHotViewReaper } from './view-pool'
app.commandLine.appendSwitch(
'disable-features',
'PostQuantumKyber,EncryptedClientHello,UseDnsHttpsSvcb,UseDnsHttpsSvcbAlpn',
)
app.commandLine.appendSwitch('disable-quic')
app.commandLine.appendSwitch('disable-background-networking')
// CDP is required by the hidden Playwright execution pool in packaged builds.
app.commandLine.appendSwitch('remote-debugging-port', String(getPlaywrightCDPPort()))
app.userAgentFallback = STANDARD_USER_AGENT
// Windows 10+ routes tray.displayBalloon through the Toast Notification API,
// which requires a registered AppUserModelID. electron-builder writes this on
// the Start Menu shortcut at install time, but we set it explicitly so dev
// runs and edge-case installs also fire the balloon reliably.
if (process.platform === 'win32') {
app.setAppUserModelId('com.geo.rankly.desktop')
}
const isDevelopmentRuntime = !app.isPackaged
function silencePackagedConsole(): void {
if (isDevelopmentRuntime) {
return
}
const noop = () => undefined
console.log = noop
console.info = noop
console.warn = noop
console.error = noop
console.debug = noop
}
silencePackagedConsole()
if (isDevelopmentRuntime) {
installObservedGlobalFetch()
}
console.info('[desktop-main] boot', {
electron: process.versions.electron,
chrome: process.versions.chrome,
node: process.versions.node,
ua: STANDARD_USER_AGENT,
})
process.on('unhandledRejection', (reason) => {
console.error('[desktop-main] unhandled rejection', reason)
})
process.on('uncaughtException', (error) => {
console.error('[desktop-main] uncaught exception', error)
})
let mainWindow: ElectronBrowserWindow | null = null
let loginWindow: ElectronBrowserWindow | null = null
let settingsWindow: ElectronBrowserWindow | null = null
let mainRendererContents: ElectronWebContents | null = null
let quitReleaseInFlight = false
let mainWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let loginWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let settingsWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let windowModeSwitchQueue: Promise<void> = Promise.resolve()
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>()
const appWindowModes = new WeakMap<ElectronBrowserWindow, RendererWindowMode>()
type WindowMode = 'login' | 'main'
type RendererWindowMode = WindowMode | 'settings'
type WindowModeSource = 'auth-state' | 'login' | 'logout'
interface WindowSizePreset {
width: number
height: number
minWidth: number
minHeight: number
resizable: boolean
}
interface WindowModeRequest {
source?: WindowModeSource
animate?: boolean
}
const WINDOW_READY_TIMEOUT_MS = 1200
const WINDOW_SIZES: Record<WindowMode, WindowSizePreset> = {
login: { width: 340, height: 540, minWidth: 340, minHeight: 540, resizable: false },
main: { width: 1320, height: 860, minWidth: 900, minHeight: 600, resizable: true },
}
const SETTINGS_WINDOW_SIZE: WindowSizePreset = {
width: 920,
height: 640,
minWidth: 760,
minHeight: 520,
resizable: true,
}
const TITLE_BAR_OVERLAY_HEIGHT = 24
function appTitleBarStyle(): 'hidden' | 'hiddenInset' {
return process.platform === 'darwin' ? 'hiddenInset' : 'hidden'
}
function appTitleBarOverlay(
color: string,
): { color: string; symbolColor: string; height: number } | undefined {
if (process.platform === 'darwin') {
return undefined
}
return {
color,
symbolColor: nativeTheme.shouldUseDarkColors ? '#f8fafc' : '#111827',
height: TITLE_BAR_OVERLAY_HEIGHT,
}
}
function suppressNativeWindowMenu(window?: ElectronBrowserWindow): void {
if (process.platform === 'darwin') {
return
}
Menu.setApplicationMenu(null)
window?.setMenu(null)
window?.setMenuBarVisibility(false)
}
function windowModePath(): string {
return join(app.getPath('userData'), 'window-mode.json')
}
function readPersistedWindowMode(): WindowMode {
try {
const raw = readFileSync(windowModePath(), 'utf8')
const data = JSON.parse(raw) as { mode?: unknown }
if (data?.mode === 'main' || data?.mode === 'login') {
return data.mode
}
} catch {
// First run, missing file, or corrupt — fall through to default.
}
return 'login'
}
function persistWindowMode(mode: WindowMode): void {
try {
writeFileSync(windowModePath(), JSON.stringify({ mode }), 'utf8')
} catch (err) {
console.warn('[desktop-main] persist window mode failed', err)
}
}
let currentWindowMode: WindowMode = 'login'
function normalizeWindowModeRequest(input: unknown): WindowModeRequest {
if (!input || typeof input !== 'object') {
return {}
}
const candidate = input as { source?: unknown; animate?: unknown }
return {
source:
candidate.source === 'login' ||
candidate.source === 'logout' ||
candidate.source === 'auth-state'
? candidate.source
: undefined,
animate: candidate.animate === true,
}
}
function currentMainWindow(): ElectronBrowserWindow | null {
if (!mainWindow) {
return null
}
if (mainWindow.isDestroyed()) {
mainWindow = null
return null
}
return mainWindow
}
function currentLoginWindow(): ElectronBrowserWindow | null {
if (!loginWindow) {
return null
}
if (loginWindow.isDestroyed()) {
loginWindow = null
return null
}
return loginWindow
}
function currentSettingsWindow(): ElectronBrowserWindow | null {
if (!settingsWindow) {
return null
}
if (settingsWindow.isDestroyed()) {
settingsWindow = null
return null
}
return settingsWindow
}
function currentActiveWindow(): ElectronBrowserWindow | null {
return currentMainWindow() ?? currentLoginWindow()
}
function rendererModeFromWindow(window: ElectronBrowserWindow): RendererWindowMode | null {
if (window.isDestroyed()) {
return null
}
const knownMode = appWindowModes.get(window)
if (knownMode) {
return knownMode
}
try {
const url = new URL(window.webContents.getURL())
const mode = url.searchParams.get('window')
return mode === 'login' || mode === 'main' || mode === 'settings' ? mode : null
} catch {
return null
}
}
function retireWindowAfterIpcReturn(window: ElectronBrowserWindow): void {
if (window.isDestroyed()) {
return
}
forceCloseWindows.add(window)
if (window.isVisible()) {
window.hide()
}
setTimeout(() => {
if (!window.isDestroyed()) {
window.destroy()
}
}, 50)
}
function retireInactiveAppWindows(mode: WindowMode, target: ElectronBrowserWindow): void {
for (const candidate of BrowserWindow.getAllWindows()) {
if (candidate === target || candidate.isDestroyed()) {
continue
}
const candidateMode = rendererModeFromWindow(candidate)
if (
(mode === 'main' && (candidateMode === 'login' || candidateMode === 'main')) ||
(mode === 'login' &&
(candidateMode === 'login' || candidateMode === 'main' || candidateMode === 'settings'))
) {
retireWindowAfterIpcReturn(candidate)
}
}
}
async function waitForWindowReady(window: ElectronBrowserWindow): Promise<void> {
if (window.isDestroyed() || !window.webContents.isLoading()) {
return
}
await new Promise<void>((resolve) => {
let settled = false
let timeout: ReturnType<typeof setTimeout> | null = null
const finish = () => {
if (settled) {
return
}
settled = true
if (timeout) {
clearTimeout(timeout)
}
window.off('ready-to-show', finish)
window.webContents.off('did-finish-load', finish)
window.webContents.off('did-fail-load', finish)
resolve()
}
timeout = setTimeout(finish, WINDOW_READY_TIMEOUT_MS)
window.once('ready-to-show', finish)
window.webContents.once('did-finish-load', finish)
window.webContents.once('did-fail-load', finish)
})
}
function captureRuntimeSnapshot() {
const snapshot = createRuntimeSnapshot()
applyDesktopHealthIndicatorFromSnapshot(snapshot, currentMainWindow())
return snapshot
}
function captureRuntimeAccountSnapshot(accountId: string) {
return createRuntimeAccountSnapshot(accountId)
}
async function ensureMainWindow(): Promise<ElectronBrowserWindow> {
const existing = currentMainWindow()
if (existing) {
return existing
}
if (!mainWindowCreatePromise) {
mainWindowCreatePromise = createAppWindow('main')
.then((window) => {
mainWindow = window
return window
})
.finally(() => {
mainWindowCreatePromise = null
})
}
return mainWindowCreatePromise
}
async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
const existing = currentLoginWindow()
if (existing) {
return existing
}
if (!loginWindowCreatePromise) {
loginWindowCreatePromise = createAppWindow('login')
.then((window) => {
loginWindow = window
return window
})
.finally(() => {
loginWindowCreatePromise = null
})
}
return loginWindowCreatePromise
}
async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
const existing = currentSettingsWindow()
if (existing) {
return existing
}
if (!settingsWindowCreatePromise) {
settingsWindowCreatePromise = createSettingsWindow()
.then((window) => {
settingsWindow = window
return window
})
.finally(() => {
settingsWindowCreatePromise = null
})
}
return settingsWindowCreatePromise
}
async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
return mode === 'main' ? ensureMainWindow() : ensureLoginWindow()
}
async function revealWindow(window: ElectronBrowserWindow): Promise<void> {
await waitForWindowReady(window)
if (window.isMinimized()) {
window.restore()
}
if (!window.isVisible()) {
window.show()
}
window.focus()
app.focus({ steal: true })
}
async function revealActiveWindow(): Promise<void> {
const window = currentActiveWindow() ?? (await ensureWindow(currentWindowMode))
await revealWindow(window)
}
function revealActiveWindowSafely(source: string): void {
void revealActiveWindow().catch((error) => {
console.error('[desktop-main] reveal active window failed', { source, error })
})
}
function windowFromIpcEvent(event: IpcMainInvokeEvent): ElectronBrowserWindow | null {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window || window.isDestroyed()) {
return null
}
return window
}
async function switchWindowMode(
mode: WindowMode,
_request: WindowModeRequest = {},
sourceWindow: ElectronBrowserWindow | null = null,
): Promise<void> {
currentWindowMode = mode
persistWindowMode(mode)
const target = await ensureWindow(mode)
const previous =
sourceWindow && sourceWindow !== target
? sourceWindow
: mode === 'main'
? currentLoginWindow()
: currentMainWindow()
if (!target.isVisible()) {
target.center()
}
await revealWindow(target)
if (previous && previous !== target && !previous.isDestroyed()) {
retireWindowAfterIpcReturn(previous)
}
const settings = currentSettingsWindow()
if (mode === 'login' && settings) {
retireWindowAfterIpcReturn(settings)
}
retireInactiveAppWindows(mode, target)
}
function queueWindowModeSwitch(
mode: WindowMode,
request: WindowModeRequest = {},
sourceWindow: ElectronBrowserWindow | null = null,
): Promise<void> {
const nextSwitch = windowModeSwitchQueue
.catch(() => undefined)
.then(() => switchWindowMode(mode, request, sourceWindow))
windowModeSwitchQueue = nextSwitch.catch(() => undefined)
return nextSwitch
}
async function openSettingsWindow(): Promise<void> {
const window = await ensureSettingsWindow();
const parent = settingsWindowParent();
if (parent && window.getParentWindow() !== parent) {
window.setParentWindow(parent);
}
await revealWindow(window);
}
function settingsWindowParent(): ElectronBrowserWindow | undefined {
if (process.platform !== "win32") {
return undefined;
}
return currentMainWindow() ?? undefined;
}
function rendererURL(): string | null {
return process.env.ELECTRON_RENDERER_URL ?? null
}
function preloadPath(): string {
return join(__dirname, '../preload/bridge.cjs')
}
async function loadRenderer(
window: ElectronBrowserWindow,
mode: RendererWindowMode,
): Promise<void> {
const devServerURL = rendererURL()
if (devServerURL) {
const url = new URL(devServerURL)
url.searchParams.set('window', mode)
await window.webContents.loadURL(url.toString())
if (mode === 'main' && isDevelopmentRuntime) {
window.webContents.openDevTools({ mode: 'detach' })
}
return
}
await window.webContents.loadFile(join(__dirname, '../renderer/index.html'), {
query: { window: mode },
})
}
async function mountRendererWebContents(
window: ElectronBrowserWindow,
mode: RendererWindowMode,
): Promise<void> {
const contents = window.webContents
contents.setWindowOpenHandler(() => ({ action: 'deny' }))
if (mode === 'main') {
if (isDevelopmentRuntime) {
registerRendererDevtoolsProxyTarget(contents)
registerObservedRequestRendererTarget(contents)
}
mainRendererContents = contents
window.on('closed', () => {
if (mainRendererContents === contents) {
mainRendererContents = null
}
})
}
await loadRenderer(window, mode)
}
async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
const initial = WINDOW_SIZES[mode]
const backgroundColor =
mode === 'login' ? '#eaf2ff' : nativeTheme.shouldUseDarkColors ? '#15191a' : '#f0f2f5'
const window = new BrowserWindow({
width: initial.width,
height: initial.height,
minWidth: initial.minWidth,
minHeight: initial.minHeight,
show: false,
title: '省心推',
icon: createDesktopHealthIcon('normal'),
titleBarStyle: appTitleBarStyle(),
titleBarOverlay: appTitleBarOverlay(mode === 'login' ? '#00000000' : backgroundColor),
trafficLightPosition: { x: 12, y: 14 },
resizable: initial.resizable,
minimizable: mode !== 'login',
maximizable: initial.resizable,
fullscreenable: initial.resizable,
autoHideMenuBar: true,
backgroundColor,
webPreferences: {
preload: preloadPath(),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
})
appWindowModes.set(window, mode)
suppressNativeWindowMenu(window)
if (!initial.resizable) {
window.setMinimumSize(initial.minWidth, initial.minHeight)
window.setMaximumSize(initial.width, initial.height)
}
window.once('closed', () => {
if (mode === 'main' && mainWindow === window) {
mainWindow = null
}
if (mode === 'login' && loginWindow === window) {
loginWindow = null
}
})
window.on('close', (event) => {
if (
mode === 'main' &&
!quitReleaseInFlight &&
!forceCloseWindows.has(window) &&
getDesktopAppSettings().keepRunningInBackground
) {
event.preventDefault()
window.hide()
// Windows: notify users once that the app is still running in the tray.
// displayBalloon is a no-op on macOS/Linux so the guard is in tray.ts.
if (process.platform === 'win32' && !hasShownBackgroundBalloon()) {
showTrayBalloon('省心推已最小化到托盘', '应用仍在后台运行,右键托盘图标可退出。')
markBackgroundBalloonShown()
}
}
})
window.on('minimize', () => {
console.warn('[desktop-main] main window minimize event', {
at: new Date().toISOString(),
stack: new Error('trace').stack,
})
})
window.on('hide', () => {
console.warn('[desktop-main] main window hide event', {
at: new Date().toISOString(),
stack: new Error('trace').stack,
})
})
window.on('blur', () => {
console.warn('[desktop-main] main window blur event', {
at: new Date().toISOString(),
visible: window.isVisible(),
minimized: window.isMinimized(),
})
})
await mountRendererWebContents(window, mode)
return window
}
async function createSettingsWindow(): Promise<ElectronBrowserWindow> {
const initial = SETTINGS_WINDOW_SIZE
const backgroundColor = nativeTheme.shouldUseDarkColors ? '#15191a' : '#f2f4f7'
const window = new BrowserWindow({
parent: settingsWindowParent(),
width: initial.width,
height: initial.height,
minWidth: initial.minWidth,
minHeight: initial.minHeight,
show: false,
title: '省心推设置',
icon: createDesktopHealthIcon('normal'),
titleBarStyle: appTitleBarStyle(),
titleBarOverlay: appTitleBarOverlay(backgroundColor),
trafficLightPosition: { x: 14, y: 15 },
resizable: initial.resizable,
maximizable: initial.resizable,
fullscreenable: false,
autoHideMenuBar: true,
backgroundColor,
webPreferences: {
preload: preloadPath(),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
})
appWindowModes.set(window, 'settings')
suppressNativeWindowMenu(window)
window.once('closed', () => {
if (settingsWindow === window) {
settingsWindow = null
}
})
await mountRendererWebContents(window, 'settings')
return window
}
function flattenError(error: unknown): Error {
if (error instanceof Error) {
const copy = new Error(error.message || 'ipc_handler_failed')
copy.name = error.name
return copy
}
return new Error(typeof error === 'string' ? error : 'ipc_handler_failed')
}
function safeHandle<Args extends unknown[], Result>(
channel: string,
handler: (...args: Args) => Promise<Result> | Result,
): void {
ipcMain.handle(channel, async (...args) => {
try {
return await handler(...(args as unknown as Args))
} catch (error) {
throw flattenError(error)
}
})
}
function isSafeExternalUrl(url: string): boolean {
try {
const parsed = new URL(url)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
function registerBridgeHandlers(): void {
ipcMain.handle('desktop:ping', () => 'pong')
ipcMain.handle('desktop:device-info', () => resolveDesktopDeviceInfo())
safeHandle(
'desktop:client-id',
(_event, scope: { tenant_id?: unknown; workspace_id?: unknown; user_id?: unknown }) =>
resolveDesktopClientID({
tenant_id: typeof scope?.tenant_id === 'number' ? scope.tenant_id : 0,
workspace_id: typeof scope?.workspace_id === 'number' ? scope.workspace_id : 0,
user_id: typeof scope?.user_id === 'number' ? scope.user_id : 0,
}),
)
ipcMain.handle('desktop:runtime-snapshot', () => captureRuntimeSnapshot())
ipcMain.handle('desktop:runtime-account-snapshot', (_event, accountId: string) =>
captureRuntimeAccountSnapshot(accountId),
)
safeHandle('desktop:bind-publish-account', async (_event, platformId: string) => {
const account = (await bindPublishAccount(platformId)) as DesktopAccountInfo
noteRuntimeAccountBound(account)
void refreshRuntimeAccounts().catch((error) => {
console.warn('[desktop-bind] background account refresh failed', {
message: error instanceof Error ? error.message : String(error),
})
})
return account
})
safeHandle('desktop:refresh-runtime-accounts', async () => {
await refreshRuntimeAccounts()
return null
})
safeHandle('desktop:probe-runtime-account', async (_event, accountId: string) => {
await requestRuntimeAccountProbe(accountId)
return captureRuntimeAccountSnapshot(accountId)
})
safeHandle(
'desktop:open-publish-account-console',
async (
_event,
account: { id: string; platform: string; platformUid: string; displayName: string },
) => {
await openPublishAccountConsole(account)
void requestRuntimeAccountProbe(account.id, {
account,
force: true,
}).catch((error) => {
console.warn('[desktop-console] background account probe failed', {
accountId: account.id,
platform: account.platform,
message: error instanceof Error ? error.message : String(error),
})
})
return null
},
)
safeHandle(
'desktop:unbind-publish-account',
async (_event, accountId: string, syncVersion: number) => {
await unbindRuntimeAccount(accountId, syncVersion)
return null
},
)
safeHandle(
'desktop:list-publish-tasks',
async (_event, params?: { page?: number; page_size?: number; title?: string }) => {
return listDesktopPublishTasks(params)
},
)
safeHandle('desktop:retry-publish-task', async (_event, taskId: string) => {
return retryDesktopPublishTask(taskId)
})
safeHandle('desktop:open-external-url', async (_event, url: string) => {
if (!isSafeExternalUrl(url)) {
throw new Error('unsupported_external_url')
}
await shell.openExternal(url)
return null
})
safeHandle('desktop:open-settings-window', async () => {
await openSettingsWindow()
return null
})
ipcMain.handle('desktop:get-app-settings', () => getDesktopAppSettings())
safeHandle(
'desktop:set-app-setting',
async (_event, key: DesktopAppSettingKey, value: boolean) => {
if (key !== 'openAtLogin' && key !== 'keepRunningInBackground') {
throw new Error('unsupported_app_setting')
}
return setDesktopAppSetting(key, value)
},
)
ipcMain.handle(
'desktop:runtime-session-sync',
(_event, session: DesktopRuntimeSessionSyncRequest | null) => {
syncRuntimeSession(session)
return null
},
)
safeHandle('desktop:runtime-session-release', async (_event, revoke?: boolean) => {
await releaseRuntimeSession({ revoke: Boolean(revoke) })
return null
})
ipcMain.handle('desktop:set-window-mode', async (event, mode: WindowMode, request?: unknown) => {
if (mode === 'login' || mode === 'main') {
await queueWindowModeSwitch(
mode,
normalizeWindowModeRequest(request),
windowFromIpcEvent(event),
)
}
return null
})
}
const hasSingleInstanceLock = initSingleInstance(() => {
revealActiveWindowSafely('single-instance')
})
if (!hasSingleInstanceLock) {
console.info('[desktop-main] another instance is already running; exiting')
app.exit(0)
} else {
app
.whenReady()
.then(async () => {
suppressNativeWindowMenu()
currentWindowMode = readPersistedWindowMode()
initDesktopAppSettings()
registerBridgeHandlers()
await initSessionRegistry()
initAccountHealth()
initTransport()
initScheduler()
initProcessMetricsSampler()
startHotViewReaper()
startHiddenPlaywrightReaper()
onRuntimeInvalidated((event) => {
syncDesktopHealthIndicator(currentMainWindow())
if (!mainRendererContents || mainRendererContents.isDestroyed()) {
return
}
mainRendererContents.send('desktop:runtime-invalidated', event)
})
await queueWindowModeSwitch(currentWindowMode)
initTray(() => {
revealActiveWindowSafely('tray')
})
startDesktopHealthIndicator(() => currentMainWindow())
})
.catch((error) => {
console.error('[desktop-main] app.whenReady failed', error)
})
app.on('activate', () => {
revealActiveWindowSafely('activate')
})
app.on('window-all-closed', () => {
if (getDesktopAppSettings().keepRunningInBackground) {
return
}
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', (event) => {
if (quitReleaseInFlight) {
return
}
quitReleaseInFlight = true
event.preventDefault()
void Promise.race([
releaseRuntimeSession(),
new Promise<void>((resolve) => setTimeout(resolve, 1500)),
]).finally(() => {
app.quit()
})
})
}