Files
MyGoNavi/windows_single_instance_windows.go
Syngnat 2597d087ad feat(windows): 支持 MSI 主程序严格单实例启动
- 仅对带 MSI 安装标识的 Windows 主程序启用单实例门禁,Portable 与特殊运行模式保持原行为
- 重复启动通过命名事件唤醒首个窗口,并兼容 Wails 运行时尚未就绪的激活请求
- 新增并发抢占、窗口激活与安装模式测试,覆盖同一安装仅保留一个主实例
2026-07-21 10:49:52 +08:00

76 lines
1.8 KiB
Go

//go:build windows
package main
import (
"errors"
"fmt"
"sync"
"golang.org/x/sys/windows"
)
// The named auto-reset event is both the lifetime lease and the activation
// mailbox. A signal remains pending while the Wails window is still starting.
func acquireWindowsMSISingleInstance(uniqueID string, onSecondInstance func()) (func(), bool, error) {
eventName, err := windows.UTF16PtrFromString(`Local\GoNavi-` + uniqueID + `-activate`)
if err != nil {
return nil, false, fmt.Errorf("build single-instance event name: %w", err)
}
activationEvent, eventErr := windows.CreateEvent(nil, 0, 0, eventName)
if errors.Is(eventErr, windows.ERROR_ALREADY_EXISTS) {
signalErr := windows.SetEvent(activationEvent)
windows.CloseHandle(activationEvent)
if signalErr != nil {
return nil, false, fmt.Errorf("activate primary GoNavi window: %w", signalErr)
}
return nil, false, nil
}
if eventErr != nil {
return nil, false, fmt.Errorf("create single-instance activation event: %w", eventErr)
}
stopEvent, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
windows.CloseHandle(activationEvent)
return nil, false, fmt.Errorf("create single-instance stop event: %w", err)
}
done := make(chan struct{})
go func() {
defer close(done)
for {
event, waitErr := windows.WaitForMultipleObjects(
[]windows.Handle{stopEvent, activationEvent},
false,
windows.INFINITE,
)
if waitErr != nil {
return
}
switch event {
case windows.WAIT_OBJECT_0:
return
case windows.WAIT_OBJECT_0 + 1:
if onSecondInstance != nil {
onSecondInstance()
}
default:
return
}
}
}()
var releaseOnce sync.Once
release := func() {
releaseOnce.Do(func() {
_ = windows.SetEvent(stopEvent)
<-done
windows.CloseHandle(stopEvent)
windows.CloseHandle(activationEvent)
})
}
return release, true, nil
}