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>
This commit is contained in:
+66
-45
@@ -44,6 +44,9 @@ type App struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
events *eventBroker
|
||||
revMu sync.Mutex
|
||||
cachedRev string
|
||||
cachedConn bool
|
||||
}
|
||||
|
||||
type UIState struct {
|
||||
@@ -111,17 +114,35 @@ func (a *App) GetState() (UIState, error) {
|
||||
// GetStateSince returns a full poll snapshot, or {rev, unchanged:true} when since
|
||||
// matches the current fingerprint (cheap idle HTTP polls).
|
||||
func (a *App) GetStateSince(since string) (UIState, error) {
|
||||
if since != "" {
|
||||
a.revMu.Lock()
|
||||
cachedRev, cachedConn := a.cachedRev, a.cachedConn
|
||||
a.revMu.Unlock()
|
||||
if cachedRev != "" && cachedRev == since && a.Mgr.Status().Connected == cachedConn {
|
||||
return UIState{Rev: cachedRev, Unchanged: true}, nil
|
||||
}
|
||||
}
|
||||
out, err := a.getState(false)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Rev = fingerprintState(out)
|
||||
a.revMu.Lock()
|
||||
a.cachedRev = out.Rev
|
||||
a.cachedConn = out.Connected
|
||||
a.revMu.Unlock()
|
||||
if since != "" && since == out.Rev {
|
||||
return UIState{Rev: out.Rev, Unchanged: true}, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) invalidateRevCache() {
|
||||
a.revMu.Lock()
|
||||
a.cachedRev = ""
|
||||
a.revMu.Unlock()
|
||||
}
|
||||
|
||||
// GetEditState returns full active proxy / hy2 secrets for the editor form (not for polls).
|
||||
func (a *App) GetEditState() (UIState, error) {
|
||||
out, err := a.getState(true)
|
||||
@@ -238,24 +259,34 @@ func resolveCoreCached(binDir string, proto config.Protocol) (path string, ready
|
||||
|
||||
func (a *App) SaveProfile(name, proxy string, systemProxy bool) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
a.mu.Unlock()
|
||||
return fmt.Errorf("укажите название профиля")
|
||||
}
|
||||
a.Mgr.SetSystemProxy(systemProxy)
|
||||
return a.Mgr.SaveActiveProfile(name, proxy)
|
||||
err := a.Mgr.SaveActiveProfile(name, proxy)
|
||||
a.mu.Unlock()
|
||||
if err == nil {
|
||||
a.NotifyState("profile")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) CreateProfile(name, proxy string, systemProxy bool) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
a.mu.Unlock()
|
||||
return fmt.Errorf("укажите название профиля")
|
||||
}
|
||||
a.Mgr.SetSystemProxy(systemProxy)
|
||||
return a.Mgr.SaveProfile(name, proxy)
|
||||
err := a.Mgr.SaveProfile(name, proxy)
|
||||
a.mu.Unlock()
|
||||
if err == nil {
|
||||
a.NotifyState("profile")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) SelectProfile(name string) error {
|
||||
@@ -271,8 +302,12 @@ func (a *App) SelectProfile(name string) error {
|
||||
|
||||
func (a *App) DeleteProfile(name string) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.Mgr.DeleteProfile(name)
|
||||
err := a.Mgr.DeleteProfile(name)
|
||||
a.mu.Unlock()
|
||||
if err == nil {
|
||||
a.NotifyState("profile")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) Connect() error { return a.ConnectProfile("") }
|
||||
@@ -368,17 +403,21 @@ func (a *App) ProbeTunnel() error {
|
||||
}
|
||||
|
||||
func (a *App) SavePrefs(connectOnLaunch, autoReconnect, systemProxy bool) error {
|
||||
return a.Mgr.SavePrefs(core.Prefs{
|
||||
err := a.Mgr.SavePrefs(core.Prefs{
|
||||
ConnectOnLaunch: connectOnLaunch,
|
||||
AutoReconnect: autoReconnect,
|
||||
SystemProxy: systemProxy,
|
||||
})
|
||||
if err == nil {
|
||||
a.NotifyState("prefs")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StartBackground runs reconnect-on-crash, optional connect-on-launch, and Dock icon sync.
|
||||
// StartBackground runs reconnect-on-crash, optional connect-on-launch, and core warmup.
|
||||
func (a *App) StartBackground() {
|
||||
go a.watchdogLoop()
|
||||
go a.dockIconLoop()
|
||||
dockicon.SetConnected(false)
|
||||
go a.warmActiveCore()
|
||||
prefs := a.Mgr.Prefs()
|
||||
if prefs.ConnectOnLaunch {
|
||||
@@ -408,27 +447,7 @@ func (a *App) warmActiveCore() {
|
||||
// Soft fail — Connect will surface the error if the user tries.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) dockIconLoop() {
|
||||
dockicon.SetConnected(false)
|
||||
var last bool
|
||||
first := true
|
||||
t := time.NewTicker(400 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
up := a.Mgr.Status().Connected
|
||||
if first || up != last {
|
||||
first = false
|
||||
last = up
|
||||
dockicon.SetConnected(up)
|
||||
}
|
||||
}
|
||||
}
|
||||
a.NotifyState("cores")
|
||||
}
|
||||
|
||||
func (a *App) watchdogLoop() {
|
||||
@@ -443,6 +462,8 @@ func (a *App) watchdogLoop() {
|
||||
continue
|
||||
}
|
||||
prefs := a.Mgr.Prefs()
|
||||
dockicon.SetConnected(false)
|
||||
a.NotifyState("disconnected")
|
||||
if !prefs.AutoReconnect {
|
||||
continue
|
||||
}
|
||||
@@ -465,17 +486,26 @@ func (a *App) InstallCore() (string, error) {
|
||||
for k, v := range paths {
|
||||
parts = append(parts, k+": "+v)
|
||||
}
|
||||
a.NotifyState("cores")
|
||||
return strings.Join(parts, " | "), nil
|
||||
}
|
||||
|
||||
func (a *App) SaveHy2(opts core.Hy2Options) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.Mgr.SaveHy2Options(opts)
|
||||
err := a.Mgr.SaveHy2Options(opts)
|
||||
a.mu.Unlock()
|
||||
if err == nil {
|
||||
a.NotifyState("hy2")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) ImportSubscription(rawURL string) (int, error) {
|
||||
return a.Mgr.ImportSubscription(rawURL)
|
||||
n, err := a.Mgr.ImportSubscription(rawURL)
|
||||
if err == nil {
|
||||
a.NotifyState("subscription")
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (a *App) PingServers() ([]netcheck.Result, error) {
|
||||
@@ -540,24 +570,14 @@ func (a *App) PingBest(autoConnect bool) (PingBestResult, error) {
|
||||
}
|
||||
|
||||
func (a *App) runPings() ([]netcheck.Result, error) {
|
||||
a.mu.Lock()
|
||||
list := a.Mgr.Profiles()
|
||||
a.mu.Unlock()
|
||||
|
||||
targets := make([]netcheck.Target, 0, len(list))
|
||||
for _, p := range list {
|
||||
targets = append(targets, netcheck.Target{
|
||||
Name: p.Name,
|
||||
Protocol: config.Protocol(p.Protocol),
|
||||
Proxy: p.Proxy,
|
||||
})
|
||||
}
|
||||
targets := a.Mgr.PingTargets()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
|
||||
defer cancel()
|
||||
out := netcheck.PingAll(ctx, targets)
|
||||
a.mu.Lock()
|
||||
a.Pings = out
|
||||
a.mu.Unlock()
|
||||
a.NotifyState("pings")
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -573,6 +593,7 @@ func (a *App) CheckUpdate() (update.Status, error) {
|
||||
}
|
||||
a.UpdateStatus = st
|
||||
a.mu.Unlock()
|
||||
a.NotifyState("update")
|
||||
return st, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,13 @@ func (b *eventBroker) publish(payload []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyState pushes a small SSE payload so the UI can refresh immediately.
|
||||
// NotifyState invalidates the idle rev cache and pushes a small SSE payload.
|
||||
func (a *App) NotifyState(kind string) {
|
||||
if a == nil || a.events == nil {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
a.invalidateRevCache()
|
||||
if a.events == nil {
|
||||
return
|
||||
}
|
||||
if kind == "" {
|
||||
|
||||
Reference in New Issue
Block a user