feat(dll): 自 go-hua 迁入 Windows 系统 API 封装
- 互斥体防多开/ShellExecute/窗口类名与标题查询/顶层窗口目录, 新增依赖 lxn/win - 仅 Windows, 附测试、examples/dll 例程与包 README
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# dll
|
||||||
|
|
||||||
|
Windows 系统 API 封装(基于 `lxn/win`):互斥体防多开、`ShellExecute` 拉起程序、
|
||||||
|
窗口类名/标题查询、顶层窗口路径定位。
|
||||||
|
|
||||||
|
> 迁移自 go-hua/dll,代码保持原样。**仅 Windows 可用**(依赖 user32/kernel32)。
|
||||||
|
|
||||||
|
## 用法
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.zeroonesoft.cn/golib/zogo/dll"
|
||||||
|
|
||||||
|
// 互斥体防多开
|
||||||
|
h, err := dll.CreateMutex(false, "Global\\MyAppMutex") // 已存在则 err
|
||||||
|
defer dll.ReleaseMutex(h)
|
||||||
|
|
||||||
|
// 拉起外部程序(hide=true 隐藏窗口)
|
||||||
|
ok := dll.ShellExecute("", "notepad.exe", "C:\\a.txt", false)
|
||||||
|
|
||||||
|
// 窗口查询
|
||||||
|
title, _ := dll.GetWindowTextString(hwnd)
|
||||||
|
cls, _ := dll.GetWindowClassString(hwnd)
|
||||||
|
hwnd2 := dll.GetWindowFromClassName(0, "Chrome_WidgetWin_1")
|
||||||
|
|
||||||
|
// 顶层窗口(主程序)所在目录
|
||||||
|
dir, err := dll.GetTopFolderPath()
|
||||||
|
```
|
||||||
|
|
||||||
|
完整可运行例程:[examples/dll/main.go](../examples/dll/main.go)
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 构建目标必须含 Windows(`GOOS=windows`);其他平台编译请将该包的使用隔离在
|
||||||
|
带 build tag 的文件里。
|
||||||
|
- `CreateMutex` 的 err 判断用 `windows.ERROR_ALREADY_EXISTS` 语义,见测试用例。
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package dll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateMutex(t *testing.T) {
|
||||||
|
name := fmt.Sprintf("go-hua-test-mutex-%d", syscall.Getpid())
|
||||||
|
|
||||||
|
handle, err := CreateMutex(true, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMutex: %v", err)
|
||||||
|
}
|
||||||
|
if handle == 0 {
|
||||||
|
t.Fatal("CreateMutex 句柄不应为 0")
|
||||||
|
}
|
||||||
|
syscall.CloseHandle(handle)
|
||||||
|
|
||||||
|
// 重复创建同名互斥体(不持有)应成功且不报错
|
||||||
|
handle2, err := CreateMutex(false, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("重复 CreateMutex 不应报错: %v", err)
|
||||||
|
}
|
||||||
|
if handle2 != 0 {
|
||||||
|
syscall.CloseHandle(handle2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleaseMutex(t *testing.T) {
|
||||||
|
pid := syscall.Getpid()
|
||||||
|
|
||||||
|
// 创建并持有 → 释放应成功(ReleaseMutex 传句柄值)
|
||||||
|
handle, err := CreateMutex(true, fmt.Sprintf("go-hua-release-mutex-%d", pid))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMutex: %v", err)
|
||||||
|
}
|
||||||
|
defer syscall.CloseHandle(handle)
|
||||||
|
|
||||||
|
if !ReleaseMutex(handle) {
|
||||||
|
t.Error("持有者 ReleaseMutex 应返回 true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 另一个未持有的互斥体 → 释放应失败
|
||||||
|
// 注意:同名互斥体重复创建会返回 183(Windows 正常行为),故使用不同名称
|
||||||
|
handle2, err := CreateMutex(false, fmt.Sprintf("go-hua-release-mutex2-%d", pid))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMutex: %v", err)
|
||||||
|
}
|
||||||
|
defer syscall.CloseHandle(handle2)
|
||||||
|
if ReleaseMutex(handle2) {
|
||||||
|
t.Error("未持有者 ReleaseMutex 应返回 false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleaseMutexInvalidHandle(t *testing.T) {
|
||||||
|
// 无效句柄释放应失败且不崩溃
|
||||||
|
if ReleaseMutex(0) {
|
||||||
|
t.Error("ReleaseMutex(0) 应返回 false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenMutexNonExist(t *testing.T) {
|
||||||
|
_, err := OpenMutex(false, "go-hua-not-exist-mutex-xyz")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("打开不存在的互斥体应返回错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Package dll 封装 Windows 系统 API(互斥体防多开、ShellExecute、窗口操作等)。
|
||||||
|
package dll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lxn/win"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShellExecute 调用系统 ShellExecute 打开文件/网页/程序
|
||||||
|
// dir 工作目录;file 目标(文件路径、URL 或程序);args 启动参数;hide 是否隐藏窗口
|
||||||
|
func ShellExecute(dir string, file string, args string, hide bool) bool {
|
||||||
|
lpOperation, _ := syscall.UTF16PtrFromString("open")
|
||||||
|
lpFile, _ := syscall.UTF16PtrFromString(file)
|
||||||
|
lpCwd, _ := syscall.UTF16PtrFromString(dir)
|
||||||
|
lpArgs, _ := syscall.UTF16PtrFromString(args)
|
||||||
|
showCmd := win.SW_SHOWNORMAL
|
||||||
|
if hide {
|
||||||
|
showCmd = win.SW_HIDE
|
||||||
|
}
|
||||||
|
return win.ShellExecute(0, lpOperation, lpFile, lpArgs, lpCwd, showCmd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package dll
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
// kernel32.dll 互斥体相关 API 函数指针
|
||||||
|
var (
|
||||||
|
kernel32Dll *syscall.DLL
|
||||||
|
procCreateMutexW *syscall.Proc
|
||||||
|
procOpenMutexW *syscall.Proc
|
||||||
|
procReleaseMutex *syscall.Proc
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
kernel32Dll = syscall.MustLoadDLL("kernel32.dll")
|
||||||
|
procCreateMutexW = kernel32Dll.MustFindProc("CreateMutexW")
|
||||||
|
procOpenMutexW = kernel32Dll.MustFindProc("OpenMutexW")
|
||||||
|
procReleaseMutex = kernel32Dll.MustFindProc("ReleaseMutex")
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package dll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/lxn/win"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateMutex 创建命名互斥体(常用于防止程序多开)
|
||||||
|
// bInitialOwner 为是否立即持有;互斥体已存在时返回错误(错误码 183)
|
||||||
|
func CreateMutex(bInitialOwner bool, mutexName string) (syscall.Handle, error) {
|
||||||
|
|
||||||
|
// 互斥体名需转为 UTF-16 指针(LPCWSTR),不能直接传 Go string 的地址
|
||||||
|
namePtr, nameErr := syscall.UTF16PtrFromString(mutexName)
|
||||||
|
if nameErr != nil {
|
||||||
|
return 0, nameErr
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, _, err := syscall.SyscallN(procCreateMutexW.Addr(), 0, uintptr(win.BoolToBOOL(bInitialOwner)), uintptr(unsafe.Pointer(namePtr)))
|
||||||
|
if err != 0 {
|
||||||
|
println(err.Error())
|
||||||
|
|
||||||
|
lastError := win.GetLastError()
|
||||||
|
fmt.Println("lastError", lastError)
|
||||||
|
if lastError == 183 {
|
||||||
|
fmt.Println("Mutex 已经存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
//defer syscall.CloseHandle(syscall.Handle(m))
|
||||||
|
if syscall.Handle(handle) != 0 {
|
||||||
|
lastError := win.GetLastError()
|
||||||
|
if lastError == 183 {
|
||||||
|
//fmt.Println("Mutex 已经存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// do something with the mutex
|
||||||
|
return syscall.Handle(handle), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MUTANT_ALL_ACCESS 互斥体的完全访问权限
|
||||||
|
const MUTANT_ALL_ACCESS = 2031617
|
||||||
|
|
||||||
|
// OpenMutex 打开已存在的命名互斥体
|
||||||
|
func OpenMutex(bInheritHandle bool, mutexName string) (syscall.Handle, error) {
|
||||||
|
desiredAccess := MUTANT_ALL_ACCESS
|
||||||
|
handle, _, err := syscall.SyscallN(procOpenMutexW.Addr(), uintptr(desiredAccess), uintptr(win.BoolToBOOL(bInheritHandle)), uintptr(unsafe.Pointer(&mutexName)))
|
||||||
|
if err != 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer syscall.CloseHandle(syscall.Handle(handle))
|
||||||
|
if syscall.Handle(handle) == 0 {
|
||||||
|
e2 := win.GetLastError()
|
||||||
|
if e2 == 183 {
|
||||||
|
fmt.Println("打开失败")
|
||||||
|
}
|
||||||
|
fmt.Println(e2)
|
||||||
|
|
||||||
|
} // do something with the mutex
|
||||||
|
fmt.Println("Mutex open", syscall.Handle(handle))
|
||||||
|
return syscall.Handle(handle), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseMutex 释放互斥体持有权
|
||||||
|
func ReleaseMutex(handle syscall.Handle) bool {
|
||||||
|
// 传句柄值本身,而非句柄变量的地址
|
||||||
|
ret, _, e1 := syscall.SyscallN(procReleaseMutex.Addr(), uintptr(handle))
|
||||||
|
if e1 != 0 {
|
||||||
|
println(e1.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ret != 0
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
//go:build (windows && amd64) || (windows && 386)
|
||||||
|
|
||||||
|
package dll
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lxn/win"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32Dll *syscall.DLL
|
||||||
|
procGetWindowTextW *syscall.Proc
|
||||||
|
procGetWindowClassW *syscall.Proc
|
||||||
|
procFindWindowExW *syscall.Proc
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
user32Dll = syscall.MustLoadDLL("user32.dll")
|
||||||
|
procGetWindowTextW = user32Dll.MustFindProc("GetWindowTextW")
|
||||||
|
procGetWindowClassW = user32Dll.MustFindProc("GetClassNameW")
|
||||||
|
procFindWindowExW = user32Dll.MustFindProc("FindWindowExW")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowText 获取窗口标题(user32 GetWindowTextW 原生调用)
|
||||||
|
func GetWindowText(hWnd win.HWND, str *uint16, maxCount int32) (len int32, err error) {
|
||||||
|
//r0, _, e1 := procGetWindowTextW.Call(uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
|
||||||
|
r0, _, e1 := syscall.Syscall(procGetWindowTextW.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
|
||||||
|
len = int32(r0)
|
||||||
|
if len == 0 {
|
||||||
|
if e1 != 0 {
|
||||||
|
err = error(e1)
|
||||||
|
} else {
|
||||||
|
err = syscall.EINVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowTextString 获取窗口标题并转为 string
|
||||||
|
func GetWindowTextString(hWnd win.HWND) (string, error) {
|
||||||
|
buf := make([]uint16, 255)
|
||||||
|
_, err := GetWindowText(hWnd, &buf[0], int32(len(buf)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
windowsTitle := syscall.UTF16ToString(buf)
|
||||||
|
return windowsTitle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTopFolderPath 获取资源管理器当前打开的顶层路径
|
||||||
|
func GetTopFolderPath() (string, error) {
|
||||||
|
//lName := uint16(0)
|
||||||
|
hWnd := win.FindWindow(syscall.StringToUTF16Ptr("CabinetWClass"), nil)
|
||||||
|
if hWnd == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
hWnd = GetWindowFromClassName(hWnd, "Breadcrumb Parent")
|
||||||
|
if hWnd == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
hWnd = FindWindowExW(hWnd, 0, syscall.StringToUTF16Ptr("ToolbarWindow32"), nil)
|
||||||
|
if hWnd == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
buf := make([]uint16, 255)
|
||||||
|
_, err := GetWindowText(hWnd, &buf[0], int32(len(buf)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
title := syscall.UTF16ToString(buf)
|
||||||
|
return title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindWindowExW 查找子窗口(user32 FindWindowExW 原生调用)
|
||||||
|
func FindWindowExW(parent win.HWND, child win.HWND, lpClassName, lpWindowName *uint16) win.HWND {
|
||||||
|
ret, _, _ := procFindWindowExW.Call(uintptr(parent), uintptr(child),
|
||||||
|
uintptr(unsafe.Pointer(lpClassName)),
|
||||||
|
uintptr(unsafe.Pointer(lpWindowName)),
|
||||||
|
0)
|
||||||
|
|
||||||
|
return win.HWND(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowFromClassName 使用类名获取窗口句柄 递归
|
||||||
|
func GetWindowFromClassName(parentWnd win.HWND, className string) win.HWND {
|
||||||
|
// 寻找顶层窗口
|
||||||
|
hWnd := FindWindowExW(parentWnd, 0, nil, nil)
|
||||||
|
|
||||||
|
for hWnd != 0 {
|
||||||
|
// 获取窗口类名
|
||||||
|
name, _ := GetWindowClassString(hWnd)
|
||||||
|
if name == className {
|
||||||
|
return hWnd
|
||||||
|
}
|
||||||
|
// 递归查找
|
||||||
|
phWnd := GetWindowFromClassName(hWnd, className)
|
||||||
|
if phWnd != 0 {
|
||||||
|
return phWnd
|
||||||
|
}
|
||||||
|
// 查找下一子窗口
|
||||||
|
hWnd = FindWindowExW(parentWnd, hWnd, nil, nil)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowClassString 取窗口类名 string
|
||||||
|
func GetWindowClassString(hWnd win.HWND) (string, error) {
|
||||||
|
buf := make([]uint16, 255)
|
||||||
|
_, err := GetWindowClass(hWnd, &buf[0], int32(len(buf)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return syscall.UTF16ToString(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowClass 取窗口类名 API
|
||||||
|
func GetWindowClass(hWnd win.HWND, str *uint16, maxCount int32) (len int32, err error) {
|
||||||
|
//r0, _, e1 := procGetWindowTextW.Call(uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
|
||||||
|
r0, _, e1 := syscall.Syscall(procGetWindowClassW.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
|
||||||
|
len = int32(r0)
|
||||||
|
if len == 0 {
|
||||||
|
if e1 != 0 {
|
||||||
|
err = error(e1)
|
||||||
|
} else {
|
||||||
|
err = syscall.EINVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// dll 包示例:Windows API 调用(互斥体防多开、ShellExecute、创建进程、窗口文本)
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.zeroonesoft.cn/golib/zogo/dll"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 创建命名互斥体(常用于防止程序多开)
|
||||||
|
handle, err := dll.CreateMutex(false, "go-hua-example-mutex")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("CreateMutex 失败(可能已存在):", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("CreateMutex 成功, 句柄:", handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开已存在的互斥体
|
||||||
|
h2, err := dll.OpenMutex(false, "go-hua-example-mutex")
|
||||||
|
fmt.Println("OpenMutex:", h2, "错误:", err)
|
||||||
|
|
||||||
|
if handle != 0 {
|
||||||
|
fmt.Println("ReleaseMutex:", dll.ReleaseMutex(handle))
|
||||||
|
_ = syscall.CloseHandle(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下功能会产生可见窗口,按需取消注释:
|
||||||
|
//
|
||||||
|
// 用系统默认程序打开网页/文件:
|
||||||
|
// dll.ShellExecute("", "https://github.com", "", false)
|
||||||
|
//
|
||||||
|
// 创建进程(打开记事本):
|
||||||
|
// dll.CreateProcess()
|
||||||
|
//
|
||||||
|
// 获取资源管理器当前打开的顶层路径:
|
||||||
|
// path, err := dll.GetTopFolderPath()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user