mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +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
@@ -0,0 +1,177 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import {describe, test} from 'node:test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
const sourcePath = fileURLToPath(new URL('./AppLoadRetry.ts', import.meta.url));
|
||||
const source = readFileSync(sourcePath, 'utf8');
|
||||
const transformedSource = esbuild.transformSync(source, {
|
||||
loader: 'ts',
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
}).code;
|
||||
|
||||
function loadAppLoadRetry() {
|
||||
const module = {exports: {}};
|
||||
const context = vm.createContext({
|
||||
module,
|
||||
exports: module.exports,
|
||||
});
|
||||
vm.runInContext(transformedSource, context, {filename: sourcePath});
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
const APP_URL = 'https://web.canary.fluxer.app/';
|
||||
const MIGRATED_URL = 'https://canary.fluxer.com/';
|
||||
const silentLogger = {info() {}, warn() {}, error() {}};
|
||||
|
||||
function createHarness({appUrl = APP_URL, fallbackUrl = null} = {}) {
|
||||
const listeners = new Map();
|
||||
const timers = [];
|
||||
const loads = [];
|
||||
const prompts = [];
|
||||
let commits = 0;
|
||||
let outcome = 'dns-failure';
|
||||
const webContents = {
|
||||
on(event, listener) {
|
||||
listeners.set(event, [...(listeners.get(event) ?? []), listener]);
|
||||
},
|
||||
emit(event, ...args) {
|
||||
for (const listener of listeners.get(event) ?? []) listener({}, ...args);
|
||||
},
|
||||
isDestroyed: () => false,
|
||||
isLoadingMainFrame: () => false,
|
||||
loadURL(url) {
|
||||
loads.push(url);
|
||||
if (outcome === 'ok') {
|
||||
webContents.emit('did-navigate', url, 200, 'OK');
|
||||
webContents.emit('did-finish-load');
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (outcome === 'aborted') {
|
||||
return Promise.reject(new Error(`ERR_ABORTED (-3) loading '${url}'`));
|
||||
}
|
||||
webContents.emit('did-fail-load', -105, 'ERR_NAME_NOT_RESOLVED', url, true);
|
||||
webContents.emit('did-finish-load');
|
||||
return Promise.reject(new Error(`ERR_NAME_NOT_RESOLVED (-105) loading '${url}'`));
|
||||
},
|
||||
};
|
||||
const {createAppLoadRetry} = loadAppLoadRetry();
|
||||
const retry = createAppLoadRetry({
|
||||
webContents,
|
||||
appUrl,
|
||||
logger: silentLogger,
|
||||
isTrustedUrl: () => true,
|
||||
getFallbackUrl: (url) => (url === MIGRATED_URL ? fallbackUrl : null),
|
||||
onRepeatedFailure: (failure) => prompts.push(failure),
|
||||
onCommitted: () => {
|
||||
commits += 1;
|
||||
},
|
||||
setTimer: (callback, delay) => {
|
||||
const timer = {callback, delay, cleared: false};
|
||||
timers.push(timer);
|
||||
return timer;
|
||||
},
|
||||
clearTimer: (timer) => {
|
||||
timer.cleared = true;
|
||||
},
|
||||
});
|
||||
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
||||
return {
|
||||
retry,
|
||||
loads,
|
||||
prompts,
|
||||
get commits() {
|
||||
return commits;
|
||||
},
|
||||
setOutcome(next) {
|
||||
outcome = next;
|
||||
},
|
||||
pendingDelays: () => timers.filter((timer) => !timer.cleared && !timer.fired).map((timer) => timer.delay),
|
||||
async fireNextTimer() {
|
||||
const timer = timers.find((candidate) => !candidate.cleared && !candidate.fired);
|
||||
assert.ok(timer, 'expected a pending retry timer');
|
||||
timer.fired = true;
|
||||
timer.callback();
|
||||
await settle();
|
||||
return timer.delay;
|
||||
},
|
||||
settle,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AppLoadRetry', () => {
|
||||
test('backs off on a DNS failure even though the error page fires did-finish-load', async () => {
|
||||
const harness = createHarness();
|
||||
harness.retry.start();
|
||||
await harness.settle();
|
||||
|
||||
const delays = [];
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
assert.equal(harness.pendingDelays().length, 1);
|
||||
delays.push(await harness.fireNextTimer());
|
||||
}
|
||||
|
||||
assert.deepEqual(delays, [1000, 2000, 4000, 8000, 16000, 30000, 30000, 30000]);
|
||||
assert.equal(harness.loads.length, 9);
|
||||
});
|
||||
|
||||
test('resets the backoff and dismisses the prompt once the app commits', async () => {
|
||||
const harness = createHarness();
|
||||
harness.retry.start();
|
||||
await harness.settle();
|
||||
await harness.fireNextTimer();
|
||||
await harness.fireNextTimer();
|
||||
assert.equal(harness.prompts.length, 1);
|
||||
|
||||
harness.setOutcome('ok');
|
||||
await harness.fireNextTimer();
|
||||
assert.equal(harness.commits, 1);
|
||||
assert.deepEqual(harness.pendingDelays(), []);
|
||||
|
||||
harness.setOutcome('dns-failure');
|
||||
harness.retry.retryNow();
|
||||
await harness.settle();
|
||||
assert.deepEqual(harness.pendingDelays(), [1000]);
|
||||
});
|
||||
|
||||
test('asks the user only after three consecutive failures', async () => {
|
||||
const harness = createHarness();
|
||||
harness.retry.start();
|
||||
await harness.settle();
|
||||
await harness.fireNextTimer();
|
||||
assert.equal(harness.prompts.length, 0);
|
||||
|
||||
await harness.fireNextTimer();
|
||||
assert.equal(harness.prompts.length, 1);
|
||||
assert.equal(harness.prompts[0].errorCode, -105);
|
||||
assert.equal(harness.prompts[0].url, APP_URL);
|
||||
});
|
||||
|
||||
test('does not schedule a retry for an aborted load', async () => {
|
||||
const harness = createHarness();
|
||||
harness.setOutcome('aborted');
|
||||
harness.retry.start();
|
||||
await harness.settle();
|
||||
|
||||
assert.deepEqual(harness.pendingDelays(), []);
|
||||
});
|
||||
|
||||
test('falls back from the migrated origin without backing off', async () => {
|
||||
const harness = createHarness({appUrl: MIGRATED_URL, fallbackUrl: APP_URL});
|
||||
harness.retry.start();
|
||||
await harness.settle();
|
||||
|
||||
assert.deepEqual(harness.loads.slice(0, 2), [MIGRATED_URL, APP_URL]);
|
||||
assert.equal(harness.retry.getAppUrl(), APP_URL);
|
||||
assert.deepEqual(harness.pendingDelays(), [1000]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const INITIAL_RETRY_DELAY_MS = 1000;
|
||||
const MAX_RETRY_DELAY_MS = 30000;
|
||||
const FAILURES_BEFORE_PROMPT = 3;
|
||||
const ERR_ABORTED = -3;
|
||||
|
||||
export interface AppLoadFailure {
|
||||
errorCode: number;
|
||||
errorDescription: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface AppLoadRetryLogger {
|
||||
info(message: string, detail?: Record<string, unknown>): void;
|
||||
warn(message: string, detail?: Record<string, unknown>): void;
|
||||
error(message: string, detail?: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
interface AppLoadRetryOptions {
|
||||
webContents: Pick<Electron.WebContents, 'on' | 'isDestroyed' | 'isLoadingMainFrame' | 'loadURL'>;
|
||||
appUrl: string;
|
||||
logger: AppLoadRetryLogger;
|
||||
isTrustedUrl: (url: string) => boolean;
|
||||
getFallbackUrl: (failedUrl: string) => string | null;
|
||||
onRepeatedFailure: (failure: AppLoadFailure) => void;
|
||||
onCommitted: () => void;
|
||||
setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
||||
}
|
||||
|
||||
export interface AppLoadRetry {
|
||||
start(): void;
|
||||
retryNow(): void;
|
||||
getAppUrl(): string;
|
||||
}
|
||||
|
||||
function isRetryableLoadError(errorCode: number): boolean {
|
||||
return errorCode < 0 && errorCode !== ERR_ABORTED;
|
||||
}
|
||||
|
||||
function getLoadErrorCode(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;
|
||||
}
|
||||
|
||||
export function createAppLoadRetry(options: AppLoadRetryOptions): AppLoadRetry {
|
||||
const {webContents, logger} = options;
|
||||
const setTimer = options.setTimer ?? setTimeout;
|
||||
const clearTimer = options.clearTimer ?? clearTimeout;
|
||||
let appUrl = options.appUrl;
|
||||
let attempt = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
const cancelTimer = () => {
|
||||
if (timer) {
|
||||
clearTimer(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
const load = (reason: string) => {
|
||||
if (webContents.isDestroyed()) return;
|
||||
webContents.loadURL(appUrl).catch((error: unknown) => {
|
||||
const errorCode = getLoadErrorCode(error);
|
||||
if (errorCode !== null && !isRetryableLoadError(errorCode)) {
|
||||
logger.info('Ignoring non-retryable app load rejection', {reason, errorCode});
|
||||
return;
|
||||
}
|
||||
schedule(`${reason}-rejected`, {error});
|
||||
});
|
||||
};
|
||||
const schedule = (reason: string, detail?: Record<string, unknown>) => {
|
||||
if (timer) return;
|
||||
const delay = Math.min(INITIAL_RETRY_DELAY_MS * 2 ** attempt, MAX_RETRY_DELAY_MS);
|
||||
attempt += 1;
|
||||
logger.warn('Scheduling app load retry', {reason, delay, attempt, ...detail});
|
||||
timer = setTimer(() => {
|
||||
timer = null;
|
||||
if (webContents.isDestroyed()) return;
|
||||
if (webContents.isLoadingMainFrame()) {
|
||||
schedule('main-frame-still-loading');
|
||||
return;
|
||||
}
|
||||
load('retry');
|
||||
}, delay);
|
||||
};
|
||||
const reset = () => {
|
||||
cancelTimer();
|
||||
attempt = 0;
|
||||
};
|
||||
const fallBackFromMigratedOrigin = (failedUrl: string, detail: Record<string, unknown>): boolean => {
|
||||
const fallbackUrl = options.getFallbackUrl(failedUrl);
|
||||
if (fallbackUrl === null || fallbackUrl === appUrl) return false;
|
||||
logger.warn('Migrated app origin failed to load, falling back to the legacy app URL', {failedUrl, ...detail});
|
||||
appUrl = fallbackUrl;
|
||||
reset();
|
||||
load('legacy-fallback');
|
||||
return true;
|
||||
};
|
||||
return {
|
||||
start() {
|
||||
webContents.on('did-navigate', (_event, url, httpResponseCode) => {
|
||||
reset();
|
||||
options.onCommitted();
|
||||
if (httpResponseCode >= 400) {
|
||||
fallBackFromMigratedOrigin(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 || !options.isTrustedUrl(validatedURL) || !isRetryableLoadError(errorCode)) {
|
||||
return;
|
||||
}
|
||||
if (fallBackFromMigratedOrigin(validatedURL, {errorCode, errorDescription})) {
|
||||
return;
|
||||
}
|
||||
schedule('did-fail-load', {errorCode, errorDescription, validatedURL});
|
||||
if (attempt >= FAILURES_BEFORE_PROMPT) {
|
||||
options.onRepeatedFailure({errorCode, errorDescription, url: validatedURL});
|
||||
}
|
||||
});
|
||||
logger.info('Loading app URL', {appUrl});
|
||||
load('initial-load');
|
||||
},
|
||||
retryNow() {
|
||||
reset();
|
||||
load('manual-retry');
|
||||
},
|
||||
getAppUrl() {
|
||||
return appUrl;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -85,6 +85,9 @@ const FALLBACK_STRINGS: Readonly<Record<string, string>> = Object.freeze({
|
||||
'desktop.badge.unreadMessages': 'Unread messages',
|
||||
'desktop.badge.unreadMessagesCount': 'Unread messages: {count}',
|
||||
'desktop.startup.failedTitle': '{appName} failed to start',
|
||||
'desktop.appLoad.failedTitle': "Can't connect",
|
||||
'desktop.appLoad.failedMessage': "{appName} can't reach its servers. It will keep trying in the background.",
|
||||
'desktop.appLoad.retry': 'Try again',
|
||||
'desktop.linuxEntry.genericName': 'Instant Messenger',
|
||||
'desktop.linuxEntry.comment': 'Instant messaging and VoIP',
|
||||
});
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -302,6 +302,11 @@ if (launchConfigurationError) {
|
||||
.then(() => app.whenReady())
|
||||
.then(async () => {
|
||||
log.info('App ready, initializing...');
|
||||
try {
|
||||
runStartupPhase('host-resolver', () => app.configureHostResolver({enableAdditionalDnsQueryTypes: false}));
|
||||
} catch (error) {
|
||||
log.error('[Init] Failed to configure the host resolver:', error);
|
||||
}
|
||||
await runStartupPhaseAsync('launch-net-log', startLaunchNetLog);
|
||||
try {
|
||||
await runStartupPhaseAsync('desktop-debug-info', async () => {
|
||||
|
||||
Reference in new issue
Block a user