Files
MyGoNavi/internal/app/window_zoom_windows.go
Syngnat e3515b9eb2 🐛 fix(windows): 修复闪退与驱动代理安装失败
- 修复 WebView2 zoom factor 跨线程调用风险,切回窗口线程执行并增加 recover 与超时保护
- 完善 Redis 命令结果 JSON-safe 兜底,避免复杂返回值格式化触发程序崩溃
- 调整 Windows driver-agent 校验逻辑,仅读取 PE Machine 字段判断架构兼容性
- 避免 COFF string table EOF 被误判为无效 Windows 可执行文件,修复驱动在线安装和本地导入失败
- 补充窗口缩放、Redis 返回值和驱动代理 PE 校验回归测试
2026-05-18 10:28:18 +08:00

151 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build windows
package app
import (
"context"
"fmt"
"reflect"
"time"
"unsafe"
)
const resetWebViewZoomInvokeTimeout = 2 * time.Second
// resetWebViewZoomFactor 通过 WebView2 ICoreWebView2Controller::put_ZoomFactor 把 WebView2
// 内部 zoom factor 重置为 1.0。这是 Windows 任务栏恢复后字体度量异常变大的根因解:
// 字体度量缓存在 WebView2 D2D/DirectWrite 层Chromium layout invalidationCSS zoom hack
// 改不了它,必须调 WebView2 COM API。
//
// 实现路径:
// 1. Wails 在 ctx 里以 key "frontend" 注入了 *desktop/windows.Frontend
// 2. Frontend.chromium 是 unexported 字段 *edge.Chromium
// 3. Frontend.mainWindow 是 unexported 字段 *windows.Window可用 Invoke 切回窗口线程
// 4. Chromium.PutZoomFactor(float64) 是 exported 方法(封装了 controller.put_ZoomFactor
//
// 用反射 + unsafe.Pointer 解锁 unexported 字段后,通过 mainWindow.Invoke 调 PutZoomFactor。
// 不需要 import wails 内部包,也不需要 fork wails。
//
// 失败时返回错误(不 panic让调用方决定是否回退到 toggle 路径。
//
// **依赖 wails v2.11/v2.12 内部实现细节**:如果 wails 升级改名了 frontend.chromium 字段或
// edge.Chromium.PutZoomFactor 方法名,此函数会返回 error。CI 中应该有跨版本兼容性测试。
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")
}
frontendValue, err := resolveWailsFrontendValue(ctx)
if err != nil {
return err
}
chromiumValue, err := accessibleWailsFrontendField(frontendValue, "chromium")
if err != nil {
return err
}
mainWindowValue, err := accessibleWailsFrontendField(frontendValue, "mainWindow")
if err != nil {
return err
}
putZoomFactor := chromiumValue.MethodByName("PutZoomFactor")
if !putZoomFactor.IsValid() {
return fmt.Errorf("PutZoomFactor method not found on chromium (go-webview2 version may have changed)")
}
if putZoomFactor.Type().NumIn() != 1 || putZoomFactor.Type().In(0).Kind() != reflect.Float64 || putZoomFactor.Type().NumOut() != 0 {
return fmt.Errorf("PutZoomFactor signature changed: expected func(float64), got %v", putZoomFactor.Type())
}
invoke := mainWindowValue.MethodByName("Invoke")
if !invoke.IsValid() {
return fmt.Errorf("mainWindow.Invoke method not found (wails version may have changed)")
}
if invoke.Type().NumIn() != 1 || invoke.Type().In(0).Kind() != reflect.Func || invoke.Type().In(0).NumIn() != 0 || invoke.Type().In(0).NumOut() != 0 || invoke.Type().NumOut() != 0 {
return fmt.Errorf("mainWindow.Invoke signature changed: expected func(func()), got %v", invoke.Type())
}
done := make(chan error, 1)
if err := safeCallInvoke(invoke, func() {
done <- safeCallPutZoomFactor(putZoomFactor, factor)
}); err != nil {
return err
}
select {
case err := <-done:
return err
case <-time.After(resetWebViewZoomInvokeTimeout):
return fmt.Errorf("timed out waiting for mainWindow.Invoke to reset WebView2 zoom factor")
}
}
func resolveWailsFrontendValue(ctx context.Context) (reflect.Value, error) {
frontendIface := ctx.Value("frontend")
if frontendIface == nil {
return reflect.Value{}, fmt.Errorf("wails frontend not found in ctx (key=\"frontend\")")
}
frontendValue := reflect.ValueOf(frontendIface)
if frontendValue.Kind() == reflect.Ptr {
if frontendValue.IsNil() {
return reflect.Value{}, fmt.Errorf("wails frontend is nil")
}
frontendValue = frontendValue.Elem()
}
if !frontendValue.IsValid() || frontendValue.Kind() != reflect.Struct {
return reflect.Value{}, fmt.Errorf("wails frontend has unexpected kind %v", frontendValue.Kind())
}
if !frontendValue.CanAddr() {
return reflect.Value{}, fmt.Errorf("wails frontend is not addressable")
}
return frontendValue, nil
}
func accessibleWailsFrontendField(frontendValue reflect.Value, fieldName string) (reflect.Value, error) {
field := frontendValue.FieldByName(fieldName)
if !field.IsValid() {
return reflect.Value{}, fmt.Errorf("wails Frontend.%s field not found (wails version may have changed)", fieldName)
}
if !field.CanAddr() {
return reflect.Value{}, fmt.Errorf("wails Frontend.%s field is not addressable", fieldName)
}
if isNilReflectValue(field) {
return reflect.Value{}, fmt.Errorf("wails Frontend.%s is nil (WebView2 not yet initialised)", fieldName)
}
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem(), nil
}
func isNilReflectValue(value reflect.Value) bool {
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return value.IsNil()
default:
return false
}
}
func safeCallInvoke(invoke reflect.Value, fn func()) (err error) {
defer func() {
if value := recover(); value != nil {
err = fmt.Errorf("mainWindow.Invoke panicked while resetting WebView2 zoom factor: %v", value)
}
}()
invoke.Call([]reflect.Value{reflect.ValueOf(fn)})
return nil
}
func safeCallPutZoomFactor(putZoomFactor reflect.Value, factor float64) (err error) {
defer func() {
if value := recover(); value != nil {
err = fmt.Errorf("PutZoomFactor panicked while resetting WebView2 zoom factor: %v", value)
}
}()
putZoomFactor.Call([]reflect.Value{reflect.ValueOf(factor)})
return nil
}