Release 3.8.2.5: cheaper idle UI poll and tighter hot paths.
Delta getState by rev, pause when hidden, cancellable background loops, hostCache bounds, unlock PollUI around engine status, corebin singleflight, PingAll worker pool. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -254,7 +254,8 @@ https://evilfox.win/
|
||||
- трей (Windows), CI workflows, REQUIRE_CODESIGN для релиза; Android gated Naive/AWG + sync версии;
|
||||
- 3.8.2+2: иконка в macOS Dock — буква N цвета морской волны при подключении, исходная при отключении;
|
||||
- 3.8.2+3: poll без os.Stat ядер + кэш host; Darwin sysproxy batch; Connect без Config.Clone;
|
||||
- 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP `/api` (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (`Navis-x.y.z.b.exe`).
|
||||
- 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP `/api` (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (`Navis-x.y.z.b.exe`);
|
||||
- 3.8.2+5: delta-poll UI (`getState` по rev); пауза опроса при скрытом окне; cancel lifecycle dock/watchdog; invalidate/лимит hostCache; PollUI без lock на `Engine.Running`; singleflight corebin; PingAll worker-pool.
|
||||
|
||||
В 3.8.1:
|
||||
- меньше нагрузка в простое: лёгкий опрос интерфейса без полного клонирования конфига, кэш путей к ядрам, лог с лимитом размера;
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
||||
versionCode = 3080204
|
||||
versionName = "3.8.2+4"
|
||||
versionCode = 3080205
|
||||
versionName = "3.8.2+5"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||
|
||||
+3
-3
@@ -42,11 +42,11 @@ if errorlevel 1 exit /b 1
|
||||
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.4 -arch arm64
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.5 -arch arm64
|
||||
if errorlevel 1 exit /b 1
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.4 -arch amd64
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.5 -arch amd64
|
||||
if errorlevel 1 exit /b 1
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.4 -arch universal
|
||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.5 -arch universal
|
||||
if errorlevel 1 exit /b 1
|
||||
|
||||
echo Built Mac GUI + CLI:
|
||||
|
||||
@@ -93,11 +93,13 @@ func main() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, shutdownSignals()...)
|
||||
<-sig
|
||||
a.Shutdown()
|
||||
_ = mgr.Disconnect()
|
||||
_ = srv.Close()
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
a.Shutdown()
|
||||
_ = mgr.Disconnect()
|
||||
_ = srv.Close()
|
||||
w.Destroy()
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+3
-3
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "3.8.2",
|
||||
"notes": "Navis 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP /api (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (Navis-x.y.z.b.exe).",
|
||||
"notes": "Navis 3.8.2+5: delta-poll UI (getState по rev, без полного JSON если без изменений); пауза опроса при скрытом окне; cancel lifecycle dock/watchdog; invalidate/лимит hostCache; PollUI без lock на Engine.Running; singleflight corebin; PingAll worker-pool.",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"sha256": "68ce68b36f5027ef222b7aca2c74fc8529b852ea3f5e7cc7c1cdd71c5162a7b6",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"sha256": "68ce68b36f5027ef222b7aca2c74fc8529b852ea3f5e7cc7c1cdd71c5162a7b6",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+3
-3
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "3.8.2",
|
||||
"notes": "Navis 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP /api (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (Navis-x.y.z.b.exe).",
|
||||
"notes": "Navis 3.8.2+5: delta-poll UI (getState по rev, без полного JSON если без изменений); пауза опроса при скрытом окне; cancel lifecycle dock/watchdog; invalidate/лимит hostCache; PollUI без lock на Engine.Running; singleflight corebin; PingAll worker-pool.",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"sha256": "68ce68b36f5027ef222b7aca2c74fc8529b852ea3f5e7cc7c1cdd71c5162a7b6",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"sha256": "68ce68b36f5027ef222b7aca2c74fc8529b852ea3f5e7cc7c1cdd71c5162a7b6",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
github.com/diskfs/go-diskfs v1.9.4
|
||||
github.com/ebitengine/purego v0.10.1
|
||||
golang.org/x/image v0.44.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.43.0
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
|
||||
+91
-19
@@ -3,6 +3,7 @@ package apphost
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -39,6 +40,8 @@ type App struct {
|
||||
OpenURL func(string) error
|
||||
OnAfterUpdate func()
|
||||
APIToken string
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type UIState struct {
|
||||
@@ -63,6 +66,8 @@ type UIState struct {
|
||||
ConnectOnLaunch bool `json:"connect_on_launch"`
|
||||
AutoReconnect bool `json:"auto_reconnect"`
|
||||
LogTail string `json:"log_tail,omitempty"`
|
||||
Rev string `json:"rev,omitempty"`
|
||||
Unchanged bool `json:"unchanged,omitempty"`
|
||||
}
|
||||
|
||||
type PingBestResult struct {
|
||||
@@ -74,10 +79,13 @@ type PingBestResult struct {
|
||||
}
|
||||
|
||||
func New(mgr *core.Manager, cfgPath string, logBuf *logbuf.Buffer) *App {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &App{
|
||||
Mgr: mgr,
|
||||
CfgPath: cfgPath,
|
||||
LogBuf: logBuf,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
UpdateStatus: update.Status{
|
||||
Current: update.DisplayVersion(),
|
||||
ManifestURL: update.DefaultManifestURL,
|
||||
@@ -85,13 +93,40 @@ func New(mgr *core.Manager, cfgPath string, logBuf *logbuf.Buffer) *App {
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown cancels background loops (dock icon / watchdog). Safe to call more than once.
|
||||
func (a *App) Shutdown() {
|
||||
if a == nil || a.cancel == nil {
|
||||
return
|
||||
}
|
||||
a.cancel()
|
||||
}
|
||||
|
||||
func (a *App) GetState() (UIState, error) {
|
||||
return a.getState(false)
|
||||
return a.GetStateSince("")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
out, err := a.getState(false)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Rev = fingerprintState(out)
|
||||
if since != "" && since == out.Rev {
|
||||
return UIState{Rev: out.Rev, Unchanged: true}, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetEditState returns full active proxy / hy2 secrets for the editor form (not for polls).
|
||||
func (a *App) GetEditState() (UIState, error) {
|
||||
return a.getState(true)
|
||||
out, err := a.getState(true)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Rev = fingerprintState(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) getState(includeSecrets bool) (UIState, error) {
|
||||
@@ -155,6 +190,23 @@ func (a *App) getState(includeSecrets bool) (UIState, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func fingerprintState(s UIState) string {
|
||||
var b strings.Builder
|
||||
b.Grow(256)
|
||||
fmt.Fprintf(&b, "%t|%s|%s|%s|%t|%s|%t|%t|%t|%s|",
|
||||
s.Connected, s.Profile, s.ActiveProfile, s.Protocol, s.CoreReady, s.LastError,
|
||||
s.SystemProxy, s.ConnectOnLaunch, s.AutoReconnect, s.Subscription)
|
||||
fmt.Fprintf(&b, "%s|%s|%t|", s.Update.Latest, s.Update.Current, s.Update.Available)
|
||||
for _, p := range s.Profiles {
|
||||
fmt.Fprintf(&b, "%s,%s,%s,%t;", p.Name, p.Protocol, p.Host, p.Active)
|
||||
}
|
||||
for _, p := range s.Pings {
|
||||
fmt.Fprintf(&b, "%s,%t,%t,%d,%s;", p.Name, p.OK, p.Soft, p.Ms, p.Error)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(b.String()))
|
||||
return hex.EncodeToString(sum[:8])
|
||||
}
|
||||
|
||||
func trimLogTail(s string, max int) string {
|
||||
if max <= 0 || len(s) <= max {
|
||||
return s
|
||||
@@ -318,7 +370,11 @@ func (a *App) StartBackground() {
|
||||
prefs := a.Mgr.Prefs()
|
||||
if prefs.ConnectOnLaunch {
|
||||
go func() {
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-time.After(1200 * time.Millisecond):
|
||||
}
|
||||
if a.Mgr.Status().Connected {
|
||||
return
|
||||
}
|
||||
@@ -331,29 +387,45 @@ func (a *App) dockIconLoop() {
|
||||
dockicon.SetConnected(false)
|
||||
var last bool
|
||||
first := true
|
||||
t := time.NewTicker(400 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
for {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
up := a.Mgr.Status().Connected
|
||||
if first || up != last {
|
||||
first = false
|
||||
last = up
|
||||
dockicon.SetConnected(up)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) watchdogLoop() {
|
||||
t := time.NewTicker(2 * time.Second)
|
||||
defer t.Stop()
|
||||
for {
|
||||
time.Sleep(2 * time.Second)
|
||||
if !a.Mgr.TakeUnexpectedDrop() {
|
||||
continue
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if !a.Mgr.TakeUnexpectedDrop() {
|
||||
continue
|
||||
}
|
||||
prefs := a.Mgr.Prefs()
|
||||
if !prefs.AutoReconnect {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-time.After(800 * time.Millisecond):
|
||||
}
|
||||
_ = a.Connect()
|
||||
}
|
||||
prefs := a.Mgr.Prefs()
|
||||
if !prefs.AutoReconnect {
|
||||
continue
|
||||
}
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
_ = a.Connect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,7 +674,7 @@ func arg[T any](args []json.RawMessage, i int, def T) T {
|
||||
func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
|
||||
switch name {
|
||||
case "getState":
|
||||
return a.GetState()
|
||||
return a.GetStateSince(arg(args, 0, ""))
|
||||
case "getEditState":
|
||||
return a.GetEditState()
|
||||
case "connect":
|
||||
|
||||
@@ -50,6 +50,33 @@ func TestHTTPBridgeGetState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBridgeGetStateUnchanged(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Active: "demo",
|
||||
Profiles: []config.Profile{
|
||||
{Name: "demo", Protocol: config.ProtocolNaive, Proxy: "https://u:p@example.com"},
|
||||
},
|
||||
}
|
||||
mgr, err := core.NewManager("", cfg, logbuf.New(0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := New(mgr, "", logbuf.New(0))
|
||||
a.APIToken = "test-token"
|
||||
|
||||
full, err := a.GetStateSince("")
|
||||
if err != nil || full.Unchanged || full.Rev == "" {
|
||||
t.Fatalf("full: unchanged=%v rev=%q err=%v", full.Unchanged, full.Rev, err)
|
||||
}
|
||||
again, err := a.GetStateSince(full.Rev)
|
||||
if err != nil || !again.Unchanged || again.Rev != full.Rev {
|
||||
t.Fatalf("delta: %+v err=%v", again, err)
|
||||
}
|
||||
if len(again.Profiles) != 0 {
|
||||
t.Fatalf("unchanged payload should omit profiles, got %d", len(again.Profiles))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBridgeRejectsBadToken(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
mgr, err := core.NewManager("", cfg, logbuf.New(0))
|
||||
|
||||
+19
-3
@@ -1,4 +1,4 @@
|
||||
// HTTP bridge for macOS GUI (Windows WebView2 already binds these as natives).
|
||||
// HTTP bridge for glaze GUI (Windows + macOS): POST /api/<method>.
|
||||
(function () {
|
||||
const methods = [
|
||||
"getState","getEditState","connect","disconnect","connectProfile","saveProfile","createProfile",
|
||||
@@ -208,6 +208,7 @@
|
||||
}
|
||||
|
||||
let listFP = "";
|
||||
let stateRev = "";
|
||||
|
||||
function profilesFingerprint(state) {
|
||||
const parts = [
|
||||
@@ -444,13 +445,22 @@
|
||||
}
|
||||
|
||||
async function refresh(opts) {
|
||||
const force = !!(opts && opts.force);
|
||||
if (document.hidden && !force && !(opts && opts.syncForm)) {
|
||||
return null;
|
||||
}
|
||||
const needSecrets = !!(opts && opts.syncForm) || (!formHydrated && !dirty);
|
||||
let state;
|
||||
if (needSecrets && typeof getEditState === "function") {
|
||||
state = await getEditState();
|
||||
} else {
|
||||
state = await getState();
|
||||
state = await getState(stateRev || "");
|
||||
}
|
||||
if (state && state.unchanged) {
|
||||
if (state.rev) stateRev = state.rev;
|
||||
return state;
|
||||
}
|
||||
if (state && state.rev) stateRev = state.rev;
|
||||
paint(state, opts);
|
||||
return state;
|
||||
}
|
||||
@@ -697,5 +707,11 @@
|
||||
setMeta(String(e), "err");
|
||||
}
|
||||
})();
|
||||
setInterval(() => { if (!busy && !modal.classList.contains("open")) refresh().catch(() => {}); }, 4000);
|
||||
setInterval(() => {
|
||||
if (document.hidden || busy || modal.classList.contains("open")) return;
|
||||
refresh().catch(() => {});
|
||||
}, 4000);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden && !busy) refresh({ force: true }).catch(() => {});
|
||||
});
|
||||
|
||||
|
||||
@@ -327,6 +327,7 @@ func WriteExample(path string) error {
|
||||
|
||||
// Save writes config as indented JSON (atomic replace).
|
||||
func Save(path string, cfg Config) error {
|
||||
InvalidateHostCache()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ type ProfileInfo struct {
|
||||
// hostCache avoids re-parsing large AWG conf bodies on every UI poll.
|
||||
var hostCache sync.Map // proxy URI/conf → host string
|
||||
|
||||
const hostCacheMax = 2048
|
||||
|
||||
func cachedProxyHost(proxy string) string {
|
||||
proxy = strings.TrimSpace(proxy)
|
||||
if proxy == "" {
|
||||
@@ -28,9 +30,32 @@ func cachedProxyHost(proxy string) string {
|
||||
}
|
||||
h := proxyHost(proxy)
|
||||
hostCache.Store(proxy, h)
|
||||
// Soft bound: subscription churn can retain stale URI keys; drop all if oversized.
|
||||
n := 0
|
||||
over := false
|
||||
hostCache.Range(func(_, _ any) bool {
|
||||
n++
|
||||
if n > hostCacheMax {
|
||||
over = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if over {
|
||||
InvalidateHostCache()
|
||||
hostCache.Store(proxy, h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// InvalidateHostCache clears parsed host strings (call after profile mutations).
|
||||
func InvalidateHostCache() {
|
||||
hostCache.Range(func(k, _ any) bool {
|
||||
hostCache.Delete(k)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// ListProfiles returns UI-friendly profile summaries.
|
||||
func (c *Config) ListProfiles() []ProfileInfo {
|
||||
out := make([]ProfileInfo, 0, len(c.Profiles))
|
||||
|
||||
+20
-10
@@ -380,19 +380,20 @@ type UIPoll struct {
|
||||
AutoReconnect bool
|
||||
}
|
||||
|
||||
// PollUI gathers status + profile list under one lock without Config().Clone().
|
||||
// PollUI gathers status + profile list without Config().Clone().
|
||||
// Config/profile fields are copied under the manager lock; engine status is
|
||||
// read after unlock so a slow Engine.Running cannot block config saves.
|
||||
func (m *Manager) PollUI(includeSecrets bool) UIPoll {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
out := UIPoll{
|
||||
Status: Status{SystemProxy: m.sys.Enabled()},
|
||||
BinDir: m.binDir,
|
||||
SystemProxy: false,
|
||||
Subscription: "",
|
||||
Hy2: Hy2Options{Congestion: "bbr", BBRProfile: "standard"},
|
||||
LastError: m.lastError,
|
||||
}
|
||||
var eng Engine
|
||||
var profCopy *config.Profile
|
||||
if m.cfg != nil {
|
||||
out.Active = m.cfg.Active
|
||||
out.SystemProxy = m.cfg.SystemProxy
|
||||
@@ -424,16 +425,25 @@ func (m *Manager) PollUI(includeSecrets bool) UIPoll {
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.engine != nil && m.engine.Running() {
|
||||
eng = m.engine
|
||||
if m.profile != nil {
|
||||
p := *m.profile
|
||||
profCopy = &p
|
||||
}
|
||||
sys := m.sys
|
||||
m.mu.Unlock()
|
||||
|
||||
out.Status = Status{SystemProxy: sys.Enabled()}
|
||||
if eng != nil && eng.Running() {
|
||||
out.Status.Connected = true
|
||||
if m.profile != nil {
|
||||
out.Status.Profile = m.profile.Name
|
||||
out.Status.Protocol = m.profile.Protocol
|
||||
if profCopy != nil {
|
||||
out.Status.Profile = profCopy.Name
|
||||
out.Status.Protocol = profCopy.Protocol
|
||||
}
|
||||
if hp, ok := m.engine.LocalHTTPProxy(); ok {
|
||||
if hp, ok := eng.LocalHTTPProxy(); ok {
|
||||
out.Status.HTTPProxy = hp
|
||||
}
|
||||
if sp, ok := m.engine.LocalSOCKSProxy(); ok {
|
||||
if sp, ok := eng.LocalSOCKSProxy(); ok {
|
||||
out.Status.SOCKSProxy = sp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package corebin
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
cache = map[string]string{}
|
||||
group singleflight.Group
|
||||
)
|
||||
|
||||
// CacheKey identifies a resolved core binary for a binDir + protocol.
|
||||
@@ -48,15 +51,26 @@ func Invalidate() {
|
||||
// Resolve caches the result of fn under key.
|
||||
// Hot-path polls trust the cache until Invalidate (no per-poll os.Stat).
|
||||
// Embedded/virtual paths never touch the filesystem.
|
||||
// Concurrent cold misses for the same key share one fn call (singleflight).
|
||||
func Resolve(key string, fn func() (string, error)) (string, error) {
|
||||
if path, ok := Get(key); ok {
|
||||
return path, nil
|
||||
}
|
||||
path, err := fn()
|
||||
v, err, _ := group.Do(key, func() (any, error) {
|
||||
if path, ok := Get(key); ok {
|
||||
return path, nil
|
||||
}
|
||||
path, err := fn()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
Set(key, path)
|
||||
return path, nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
Set(key, path)
|
||||
path, _ := v.(string)
|
||||
return path, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ package corebin
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestResolveTrustsCacheWithoutStat(t *testing.T) {
|
||||
@@ -35,6 +38,36 @@ func TestResolveTrustsCacheWithoutStat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSingleflight(t *testing.T) {
|
||||
Invalidate()
|
||||
key := CacheKey(t.TempDir(), "naive")
|
||||
var calls atomic.Int32
|
||||
var started sync.WaitGroup
|
||||
started.Add(1)
|
||||
fn := func() (string, error) {
|
||||
calls.Add(1)
|
||||
started.Wait()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return "embedded-test-core", nil
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if _, err := Resolve(key, fn); err != nil {
|
||||
t.Errorf("resolve: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
started.Done()
|
||||
wg.Wait()
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("expected 1 cold resolve, got %d", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualEmbeddedAWG(t *testing.T) {
|
||||
Invalidate()
|
||||
key := CacheKey("/bin", "awg")
|
||||
|
||||
+16
-10
@@ -57,20 +57,26 @@ func PingAll(ctx context.Context, targets []Target) []Result {
|
||||
if len(targets) < workers {
|
||||
workers = len(targets)
|
||||
}
|
||||
sem := make(chan struct{}, workers)
|
||||
jobs := make(chan int, len(targets))
|
||||
for i := range targets {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i, t := range targets {
|
||||
for w := 0; w < workers; w++ {
|
||||
wg.Add(1)
|
||||
go func(i int, t Target) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
if strings.TrimSpace(t.Proxy) == "" {
|
||||
out[i] = Result{Name: t.Name, Error: "нет ссылки сервера"}
|
||||
return
|
||||
for i := range jobs {
|
||||
t := targets[i]
|
||||
if strings.TrimSpace(t.Proxy) == "" {
|
||||
out[i] = Result{Name: t.Name, Error: "нет ссылки сервера"}
|
||||
continue
|
||||
}
|
||||
out[i] = PingProfile(ctx, t.Name, t.Protocol, t.Proxy)
|
||||
}
|
||||
out[i] = PingProfile(ctx, t.Name, t.Protocol, t.Proxy)
|
||||
}(i, t)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const CurrentVersion = "3.8.2"
|
||||
|
||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||
const BuildNumber = 4
|
||||
const BuildNumber = 5
|
||||
|
||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||
|
||||
@@ -77,9 +77,9 @@ h = hashlib.sha256(binp.read_bytes()).hexdigest()
|
||||
zh = hashlib.sha256(zipp.read_bytes()).hexdigest() if zipp.exists() else ""
|
||||
dh = hashlib.sha256(dmgp.read_bytes()).hexdigest() if dmgp.exists() else ""
|
||||
notes = (
|
||||
f"Navis {ver}+{build}: единый GUI Windows/macOS на glaze + HTTP /api "
|
||||
f"(вместо WebView2 Bind); автоинкремент номера сборки при compile; "
|
||||
f"артефакты Windows с версией в имени (Navis-x.y.z.b.exe)."
|
||||
f"Navis {ver}+{build}: delta-poll UI (getState по rev); пауза опроса при скрытом окне; "
|
||||
f"cancel lifecycle dock/watchdog; invalidate/лимит hostCache; "
|
||||
f"PollUI без lock на Engine.Running; singleflight corebin; PingAll worker-pool."
|
||||
)
|
||||
paths = [
|
||||
Path('dist/update.json'),
|
||||
|
||||
+3
-3
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"version": "3.8.2",
|
||||
"notes": "Navis 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP /api (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (Navis-x.y.z.b.exe).",
|
||||
"notes": "Navis 3.8.2+5: delta-poll UI (getState по rev, без полного JSON если без изменений); пауза опроса при скрытом окне; cancel lifecycle dock/watchdog; invalidate/лимит hostCache; PollUI без lock на Engine.Running; singleflight corebin; PingAll worker-pool.",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"sha256": "68ce68b36f5027ef222b7aca2c74fc8529b852ea3f5e7cc7c1cdd71c5162a7b6",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9",
|
||||
"sha256": "68ce68b36f5027ef222b7aca2c74fc8529b852ea3f5e7cc7c1cdd71c5162a7b6",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
|
||||
+4
-4
@@ -4,13 +4,13 @@
|
||||
"Major": 3,
|
||||
"Minor": 8,
|
||||
"Patch": 2,
|
||||
"Build": 4
|
||||
"Build": 5
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 3,
|
||||
"Minor": 8,
|
||||
"Patch": 2,
|
||||
"Build": 4
|
||||
"Build": 5
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags": "00",
|
||||
@@ -21,12 +21,12 @@
|
||||
"StringFileInfo": {
|
||||
"CompanyName": "EvilFox",
|
||||
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||
"FileVersion": "3.8.2.4",
|
||||
"FileVersion": "3.8.2.5",
|
||||
"InternalName": "Navis",
|
||||
"LegalCopyright": "Copyright (c) EvilFox",
|
||||
"OriginalFilename": "Navis.exe",
|
||||
"ProductName": "Navis",
|
||||
"ProductVersion": "3.8.2.4",
|
||||
"ProductVersion": "3.8.2.5",
|
||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user