fix: harden desktop monitoring recovery
Desktop Client Build / Resolve Build Metadata (push) Successful in 37s
Backend CI / Backend (push) Failing after 9m26s
Desktop Client Build / Build Desktop Client (push) Successful in 22m29s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 35s

This commit is contained in:
2026-06-23 23:12:25 +08:00
parent b4ebf34728
commit 04bd3e42e0
16 changed files with 1329 additions and 164 deletions
+71 -6
View File
@@ -53,7 +53,7 @@ import {
installObservedGlobalFetch,
registerObservedRequestRendererTarget,
} from './network-observer'
import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from './playwright-cdp'
import { getPlaywrightCDPPort, preparePlaywrightCDPPort, startHiddenPlaywrightReaper } from './playwright-cdp'
import { initProcessMetricsSampler } from './process-metrics'
import { registerRendererDevtoolsProxyTarget } from './renderer-devtools-proxy'
import {
@@ -112,8 +112,8 @@ app.commandLine.appendSwitch(
)
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()))
// CDP is required by the hidden Playwright execution pool. Electron only reads
// this switch during early startup, so select the port before app.whenReady().
app.userAgentFallback = STANDARD_USER_AGENT
// Windows 10+ routes tray.displayBalloon through the Toast Notification API,
@@ -151,6 +151,7 @@ console.info('[desktop-main] boot', {
chrome: process.versions.chrome,
node: process.versions.node,
ua: STANDARD_USER_AGENT,
cdpPort: getPlaywrightCDPPort(),
})
process.on('unhandledRejection', (reason) => {
@@ -166,6 +167,7 @@ let loginWindow: ElectronBrowserWindow | null = null
let settingsWindow: ElectronBrowserWindow | null = null
let mainRendererContents: ElectronWebContents | null = null
let quitReleaseInFlight = false
let cdpRecoveryRelaunchInFlight = false
let quitAfterSaasLogoutPromise: Promise<void> | null = null
let mainWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let loginWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
@@ -176,6 +178,10 @@ let canHandleDesktopDeepLinks = false
const pendingDesktopDeepLinks: string[] = []
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>()
const appWindowModes = new WeakMap<ElectronBrowserWindow, RendererWindowMode>()
const maxCDPRecoveryRelaunchesPerWindow = 3
const cdpRecoveryRelaunchWindowMs = 10 * 60_000
const cdpRecoveryRelaunchDelayMs = 1_500
const cdpFatalArg = '--geo-cdp-fatal'
type WindowMode = 'login' | 'main'
type RendererWindowMode = WindowMode | 'settings'
@@ -547,6 +553,50 @@ function quitAfterClearingSaasSession(): void {
})
}
function requestPlaywrightCDPRecoveryRelaunch(message: string): void {
if (cdpRecoveryRelaunchInFlight) {
return
}
const now = Date.now()
const recentRelaunches = process.argv
.filter((arg) => arg.startsWith('--geo-cdp-recovery-at='))
.map((arg) => Number.parseInt(arg.split('=')[1] ?? '', 10))
.filter((timestamp) => Number.isFinite(timestamp) && now - timestamp < cdpRecoveryRelaunchWindowMs)
if (recentRelaunches.length >= maxCDPRecoveryRelaunchesPerWindow) {
console.error('[desktop-main] playwright cdp recovery relaunch suppressed', {
message,
recentRelaunches: recentRelaunches.length,
})
showTrayBalloon(
'浏览器自动化通道异常',
'客户端已停止采集。请重启客户端或检查安全软件/端口占用。',
)
return
}
cdpRecoveryRelaunchInFlight = true
console.warn('[desktop-main] scheduling playwright cdp recovery relaunch', {
message,
cdpPort: getPlaywrightCDPPort(),
})
showTrayBalloon('正在恢复浏览器自动化通道', '客户端将自动重启后台进程,登录状态会保留。')
setTimeout(() => {
quitReleaseInFlight = true
app.relaunch({
args: [
...process.argv.slice(1).filter((arg) => !arg.startsWith('--geo-cdp-recovery-at=')),
...recentRelaunches.map((timestamp) => `--geo-cdp-recovery-at=${timestamp}`),
`--geo-cdp-recovery-at=${now}`,
...(recentRelaunches.length + 1 >= maxCDPRecoveryRelaunchesPerWindow ? [cdpFatalArg] : []),
],
})
app.exit(0)
}, cdpRecoveryRelaunchDelayMs)
}
function windowFromIpcEvent(event: IpcMainInvokeEvent): ElectronBrowserWindow | null {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window || window.isDestroyed()) {
@@ -1197,9 +1247,15 @@ if (!hasSingleInstanceLock) {
console.info('[desktop-main] another instance is already running; exiting')
app.exit(0)
} else {
app
.whenReady()
preparePlaywrightCDPPort()
.then((selectedPlaywrightCDPPort) => {
app.commandLine.appendSwitch('remote-debugging-port', String(selectedPlaywrightCDPPort))
return app.whenReady()
})
.then(async () => {
console.info('[desktop-main] playwright cdp port selected', {
cdpPort: getPlaywrightCDPPort(),
})
suppressNativeWindowMenu()
registerDesktopProtocolClient()
currentWindowMode = readPersistedWindowMode()
@@ -1215,6 +1271,15 @@ if (!hasSingleInstanceLock) {
startHiddenPlaywrightReaper()
onRuntimeInvalidated((event) => {
syncDesktopHealthIndicator(currentMainWindow())
if (event.reason === 'playwright-cdp-fatal') {
showTrayBalloon(
'浏览器自动化组件不可用',
event.message ?? '客户端已停止采集,请重启客户端或检查安全软件/端口占用。',
)
}
if (event.reason === 'playwright-cdp-recovery') {
requestPlaywrightCDPRecoveryRelaunch(event.message ?? 'playwright_cdp_endpoint_unavailable')
}
if (!mainRendererContents || mainRendererContents.isDestroyed()) {
return
}
@@ -1234,7 +1299,7 @@ if (!hasSingleInstanceLock) {
flushPendingDesktopDeepLinks()
})
.catch((error) => {
console.error('[desktop-main] app.whenReady failed', error)
console.error('[desktop-main] startup failed', error)
})
app.on('activate', () => {