7e96370229
The URL test history update hook and the Clash mode update hook were single-slot: the API service's attached service overwrote the hook set by the daemon, so clients stopped receiving group updates. Replace both with multicast hook lists. Also share a single URL test history storage via context: Clash API looked it up under a key nobody registered and fell back to its own empty storage, so dashboards showed no delay once an API service was configured. Selector changes now notify through the shared storage, covering selections made from any API surface.
144 lines
2.9 KiB
Go
144 lines
2.9 KiB
Go
package adapter
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing/common/observable"
|
|
"github.com/sagernet/sing/common/varbin"
|
|
)
|
|
|
|
type ClashServer interface {
|
|
LifecycleService
|
|
Mode() string
|
|
ModeList() []string
|
|
SetMode(mode string)
|
|
AddModeUpdateHook(hook *observable.Subscriber[struct{}])
|
|
}
|
|
|
|
type URLTestHistory struct {
|
|
Time time.Time `json:"time"`
|
|
Delay uint16 `json:"delay"`
|
|
}
|
|
|
|
type V2RayServer interface {
|
|
LifecycleService
|
|
StatsService() ConnectionTracker
|
|
}
|
|
|
|
type CacheFile interface {
|
|
LifecycleService
|
|
|
|
StoreFakeIP() bool
|
|
FakeIPStorage
|
|
|
|
StoreRDRC() bool
|
|
RDRCStore
|
|
|
|
StoreDNS() bool
|
|
DNSCacheStore
|
|
|
|
SetDisableExpire(disableExpire bool)
|
|
SetOptimisticTimeout(timeout time.Duration)
|
|
|
|
LoadMode() string
|
|
StoreMode(mode string) error
|
|
LoadSelected(group string) string
|
|
StoreSelected(group string, selected string) error
|
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
|
StoreGroupExpand(group string, expand bool) error
|
|
LoadRuleSet(tag string) *SavedBinary
|
|
SaveRuleSet(tag string, set *SavedBinary) error
|
|
}
|
|
|
|
type SavedBinary struct {
|
|
Content []byte
|
|
LastUpdated time.Time
|
|
LastEtag string
|
|
}
|
|
|
|
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
|
var buffer bytes.Buffer
|
|
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = buffer.Write(s.Content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = buffer.WriteString(s.LastEtag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
|
reader := bytes.NewReader(data)
|
|
var version uint8
|
|
err := binary.Read(reader, binary.BigEndian, &version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
contentLength, err := binary.ReadUvarint(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Content = make([]byte, contentLength)
|
|
_, err = io.ReadFull(reader, s.Content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var lastUpdated int64
|
|
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.LastUpdated = time.Unix(lastUpdated, 0)
|
|
etagLength, err := binary.ReadUvarint(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
etagBytes := make([]byte, etagLength)
|
|
_, err = io.ReadFull(reader, etagBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.LastEtag = string(etagBytes)
|
|
return nil
|
|
}
|
|
|
|
type OutboundGroup interface {
|
|
Outbound
|
|
Now() string
|
|
All() []string
|
|
}
|
|
|
|
type URLTestGroup interface {
|
|
OutboundGroup
|
|
URLTest(ctx context.Context) (map[string]uint16, error)
|
|
}
|
|
|
|
func OutboundTag(detour Outbound) string {
|
|
if group, isGroup := detour.(OutboundGroup); isGroup {
|
|
return group.Now()
|
|
}
|
|
return detour.Tag()
|
|
}
|