粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
172 lines
5.6 KiB
Go
172 lines
5.6 KiB
Go
//go:build windows
|
||
|
||
package main
|
||
|
||
import (
|
||
"strings"
|
||
"sync/atomic"
|
||
"syscall"
|
||
"time"
|
||
"unsafe"
|
||
|
||
"golang.org/x/sys/windows"
|
||
"u-desk/internal/common"
|
||
)
|
||
|
||
// Chrome_WidgetWin 系列类名是 WebView2 的宿主窗口层级
|
||
const webviewHostClassFragment = "Chrome_WidgetWin"
|
||
|
||
// wmSize Win32 窗口尺寸变化消息
|
||
const wmSizeMsg uint32 = 0x0005
|
||
|
||
var (
|
||
// user32 对这三个 API 只导出无后缀名(无 A/W 变体,带 W 后缀找不到 proc 会 panic)
|
||
procGetClientRectFix = windows.NewLazySystemDLL("user32.dll").NewProc("GetClientRect")
|
||
procSetWindowPosFix = windows.NewLazySystemDLL("user32.dll").NewProc("SetWindowPos")
|
||
procGetAncestorFix = windows.NewLazySystemDLL("user32.dll").NewProc("GetAncestor")
|
||
)
|
||
|
||
// 修复去重:同一时刻只允许一个校验在跑(回调写共享切片要求互斥)
|
||
var webviewFixRunning atomic.Bool
|
||
|
||
// WM_SIZE 去重:脏标记 + 单 worker(每条 WM_SIZE 只置位,不再批量 spawn goroutine)
|
||
var (
|
||
webviewSizeDirty atomic.Bool
|
||
webviewSizeWorkerActive atomic.Bool
|
||
)
|
||
|
||
// 当前校验的目标主窗口(枚举回调过滤直属子窗口用)
|
||
var webviewTargetHwnd uintptr
|
||
|
||
// webviewHosts 收集结果容器;enumChildWebviewHosts 回调写入
|
||
var webviewHosts []windows.HWND
|
||
|
||
// 包级变量:syscall.NewCallback 指向的 Go 闭包必须存活于整个进程生命周期
|
||
var enumChildWebviewHosts = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr {
|
||
// GA_PARENT(1):只收直属子窗口,避免动到更深层的 WebView2 内部窗口
|
||
parent, _, _ := procGetAncestorFix.Call(h, 1)
|
||
if parent != webviewTargetHwnd {
|
||
return 1
|
||
}
|
||
var buf [256]uint16
|
||
if _, err := windows.GetClassName(windows.HWND(h), &buf[0], int32(len(buf))); err != nil {
|
||
return 1
|
||
}
|
||
if strings.Contains(syscall.UTF16ToString(buf[:]), webviewHostClassFragment) {
|
||
webviewHosts = append(webviewHosts, windows.HWND(h))
|
||
}
|
||
return 1
|
||
})
|
||
|
||
// checkAndFixFill 校验 WebView2 宿主是否铺满窗口客户区,未铺满则强制铺满。
|
||
// wails alpha 的 WM_SIZE 处理存在竞态:主线程忙时消息被 resizePending 标志丢弃,
|
||
// 迟到执行的重铺又使用闭包捕获的过期 bounds,webview 会停在中间尺寸且不再有
|
||
// 新消息来纠正,表现为整个应用内容收窄、四周露出窗口底色。
|
||
func checkAndFixFill(hwnd uintptr) {
|
||
if hwnd == 0 {
|
||
return
|
||
}
|
||
var rc struct{ Left, Top, Right, Bottom int32 }
|
||
if r, _, _ := procGetClientRectFix.Call(hwnd, uintptr(unsafe.Pointer(&rc))); r == 0 {
|
||
return
|
||
}
|
||
cw, ch := int(rc.Right-rc.Left), int(rc.Bottom-rc.Top)
|
||
if cw <= 0 || ch <= 0 {
|
||
return
|
||
}
|
||
|
||
webviewTargetHwnd = hwnd
|
||
webviewHosts = webviewHosts[:0]
|
||
windows.EnumChildWindows(windows.HWND(hwnd), enumChildWebviewHosts, nil)
|
||
for _, h := range webviewHosts {
|
||
// SWP_NOZORDER(0x0004):只改尺寸位置,不干预 wails 管理的层级
|
||
procSetWindowPosFix.Call(uintptr(h), 0, 0, 0, uintptr(cw), uintptr(ch), 0x0004)
|
||
}
|
||
}
|
||
|
||
// runGuarded 同步调用点的 panic 兜底:panic 取证落盘后不外抛,
|
||
// 让调用方(如周期循环)能继续跑下一轮
|
||
func runGuarded(fn func()) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
common.AppendPanicLog("webview-fix", r)
|
||
}
|
||
}()
|
||
fn()
|
||
}
|
||
|
||
// fixWebviewFill WM_SIZE 路径:等 resize 序列结束、wails 内部去抖落定后校验一次。
|
||
// WM_SIZE 只置脏标记;活跃期间的后续 WM_SIZE 合并进去(worker 循环消费),
|
||
// 拖拽缩放的风暴从"每条消息一个 goroutine"降为全程单 worker
|
||
func fixWebviewFill(hwnd uintptr) {
|
||
webviewTargetHwnd = hwnd
|
||
webviewSizeDirty.Store(true)
|
||
if !webviewSizeWorkerActive.CompareAndSwap(false, true) {
|
||
return
|
||
}
|
||
common.Go("webview-fix", func() {
|
||
defer webviewSizeWorkerActive.Store(false)
|
||
for webviewSizeDirty.CompareAndSwap(true, false) {
|
||
time.Sleep(150 * time.Millisecond)
|
||
if webviewFixRunning.CompareAndSwap(false, true) {
|
||
runGuarded(func() { checkAndFixFill(webviewTargetHwnd) })
|
||
webviewFixRunning.Store(false)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// stopWebviewWatch 周期兜底循环的停止信号;StartWebviewFillWatch 重入时先关旧的(重启语义)
|
||
var stopWebviewWatch chan struct{}
|
||
|
||
// StartWebviewFillWatch 周期兜底:覆盖"无新 WM_SIZE 的迟到重放"场景
|
||
// (主线程忙时排队的过期重绘在窗口静止时才执行,此时不会产生任何 WM_SIZE)
|
||
func StartWebviewFillWatch() {
|
||
if stopWebviewWatch != nil {
|
||
select {
|
||
case <-stopWebviewWatch: // 旧循环已自行退出
|
||
default:
|
||
close(stopWebviewWatch)
|
||
}
|
||
}
|
||
stop := make(chan struct{})
|
||
stopWebviewWatch = stop
|
||
common.Go("webview-watch", func() {
|
||
for {
|
||
select {
|
||
case <-stop:
|
||
return
|
||
case <-time.After(3 * time.Second):
|
||
}
|
||
if webviewFixRunning.CompareAndSwap(false, true) {
|
||
runGuarded(func() {
|
||
checkAndFixFill(findMainWindowHwnd())
|
||
})
|
||
webviewFixRunning.Store(false)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// findMainWindowHwnd 找本进程的可见主窗口
|
||
// 回调必须进程级复用(同 enumChildWebviewHosts):runtime 的 Windows callback
|
||
// 硬上限 2000 个且按闭包指针去重,每 3 秒新建一个约 100 分钟即耗尽,
|
||
// 触发 runtime throw("too many callback functions") 直接静默退出(无法 recover)
|
||
var mainWindowHwndResult uintptr
|
||
|
||
var enumMainWindowHwnd = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr {
|
||
var winPid uint32
|
||
windows.GetWindowThreadProcessId(windows.HWND(h), &winPid)
|
||
if winPid == windows.GetCurrentProcessId() && windows.IsWindowVisible(windows.HWND(h)) {
|
||
mainWindowHwndResult = h
|
||
return 0
|
||
}
|
||
return 1
|
||
})
|
||
|
||
func findMainWindowHwnd() uintptr {
|
||
mainWindowHwndResult = 0
|
||
windows.EnumWindows(enumMainWindowHwnd, nil)
|
||
return mainWindowHwndResult
|
||
}
|