mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): repair update proxy/session targeting, quit-and-install crash, and about layout
Update downloads never used the configured proxy because electron-updater runs all traffic on its own session partition, while the proxy was applied to app/defaultSession only. Point proxy config at autoUpdater.netSession, and make the system fallback an explicit `mode: 'system'` since an empty setProxy config means fixed_servers with no rules (= direct), which also silently downgraded the default session away from the OS proxy. Disable differential download: blockmap-driven ranged requests against the GitHub CDN are RTT-bound and run far below line speed on both macOS and Windows; full downloads restore expected bandwidth. Guard captureWindowState and the resize forwarder against destroyed windows: quitAndInstall tears the window down while late move/resize/close events still fire, crashing the main process with "Object has been destroyed". Widen the About page container from max-w-lg to max-w-2xl so release notes markdown is no longer cramped.
This commit is contained in:
parent
b28867d551
commit
e5f1c415f8
@ -14,7 +14,7 @@ import {
|
||||
import { installApplicationMenu } from './services/menu'
|
||||
import { acquireSingleInstanceLock } from './services/singleInstance'
|
||||
import { installTray, shouldInstallTray, type TrayController } from './services/tray'
|
||||
import { ElectronUpdaterService } from './services/updater'
|
||||
import { ElectronUpdaterService, updaterSessionProxyConfig } from './services/updater'
|
||||
import { createUpdateSmokeUpdaterFromEnv } from './services/updateSmoke'
|
||||
import { ElectronTerminalService, type TerminalSpawnInput } from './services/terminal'
|
||||
import { ElectronPreviewService, type PreviewBounds } from './services/preview'
|
||||
@ -150,14 +150,9 @@ function getUpdaterService() {
|
||||
const smokeUpdater = createUpdateSmokeUpdaterFromEnv(process.env)
|
||||
updaterService ??= new ElectronUpdaterService(smokeUpdater ?? autoUpdater, {
|
||||
async apply(proxy) {
|
||||
const config = proxy
|
||||
? { proxyRules: proxy, proxyBypassRules: '<local>' }
|
||||
: {}
|
||||
await Promise.all([
|
||||
app.setProxy(config),
|
||||
session.defaultSession.setProxy(config),
|
||||
])
|
||||
await session.defaultSession.forceReloadProxyConfig()
|
||||
// Update traffic runs on electron-updater's own session partition;
|
||||
// configuring app/defaultSession proxies never reaches it.
|
||||
await autoUpdater.netSession.setProxy(updaterSessionProxyConfig(proxy))
|
||||
},
|
||||
}, {
|
||||
updateConfigPath: !smokeUpdater && app.isPackaged ? path.join(process.resourcesPath, 'app-update.yml') : undefined,
|
||||
@ -376,7 +371,8 @@ async function createMainWindow() {
|
||||
})
|
||||
|
||||
mainWindow.on('resize', () => {
|
||||
mainWindow?.webContents.send(ELECTRON_EVENT_CHANNELS.windowResized)
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||
mainWindow.webContents.send(ELECTRON_EVENT_CHANNELS.windowResized)
|
||||
})
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
writeWindowSmokeSnapshot(mainWindow, 'did-finish-load')
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ElectronUpdaterService, normalizeUpdateInfo, type ElectronUpdaterLike } from './updater'
|
||||
import { ElectronUpdaterService, normalizeUpdateInfo, updaterSessionProxyConfig, type ElectronUpdaterLike } from './updater'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@ -145,6 +145,22 @@ describe('Electron updater service', () => {
|
||||
expect(localUpdater.checkForUpdates).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('disables differential download so update downloads run at full bandwidth', () => {
|
||||
const localUpdater = fakeUpdater()
|
||||
|
||||
void new ElectronUpdaterService(localUpdater)
|
||||
|
||||
expect(localUpdater.disableDifferentialDownload).toBe(true)
|
||||
})
|
||||
|
||||
it('maps proxy settings to the updater net session proxy config', () => {
|
||||
expect(updaterSessionProxyConfig(null)).toEqual({ mode: 'system' })
|
||||
expect(updaterSessionProxyConfig('http://127.0.0.1:7890')).toEqual({
|
||||
proxyRules: 'http://127.0.0.1:7890',
|
||||
proxyBypassRules: '<local>',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not hide non-metadata updater failures', async () => {
|
||||
const service = new ElectronUpdaterService(updater)
|
||||
updater.checkForUpdates.mockRejectedValueOnce(new Error('feed unavailable'))
|
||||
|
||||
@ -17,6 +17,7 @@ export type ElectronUpdateCheckOptions = {
|
||||
|
||||
export type ElectronUpdaterLike = {
|
||||
autoDownload: boolean
|
||||
disableDifferentialDownload?: boolean
|
||||
logger?: unknown
|
||||
checkForUpdates(): Promise<ElectronUpdateCheckResult>
|
||||
downloadUpdate(): Promise<unknown>
|
||||
@ -38,6 +39,22 @@ export type ElectronUpdaterRuntimeOptions = {
|
||||
updateConfigPath?: string
|
||||
}
|
||||
|
||||
export type UpdaterSessionProxyConfig = {
|
||||
mode?: 'system'
|
||||
proxyRules?: string
|
||||
proxyBypassRules?: string
|
||||
}
|
||||
|
||||
// electron-updater performs all update traffic (metadata + downloads) on its
|
||||
// own session partition, so proxy settings must target that session. Passing
|
||||
// an empty config would mean fixed_servers with no rules (= direct), so the
|
||||
// system fallback has to be an explicit `mode: 'system'`.
|
||||
export function updaterSessionProxyConfig(proxy: string | null): UpdaterSessionProxyConfig {
|
||||
return proxy
|
||||
? { proxyRules: proxy, proxyBypassRules: '<local>' }
|
||||
: { mode: 'system' }
|
||||
}
|
||||
|
||||
export function normalizeUpdateInfo(info: ElectronUpdateInfo | undefined): ElectronUpdateMetadata | null {
|
||||
if (!info?.version) return null
|
||||
const releaseNotes = Array.isArray(info.releaseNotes)
|
||||
@ -86,6 +103,9 @@ export class ElectronUpdaterService {
|
||||
this.proxyController = proxyController
|
||||
this.updateConfigPath = runtimeOptions.updateConfigPath
|
||||
this.updater.autoDownload = false
|
||||
// Differential download issues many small sequential range requests and is
|
||||
// RTT-bound against the GitHub CDN, so it downloads far below line speed.
|
||||
this.updater.disableDifferentialDownload = true
|
||||
this.updater.logger = null
|
||||
}
|
||||
|
||||
|
||||
@ -124,6 +124,7 @@ describe('Electron window service', () => {
|
||||
|
||||
it('does not capture minimized windows', () => {
|
||||
const window = {
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => true,
|
||||
isMaximized: () => false,
|
||||
getBounds: () => ({ x: 0, y: 0, width: 1280, height: 820 }),
|
||||
@ -132,6 +133,20 @@ describe('Electron window service', () => {
|
||||
expect(captureWindowState(window as never)).toBeNull()
|
||||
})
|
||||
|
||||
it('does not capture destroyed windows', () => {
|
||||
const destroyedAccess = () => {
|
||||
throw new TypeError('Object has been destroyed')
|
||||
}
|
||||
const window = {
|
||||
isDestroyed: () => true,
|
||||
isMinimized: destroyedAccess,
|
||||
isMaximized: destroyedAccess,
|
||||
getBounds: destroyedAccess,
|
||||
}
|
||||
|
||||
expect(captureWindowState(window as never)).toBeNull()
|
||||
})
|
||||
|
||||
it('restores persisted bounds and maximized state when reopening the window', () => {
|
||||
const state = { x: 12, y: 34, width: 1400, height: 900, maximized: true }
|
||||
const maximize = vi.fn()
|
||||
@ -260,6 +275,7 @@ describe('Electron window service', () => {
|
||||
hide: vi.fn(),
|
||||
isSimpleFullScreen: () => false,
|
||||
isFullScreen: () => false,
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => false,
|
||||
isMaximized: () => false,
|
||||
getBounds: () => ({ x: 0, y: 0, width: 1280, height: 820 }),
|
||||
@ -398,6 +414,7 @@ describe('Electron window service', () => {
|
||||
handlers.set(event, handler)
|
||||
}),
|
||||
hide: vi.fn(),
|
||||
isDestroyed: () => false,
|
||||
isMinimized: () => false,
|
||||
isMaximized: () => false,
|
||||
getBounds: () => ({ x: 0, y: 0, width: 1280, height: 820 }),
|
||||
@ -416,4 +433,44 @@ describe('Electron window service', () => {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('ignores late move and resize events after the window is destroyed during quit-and-install', () => {
|
||||
const tmp = mkdtempSync(path.join(tmpdir(), 'electron-window-destroyed-events-'))
|
||||
try {
|
||||
const handlers = new Map<string, (...args: never[]) => void>()
|
||||
let destroyed = false
|
||||
const destroyedAccess = () => {
|
||||
if (destroyed) throw new TypeError('Object has been destroyed')
|
||||
return false
|
||||
}
|
||||
const app = fakeApp(tmp)
|
||||
const window = {
|
||||
on: vi.fn((event: string, handler: (...args: never[]) => void) => {
|
||||
handlers.set(event, handler)
|
||||
}),
|
||||
hide: vi.fn(),
|
||||
isDestroyed: () => destroyed,
|
||||
isMinimized: destroyedAccess,
|
||||
isMaximized: destroyedAccess,
|
||||
getBounds: () => {
|
||||
if (destroyed) throw new TypeError('Object has been destroyed')
|
||||
return { x: 0, y: 0, width: 1280, height: 820 }
|
||||
},
|
||||
}
|
||||
|
||||
installWindowLifecycle({
|
||||
app: app as never,
|
||||
window: window as never,
|
||||
shouldQuit: () => true,
|
||||
})
|
||||
|
||||
destroyed = true
|
||||
|
||||
expect(() => handlers.get('move')?.()).not.toThrow()
|
||||
expect(() => handlers.get('resize')?.()).not.toThrow()
|
||||
expect(() => handlers.get('close')?.({ preventDefault: vi.fn() } as never)).not.toThrow()
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -129,6 +129,9 @@ export function writeWindowState(
|
||||
}
|
||||
|
||||
export function captureWindowState(window: BrowserWindow): StoredWindowState | null {
|
||||
// quitAndInstall/quit can emit late move/resize/close events on an already
|
||||
// torn-down native window; touching it then throws "Object has been destroyed".
|
||||
if (window.isDestroyed()) return null
|
||||
if (window.isMinimized()) return null
|
||||
const bounds = window.getBounds()
|
||||
const state = {
|
||||
|
||||
@ -4512,7 +4512,7 @@ function AboutSettings() {
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0 max-w-lg mx-auto flex flex-col items-center py-6">
|
||||
<div className="w-full min-w-0 max-w-2xl mx-auto flex flex-col items-center py-6">
|
||||
{/* Logo + App Name + Version */}
|
||||
<img src={publicAssetPath('app-icon.png')} alt="Claude Code Haha" className="w-20 h-20 mb-4" />
|
||||
<h1 className="text-xl font-bold text-[var(--color-text-primary)]">Claude Code Haha</h1>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user