Files
navi/cmd/vpnapp/gui_desktop.go
T
M4andCursor 57d719f02f
ci / test (macos-latest) (push) Canceled after 0s
ci / test (ubuntu-latest) (push) Canceled after 0s
ci / test (windows-latest) (push) Canceled after 0s
Release 3.8.2.11: idle rev-cache, port-safe Connect, single-instance tray UX.
Faster unchanged polls; orphan-core cleanup on listen ports; clearer busy-port errors; cores only from binDir; incremental server list; CSP without Google Fonts; single-instance activates existing window; close-to-tray without kill-others or balloon.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 17:27:47 +03:00

136 lines
3.3 KiB
Go

//go:build windows || darwin
package main
import (
"log"
"net/http"
"os"
"os/signal"
"runtime"
"time"
"github.com/crgimenes/glaze"
"vpnclient/internal/apphost"
"vpnclient/internal/config"
"vpnclient/internal/core"
"vpnclient/internal/dockicon"
"vpnclient/internal/logbuf"
"vpnclient/internal/singleinstance"
"vpnclient/internal/trayhost"
"vpnclient/internal/update"
)
func main() {
if update.MaybeFinishUpdate(os.Args[1:]) {
return
}
update.CleanupStaleDownloads()
releaseInstance, err := singleinstance.Lock("Navis")
if err != nil {
if err == singleinstance.ErrAlreadyRunning {
// Existing window was activated when possible.
return
}
fatalf("Не удалось получить single-instance lock:\n%v", err)
}
defer releaseInstance()
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
cfgPath, err := config.LocateConfig()
if err != nil {
fatalf("Не удалось найти путь конфига: %v", err)
}
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
if err := config.WriteExample(cfgPath); err != nil {
fatalf("Не удалось создать конфиг: %v", err)
}
}
cfg, err := config.Load(cfgPath)
if err != nil {
fatalf("Ошибка конфига %s: %v", cfgPath, err)
}
logBuf := logbuf.New(0)
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
if err != nil {
fatalf("%v", err)
}
a := apphost.New(mgr, cfgPath, logBuf)
a.OpenURL = openExternal
a.OnAfterUpdate = func() {
// Hard-exit so the OS unlocks the binary for replacement.
go func() {
time.Sleep(300 * time.Millisecond)
_ = a.Disconnect()
os.Exit(0)
}()
}
ln, uiURL, token, err := apphost.ListenLocal()
if err != nil {
fatalf("listen: %v", err)
}
a.APIToken = token
srv := &http.Server{
Handler: a.Handler(),
ReadHeaderTimeout: 5 * time.Second,
// No WriteTimeout: SSE /api/events is long-lived.
}
go func() {
_ = srv.Serve(ln)
}()
a.StartBackground()
dockicon.OnIconChange = trayhost.SetConnectedIcon
trayOK := trayhost.Start("Navis", trayhost.Hooks{
Connect: func() { _ = a.Connect() },
Disconnect: func() { _ = a.Disconnect() },
Show: showMainWindow,
Quit: func() {
_ = a.Disconnect()
a.Shutdown()
closeMainWindow()
},
IsUp: func() bool { return a.Mgr.Status().Connected },
})
go a.AutoCheckUpdate()
w, err := glaze.New(false)
if err != nil || w == nil {
log.Printf("webview unavailable (%v); opening default browser", err)
if err2 := openExternal(uiURL); err2 != nil {
_ = srv.Close()
fatalf("Не удалось открыть интерфейс:\n%v\n%v\n%s", err, err2, uiURL)
}
log.Printf("Navis UI: %s", uiURL)
sig := make(chan os.Signal, 1)
signal.Notify(sig, shutdownSignals()...)
<-sig
a.Shutdown()
_ = mgr.Disconnect()
_ = srv.Close()
return
}
a.OnQuit = closeMainWindow
defer func() {
a.Shutdown()
_ = mgr.Disconnect()
_ = srv.Close()
w.Destroy()
}()
w.SetTitle("Navis")
w.SetSize(500, 900, glaze.HintNone)
// Close→hide only when tray is alive; otherwise X would leave invisible zombies.
decorateWindow(w, trayOK)
w.Navigate(uiURL)
log.Printf("Navis UI: %s (tray=%v hideOnClose=%v)", uiURL, trayOK, trayOK)
w.Run()
}