mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(desktop): back off failed app loads and offer a retry (#2980)
This commit is contained in:
1 parent
6a6119ed1e
commit
677ef8491e
41 files changed
+3012
-2367
No files matched your search
@@ -21,6 +21,7 @@ import {
|
||||
} from '@electron/common/DesktopConfig';
|
||||
import {createChildLogger} from '@electron/common/Logger';
|
||||
import type {DesktopWindowBehaviorSettings} from '@electron/common/Types';
|
||||
import {createAppLoadRetry} from '@electron/main/AppLoadRetry';
|
||||
import {
|
||||
shouldForwardRendererConsoleToMainLog,
|
||||
shouldIgnoreWindowStateForLaunch,
|
||||
@@ -29,19 +30,18 @@ import {
|
||||
import {hasActiveDesktopTray, refreshDesktopTrayMenu} from '@electron/main/DesktopTray';
|
||||
import {drainPendingDisplayMediaRequests, registerDisplayMediaRequestHandler} from '@electron/main/DisplayMedia';
|
||||
import {shouldDisableV8CodeCache} from '@electron/main/LaunchOptions';
|
||||
import {t} from '@electron/main/MainI18n';
|
||||
import {openExternalDeduped} from '@electron/main/OpenExternal';
|
||||
import {registerSpellcheck} from '@electron/main/Spellcheck';
|
||||
import {resetStreamingPriority} from '@electron/main/StreamingPriority';
|
||||
import {getMainWindowRendererGoneAction} from '@electron/main/WindowRendererLifecycle';
|
||||
import {refreshWindowsBadgeOverlay} from '@electron/main/WindowsBadge';
|
||||
import {app, BrowserWindow, screen} from 'electron';
|
||||
import {app, BrowserWindow, dialog, screen} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const logger = createChildLogger('Window');
|
||||
const VISIBILITY_MARGIN = 32;
|
||||
const INITIAL_APP_LOAD_RETRY_DELAY_MS = 1000;
|
||||
const MAX_APP_LOAD_RETRY_DELAY_MS = 30000;
|
||||
const RENDERER_GONE_REPEAT_WINDOW_MS = 30000;
|
||||
const OPAQUE_WINDOW_BACKGROUND_COLOR = '#1a1a1a';
|
||||
const TRANSPARENT_WINDOW_BACKGROUND_COLOR = '#00000000';
|
||||
@@ -91,18 +91,6 @@ const trustedRendererPermissionTypes = new Set([
|
||||
]);
|
||||
const POPOUT_NAMESPACE = 'fluxer_';
|
||||
|
||||
function shouldRetryAppLoadFailure(errorCode: number): boolean {
|
||||
return errorCode < 0 && errorCode !== -3;
|
||||
}
|
||||
|
||||
function getElectronLoadErrorCode(error: unknown): number | null {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const match = /\(([-\d]+)\)/.exec(message);
|
||||
if (!match) return null;
|
||||
const value = Number.parseInt(match[1], 10);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function getOrigin(url?: string): string | null {
|
||||
if (!url) return null;
|
||||
try {
|
||||
@@ -733,7 +721,6 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
|
||||
const acceptFirstMouseOnFocus = isMac;
|
||||
initialUseNativeTitleBar = useNativeTitleBar;
|
||||
initialAllowTransparency = allowTransparency;
|
||||
let appUrl = getAppUrl();
|
||||
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
@@ -746,7 +733,7 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
|
||||
...getTitleBarWindowOptions(useNativeTitleBar),
|
||||
trafficLightPosition: isMac ? CUSTOM_TITLEBAR_TRAFFIC_LIGHT_POSITION : undefined,
|
||||
acceptFirstMouse: acceptFirstMouseOnFocus,
|
||||
webPreferences: getSharedWebPreferences(allowTransparency, useNativeTitleBar, appUrl),
|
||||
webPreferences: getSharedWebPreferences(allowTransparency, useNativeTitleBar, getAppUrl()),
|
||||
};
|
||||
if (isLinux) {
|
||||
const iconPath = getLinuxWindowIconPath();
|
||||
@@ -976,81 +963,55 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
|
||||
});
|
||||
registerDisplayMediaRequestHandler(session, webContents);
|
||||
logPhase('handlers');
|
||||
let appLoadRetryAttempt = 0;
|
||||
let appLoadRetryTimer: NodeJS.Timeout | null = null;
|
||||
const clearAppLoadRetry = () => {
|
||||
let appLoadFailurePrompt: AbortController | null = null;
|
||||
const dismissAppLoadFailurePrompt = () => {
|
||||
appLoadFailurePrompt?.abort();
|
||||
appLoadFailurePrompt = null;
|
||||
};
|
||||
const appLoadRetry = createAppLoadRetry({
|
||||
webContents,
|
||||
appUrl: getAppUrl(),
|
||||
logger,
|
||||
isTrustedUrl: isTrustedOrigin,
|
||||
getFallbackUrl: getAppUrlFallback,
|
||||
onCommitted: dismissAppLoadFailurePrompt,
|
||||
onRepeatedFailure: (failure) => {
|
||||
const window = mainWindow;
|
||||
if (appLoadFailurePrompt || !isAliveWindow(window) || !window.isVisible()) return;
|
||||
const prompt = new AbortController();
|
||||
appLoadFailurePrompt = prompt;
|
||||
void dialog
|
||||
.showMessageBox(window, {
|
||||
type: 'warning',
|
||||
buttons: [t('desktop.appLoad.retry'), t('desktop.tray.quit')],
|
||||
defaultId: 0,
|
||||
cancelId: 0,
|
||||
title: t('desktop.appLoad.failedTitle'),
|
||||
message: t('desktop.appLoad.failedMessage'),
|
||||
detail: `${failure.errorDescription} (${failure.errorCode})\n${failure.url}`,
|
||||
noLink: true,
|
||||
signal: prompt.signal,
|
||||
})
|
||||
.then(({response}) => {
|
||||
if (appLoadFailurePrompt === prompt) appLoadFailurePrompt = null;
|
||||
if (prompt.signal.aborted || isQuitting) return;
|
||||
if (response === 1) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
appLoadRetry.retryNow();
|
||||
});
|
||||
},
|
||||
});
|
||||
webContents.on('did-finish-load', () => {
|
||||
rendererGoneReloaded = false;
|
||||
mainWindowRendererGone = false;
|
||||
if (appLoadRetryTimer) {
|
||||
clearTimeout(appLoadRetryTimer);
|
||||
appLoadRetryTimer = null;
|
||||
}
|
||||
appLoadRetryAttempt = 0;
|
||||
};
|
||||
const scheduleAppLoadRetry = (reason: string, detail?: Record<string, unknown>) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
if (appLoadRetryTimer) return;
|
||||
const delay = Math.min(INITIAL_APP_LOAD_RETRY_DELAY_MS * 2 ** appLoadRetryAttempt, MAX_APP_LOAD_RETRY_DELAY_MS);
|
||||
appLoadRetryAttempt += 1;
|
||||
logger.warn('Scheduling app load retry', {reason, delay, attempt: appLoadRetryAttempt, ...detail});
|
||||
appLoadRetryTimer = setTimeout(() => {
|
||||
appLoadRetryTimer = null;
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
if (mainWindow.webContents.isLoadingMainFrame()) {
|
||||
scheduleAppLoadRetry('main-frame-still-loading');
|
||||
return;
|
||||
}
|
||||
mainWindow.loadURL(appUrl).catch((error) => {
|
||||
scheduleAppLoadRetry('load-url-rejected', {error});
|
||||
});
|
||||
}, delay);
|
||||
};
|
||||
const fallBackFromMigratedAppOrigin = (failedUrl: string, detail: Record<string, unknown>): boolean => {
|
||||
const fallbackUrl = getAppUrlFallback(failedUrl);
|
||||
if (!mainWindow || mainWindow.isDestroyed() || fallbackUrl === null || appUrl === fallbackUrl) {
|
||||
return false;
|
||||
}
|
||||
logger.warn('Migrated app origin failed to load, falling back to the legacy app URL', {failedUrl, ...detail});
|
||||
appUrl = fallbackUrl;
|
||||
clearAppLoadRetry();
|
||||
mainWindow.loadURL(appUrl).catch((error) => {
|
||||
scheduleAppLoadRetry('legacy-fallback-load-url-rejected', {error});
|
||||
});
|
||||
return true;
|
||||
};
|
||||
webContents.on('did-finish-load', clearAppLoadRetry);
|
||||
webContents.on('did-navigate', (_event, url, httpResponseCode) => {
|
||||
if (httpResponseCode >= 400) {
|
||||
fallBackFromMigratedAppOrigin(url, {httpResponseCode});
|
||||
}
|
||||
});
|
||||
webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
if (isMainFrame) {
|
||||
logger.error('App main-frame load failed', {errorCode, errorDescription, validatedURL});
|
||||
}
|
||||
if (!isMainFrame || !isTrustedOrigin(validatedURL) || !shouldRetryAppLoadFailure(errorCode)) {
|
||||
return;
|
||||
}
|
||||
if (fallBackFromMigratedAppOrigin(validatedURL, {errorCode, errorDescription})) {
|
||||
return;
|
||||
}
|
||||
scheduleAppLoadRetry('did-fail-load', {errorCode, errorDescription, validatedURL});
|
||||
});
|
||||
const loadAppUrl = (): void => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
logger.info('Loading app URL', {appUrl});
|
||||
mainWindow.loadURL(appUrl).catch((error) => {
|
||||
const errorCode = getElectronLoadErrorCode(error);
|
||||
if (errorCode !== null && !shouldRetryAppLoadFailure(errorCode)) {
|
||||
logger.info('Ignoring non-retryable initial app load rejection', {errorCode});
|
||||
return;
|
||||
}
|
||||
logger.error('Failed to load app URL:', error);
|
||||
scheduleAppLoadRetry('initial-load-url-rejected', {error});
|
||||
});
|
||||
void clearStartupRenderingCaches(session).then(() => {
|
||||
if (!isAliveWindow(mainWindow)) return;
|
||||
appLoadRetry.start();
|
||||
logPhase('load-url-dispatched');
|
||||
};
|
||||
void clearStartupRenderingCaches(session).then(loadAppUrl);
|
||||
});
|
||||
webContents.on('will-navigate', (event, url) => {
|
||||
if (!isTrustedOrigin(url)) {
|
||||
event.preventDefault();
|
||||
@@ -1095,7 +1056,11 @@ export function createWindow(options: CreateWindowOptions = {}): BrowserWindow {
|
||||
transparent: allowPopoutTransparency,
|
||||
hasShadow: getWindowHasShadow(allowPopoutTransparency),
|
||||
show: true,
|
||||
webPreferences: getSharedWebPreferences(allowPopoutTransparency, getActiveUseNativeTitleBar(), appUrl),
|
||||
webPreferences: getSharedWebPreferences(
|
||||
allowPopoutTransparency,
|
||||
getActiveUseNativeTitleBar(),
|
||||
appLoadRetry.getAppUrl(),
|
||||
),
|
||||
};
|
||||
return {action: 'allow', overrideBrowserWindowOptions};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user