Golang 在 Windows 下防止程序多开

当用户运行多个程序实例,如果操作的是同一资源,可能会造成数据不一致。所以有一个防止多开的需求,原理上是利用 CreateMutex 这个函数创建互斥对象。

func checkSingleton() (windows.Handle, error) {
	path, err := os.Executable()
	if err != nil {
		return 0, err
	}
	hashName := md5.Sum([]byte(path))
	name, err := syscall.UTF16PtrFromString("Local\\" + hex.EncodeToString(hashName[:]))
	if err != nil {
		return 0, err
	}
	return windows.CreateMutex(nil, false, name)
}

检查error,如果是syscall.ERROR_ALREADY_EXISTS,则说明有一个程序已经在运行。

上面创建的互斥对象是局部的(有Local前缀),多个用户登录还是可以同时多开程序的。如果需要全局限制,则替换为Global前缀。如果你的程序以服务的形式运行,则可能需要设置互斥对象的权限。

func checkSingleton() (windows.Handle, error) {
	path, err := os.Executable()
	if err != nil {
		panic(err)
	}
	hashName := md5.Sum([]byte(path))
	name, err := syscall.UTF16PtrFromString("Global\\" + hex.EncodeToString(hashName[:]))
	if err != nil {
		panic(err)
	}
	sa := windows.SecurityAttributes{}
	sd, err := windows.SecurityDescriptorFromString("D:(A;;GA;;;WD)")
	if err != nil {
		panic(err)
	}
	sa.Length = uint32(unsafe.Sizeof(sa))
	sa.SecurityDescriptor = sd
	return windows.CreateMutex(&sa, false, name)
}

若要用户体验更好的话:如果程序已经在运行,则显示已在运行的程序的主窗口,然后退出本次程序的启动。我的思路是先查找本程序的窗口,然后对比窗口程序和本程序的文件路径,如果一致则显示该窗口。

func showMainWindow() error {
	var windowToShow win.HWND
	path, err := os.Executable()
	if err != nil {
		return err
	}
	execFileInfo, err := os.Stat(path)
	if err != nil {
		return err
	}
	syscall.MustLoadDLL("user32.dll").MustFindProc("EnumWindows").Call(
		syscall.NewCallback(func(hwnd syscall.Handle, lparam uintptr) uintptr {
			className := make([]uint16, windows.MAX_PATH)
			if _, err = win.GetClassName(win.HWND(hwnd), &className[0], len(className)); err != nil {
				return 1
			}
			if windows.UTF16ToString(className) == "\\o/ Walk_MainWindow_Class \\o/" {
				var pid uint32
				var imageName string
				var imageFileInfo fs.FileInfo
				if _, err = windows.GetWindowThreadProcessId(windows.HWND(hwnd), &pid); err != nil {
					return 1
				}
				imageName, err = getImageName(pid)
				if err != nil {
					return 1
				}
				imageFileInfo, err = os.Stat(imageName)
				if err != nil {
					return 1
				}
				if os.SameFile(execFileInfo, imageFileInfo) {
					windowToShow = win.HWND(hwnd)
					return 0
				}
			}
			return 1
		}), 0)
	if windowToShow != 0 {
		if win.IsIconic(windowToShow) {
			win.ShowWindow(windowToShow, win.SW_RESTORE)
		} else {
			win.SetForegroundWindow(windowToShow)
		}
	}
	return nil
}

func getImageName(pid uint32) (string, error) {
	proc, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
	if err != nil {
		return "", err
	}
	defer windows.CloseHandle(proc)
	var exeNameBuf [261]uint16
	exeNameLen := uint32(len(exeNameBuf) - 1)
	err = windows.QueryFullProcessImageName(proc, 0, &exeNameBuf[0], &exeNameLen)
	if err != nil {
		return "", err
	}
	return windows.UTF16ToString(exeNameBuf[:exeNameLen]), nil
}

延伸阅读