01fd572a89
## 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
65 lines
1.7 KiB
Go
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()
|
|
})
|
|
}
|
|
}()
|
|
}
|