feat: 单实例锁与窗口恢复机制
- GUI 单实例锁: 新增 internal/instance 包,首实例监听专用端点, 第二实例发送 focus 信号后静默退出 (Unix: gui.sock, Win: 18924) - 守护进程弱锁修复: NewServer() 在 os.Remove 前先 net.Dial 探测, 有活进程时拒绝启动,避免静默抢占 socket 导致孤儿进程 - macOS .app 重开恢复: 通过 class_addMethod 给 GLFWApplicationDelegate 注册 applicationShouldHandleReopen:hasVisibleWindows: 回调, 绕过 GLFW 直接用 Cocoa makeKeyAndOrderFront: 显示窗口 - 三平台覆盖: macOS/Linux 用 unix socket, Windows 用 TCP 端口
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"lmvpn/internal/config"
|
||||
"lmvpn/internal/db"
|
||||
"lmvpn/internal/i18n"
|
||||
"lmvpn/internal/instance"
|
||||
"lmvpn/internal/ipc"
|
||||
"lmvpn/internal/keychain"
|
||||
"lmvpn/internal/log"
|
||||
@@ -54,6 +56,7 @@ type App struct {
|
||||
currentProfile *model.ServerProfile
|
||||
defaultProfileID int64
|
||||
langSetting string
|
||||
windowHidden bool
|
||||
}
|
||||
|
||||
// Run initialises and starts the GUI application.
|
||||
@@ -66,6 +69,17 @@ func Run() {
|
||||
// Logging.
|
||||
log.Init(log.RoleGUI, paths.LogFile())
|
||||
|
||||
// Single-instance enforcement: the first instance holds the lock;
|
||||
// a second instance signals "focus" to the first and exits.
|
||||
focusCh, err := instance.Acquire()
|
||||
if err != nil {
|
||||
if errors.Is(err, instance.ErrAlreadyRunning) {
|
||||
log.L().Info("another instance is running, exiting")
|
||||
os.Exit(0)
|
||||
}
|
||||
log.L().Warn("instance lock failed, continuing", "error", err)
|
||||
}
|
||||
|
||||
// Database.
|
||||
store, err := db.Open()
|
||||
if err != nil {
|
||||
@@ -101,6 +115,44 @@ func Run() {
|
||||
// System tray.
|
||||
a.setupTray()
|
||||
|
||||
// Listen for "focus" signals from second instances and bring the
|
||||
// window to the front.
|
||||
if focusCh != nil {
|
||||
go func() {
|
||||
for range focusCh {
|
||||
fyne.Do(func() {
|
||||
a.windowHidden = false
|
||||
showAndActivate()
|
||||
a.window.Show()
|
||||
a.window.RequestFocus()
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Register a Cocoa handler so that re-opening the .app bundle
|
||||
// (which does NOT start a new process on macOS) brings the hidden
|
||||
// window back. On other platforms this is a no-op. This must be
|
||||
// deferred until after the Fyne/GLFW event loop has started,
|
||||
// because GLFWApplicationDelegate is not created until glfw.Init()
|
||||
// runs inside runGL().
|
||||
a.fyneApp.Lifecycle().SetOnStarted(func() {
|
||||
onAppActive = func() {
|
||||
if a.windowHidden {
|
||||
a.windowHidden = false
|
||||
showAndActivate()
|
||||
fyne.Do(func() {
|
||||
if a.windowHidden {
|
||||
return
|
||||
}
|
||||
a.window.Show()
|
||||
a.window.RequestFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
registerReopenHandler()
|
||||
})
|
||||
|
||||
// Auto-connect if configured.
|
||||
if cfg.AutoConnect && a.currentProfile != nil {
|
||||
go func() {
|
||||
@@ -111,6 +163,7 @@ func Run() {
|
||||
|
||||
a.window.SetCloseIntercept(func() {
|
||||
if cfg.CloseToTray {
|
||||
a.windowHidden = true
|
||||
hideDockIcon()
|
||||
a.window.Hide()
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user