mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
170 lines
4.2 KiB
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)
|
|
}
|