Initial D-InSAR bundle restore tool

This commit is contained in:
Harmon
2026-05-11 10:43:04 +08:00
commit e595231e04
8 changed files with 2082 additions and 0 deletions
+591
View File
@@ -0,0 +1,591 @@
//go:build windows
package main
import (
"fmt"
"io"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
)
const (
windowTitle = "D-InSAR 数据包恢复工具"
wmDestroy = 0x0002
wmCommand = 0x0111
wmClose = 0x0010
wmSetFont = 0x0030
bnClicked = 0
wsOverlappedWindow = 0x00CF0000
wsVisible = 0x10000000
wsChild = 0x40000000
wsTabStop = 0x00010000
wsBorder = 0x00800000
wsVScroll = 0x00200000
wsHScroll = 0x00100000
esLeft = 0x0000
esMultiline = 0x0004
esAutoVScroll = 0x0040
esAutoHScroll = 0x0080
esReadOnly = 0x0800
bsPushButton = 0x00000000
bsAutoCheckBox = 0x00000003
swShow = 5
cwUseDefault = 0x80000000
gwlUserData = -21
idInputEdit = 1001
idInputBrowse = 1002
idOutputEdit = 1003
idOutputBrowse = 1004
idDryRun = 1005
idOverwrite = 1006
idLimitEdit = 1007
idStart = 1008
idLogEdit = 1009
idCheck = 1010
bmGetCheck = 0x00F0
bstChecked = 1
maxPathBuffer = 4096
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
shell32 = syscall.NewLazyDLL("shell32.dll")
ole32 = syscall.NewLazyDLL("ole32.dll")
procRegisterClassExW = user32.NewProc("RegisterClassExW")
procCreateWindowExW = user32.NewProc("CreateWindowExW")
procDefWindowProcW = user32.NewProc("DefWindowProcW")
procDestroyWindow = user32.NewProc("DestroyWindow")
procDispatchMessageW = user32.NewProc("DispatchMessageW")
procGetMessageW = user32.NewProc("GetMessageW")
procPostQuitMessage = user32.NewProc("PostQuitMessage")
procSendMessageW = user32.NewProc("SendMessageW")
procShowWindow = user32.NewProc("ShowWindow")
procUpdateWindow = user32.NewProc("UpdateWindow")
procSetWindowTextW = user32.NewProc("SetWindowTextW")
procGetWindowTextW = user32.NewProc("GetWindowTextW")
procGetWindowTextLenW = user32.NewProc("GetWindowTextLengthW")
procEnableWindow = user32.NewProc("EnableWindow")
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW")
procMessageBoxW = user32.NewProc("MessageBoxW")
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
procCreateFontW = gdi32.NewProc("CreateFontW")
procSHBrowseForFolderW = shell32.NewProc("SHBrowseForFolderW")
procSHGetPathFromIDListW = shell32.NewProc("SHGetPathFromIDListW")
procCoTaskMemFree = ole32.NewProc("CoTaskMemFree")
procOleInitialize = ole32.NewProc("OleInitialize")
procOleUninitialize = ole32.NewProc("OleUninitialize")
)
type point struct {
x int32
y int32
}
type msg struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt point
}
type wndClassEx struct {
cbSize uint32
style uint32
lpfnWndProc uintptr
cbClsExtra int32
cbWndExtra int32
hInstance uintptr
hIcon uintptr
hCursor uintptr
hbrBackground uintptr
lpszMenuName *uint16
lpszClassName *uint16
hIconSm uintptr
}
type browseInfo struct {
hwndOwner uintptr
pidlRoot uintptr
pszDisplayName *uint16
lpszTitle *uint16
ulFlags uint32
lpfn uintptr
lParam uintptr
iImage int32
}
type guiApp struct {
hwnd uintptr
font uintptr
inputEdit uintptr
outputEdit uintptr
dryRun uintptr
overwrite uintptr
limitEdit uintptr
startButton uintptr
checkButton uintptr
logEdit uintptr
mu sync.Mutex
logText string
running bool
}
func runGUI() error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
procOleInitialize.Call(0)
defer procOleUninitialize.Call()
instance, _, _ := procGetModuleHandleW.Call(0)
className, _ := syscall.UTF16PtrFromString("DinsarRestoreWindow")
title, _ := syscall.UTF16PtrFromString(windowTitle)
wc := wndClassEx{
cbSize: uint32(unsafe.Sizeof(wndClassEx{})),
lpfnWndProc: syscall.NewCallback(windowProc),
hInstance: instance,
hbrBackground: 5 + 1,
lpszClassName: className,
}
if atom, _, err := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 {
return fmt.Errorf("RegisterClassExW failed: %v", err)
}
app := &guiApp{}
hwnd, _, err := procCreateWindowExW.Call(
0,
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(title)),
wsOverlappedWindow|wsVisible,
cwUseDefault,
cwUseDefault,
1040,
720,
0,
0,
instance,
uintptr(unsafe.Pointer(app)),
)
if hwnd == 0 {
return fmt.Errorf("CreateWindowExW failed: %v", err)
}
app.hwnd = hwnd
procShowWindow.Call(hwnd, swShow)
procUpdateWindow.Call(hwnd)
var m msg
for {
ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
if int32(ret) <= 0 {
break
}
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}
return nil
}
func windowProc(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr {
app := appFromWindow(hwnd)
switch msg {
case wmClose:
if app != nil && app.isRunning() {
messageBox(hwnd, "任务正在运行,请等待结束。", windowTitle)
return 0
}
procDestroyWindow.Call(hwnd)
return 0
case wmDestroy:
procPostQuitMessage.Call(0)
return 0
case wmCommand:
id := int(wParam & 0xffff)
notify := int((wParam >> 16) & 0xffff)
if notify == bnClicked && app != nil {
switch id {
case idInputBrowse:
if selected := chooseDirectory(hwnd, "选择 BundleRoot 输入目录"); selected != "" {
setWindowText(app.inputEdit, selected)
}
case idOutputBrowse:
if selected := chooseDirectory(hwnd, "选择 OutputRoot 输出目录"); selected != "" {
setWindowText(app.outputEdit, selected)
}
case idStart:
app.startRestore()
case idCheck:
app.startCheck()
}
}
return 0
case 0x0081: // WM_NCCREATE
cs := (*createStruct)(unsafe.Pointer(lParam))
procSetWindowLongPtrW.Call(hwnd, winIndex(gwlUserData), cs.createParams)
return 1
case 0x0001: // WM_CREATE
app = appFromWindow(hwnd)
if app != nil {
app.hwnd = hwnd
app.createControls()
}
return 0
}
ret, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
return ret
}
type createStruct struct {
createParams uintptr
instance uintptr
menu uintptr
parent uintptr
cy int32
cx int32
y int32
x int32
style int32
name *uint16
class *uint16
exStyle uint32
}
func appFromWindow(hwnd uintptr) *guiApp {
if hwnd == 0 {
return nil
}
ptr, _, _ := procGetWindowLongPtrW.Call(hwnd, winIndex(gwlUserData))
if ptr == 0 {
return nil
}
return (*guiApp)(unsafe.Pointer(ptr))
}
func (a *guiApp) createControls() {
a.font = createFont("Microsoft YaHei UI", 20)
createLabel(a.hwnd, "输入目录 BundleRoot", 24, 28, 210, 30, a.font)
a.inputEdit = createEdit(a.hwnd, "", idInputEdit, 240, 24, 640, 34, false, a.font)
createButton(a.hwnd, "选择...", idInputBrowse, 895, 24, 100, 34, a.font)
createLabel(a.hwnd, "输出目录 OutputRoot", 24, 78, 210, 30, a.font)
a.outputEdit = createEdit(a.hwnd, "", idOutputEdit, 240, 74, 640, 34, false, a.font)
createButton(a.hwnd, "选择...", idOutputBrowse, 895, 74, 100, 34, a.font)
a.dryRun = createCheckBox(a.hwnd, "Dry run 只打印计划", idDryRun, 240, 128, 230, 32, a.font)
a.overwrite = createCheckBox(a.hwnd, "覆盖已有 Task", idOverwrite, 500, 128, 190, 32, a.font)
createLabel(a.hwnd, "Limit", 720, 130, 60, 30, a.font)
a.limitEdit = createEdit(a.hwnd, "", idLimitEdit, 780, 126, 100, 34, false, a.font)
a.checkButton = createButton(a.hwnd, "数据自检", idCheck, 760, 174, 105, 38, a.font)
a.startButton = createButton(a.hwnd, "开始恢复", idStart, 890, 174, 105, 38, a.font)
createLabel(a.hwnd, "运行日志", 24, 226, 140, 30, a.font)
a.logEdit = createEdit(a.hwnd, "", idLogEdit, 24, 262, 970, 365, true, a.font)
a.appendLog("请选择输入目录后可先数据自检;选择输出目录后可开始恢复。\r\n")
}
func (a *guiApp) startRestore() {
if a.isRunning() {
return
}
input := strings.TrimSpace(getWindowText(a.inputEdit))
output := strings.TrimSpace(getWindowText(a.outputEdit))
if input == "" {
messageBox(a.hwnd, "请选择输入目录 BundleRoot。", windowTitle)
return
}
if output == "" {
messageBox(a.hwnd, "请选择输出目录 OutputRoot。", windowTitle)
return
}
limit, ok := a.parseLimit()
if !ok {
return
}
cfg := config{
inputRoot: input,
outputRoot: output,
skipExisting: true,
overwrite: isChecked(a.overwrite),
limit: limit,
dryRun: isChecked(a.dryRun),
logWriter: guiLogWriter{app: a},
}
if cfg.overwrite {
cfg.skipExisting = false
}
a.setRunning(true)
a.setLog("")
a.appendLog("开始执行...\r\n")
go func() {
result, err := run(cfg)
if err != nil {
a.appendLog("错误: " + err.Error() + "\r\n")
messageBox(a.hwnd, "执行失败:\r\n"+err.Error(), windowTitle)
} else {
a.appendLog(fmt.Sprintf("完成: restored=%d skipped=%d failed=%d warnings=%d\r\n",
result.report.Restored,
result.report.Skipped,
result.report.Failed,
len(result.report.Warnings),
))
messageBox(a.hwnd, "执行完成。", windowTitle)
}
a.setRunning(false)
}()
}
func (a *guiApp) startCheck() {
if a.isRunning() {
return
}
input := strings.TrimSpace(getWindowText(a.inputEdit))
if input == "" {
messageBox(a.hwnd, "请选择输入目录 BundleRoot。", windowTitle)
return
}
limit, ok := a.parseLimit()
if !ok {
return
}
cfg := config{
inputRoot: input,
limit: limit,
checkOnly: true,
logWriter: guiLogWriter{app: a},
}
a.setRunning(true)
a.setLog("")
a.appendLog("开始数据自检...\r\n")
go func() {
result, err := runCheck(cfg)
if err != nil {
a.appendLog("错误: " + err.Error() + "\r\n")
messageBox(a.hwnd, "自检失败:\r\n"+err.Error(), windowTitle)
} else if result.report.Failed > 0 {
a.appendLog(fmt.Sprintf("自检完成: total=%d checked=%d valid=%d failed=%d warnings=%d\r\n",
result.report.TotalPairs,
result.report.Checked,
result.report.Valid,
result.report.Failed,
len(result.report.Warnings),
))
messageBox(a.hwnd, fmt.Sprintf("自检完成,但发现 %d 个失败项。请查看日志。", result.report.Failed), windowTitle)
} else {
a.appendLog(fmt.Sprintf("自检通过: total=%d checked=%d valid=%d warnings=%d\r\n",
result.report.TotalPairs,
result.report.Checked,
result.report.Valid,
len(result.report.Warnings),
))
messageBox(a.hwnd, "自检通过。", windowTitle)
}
a.setRunning(false)
}()
}
func (a *guiApp) parseLimit() (int, bool) {
limitText := strings.TrimSpace(getWindowText(a.limitEdit))
if limitText == "" {
return 0, true
}
parsed, err := strconv.Atoi(limitText)
if err != nil || parsed < 0 {
messageBox(a.hwnd, "Limit 必须是大于等于 0 的整数。", windowTitle)
return 0, false
}
return parsed, true
}
func (a *guiApp) isRunning() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.running
}
func (a *guiApp) setRunning(running bool) {
a.mu.Lock()
a.running = running
a.mu.Unlock()
enable := uintptr(1)
if running {
enable = 0
}
procEnableWindow.Call(a.startButton, enable)
procEnableWindow.Call(a.checkButton, enable)
procEnableWindow.Call(a.inputEdit, enable)
procEnableWindow.Call(a.outputEdit, enable)
procEnableWindow.Call(a.dryRun, enable)
procEnableWindow.Call(a.overwrite, enable)
procEnableWindow.Call(a.limitEdit, enable)
}
func (a *guiApp) setLog(text string) {
a.mu.Lock()
a.logText = text
a.mu.Unlock()
setWindowText(a.logEdit, text)
}
func (a *guiApp) appendLog(text string) {
a.mu.Lock()
a.logText += text
fullText := a.logText
a.mu.Unlock()
setWindowText(a.logEdit, fullText)
}
type guiLogWriter struct {
app *guiApp
}
func (w guiLogWriter) Write(p []byte) (int, error) {
if w.app != nil {
w.app.appendLog(strings.ReplaceAll(string(p), "\n", "\r\n"))
}
return len(p), nil
}
func createLabel(parent uintptr, text string, x, y, width, height int32, font uintptr) uintptr {
return createControl("STATIC", text, wsChild|wsVisible, 0, parent, 0, x, y, width, height, font)
}
func createEdit(parent uintptr, text string, id int, x, y, width, height int32, multiline bool, font uintptr) uintptr {
style := uint32(wsChild | wsVisible | wsBorder | wsTabStop | esLeft | esAutoHScroll)
if multiline {
style = wsChild | wsVisible | wsBorder | wsVScroll | wsHScroll | esLeft | esMultiline | esAutoVScroll | esAutoHScroll | esReadOnly
}
return createControl("EDIT", text, style, 0, parent, id, x, y, width, height, font)
}
func createButton(parent uintptr, text string, id int, x, y, width, height int32, font uintptr) uintptr {
return createControl("BUTTON", text, wsChild|wsVisible|wsTabStop|bsPushButton, 0, parent, id, x, y, width, height, font)
}
func createCheckBox(parent uintptr, text string, id int, x, y, width, height int32, font uintptr) uintptr {
return createControl("BUTTON", text, wsChild|wsVisible|wsTabStop|bsAutoCheckBox, 0, parent, id, x, y, width, height, font)
}
func createControl(className string, text string, style uint32, exStyle uint32, parent uintptr, id int, x, y, width, height int32, font uintptr) uintptr {
classPtr, _ := syscall.UTF16PtrFromString(className)
textPtr, _ := syscall.UTF16PtrFromString(text)
hwnd, _, _ := procCreateWindowExW.Call(
uintptr(exStyle),
uintptr(unsafe.Pointer(classPtr)),
uintptr(unsafe.Pointer(textPtr)),
uintptr(style),
uintptr(x),
uintptr(y),
uintptr(width),
uintptr(height),
parent,
uintptr(id),
0,
0,
)
if hwnd != 0 && font != 0 {
procSendMessageW.Call(hwnd, wmSetFont, font, 1)
}
return hwnd
}
func createFont(name string, size int32) uintptr {
namePtr, _ := syscall.UTF16PtrFromString(name)
font, _, _ := procCreateFontW.Call(
uintptr(-size),
0,
0,
0,
400,
0,
0,
0,
1,
0,
0,
0,
0,
uintptr(unsafe.Pointer(namePtr)),
)
return font
}
func setWindowText(hwnd uintptr, text string) {
ptr, _ := syscall.UTF16PtrFromString(text)
procSetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(ptr)))
}
func getWindowText(hwnd uintptr) string {
length, _, _ := procGetWindowTextLenW.Call(hwnd)
buffer := make([]uint16, int(length)+1)
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)))
return syscall.UTF16ToString(buffer)
}
func isChecked(hwnd uintptr) bool {
ret, _, _ := procSendMessageW.Call(hwnd, bmGetCheck, 0, 0)
return ret == bstChecked
}
func messageBox(hwnd uintptr, text string, title string) {
textPtr, _ := syscall.UTF16PtrFromString(text)
titlePtr, _ := syscall.UTF16PtrFromString(title)
procMessageBoxW.Call(hwnd, uintptr(unsafe.Pointer(textPtr)), uintptr(unsafe.Pointer(titlePtr)), 0)
}
func chooseDirectory(hwnd uintptr, title string) string {
titlePtr, _ := syscall.UTF16PtrFromString(title)
displayName := make([]uint16, maxPathBuffer)
bi := browseInfo{
hwndOwner: hwnd,
pszDisplayName: &displayName[0],
lpszTitle: titlePtr,
ulFlags: 0x00000001 | 0x00000010 | 0x00000040,
}
pidl, _, _ := procSHBrowseForFolderW.Call(uintptr(unsafe.Pointer(&bi)))
if pidl == 0 {
return ""
}
defer procCoTaskMemFree.Call(pidl)
pathBuffer := make([]uint16, maxPathBuffer)
ret, _, _ := procSHGetPathFromIDListW.Call(pidl, uintptr(unsafe.Pointer(&pathBuffer[0])))
if ret == 0 {
return ""
}
return syscall.UTF16ToString(pathBuffer)
}
func winIndex(value int32) uintptr {
return uintptr(value)
}
var _ io.Writer = guiLogWriter{}