Remove main_cmd.go and main_ui.go files, simplifying the project structure. Update Makefile to reflect changes in build targets, removing UI-related entries. Refactor UI components in ui2 package to eliminate Windows-specific build constraints, enhancing cross-platform compatibility.

This commit is contained in:
kony
2026-08-17 18:44:03 +08:00
parent 215b0e2067
commit 5901ca9f25
25 changed files with 365 additions and 236 deletions
+17 -32
View File
@@ -12,16 +12,6 @@ GOBUILD=GO111MODULE=on \
-X "go2.GitStatus=$(GitStatus)" \
-X "go2.BuildTime=$(BuildTime)" \
-w -s -buildid='
GOBUILD_UI=CGO_ENABLED=0 GO111MODULE=on \
GOPROXY="https://goproxy.cn,direct" \
GOEXPERIMENT=jsonv2 \
GOEXPERIMENT=loopvar \
go build -trimpath -ldflags \
'-H=windowsgui -X "go2.GitCommitLog=$(GitCommitLog)" \
-X "go2.GitStatus=$(GitStatus)" \
-X "go2.BuildTime=$(BuildTime)" \
-w -s -buildid='
LINUX_PLATFORM_LIST = \
linux-386-cmd \
linux-amd64-cmd \
@@ -40,7 +30,6 @@ DARWIN_PLATFORM_LIST = \
darwin-arm64-cmd \
WINDOWS_PLATFORM_LIST = \
windows-amd64-ui \
windows-amd64-cmd \
WINDOWS_PLATFORM2_LIST = \
@@ -56,59 +45,55 @@ windows: create_nac $(WINDOWS_PLATFORM_LIST) rm_nac strip
linux: $(LINUX_PLATFORM_LIST) strip
linux-386-cmd:
GOARCH=386 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-cmd:
GOARCH=amd64 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-loong64-cmd:
GOARCH=loong64 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-cmd:
GOARCH=mips GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=mips GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mipsle-cmd:
GOARCH=mipsle GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=mipsle GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips64-cmd:
GOARCH=mips64 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips64le-cmd:
GOARCH=mips64le GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-riscv64-cmd:
GOARCH=riscv64 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm-cmd:
GOARCH=arm GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=arm GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv6l-cmd:
GOARCH=arm GOARM=6 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=arm GOARM=6 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64-cmd:
GOARCH=arm64 GOOS=linux $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-cmd:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64-cmd:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
windows-amd64-cmd:
GOARCH=amd64 GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64-cmd:
GOARCH=arm64 GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-ui:
mkdir -p $(BINDIR)
GOARCH=amd64 GOOS=windows $(GOBUILD_UI) -o $(BINDIR)/$(NAME)-$@.exe
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-386-cmd:
GOARCH=386 GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm-cmd:
GOARCH=arm GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
GOARCH=arm GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
create_nac:
rsrc -manifest nac.manifest -ico assert/favicon.ico -o nac.syso
+5
View File
@@ -0,0 +1,5 @@
//go:build !windows
package main
func hideUIConsole() {}
+11
View File
@@ -0,0 +1,11 @@
//go:build windows
package main
import "golang.org/x/sys/windows"
func hideUIConsole() {
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
procFreeConsole := kernel32.NewProc("FreeConsole")
_, _, _ = procFreeConsole.Call()
}
+55 -2
View File
@@ -1,8 +1,7 @@
//go:build cmd
package main
import (
"embed"
"flag"
go2log "go2/log"
go2pool "go2/pool"
@@ -10,6 +9,7 @@ import (
"goodlink/pro"
_ "goodlink/pro"
"goodlink/stun2"
"goodlink/ui2"
"goodlink/utils"
goodlink_config "goodlink3/config"
"log"
@@ -23,6 +23,9 @@ import (
"time"
)
//go:embed assert/tray_idle.ico assert/tray_warning.ico assert/tray_danger.ico assert/tray_success.ico
var trayIcons embed.FS
func main2() {
log.Println("官方网址: https://gitee.com/konyshe/goodlink")
@@ -79,7 +82,57 @@ func main2() {
pro.StopRemote()
}
func runUI() {
// 隐藏 UI 主进程控制台窗口(仅 Windows;从已有终端启动时只脱离本进程)
hideUIConsole()
// 检查单实例,如果不是第一个实例则退出
// 必须在创建任何UI资源之前检查,避免影响已运行的实例
if !utils.CheckSingleInstance() {
// 已有实例运行,直接退出
return
}
// 启动前清理遗留的cmd进程
utils.CleanupOrphanedCmdProcesses()
goodlink_config.DeleteLocalConfig()
config.SetVersion(GetVersionFromAppConfig())
idle, _ := trayIcons.ReadFile("assert/tray_idle.ico")
warning, _ := trayIcons.ReadFile("assert/tray_warning.ico")
danger, _ := trayIcons.ReadFile("assert/tray_danger.ico")
success, _ := trayIcons.ReadFile("assert/tray_success.ico")
ui2.InitTrayIcons(idle, warning, danger, success)
ui2.Init()
uiURL, err := ui2.StartServer()
if err != nil {
return
}
// 监听显示窗口请求(二次启动时重新打开浏览器)
go func() {
for range utils.GetShowWindowChan() {
ui2.OpenBrowser(uiURL)
}
}()
ui2.OpenBrowser(uiURL)
// Windows:主线程运行托盘消息循环;Linux/macOS:等待信号后退出
ui2.RunLoop(uiURL)
}
func main() {
// 无任何参数时启动 UIWindows/Linux/macOS
if len(os.Args) <= 1 {
runUI()
return
}
config.SetVersion(GetVersionFromAppConfig())
config.Help()
-61
View File
@@ -1,61 +0,0 @@
//go:build windows && !cmd
package main
import (
"embed"
"goodlink/config"
"goodlink/ui2"
"goodlink/utils"
goodlink_config "goodlink3/config"
"github.com/getlantern/systray"
)
//go:embed assert/tray_idle.ico assert/tray_warning.ico assert/tray_danger.ico assert/tray_success.ico
var trayIcons embed.FS
func main() {
// 检查单实例,如果不是第一个实例则退出
// 必须在创建任何UI资源之前检查,避免影响已运行的实例
if !utils.CheckSingleInstance() {
// 已有实例运行,直接退出
return
}
// 启动前清理遗留的cmd进程
utils.CleanupOrphanedCmdProcesses()
goodlink_config.DeleteLocalConfig()
config.SetVersion(GetVersionFromAppConfig())
config.Help()
idle, _ := trayIcons.ReadFile("assert/tray_idle.ico")
warning, _ := trayIcons.ReadFile("assert/tray_warning.ico")
danger, _ := trayIcons.ReadFile("assert/tray_danger.ico")
success, _ := trayIcons.ReadFile("assert/tray_success.ico")
ui2.InitTrayIcons(idle, warning, danger, success)
ui2.Init()
uiURL, err := ui2.StartServer()
if err != nil {
return
}
// 监听显示窗口请求(二次启动时重新打开浏览器)
go func() {
for range utils.GetShowWindowChan() {
ui2.OpenBrowser(uiURL)
}
}()
ui2.OpenBrowser(uiURL)
// 主线程运行托盘消息循环;退出时停止子进程
systray.Run(func() {
ui2.SetupTray(uiURL)
}, ui2.OnTrayExit)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build darwin
package ui2
import "os/exec"
func OpenBrowser(url string) {
_ = exec.Command("open", url).Start()
}
+9
View File
@@ -0,0 +1,9 @@
//go:build linux
package ui2
import "os/exec"
func OpenBrowser(url string) {
_ = exec.Command("xdg-open", url).Start()
}
+17
View File
@@ -0,0 +1,17 @@
//go:build windows
package ui2
import (
"os/exec"
"syscall"
)
func OpenBrowser(url string) {
cmd := exec.Command("cmd", "/c", "start", "", url)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
_ = cmd.Start()
}
-2
View File
@@ -1,5 +1,3 @@
//go:build windows
package ui2
import (
-2
View File
@@ -1,5 +1,3 @@
//go:build windows
package ui2
import (
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package ui2
import "os/exec"
func hideChildWindow(cmd *exec.Cmd) {}
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package ui2
import (
"os/exec"
"syscall"
)
func hideChildWindow(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
}
-13
View File
@@ -1,5 +1,3 @@
//go:build windows
package ui2
import (
@@ -8,8 +6,6 @@ import (
"io/fs"
"net"
"net/http"
"os/exec"
"syscall"
"time"
)
@@ -18,15 +14,6 @@ var webFS embed.FS
const defaultUIAddr = "127.0.0.1:16780"
func OpenBrowser(url string) {
cmd := exec.Command("cmd", "/c", "start", "", url)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
_ = cmd.Start()
}
func StartServer() (string, error) {
ln, err := net.Listen("tcp", defaultUIAddr)
if err != nil {
+2 -8
View File
@@ -1,5 +1,3 @@
//go:build windows
package ui2
import (
@@ -13,7 +11,6 @@ import (
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"goodlink/pro"
@@ -116,11 +113,8 @@ func startCmdProcess() error {
m_cmd_mutex.Lock()
m_cmd_process = exec.Command(cmdPath, args...)
// 隐藏子进程窗口
m_cmd_process.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
// 隐藏子进程窗口(仅 Windows
hideChildWindow(m_cmd_process)
// 获取输出管道
stdout, err := m_cmd_process.StdoutPipe()
-2
View File
@@ -1,5 +1,3 @@
//go:build windows
package ui2
import (
+6
View File
@@ -54,6 +54,12 @@ func UpdateTrayIcon(state buttonState) {
}
}
func RunLoop(uiURL string) {
systray.Run(func() {
SetupTray(uiURL)
}, OnTrayExit)
}
func SetupTray(uiURL string) {
stateMu.RLock()
btn := currentButton
+20
View File
@@ -0,0 +1,20 @@
//go:build !windows
package ui2
import (
"os"
"os/signal"
"syscall"
)
func InitTrayIcons(_, _, _, _ []byte) {}
func UpdateTrayIcon(_ buttonState) {}
func RunLoop(_ string) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
StopCmdProcess()
}
-2
View File
@@ -1,5 +1,3 @@
//go:build windows
package ui2
import (
+12
View File
@@ -0,0 +1,12 @@
package utils
import "os"
// GetCmdExePath 返回当前可执行文件路径,UI 通过带参数 fork 自身走 CLI
func GetCmdExePath() string {
exePath, err := os.Executable()
if err != nil {
return os.Args[0]
}
return exePath
}
+7 -22
View File
@@ -11,41 +11,28 @@ import (
"syscall"
)
// 获取 cmd 版本可执行文件路径
func GetCmdExePath() string {
exePath, err := os.Executable()
if err != nil {
return "goodlink-windows-amd64-cmd.exe"
}
dir := filepath.Dir(exePath)
return filepath.Join(dir, "goodlink-windows-amd64-cmd.exe")
}
// killProcess 强制终止进程(使用 taskkill 确保终止)
// KillProcess 强制终止进程(使用 taskkill 确保终止)
func KillProcess(pid int) {
// 先尝试用 taskkill /F /T 强制终止进程树
if pid == os.Getpid() {
return
}
cmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", pid))
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()
}
// CleanupOrphanedCmdProcesses 清理遗留的cmd进程(通过进程名查找并终止)
// CleanupOrphanedCmdProcesses 清理遗留的工作进程(通过进程名查找并终止,跳过当前 PID
func CleanupOrphanedCmdProcesses() {
selfPid := os.Getpid()
cmdExeName := filepath.Base(GetCmdExePath())
// 使用 tasklist 查找所有匹配的进程
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s", cmdExeName), "/FO", "CSV", "/NH")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
output, err := cmd.Output()
if err != nil {
// 如果 tasklist 失败,尝试直接使用 taskkill 按进程名终止
killCmd := exec.Command("taskkill", "/F", "/IM", cmdExeName)
killCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
killCmd.Run()
return
}
// 解析输出,提取PID
lines := strings.Split(string(output), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
@@ -53,13 +40,11 @@ func CleanupOrphanedCmdProcesses() {
continue
}
// CSV格式: "进程名","PID","会话名","会话#","内存使用"
// 例如: "goodlink-windows-amd64-cmd.exe","1234","Console","1","12345 K"
parts := strings.Split(line, ",")
if len(parts) >= 2 {
// 移除引号
pidStr := strings.Trim(parts[1], "\"")
var pid int
if _, err := fmt.Sscanf(pidStr, "%d", &pid); err == nil {
if _, err := fmt.Sscanf(pidStr, "%d", &pid); err == nil && pid != selfPid {
KillProcess(pid)
}
}
+38
View File
@@ -0,0 +1,38 @@
//go:build unix
package utils
import (
"fmt"
"os"
"os/exec"
"strings"
)
func KillProcess(pid int) {
if pid == os.Getpid() {
return
}
proc, err := os.FindProcess(pid)
if err != nil {
return
}
_ = proc.Kill()
}
func CleanupOrphanedCmdProcesses() {
selfPid := os.Getpid()
exePath := GetCmdExePath()
out, err := exec.Command("pgrep", "-f", exePath).Output()
if err != nil {
return
}
for _, field := range strings.Fields(string(out)) {
var pid int
if _, err := fmt.Sscanf(field, "%d", &pid); err == nil && pid != selfPid {
KillProcess(pid)
}
}
}
-2
View File
@@ -1,5 +1,3 @@
//go:build cmd
package utils
import (
+10 -88
View File
@@ -1,104 +1,41 @@
//go:build windows
package utils
import (
"fmt"
"log"
"os"
"path/filepath"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
const (
MUTEX_NAME = "Global\\GoodLinkSingleInstance"
NOTIFY_FILE = "goodlink_notify.tmp"
NOTIFY_CHECK_INTERVAL = 500 * time.Millisecond
)
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procCreateMutex = kernel32.NewProc("CreateMutexW")
procCloseHandle = kernel32.NewProc("CloseHandle")
mutexHandle uintptr
showWindowChan chan struct{} // 用于通知主线程显示窗口的channel
)
// CreateMutex 创建互斥锁
func CreateMutex(name string) (uintptr, error) {
namePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return 0, err
}
ret, _, err := procCreateMutex.Call(
0,
0,
uintptr(unsafe.Pointer(namePtr)),
)
if ret == 0 {
return 0, fmt.Errorf("创建互斥锁失败: %v", err)
}
// 检查是否已存在(ERROR_ALREADY_EXISTS = 183
if err.(syscall.Errno) == 183 {
return ret, fmt.Errorf("实例已存在")
}
return ret, nil
}
// CloseHandle 关闭句柄
func CloseHandle(handle uintptr) error {
ret, _, _ := procCloseHandle.Call(handle)
if ret == 0 {
return fmt.Errorf("关闭句柄失败")
}
return nil
}
var showWindowChan chan struct{} // 用于通知主线程显示窗口的channel
// NotifyExistingInstance 通知已存在的实例显示窗口
func NotifyExistingInstance() error {
// 创建通知文件
exePath, err := os.Executable()
notifyPath, err := notifyFilePath()
if err != nil {
return fmt.Errorf("获取可执行文件路径失败: %v", err)
return err
}
dir := filepath.Dir(exePath)
notifyPath := filepath.Join(dir, NOTIFY_FILE)
// 写入通知文件
err = os.WriteFile(notifyPath, []byte("SHOW_WINDOW"), 0644)
if err != nil {
return fmt.Errorf("写入通知文件失败: %v", err)
}
return nil
return os.WriteFile(notifyPath, []byte("SHOW_WINDOW"), 0644)
}
// StartInstanceListener 启动实例监听器,监听来自其他实例的显示窗口请求
func StartInstanceListener() {
go func() {
exePath, err := os.Executable()
notifyPath, err := notifyFilePath()
if err != nil {
log.Printf("获取可执行文件路径失败: %v", err)
log.Printf("获取通知文件路径失败: %v", err)
return
}
dir := filepath.Dir(exePath)
notifyPath := filepath.Join(dir, NOTIFY_FILE)
for {
// 检查通知文件是否存在
if _, err := os.Stat(notifyPath); err == nil {
// 文件存在,读取内容
data, err := os.ReadFile(notifyPath)
if err == nil && string(data) == "SHOW_WINDOW" {
// 删除通知文件
os.Remove(notifyPath)
// 通过channel通知主线程显示窗口(非阻塞)
@@ -120,25 +57,10 @@ func GetShowWindowChan() <-chan struct{} {
return showWindowChan
}
// CheckSingleInstance 检查是否为单实例,如果不是第一个实例则返回false
func CheckSingleInstance() bool {
handle, err := CreateMutex(MUTEX_NAME)
func exeDir() (string, error) {
exePath, err := os.Executable()
if err != nil {
// 实例已存在,通知第一个实例显示窗口
if notifyErr := NotifyExistingInstance(); notifyErr != nil {
log.Printf("通知已存在实例失败: %v", notifyErr)
}
return false
return "", err
}
// 保存互斥锁句柄,程序退出时系统会自动释放
mutexHandle = handle
// 初始化channel(带缓冲,避免阻塞)
showWindowChan = make(chan struct{}, 1)
// 启动监听器,等待其他实例的请求
StartInstanceListener()
return true
return filepath.Dir(exePath), nil
}
+40
View File
@@ -0,0 +1,40 @@
//go:build unix
package utils
import (
"log"
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
var lockFile *os.File
func notifyFilePath() (string, error) {
return filepath.Join(os.TempDir(), NOTIFY_FILE), nil
}
// CheckSingleInstance 检查是否为单实例,如果不是第一个实例则返回false
func CheckSingleInstance() bool {
lockPath := filepath.Join(os.TempDir(), "goodlink.single.lock")
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
log.Printf("创建单实例锁文件失败: %v", err)
return true
}
if err := unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
f.Close()
if notifyErr := NotifyExistingInstance(); notifyErr != nil {
log.Printf("通知已存在实例失败: %v", notifyErr)
}
return false
}
lockFile = f
showWindowChan = make(chan struct{}, 1)
StartInstanceListener()
return true
}
+85
View File
@@ -0,0 +1,85 @@
//go:build windows
package utils
import (
"fmt"
"log"
"path/filepath"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
const MUTEX_NAME = "Global\\GoodLinkSingleInstance"
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procCreateMutex = kernel32.NewProc("CreateMutexW")
procCloseHandle = kernel32.NewProc("CloseHandle")
mutexHandle uintptr
)
func notifyFilePath() (string, error) {
dir, err := exeDir()
if err != nil {
return "", fmt.Errorf("获取可执行文件路径失败: %v", err)
}
return filepath.Join(dir, NOTIFY_FILE), nil
}
func CreateMutex(name string) (uintptr, error) {
namePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return 0, err
}
ret, _, err := procCreateMutex.Call(
0,
0,
uintptr(unsafe.Pointer(namePtr)),
)
if ret == 0 {
return 0, fmt.Errorf("创建互斥锁失败: %v", err)
}
// 检查是否已存在(ERROR_ALREADY_EXISTS = 183
if err.(syscall.Errno) == 183 {
return ret, fmt.Errorf("实例已存在")
}
return ret, nil
}
func CloseHandle(handle uintptr) error {
ret, _, _ := procCloseHandle.Call(handle)
if ret == 0 {
return fmt.Errorf("关闭句柄失败")
}
return nil
}
// CheckSingleInstance 检查是否为单实例,如果不是第一个实例则返回false
func CheckSingleInstance() bool {
handle, err := CreateMutex(MUTEX_NAME)
if err != nil {
// 实例已存在,通知第一个实例显示窗口
if notifyErr := NotifyExistingInstance(); notifyErr != nil {
log.Printf("通知已存在实例失败: %v", notifyErr)
}
return false
}
// 保存互斥锁句柄,程序退出时系统会自动释放
mutexHandle = handle
// 初始化channel(带缓冲,避免阻塞)
showWindowChan = make(chan struct{}, 1)
// 启动监听器,等待其他实例的请求
StartInstanceListener()
return true
}