From 14bbf6eedceb76230db7e97b34c7a1e38d2f1ba8 Mon Sep 17 00:00:00 2001 From: Dreamacro <8615343+Dreamacro@users.noreply.github.com> Date: Thu, 18 Feb 2021 23:41:50 +0800 Subject: [PATCH] Feature: support store group selected node to cache (enable by default) --- component/profile/cachefile/cache.go | 115 +++++++++++++++++++++++++++ component/profile/profile.go | 10 +++ config/config.go | 13 ++- constant/path.go | 4 + hub/executor/executor.go | 40 ++++++++++ hub/route/proxies.go | 2 + 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 component/profile/cachefile/cache.go create mode 100644 component/profile/profile.go diff --git a/component/profile/cachefile/cache.go b/component/profile/cachefile/cache.go new file mode 100644 index 00000000..cb23f87b --- /dev/null +++ b/component/profile/cachefile/cache.go @@ -0,0 +1,115 @@ +package cachefile + +import ( + "bytes" + "encoding/gob" + "io/ioutil" + "os" + "sync" + + "github.com/Dreamacro/clash/component/profile" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" +) + +var ( + initOnce sync.Once + fileMode os.FileMode = 0666 + defaultCache *CacheFile +) + +type cache struct { + Selected map[string]string +} + +// CacheFile store and update the cache file +type CacheFile struct { + path string + model *cache + enc *gob.Encoder + buf *bytes.Buffer + mux sync.Mutex +} + +func (c *CacheFile) SetSelected(group, selected string) { + if !profile.StoreSelected.Load() { + return + } + + c.mux.Lock() + defer c.mux.Unlock() + + model, err := c.element() + if err != nil { + log.Warnln("[CacheFile] read cache %s failed: %s", c.path, err.Error()) + return + } + + model.Selected[group] = selected + c.buf.Reset() + if err := c.enc.Encode(model); err != nil { + log.Warnln("[CacheFile] encode gob failed: %s", err.Error()) + return + } + + if err := ioutil.WriteFile(c.path, c.buf.Bytes(), fileMode); err != nil { + log.Warnln("[CacheFile] write cache to %s failed: %s", c.path, err.Error()) + return + } +} + +func (c *CacheFile) SelectedMap() map[string]string { + if !profile.StoreSelected.Load() { + return nil + } + + c.mux.Lock() + defer c.mux.Unlock() + + model, err := c.element() + if err != nil { + log.Warnln("[CacheFile] read cache %s failed: %s", c.path, err.Error()) + return nil + } + + mapping := map[string]string{} + for k, v := range model.Selected { + mapping[k] = v + } + return mapping +} + +func (c *CacheFile) element() (*cache, error) { + if c.model != nil { + return c.model, nil + } + + model := &cache{ + Selected: map[string]string{}, + } + + if buf, err := ioutil.ReadFile(c.path); err == nil { + bufReader := bytes.NewBuffer(buf) + dec := gob.NewDecoder(bufReader) + if err := dec.Decode(model); err != nil { + return nil, err + } + } + + c.model = model + return c.model, nil +} + +// Cache return singleton of CacheFile +func Cache() *CacheFile { + initOnce.Do(func() { + buf := &bytes.Buffer{} + defaultCache = &CacheFile{ + path: C.Path.Cache(), + buf: buf, + enc: gob.NewEncoder(buf), + } + }) + + return defaultCache +} diff --git a/component/profile/profile.go b/component/profile/profile.go new file mode 100644 index 00000000..ca9b1b88 --- /dev/null +++ b/component/profile/profile.go @@ -0,0 +1,10 @@ +package profile + +import ( + "go.uber.org/atomic" +) + +var ( + // StoreSelected is a global switch for storing selected proxy to cache + StoreSelected = atomic.NewBool(true) +) diff --git a/config/config.go b/config/config.go index 8408fb52..51faac91 100644 --- a/config/config.go +++ b/config/config.go @@ -73,6 +73,11 @@ type FallbackFilter struct { Domain []string `yaml:"domain"` } +// Profile config +type Profile struct { + StoreSelected bool `yaml:"store-selected"` +} + // Experimental config type Experimental struct{} @@ -82,6 +87,7 @@ type Config struct { DNS *DNS Experimental *Experimental Hosts *trie.DomainTrie + Profile *Profile Rules []C.Rule Users []auth.AuthUser Proxies map[string]C.Proxy @@ -129,6 +135,7 @@ type RawConfig struct { Hosts map[string]string `yaml:"hosts"` DNS RawDNS `yaml:"dns"` Experimental Experimental `yaml:"experimental"` + Profile Profile `yaml:"profile"` Proxy []map[string]interface{} `yaml:"proxies"` ProxyGroup []map[string]interface{} `yaml:"proxy-groups"` Rule []string `yaml:"rules"` @@ -145,7 +152,7 @@ func Parse(buf []byte) (*Config, error) { } func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { - // config with some default value + // config with default value rawCfg := &RawConfig{ AllowLan: false, BindAddress: "*", @@ -169,6 +176,9 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { "8.8.8.8", }, }, + Profile: Profile{ + StoreSelected: true, + }, } if err := yaml.Unmarshal(buf, &rawCfg); err != nil { @@ -182,6 +192,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config := &Config{} config.Experimental = &rawCfg.Experimental + config.Profile = &rawCfg.Profile general, err := parseGeneral(rawCfg) if err != nil { diff --git a/constant/path.go b/constant/path.go index 6f4af046..021721ec 100644 --- a/constant/path.go +++ b/constant/path.go @@ -56,3 +56,7 @@ func (p *path) Resolve(path string) string { func (p *path) MMDB() string { return P.Join(p.homeDir, "Country.mmdb") } + +func (p *path) Cache() string { + return P.Join(p.homeDir, ".cache") +} diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 4bf40f17..bea2a1ab 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -6,9 +6,13 @@ import ( "os" "sync" + "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/outboundgroup" "github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/profile" + "github.com/Dreamacro/clash/component/profile/cachefile" "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/config" @@ -72,6 +76,7 @@ func ApplyConfig(cfg *config.Config, force bool) { updateDNS(cfg.DNS) updateHosts(cfg.Hosts) updateExperimental(cfg) + updateProfile(cfg) } func GetGeneral() *config.General { @@ -209,3 +214,38 @@ func updateUsers(users []auth.AuthUser) { log.Infoln("Authentication of local server updated") } } + +func updateProfile(cfg *config.Config) { + profileCfg := cfg.Profile + + profile.StoreSelected.Store(profileCfg.StoreSelected) + if profileCfg.StoreSelected { + patchSelectGroup(cfg.Proxies) + } +} + +func patchSelectGroup(proxies map[string]C.Proxy) { + mapping := cachefile.Cache().SelectedMap() + if mapping == nil { + return + } + + for name, proxy := range proxies { + outbound, ok := proxy.(*outbound.Proxy) + if !ok { + continue + } + + selector, ok := outbound.ProxyAdapter.(*outboundgroup.Selector) + if !ok { + continue + } + + selected, exist := mapping[name] + if !exist { + continue + } + + selector.Set(selected) + } +} diff --git a/hub/route/proxies.go b/hub/route/proxies.go index e3cb066c..693f6c79 100644 --- a/hub/route/proxies.go +++ b/hub/route/proxies.go @@ -9,6 +9,7 @@ import ( "github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapters/outboundgroup" + "github.com/Dreamacro/clash/component/profile/cachefile" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/tunnel" @@ -91,6 +92,7 @@ func updateProxy(w http.ResponseWriter, r *http.Request) { return } + cachefile.Cache().SetSelected(proxy.Name(), req.Name) render.NoContent(w, r) }