Files
gosentry/src/ui/singleinstance.go
T
mix 01fd572a89 Refactoring complete: v0.4.0 architectural milestone (#1)
## Summary

Completed Phase 5 refactoring and reached the target architecture.

**Architectural milestone achieved:**
- Service layer owns all state and is the sole writer
- UI is a thin Fyne view, all widget updates marshaled via `fyne.Do`
- Core engines are stateless and injectable
- Domain types are pure (no `yaml:"-"` fields)
- Full module builds and `go vet ./...` clean

## Changes

- Bump version: 0.3.6 → 0.4.0
- Update CHANGELOG with Phase 5 summary
- Add ROADMAP "Refactoring Follow-Ups" section

## Known follow-up work

1. **Linux test build broken** — `runner_test.go` needs `//go:build windows` tag
2. **File-size limits exceeded** — `operations.go` (486 lines), `jobs_view.go` (415 lines)

See ROADMAP.md for details.

---------

Co-authored-by: mixeme <mix.public@ya.ru>
Reviewed-on: #1
2026-06-22 08:05:10 +03:00

65 lines
1.7 KiB
Go

package ui
import (
"io"
"net"
"strings"
"time"
"fyne.io/fyne/v2"
)
const singleInstanceAddress = "127.0.0.1:37653"
const singleInstanceShowCommand = "show"
func acquireSingleInstance(showExisting bool) (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 {
// The first instance listens only on localhost and understands one tiny
// command: "show". That keeps the implementation dependency-free and easy
// to inspect, which matters more here than introducing a named-pipe or
// platform-specific IPC abstraction just to focus an existing window.
if showExisting {
_, _ = 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
}
// Accept runs on its own goroutine, so focusing the window must be
// marshaled onto the main thread like every other widget update.
fyne.Do(func() {
w.Show()
w.RequestFocus()
})
}
}()
}