package service import ( "context" "fmt" "sync" "time" "github.com/prism/proxy/internal/cache" "github.com/prism/proxy/internal/configbuilder" "github.com/prism/proxy/internal/core" "github.com/prism/proxy/internal/geodata" applog "github.com/prism/proxy/internal/log" "github.com/prism/proxy/internal/mihomoapi" "github.com/prism/proxy/internal/store" "github.com/prism/proxy/internal/subscription" ) type App struct { Store *store.Store Cache *cache.Cache Engine *core.Engine Builder *configbuilder.Builder Subscription *subscription.Service Logs *applog.Collector DataDir string LastReloadError string engineMu sync.RWMutex engineOK bool engineAt time.Time } func (a *App) ReloadEngine(ctx context.Context) error { geodata.InitHomeDir(a.DataDir) settings, err := a.Store.GetSettings(ctx) if err != nil { return err } geodata.ApplyMihomoURLs(geodata.ConfigFromSettings(settings)) yaml, err := a.Builder.Build(ctx) if err != nil { return err } apiPort := settings["mihomo_api_port"] if apiPort == "" { apiPort = "9090" } secret := settings["api_secret"] a.Engine.Configure("127.0.0.1:"+apiPort, secret) if err := a.applyConfig(ctx, yaml); err != nil { a.Logs.Add("warn", "prism", "full config failed, trying minimal: "+err.Error()) minimal, mErr := a.Builder.BuildMinimal(ctx) if mErr != nil { return err } if mErr := a.applyConfig(ctx, minimal); mErr != nil { a.LastReloadError = err.Error() if last := a.Cache.GetLastYAML(); last != nil { if fallbackErr := a.Engine.Reload(last); fallbackErr == nil { a.Logs.Add("warn", "prism", "kept previous engine config after reload failure") } } return err } a.LastReloadError = "rules simplified: " + err.Error() a.Logs.Add("warn", "prism", a.LastReloadError) return nil } return nil } func (a *App) applyConfig(ctx context.Context, yaml []byte) error { if err := a.Engine.Reload(yaml); err != nil { return err } a.LastReloadError = "" a.Cache.SetLastYAML(yaml) a.Logs.Add("info", "prism", "engine reloaded successfully") a.engineMu.Lock() a.engineOK = true a.engineAt = time.Now() a.engineMu.Unlock() _ = a.Store.AddAuditLog(ctx, "reload", "engine", "config applied") return nil } func (a *App) MihomoAPIBase(ctx context.Context) (string, string, error) { settings, err := a.Store.GetSettings(ctx) if err != nil { return "", "", err } port := settings["mihomo_api_port"] if port == "" { port = "9090" } return fmt.Sprintf("http://127.0.0.1:%s", port), settings["api_secret"], nil } func (a *App) EngineReachable(ctx context.Context) bool { if a.Engine.Running() { return true } a.engineMu.RLock() if time.Since(a.engineAt) < 15*time.Second { ok := a.engineOK a.engineMu.RUnlock() return ok } a.engineMu.RUnlock() ok := a.probeEngine(ctx) a.engineMu.Lock() a.engineOK = ok a.engineAt = time.Now() a.engineMu.Unlock() return ok } // EngineStatusCached returns last-known engine reachability without blocking on Mihomo. func (a *App) EngineStatusCached() bool { if a.Engine.Running() { return true } a.engineMu.RLock() defer a.engineMu.RUnlock() return a.engineOK } func (a *App) RefreshEngineStatusAsync() { go func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() ok := a.probeEngine(ctx) a.engineMu.Lock() a.engineOK = ok a.engineAt = time.Now() a.engineMu.Unlock() }() } func (a *App) probeEngine(ctx context.Context) bool { base, secret, err := a.MihomoAPIBase(ctx) if err != nil { return false } return mihomoapi.New(base, secret).Reachable() }