Files
Ant-Browser/single_instance.go

170 lines
4.2 KiB
Go

package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"net"
"os"
"path/filepath"
"strings"
"time"
)
const singleInstanceLockFile = "app-instance.lock"
type singleInstanceLockInfo struct {
PID int `json:"pid"`
Addr string `json:"addr"`
}
type singleInstanceGuard struct {
lockPath string
lock *singleInstanceFileLock
listener net.Listener
activation chan singleInstanceActivationRequest
}
type singleInstanceActivationRequest struct {
done chan struct{}
}
func acquireSingleInstance(appRoot string) (*singleInstanceGuard, bool, error) {
stateRoot := singleInstanceStateRoot(appRoot)
if err := os.MkdirAll(stateRoot, 0o755); err != nil {
return nil, false, fmt.Errorf("准备单实例状态目录失败: %w", err)
}
lockPath := filepath.Join(stateRoot, singleInstanceLockFile)
for attempt := 0; attempt < 3; attempt++ {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, false, fmt.Errorf("启动单实例监听失败: %w", err)
}
lock, acquired, err := tryLockSingleInstanceFile(lockPath)
if err != nil {
_ = listener.Close()
return nil, false, fmt.Errorf("创建单实例锁失败: %w", err)
}
if acquired {
info := singleInstanceLockInfo{PID: os.Getpid(), Addr: listener.Addr().String()}
if encodeErr := writeSingleInstanceLockInfo(lock.file, info); encodeErr != nil {
_ = lock.Close()
_ = listener.Close()
return nil, false, fmt.Errorf("写入单实例锁失败: %w", encodeErr)
}
guard := &singleInstanceGuard{
lockPath: lockPath,
lock: lock,
listener: listener,
activation: make(chan singleInstanceActivationRequest, 8),
}
go guard.serve()
return guard, true, nil
}
_ = listener.Close()
if signalExistingSingleInstance(lockPath) {
return nil, false, nil
}
return nil, false, fmt.Errorf("单实例锁被占用且无法唤醒已有应用")
}
return nil, false, fmt.Errorf("单实例锁被占用且无法唤醒已有应用")
}
func writeSingleInstanceLockInfo(file *os.File, info singleInstanceLockInfo) error {
if file == nil {
return fmt.Errorf("单实例锁文件未打开")
}
if err := file.Truncate(0); err != nil {
return err
}
if _, err := file.Seek(0, 0); err != nil {
return err
}
if err := json.NewEncoder(file).Encode(info); err != nil {
return err
}
return file.Sync()
}
func signalExistingSingleInstance(lockPath string) bool {
for attempt := 0; attempt < 5; attempt++ {
info, err := readSingleInstanceLock(lockPath)
if err == nil && strings.TrimSpace(info.Addr) != "" {
grantExistingSingleInstanceForeground(info.PID)
conn, dialErr := net.DialTimeout("tcp", info.Addr, 350*time.Millisecond)
if dialErr == nil {
_ = conn.SetDeadline(time.Now().Add(1200 * time.Millisecond))
_, _ = conn.Write([]byte("activate\n"))
_, _ = bufio.NewReader(conn).ReadString('\n')
_ = conn.Close()
activateExistingSingleInstanceWindow(info.PID)
return true
}
activateExistingSingleInstanceWindow(info.PID)
}
time.Sleep(120 * time.Millisecond)
}
return false
}
func readSingleInstanceLock(lockPath string) (singleInstanceLockInfo, error) {
var info singleInstanceLockInfo
data, err := os.ReadFile(lockPath)
if err != nil {
return info, err
}
if err := json.Unmarshal(data, &info); err != nil {
return info, err
}
return info, nil
}
func (g *singleInstanceGuard) serve() {
for {
conn, err := g.listener.Accept()
if err != nil {
return
}
go g.handleConn(conn)
}
}
func (g *singleInstanceGuard) handleConn(conn net.Conn) {
defer conn.Close()
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
message, _ := bufio.NewReader(conn).ReadString('\n')
if strings.TrimSpace(message) != "activate" {
return
}
done := make(chan struct{})
select {
case g.activation <- singleInstanceActivationRequest{done: done}:
select {
case <-done:
case <-time.After(3 * time.Second):
}
default:
}
_, _ = conn.Write([]byte("ok\n"))
}
func (g *singleInstanceGuard) Close() {
if g == nil {
return
}
if err := g.listener.Close(); err != nil {
log.Printf("关闭单实例监听失败: %v", err)
}
if g.lock != nil {
if err := g.lock.Close(); err != nil {
log.Printf("释放单实例锁失败: %v", err)
}
}
_ = os.Remove(g.lockPath)
}