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