Release 3.8.2.1: reliability, UI probe/logs, CI, signing gates.
Clear sysproxy on core death; probe+reconnect; subscription prune; Best ignores soft UDP; Windows tray; Android protocol gate; CI workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+138
-46
@@ -41,23 +41,27 @@ type App struct {
|
||||
}
|
||||
|
||||
type UIState struct {
|
||||
Connected bool `json:"connected"`
|
||||
Profile string `json:"profile,omitempty"`
|
||||
ActiveProfile string `json:"active_profile,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
HTTPProxy string `json:"http_proxy,omitempty"`
|
||||
SOCKSProxy string `json:"socks_proxy,omitempty"`
|
||||
SystemProxy bool `json:"system_proxy"`
|
||||
Proxy string `json:"proxy"`
|
||||
CoreReady bool `json:"core_ready"`
|
||||
CorePath string `json:"core_path,omitempty"`
|
||||
ConfigPath string `json:"config_path"`
|
||||
Profiles []config.ProfileInfo `json:"profiles"`
|
||||
Version string `json:"version"`
|
||||
Update update.Status `json:"update"`
|
||||
Pings []netcheck.Result `json:"pings"`
|
||||
Subscription string `json:"subscription_url"`
|
||||
Hy2 core.Hy2Options `json:"hy2"`
|
||||
Connected bool `json:"connected"`
|
||||
Profile string `json:"profile,omitempty"`
|
||||
ActiveProfile string `json:"active_profile,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
HTTPProxy string `json:"http_proxy,omitempty"`
|
||||
SOCKSProxy string `json:"socks_proxy,omitempty"`
|
||||
SystemProxy bool `json:"system_proxy"`
|
||||
Proxy string `json:"proxy"`
|
||||
CoreReady bool `json:"core_ready"`
|
||||
CorePath string `json:"core_path,omitempty"`
|
||||
ConfigPath string `json:"config_path"`
|
||||
Profiles []config.ProfileInfo `json:"profiles"`
|
||||
Version string `json:"version"`
|
||||
Update update.Status `json:"update"`
|
||||
Pings []netcheck.Result `json:"pings"`
|
||||
Subscription string `json:"subscription_url"`
|
||||
Hy2 core.Hy2Options `json:"hy2"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
ConnectOnLaunch bool `json:"connect_on_launch"`
|
||||
AutoReconnect bool `json:"auto_reconnect"`
|
||||
LogTail string `json:"log_tail,omitempty"`
|
||||
}
|
||||
|
||||
type PingBestResult struct {
|
||||
@@ -117,23 +121,29 @@ func (a *App) getState(includeSecrets bool) (UIState, error) {
|
||||
}
|
||||
|
||||
out := UIState{
|
||||
Connected: st.Connected,
|
||||
Profile: st.Profile,
|
||||
ActiveProfile: poll.Active,
|
||||
Protocol: string(st.Protocol),
|
||||
HTTPProxy: st.HTTPProxy,
|
||||
SOCKSProxy: st.SOCKSProxy,
|
||||
SystemProxy: poll.SystemProxy,
|
||||
Proxy: proxy,
|
||||
CoreReady: coreReady,
|
||||
CorePath: corePath,
|
||||
ConfigPath: cfgPath,
|
||||
Profiles: profiles,
|
||||
Version: update.DisplayVersion(),
|
||||
Update: upd,
|
||||
Pings: pings,
|
||||
Subscription: poll.Subscription,
|
||||
Hy2: hy2,
|
||||
Connected: st.Connected,
|
||||
Profile: st.Profile,
|
||||
ActiveProfile: poll.Active,
|
||||
Protocol: string(st.Protocol),
|
||||
HTTPProxy: st.HTTPProxy,
|
||||
SOCKSProxy: st.SOCKSProxy,
|
||||
SystemProxy: poll.SystemProxy,
|
||||
Proxy: proxy,
|
||||
CoreReady: coreReady,
|
||||
CorePath: corePath,
|
||||
ConfigPath: cfgPath,
|
||||
Profiles: profiles,
|
||||
Version: update.DisplayVersion(),
|
||||
Update: upd,
|
||||
Pings: pings,
|
||||
Subscription: poll.Subscription,
|
||||
Hy2: hy2,
|
||||
LastError: poll.LastError,
|
||||
ConnectOnLaunch: poll.ConnectOnLaunch,
|
||||
AutoReconnect: poll.AutoReconnect,
|
||||
}
|
||||
if includeSecrets && a.LogBuf != nil {
|
||||
out.LogTail = trimLogTail(a.LogBuf.String(), 4000)
|
||||
}
|
||||
if out.Protocol == "" {
|
||||
out.Protocol = string(poll.ActiveProtocol)
|
||||
@@ -144,6 +154,13 @@ func (a *App) getState(includeSecrets bool) (UIState, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func trimLogTail(s string, max int) string {
|
||||
if max <= 0 || len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return "…\n" + s[len(s)-max:]
|
||||
}
|
||||
|
||||
func resolveCoreCached(binDir string, proto config.Protocol) (path string, ready bool) {
|
||||
key := corebin.CacheKey(binDir, corebin.ProtoKey(string(proto)))
|
||||
var err error
|
||||
@@ -229,19 +246,93 @@ func (a *App) ConnectProfile(name string) error {
|
||||
}
|
||||
a.mu.Unlock()
|
||||
|
||||
// Do not hold a.mu across EnsureCore/Connect — getState polling would freeze the UI.
|
||||
if _, err := a.Mgr.EnsureCore(""); err != nil {
|
||||
return err
|
||||
var last error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if _, err := a.Mgr.EnsureCore(""); err != nil {
|
||||
a.Mgr.SetLastError(err.Error())
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
err := a.Mgr.Connect(ctx, "")
|
||||
cancel()
|
||||
if err != nil {
|
||||
a.Mgr.SetLastError(err.Error())
|
||||
last = err
|
||||
continue
|
||||
}
|
||||
pctx, pcancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
perr := a.Mgr.Probe(pctx, "")
|
||||
pcancel()
|
||||
if perr == nil {
|
||||
return nil
|
||||
}
|
||||
last = fmt.Errorf("туннель не прошёл проверку: %w", perr)
|
||||
a.Mgr.SetLastError(last.Error())
|
||||
_ = a.Mgr.Disconnect()
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
return a.Mgr.Connect(ctx, "")
|
||||
return last
|
||||
}
|
||||
|
||||
func (a *App) Disconnect() error {
|
||||
return a.Mgr.Disconnect()
|
||||
}
|
||||
|
||||
func (a *App) GetLogs() string {
|
||||
if a.LogBuf == nil {
|
||||
return ""
|
||||
}
|
||||
return trimLogTail(a.LogBuf.String(), 12000)
|
||||
}
|
||||
|
||||
func (a *App) ProbeTunnel() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
err := a.Mgr.Probe(ctx, "")
|
||||
if err != nil {
|
||||
a.Mgr.SetLastError(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) SavePrefs(connectOnLaunch, autoReconnect, systemProxy bool) error {
|
||||
return a.Mgr.SavePrefs(core.Prefs{
|
||||
ConnectOnLaunch: connectOnLaunch,
|
||||
AutoReconnect: autoReconnect,
|
||||
SystemProxy: systemProxy,
|
||||
})
|
||||
}
|
||||
|
||||
// StartBackground runs reconnect-on-crash and optional connect-on-launch.
|
||||
func (a *App) StartBackground() {
|
||||
go a.watchdogLoop()
|
||||
prefs := a.Mgr.Prefs()
|
||||
if prefs.ConnectOnLaunch {
|
||||
go func() {
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
if a.Mgr.Status().Connected {
|
||||
return
|
||||
}
|
||||
_ = a.Connect()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) watchdogLoop() {
|
||||
for {
|
||||
time.Sleep(2 * time.Second)
|
||||
if !a.Mgr.TakeUnexpectedDrop() {
|
||||
continue
|
||||
}
|
||||
prefs := a.Mgr.Prefs()
|
||||
if !prefs.AutoReconnect {
|
||||
continue
|
||||
}
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
_ = a.Connect()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) InstallCore() (string, error) {
|
||||
paths, err := a.Mgr.EnsureAllCores()
|
||||
if err != nil {
|
||||
@@ -316,12 +407,7 @@ func (a *App) PingBest(autoConnect bool) (PingBestResult, error) {
|
||||
}
|
||||
a.mu.Unlock()
|
||||
|
||||
if _, err := a.Mgr.EnsureCore(""); err != nil {
|
||||
return out, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
if err := a.Mgr.Connect(ctx, ""); err != nil {
|
||||
if err := a.ConnectProfile(""); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Connected = true
|
||||
@@ -527,6 +613,12 @@ func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
|
||||
return nil, a.SaveHy2(opts)
|
||||
case "importSubscription":
|
||||
return a.ImportSubscription(arg(args, 0, ""))
|
||||
case "getLogs":
|
||||
return a.GetLogs(), nil
|
||||
case "probeTunnel":
|
||||
return nil, a.ProbeTunnel()
|
||||
case "savePrefs":
|
||||
return nil, a.SavePrefs(arg(args, 0, false), arg(args, 1, false), arg(args, 2, true))
|
||||
case "quit":
|
||||
go func() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
Reference in New Issue
Block a user