Release version 0.2.4

Prevent repeated application launches by using a local single-instance control channel. A second process forwards a show command to the already running instance and exits.

Bump the application version to 0.2.4 and update README artifact examples plus docs/CHANGELOG.md.
This commit is contained in:
mixeme
2026-06-16 08:01:42 +03:00
parent e8e0060063
commit c1bd8c952c
4 changed files with 67 additions and 8 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ package core
// Version is the application version shown in the GUI and used by build
// scripts in artifact names. It is a var rather than a const so release builds
// can override it with Go ldflags when CI tags a build.
var Version = "0.2.3"
var Version = "0.2.4"
+54
View File
@@ -2,6 +2,8 @@ package gui
import (
"fmt"
"io"
"net"
"net/url"
"runtime"
"runtime/debug"
@@ -31,6 +33,8 @@ const settingsLabelWidth float32 = 140
const settingsControlWidth float32 = 330
const settingsStatusWidth float32 = 280
const projectRepositoryURL = "https://gitea.mixdep.ru/mix/gosentry"
const singleInstanceAddress = "127.0.0.1:37653"
const singleInstanceShowCommand = "show"
// The GUI package aliases core types to keep widget callbacks short. The actual
// durable model still lives in src/core, so GUI code does not define a second
@@ -40,6 +44,14 @@ type event = core.RunRecord
func Run() {
started := time.Now()
instanceListener, primary := acquireSingleInstance()
if !primary {
return
}
if instanceListener != nil {
defer instanceListener.Close()
}
// A stable app ID lets Fyne persist desktop preferences consistently across
// launches and gives tray/window integration a predictable identity.
a := app.NewWithID(appID)
@@ -49,6 +61,7 @@ func Run() {
configureSystemTray(a, w)
w.Resize(fyne.NewSize(1120, 720))
w.SetContent(newMainView(w, started))
serveSingleInstance(instanceListener, w)
w.ShowAndRun()
}
@@ -83,6 +96,47 @@ func configureSystemTray(a fyne.App, w fyne.Window) {
})
}
func acquireSingleInstance() (net.Listener, bool) {
listener, err := net.Listen("tcp", singleInstanceAddress)
if err == nil {
return listener, true
}
connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second)
if dialErr == nil {
_, _ = io.WriteString(connection, singleInstanceShowCommand)
_ = connection.Close()
return nil, false
}
// If the port is unavailable but does not answer as GoSentry, continue
// startup instead of making the application impossible to open because of an
// unrelated local listener. In the normal duplicate-start case the dial above
// succeeds and this process exits after waking the first instance.
return nil, true
}
func serveSingleInstance(listener net.Listener, w fyne.Window) {
if listener == nil {
return
}
go func() {
for {
connection, err := listener.Accept()
if err != nil {
return
}
command, _ := io.ReadAll(io.LimitReader(connection, 32))
_ = connection.Close()
if strings.TrimSpace(string(command)) != singleInstanceShowCommand {
continue
}
w.Show()
w.RequestFocus()
}
}()
}
func newMainView(w fyne.Window, started time.Time) fyne.CanvasObject {
store, jobs, err := core.OpenStore()
if err != nil {