mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 17:00:21 +08:00
🐛 fix(windows): 修复在线更新挂起与 WebView2 启动闪退
- 隐藏并释放 Windows 更新脚本进程,避免在线更新打开 cmd 并挂起 - 为更新脚本等待宿主进程退出增加超时保护 - 收窄自动 WebView2 zoom reset 触发条件并补充异常兜底 - 补充 Windows 更新启动与窗口缩放回归测试 Refs #468
This commit is contained in:
@@ -72,7 +72,7 @@ import {
|
||||
splitConflictsByContext,
|
||||
type ConflictInfo,
|
||||
} from './utils/shortcuts';
|
||||
import { resolveTitleBarToggleIconKey, resolveWindowsScaleCheckDelayMs, shouldApplyWindowsScaleFix, shouldToggleMaximisedWindowForScaleFix, type WindowScaleFixReason, type WindowsScaleCheckTrigger } from './utils/windowStateUi';
|
||||
import { resolveTitleBarToggleIconKey, resolveWindowsScaleCheckDelayMs, shouldApplyWindowsScaleFix, shouldResetWebViewZoomForScaleFix, shouldToggleMaximisedWindowForScaleFix, type WindowScaleFixReason, type WindowsScaleCheckTrigger } from './utils/windowStateUi';
|
||||
import { resolveVisibleStartupWindowBounds } from './utils/windowRestoreBounds';
|
||||
import {
|
||||
SIDEBAR_UTILITY_ITEM_KEYS,
|
||||
@@ -676,7 +676,7 @@ function App() {
|
||||
// 让 WebView2 重算 D2D/DirectWrite 字体度量。完全不动窗口、零动画。
|
||||
// backend 失败(wails 升级破坏反射 / 非 Windows)时回退到 dispatch resize 兜底;
|
||||
// 用户仍可按 Ctrl+Shift+0 手动 toggle 修复。
|
||||
if (hasViewportScaleDrift) {
|
||||
if (shouldResetWebViewZoomForScaleFix(reason, hasViewportScaleDrift)) {
|
||||
try {
|
||||
const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.();
|
||||
if (!res?.success) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resolveTitleBarToggleIconKey,
|
||||
resolveWindowsScaleCheckDelayMs,
|
||||
shouldApplyWindowsScaleFix,
|
||||
shouldResetWebViewZoomForScaleFix,
|
||||
shouldToggleMaximisedWindowForScaleFix,
|
||||
} from './windowStateUi';
|
||||
|
||||
@@ -26,6 +27,13 @@ describe('windowStateUi', () => {
|
||||
expect(shouldToggleMaximisedWindowForScaleFix('restore', true)).toBe(false);
|
||||
});
|
||||
|
||||
it('only calls the backend WebView2 zoom reset after a real restore drift', () => {
|
||||
expect(shouldResetWebViewZoomForScaleFix('restore', true)).toBe(true);
|
||||
expect(shouldResetWebViewZoomForScaleFix('restore', false)).toBe(false);
|
||||
expect(shouldResetWebViewZoomForScaleFix('activation', true)).toBe(false);
|
||||
expect(shouldResetWebViewZoomForScaleFix('ratio-change', true)).toBe(false);
|
||||
});
|
||||
|
||||
it('debounces resize-triggered Windows scale checks until window transitions settle', () => {
|
||||
expect(resolveWindowsScaleCheckDelayMs('resize')).toBeGreaterThan(0);
|
||||
expect(resolveWindowsScaleCheckDelayMs('focus')).toBe(0);
|
||||
|
||||
@@ -20,6 +20,11 @@ export const shouldToggleMaximisedWindowForScaleFix = (
|
||||
hasViewportScaleDrift: boolean,
|
||||
): boolean => reason === 'ratio-change' && hasViewportScaleDrift;
|
||||
|
||||
export const shouldResetWebViewZoomForScaleFix = (
|
||||
reason: WindowScaleFixReason,
|
||||
hasViewportScaleDrift: boolean,
|
||||
): boolean => reason === 'restore' && hasViewportScaleDrift;
|
||||
|
||||
export const resolveWindowsScaleCheckDelayMs = (trigger: WindowsScaleCheckTrigger): number =>
|
||||
trigger === 'resize' ? 240 : 0;
|
||||
|
||||
|
||||
@@ -136,7 +136,16 @@ func (a *App) SetMacNativeWindowControls(enabled bool) {
|
||||
// ResetWebViewZoom 把 WebView2 zoom factor 强制重置为 1.0,让 WebView2 重算字体度量。
|
||||
// 用于 Windows 任务栏恢复后字体异常变大的"零感知"修复:不动窗口、零动画。
|
||||
// 仅 Windows 上生效,其他平台返回错误(前端按需忽略)。
|
||||
func (a *App) ResetWebViewZoom() connection.QueryResult {
|
||||
func (a *App) ResetWebViewZoom() (result connection.QueryResult) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
logger.Errorf("重置 WebView2 zoom 失败:%v", recovered)
|
||||
result = connection.QueryResult{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("重置 WebView2 zoom 失败:%v", recovered),
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := resetWebViewZoomFactor(a.ctx, 1.0); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -947,7 +947,15 @@ func launchWindowsUpdate(staged *stagedUpdate, targetExe string, pid int) error
|
||||
|
||||
logger.Infof("启动 Windows 更新脚本:target=%s script=%s log=%s", targetExe, scriptPath, logPath)
|
||||
cmd := buildWindowsLaunchCommand(scriptPath)
|
||||
return cmd.Start()
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.Process != nil {
|
||||
if err := cmd.Process.Release(); err != nil {
|
||||
logger.Warnf("释放 Windows 更新脚本进程句柄失败:%v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func launchMacUpdate(staged *stagedUpdate, targetExe string, pid int) error {
|
||||
@@ -993,6 +1001,7 @@ set "TARGET_OLD=%TARGET%.old"
|
||||
set "STAGED=__GONAVI_UPDATE_STAGED__"
|
||||
set "LOG_FILE=__GONAVI_UPDATE_LOG__"
|
||||
set PID=__GONAVI_UPDATE_PID__
|
||||
set /a WAIT_PID_SECONDS=0
|
||||
|
||||
call :log updater started
|
||||
if not exist "%SOURCE%" (
|
||||
@@ -1035,7 +1044,12 @@ if /I "%SOURCE_EXT%"==".zip" (
|
||||
:waitloop
|
||||
tasklist /FI "PID eq %PID%" | find "%PID%" >nul
|
||||
if %ERRORLEVEL%==0 (
|
||||
if !WAIT_PID_SECONDS! GEQ 90 (
|
||||
call :log host process still running after !WAIT_PID_SECONDS! seconds, aborting update
|
||||
exit /b 1
|
||||
)
|
||||
timeout /t 1 /nobreak >nul
|
||||
set /a WAIT_PID_SECONDS+=1
|
||||
goto waitloop
|
||||
)
|
||||
call :log host process exited
|
||||
@@ -1108,7 +1122,9 @@ exit /b 0
|
||||
}
|
||||
|
||||
func buildWindowsLaunchCommand(scriptPath string) *exec.Cmd {
|
||||
return exec.Command("cmd.exe", "/D", "/C", "call", scriptPath)
|
||||
cmd := exec.Command("cmd.exe", "/D", "/C", "call", scriptPath)
|
||||
configureWindowsUpdateCommand(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func buildMacScript(dmgPath, targetApp, stagedDir, mountDir, logPath string, pid int) string {
|
||||
|
||||
19
internal/app/methods_update_windows_process_test.go
Normal file
19
internal/app/methods_update_windows_process_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build windows
|
||||
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildWindowsLaunchCommandHidesConsoleWindow(t *testing.T) {
|
||||
cmd := buildWindowsLaunchCommand(`C:\tmp\gonavi-update\update.cmd`)
|
||||
|
||||
if cmd.SysProcAttr == nil {
|
||||
t.Fatalf("expected Windows update launcher to configure SysProcAttr")
|
||||
}
|
||||
if !cmd.SysProcAttr.HideWindow {
|
||||
t.Fatalf("expected Windows update launcher to hide the console window")
|
||||
}
|
||||
if cmd.SysProcAttr.CreationFlags&windowsCreateNoWindow == 0 {
|
||||
t.Fatalf("expected Windows update launcher to set CREATE_NO_WINDOW, flags=%#x", cmd.SysProcAttr.CreationFlags)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ func TestBuildWindowsScriptWin10Fixes(t *testing.T) {
|
||||
{"exponential backoff tier 2", `if !RETRY! GEQ 6 set /a WAIT=3`},
|
||||
{"exponential backoff tier 3", `if !RETRY! GEQ 9 set /a WAIT=5`},
|
||||
{"retry limit 15", `if !RETRY! LSS 15`},
|
||||
{"host exit wait timeout", `if !WAIT_PID_SECONDS! GEQ 90 (`},
|
||||
{"cleanup old file", `del /F /Q "%TARGET_OLD%"`},
|
||||
}
|
||||
for _, fix := range win10Fixes {
|
||||
@@ -110,7 +111,7 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWindowsLaunchCommandUsesDirectCall(t *testing.T) {
|
||||
func TestBuildWindowsLaunchCommandUsesDirectHiddenCall(t *testing.T) {
|
||||
cmd := buildWindowsLaunchCommand(`C:\tmp\gonavi-update\update.cmd`)
|
||||
|
||||
if !strings.EqualFold(cmd.Args[0], cmd.Path) && !strings.HasSuffix(strings.ToLower(cmd.Path), `\cmd.exe`) {
|
||||
|
||||
@@ -26,7 +26,12 @@ import (
|
||||
//
|
||||
// **依赖 wails v2.11/v2.12 内部实现细节**:如果 wails 升级改名了 frontend.chromium 字段或
|
||||
// edge.Chromium.PutZoomFactor 方法名,此函数会返回 error。CI 中应该有跨版本兼容性测试。
|
||||
func resetWebViewZoomFactor(ctx context.Context, factor float64) error {
|
||||
func resetWebViewZoomFactor(ctx context.Context, factor float64) (err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = fmt.Errorf("reset WebView2 zoom panic: %v", recovered)
|
||||
}
|
||||
}()
|
||||
if ctx == nil {
|
||||
return fmt.Errorf("ctx is nil")
|
||||
}
|
||||
|
||||
@@ -28,6 +28,16 @@ type fakeFrontend struct {
|
||||
chromium *fakeChromium
|
||||
}
|
||||
|
||||
type panicChromium struct{}
|
||||
|
||||
func (p *panicChromium) PutZoomFactor(float64) {
|
||||
panic("webview2 zoom reset failed")
|
||||
}
|
||||
|
||||
type panicFrontend struct {
|
||||
chromium *panicChromium
|
||||
}
|
||||
|
||||
// 测试必须用 wails 一致的 string key "frontend" 作为 context.WithValue 的 key,
|
||||
// 否则反射拿不到。go vet 会警告 string key,用本地 stringContextKey 帮助函数封装来抑制。
|
||||
// 这层封装等价于直接传字符串字面量,行为完全一致。
|
||||
@@ -85,3 +95,15 @@ func TestResetWebViewZoomFactorErrorsWhenFrontendMissing(t *testing.T) {
|
||||
t.Fatalf("expected error to mention frontend, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetWebViewZoomFactorRecoversFromPutZoomFactorPanic(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), stringContextKey("frontend"), &panicFrontend{chromium: &panicChromium{}})
|
||||
|
||||
err := resetWebViewZoomFactor(ctx, 1.0)
|
||||
if err == nil {
|
||||
t.Fatal("expected panic to be converted to error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "panic") {
|
||||
t.Fatalf("expected error to mention panic, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
7
internal/app/windows_update_process_other.go
Normal file
7
internal/app/windows_update_process_other.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package app
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func configureWindowsUpdateCommand(_ *exec.Cmd) {}
|
||||
20
internal/app/windows_update_process_windows.go
Normal file
20
internal/app/windows_update_process_windows.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const windowsCreateNoWindow = 0x08000000
|
||||
|
||||
func configureWindowsUpdateCommand(cmd *exec.Cmd) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: windowsCreateNoWindow,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user