|
|
|
@@ -2,16 +2,95 @@ import type { BrowserWindow } from 'electron/main'
|
|
|
|
|
|
|
|
|
|
import { STANDARD_USER_AGENT } from './user-agent'
|
|
|
|
|
|
|
|
|
|
const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT/i
|
|
|
|
|
const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|\((-3|-20)\) loading/i
|
|
|
|
|
const errorPagePrefix = 'data:text/html'
|
|
|
|
|
|
|
|
|
|
const BLANK_PAGE_PROBE_DELAY_MS = 7_000
|
|
|
|
|
const BLANK_PAGE_NODE_THRESHOLD = 3
|
|
|
|
|
const BLANK_PAGE_TEXT_THRESHOLD = 5
|
|
|
|
|
|
|
|
|
|
interface WindowNavigationState {
|
|
|
|
|
readyForDetection: boolean
|
|
|
|
|
lastMainFrameError: string | null
|
|
|
|
|
blankPageWatchdog: NodeJS.Timeout | null
|
|
|
|
|
blankPageRetried: boolean
|
|
|
|
|
context: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const navigationStateRegistry = new WeakMap<BrowserWindow, WindowNavigationState>()
|
|
|
|
|
|
|
|
|
|
function clearBlankPageWatchdog(state: WindowNavigationState): void {
|
|
|
|
|
if (state.blankPageWatchdog) {
|
|
|
|
|
clearTimeout(state.blankPageWatchdog)
|
|
|
|
|
state.blankPageWatchdog = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function probePageBodyShape(
|
|
|
|
|
window: BrowserWindow,
|
|
|
|
|
): Promise<{ nodeCount: number; textLength: number; readyState: string } | null> {
|
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return (await window.webContents.executeJavaScript(
|
|
|
|
|
`(() => ({
|
|
|
|
|
nodeCount: document.body ? document.body.querySelectorAll('*').length : 0,
|
|
|
|
|
textLength: document.body ? (document.body.innerText || '').trim().length : 0,
|
|
|
|
|
readyState: document.readyState,
|
|
|
|
|
}))()`,
|
|
|
|
|
true,
|
|
|
|
|
)) as { nodeCount: number; textLength: number; readyState: string }
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleBlankPageWatchdog(window: BrowserWindow, state: WindowNavigationState): void {
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
|
|
|
|
|
state.blankPageWatchdog = setTimeout(() => {
|
|
|
|
|
state.blankPageWatchdog = null
|
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentURL = window.webContents.getURL()
|
|
|
|
|
if (currentURL.startsWith(errorPagePrefix) || currentURL === 'about:blank') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void probePageBodyShape(window).then((shape) => {
|
|
|
|
|
if (!shape || window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const looksBlank =
|
|
|
|
|
shape.nodeCount < BLANK_PAGE_NODE_THRESHOLD &&
|
|
|
|
|
shape.textLength < BLANK_PAGE_TEXT_THRESHOLD
|
|
|
|
|
|
|
|
|
|
if (!looksBlank) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.blankPageRetried) {
|
|
|
|
|
console.warn(`[desktop-window] ${state.context} still blank after auto-reload`, {
|
|
|
|
|
url: currentURL,
|
|
|
|
|
...shape,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.blankPageRetried = true
|
|
|
|
|
console.warn(`[desktop-window] ${state.context} blank page detected, reloading once`, {
|
|
|
|
|
url: currentURL,
|
|
|
|
|
...shape,
|
|
|
|
|
})
|
|
|
|
|
window.webContents.reloadIgnoringCache()
|
|
|
|
|
})
|
|
|
|
|
}, BLANK_PAGE_PROBE_DELAY_MS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(value: string): string {
|
|
|
|
|
return value
|
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
@@ -126,6 +205,9 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|
|
|
|
navigationStateRegistry.set(window, {
|
|
|
|
|
readyForDetection: false,
|
|
|
|
|
lastMainFrameError: null,
|
|
|
|
|
blankPageWatchdog: null,
|
|
|
|
|
blankPageRetried: false,
|
|
|
|
|
context,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
window.webContents.on('did-start-loading', () => {
|
|
|
|
@@ -134,6 +216,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
state.readyForDetection = false
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
window.webContents.on(
|
|
|
|
@@ -147,6 +230,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|
|
|
|
if (state) {
|
|
|
|
|
state.readyForDetection = false
|
|
|
|
|
state.lastMainFrameError = `${errorCode}:${errorDescription}`
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.warn(`[desktop-window] ${context} main-frame load failed`, {
|
|
|
|
@@ -167,6 +251,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|
|
|
|
state.readyForDetection = !currentURL.startsWith(errorPagePrefix)
|
|
|
|
|
if (state.readyForDetection) {
|
|
|
|
|
state.lastMainFrameError = null
|
|
|
|
|
scheduleBlankPageWatchdog(window, state)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
@@ -174,10 +259,18 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (state) {
|
|
|
|
|
state.readyForDetection = false
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.error(`[desktop-window] ${context} renderer gone`, details)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
window.once('closed', () => {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (state) {
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isWindowReadyForDetection(window: BrowserWindow): boolean {
|
|
|
|
@@ -193,6 +286,12 @@ export async function loadWindowURLSafely(
|
|
|
|
|
targetURL: string,
|
|
|
|
|
context: string,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (state) {
|
|
|
|
|
state.blankPageRetried = false
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
|
|
|
|
} catch (error) {
|
|
|
|
|