mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Wed Dec 3 19:42:53 CET 2025
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -1200,3 +1200,4 @@ Update On Sat Nov 29 19:37:01 CET 2025
|
||||
Update On Sun Nov 30 19:38:16 CET 2025
|
||||
Update On Mon Dec 1 19:44:38 CET 2025
|
||||
Update On Tue Dec 2 19:43:40 CET 2025
|
||||
Update On Wed Dec 3 19:42:45 CET 2025
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -14,17 +13,18 @@ import (
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
"github.com/saba-futai/sudoku/pkg/crypto"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/sudoku"
|
||||
)
|
||||
|
||||
type Sudoku struct {
|
||||
*Base
|
||||
option *SudokuOption
|
||||
table *sudoku.Table
|
||||
table *sudokuobfs.Table
|
||||
baseConf apis.ProtocolConfig
|
||||
}
|
||||
|
||||
@@ -72,12 +72,45 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||
return nil, C.ErrNotSupport
|
||||
if err := s.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := s.buildConfig(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := s.dialer.DialContext(ctx, "tcp", s.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
safeConnClose(c, err)
|
||||
}()
|
||||
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
|
||||
c, err = s.handshakeConn(c, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = sudoku.WritePreface(c); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, fmt.Errorf("send uot preface failed: %w", err)
|
||||
}
|
||||
|
||||
return newPacketConn(N.NewThreadSafePacketConn(sudoku.NewUoTPacketConn(c)), s), nil
|
||||
}
|
||||
|
||||
// SupportUOT implements C.ProxyAdapter
|
||||
func (s *Sudoku) SupportUOT() bool {
|
||||
return false // Sudoku protocol only supports TCP
|
||||
return true
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
@@ -101,14 +134,14 @@ func (s *Sudoku) buildConfig(metadata *C.Metadata) (*apis.ProtocolConfig, error)
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) {
|
||||
func (s *Sudoku) handshakeConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) {
|
||||
if !cfg.DisableHTTPMask {
|
||||
if err = httpmask.WriteRandomRequestHeader(rawConn, cfg.ServerAddress); err != nil {
|
||||
return nil, fmt.Errorf("write http mask failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
obfsConn := sudoku.NewConn(rawConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, false)
|
||||
obfsConn := sudokuobfs.NewConn(rawConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, false)
|
||||
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setup crypto failed: %w", err)
|
||||
@@ -120,7 +153,21 @@ func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.C
|
||||
return nil, fmt.Errorf("send handshake failed: %w", err)
|
||||
}
|
||||
|
||||
if err = writeTargetAddress(cConn, cfg.TargetAddress); err != nil {
|
||||
return cConn, nil
|
||||
}
|
||||
|
||||
func (s *Sudoku) streamConn(rawConn net.Conn, cfg *apis.ProtocolConfig) (_ net.Conn, err error) {
|
||||
cConn, err := s.handshakeConn(rawConn, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrBuf, err := sudoku.EncodeAddress(cfg.TargetAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode target address failed: %w", err)
|
||||
}
|
||||
|
||||
if _, err = cConn.Write(addrBuf); err != nil {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("send target address failed: %w", err)
|
||||
}
|
||||
@@ -153,7 +200,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
table := sudoku.NewTable(seed, tableType)
|
||||
table := sudokuobfs.NewTable(seed, tableType)
|
||||
log.Infoln("[Sudoku] Tables initialized (%s) in %v", tableType, time.Since(start))
|
||||
|
||||
defaultConf := apis.DefaultConfig()
|
||||
@@ -191,7 +238,7 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
||||
name: option.Name,
|
||||
addr: baseConf.ServerAddress,
|
||||
tp: C.Sudoku,
|
||||
udp: false,
|
||||
udp: true,
|
||||
tfo: option.TFO,
|
||||
mpTcp: option.MPTCP,
|
||||
iface: option.Interface,
|
||||
@@ -213,40 +260,3 @@ func buildSudokuHandshakePayload(key string) [16]byte {
|
||||
copy(payload[8:], hash[:8])
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeTargetAddress(w io.Writer, rawAddr string) error {
|
||||
host, portStr, err := net.SplitHostPort(rawAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
portInt, err := net.LookupPort("tcp", portStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
buf = append(buf, 0x01) // IPv4
|
||||
buf = append(buf, ip4...)
|
||||
} else {
|
||||
buf = append(buf, 0x04) // IPv6
|
||||
buf = append(buf, ip...)
|
||||
}
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return fmt.Errorf("domain too long")
|
||||
}
|
||||
buf = append(buf, 0x03) // domain
|
||||
buf = append(buf, byte(len(host)))
|
||||
buf = append(buf, host...)
|
||||
}
|
||||
|
||||
var portBytes [2]byte
|
||||
binary.BigEndian.PutUint16(portBytes[:], uint16(portInt))
|
||||
buf = append(buf, portBytes[:]...)
|
||||
|
||||
_, err = w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -150,6 +150,14 @@ func (f *Fallback) ForceSet(name string) {
|
||||
f.selected = name
|
||||
}
|
||||
|
||||
func (f *Fallback) Providers() []P.ProxyProvider {
|
||||
return f.providers
|
||||
}
|
||||
|
||||
func (f *Fallback) Proxies() []C.Proxy {
|
||||
return f.GetProxies(false)
|
||||
}
|
||||
|
||||
func NewFallback(option *GroupCommonOption, providers []P.ProxyProvider) *Fallback {
|
||||
return &Fallback{
|
||||
GroupBase: NewGroupBase(GroupBaseOption{
|
||||
|
||||
@@ -239,6 +239,18 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Providers() []P.ProxyProvider {
|
||||
return lb.providers
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Proxies() []C.Proxy {
|
||||
return lb.GetProxies(false)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Now() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func NewLoadBalance(option *GroupCommonOption, providers []P.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
|
||||
var strategyFn strategyFn
|
||||
switch strategy {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
//go:build android && cmfa
|
||||
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
P "github.com/metacubex/mihomo/constant/provider"
|
||||
)
|
||||
|
||||
type ProxyGroup interface {
|
||||
C.ProxyAdapter
|
||||
|
||||
Providers() []P.ProxyProvider
|
||||
Proxies() []C.Proxy
|
||||
Now() string
|
||||
}
|
||||
|
||||
func (f *Fallback) Providers() []P.ProxyProvider {
|
||||
return f.providers
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Providers() []P.ProxyProvider {
|
||||
return lb.providers
|
||||
}
|
||||
|
||||
func (f *Fallback) Proxies() []C.Proxy {
|
||||
return f.GetProxies(false)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Proxies() []C.Proxy {
|
||||
return lb.GetProxies(false)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Now() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Selector) Providers() []P.ProxyProvider {
|
||||
return s.providers
|
||||
}
|
||||
|
||||
func (s *Selector) Proxies() []C.Proxy {
|
||||
return s.GetProxies(false)
|
||||
}
|
||||
|
||||
func (u *URLTest) Providers() []P.ProxyProvider {
|
||||
return u.providers
|
||||
}
|
||||
|
||||
func (u *URLTest) Proxies() []C.Proxy {
|
||||
return u.GetProxies(false)
|
||||
}
|
||||
@@ -108,6 +108,14 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||
return proxies[0]
|
||||
}
|
||||
|
||||
func (s *Selector) Providers() []P.ProxyProvider {
|
||||
return s.providers
|
||||
}
|
||||
|
||||
func (s *Selector) Proxies() []C.Proxy {
|
||||
return s.GetProxies(false)
|
||||
}
|
||||
|
||||
func NewSelector(option *GroupCommonOption, providers []P.ProxyProvider) *Selector {
|
||||
return &Selector{
|
||||
GroupBase: NewGroupBase(GroupBaseOption{
|
||||
|
||||
@@ -185,6 +185,14 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *URLTest) Providers() []P.ProxyProvider {
|
||||
return u.providers
|
||||
}
|
||||
|
||||
func (u *URLTest) Proxies() []C.Proxy {
|
||||
return u.GetProxies(false)
|
||||
}
|
||||
|
||||
func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
|
||||
return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
P "github.com/metacubex/mihomo/constant/provider"
|
||||
)
|
||||
|
||||
type ProxyGroup interface {
|
||||
C.ProxyAdapter
|
||||
|
||||
Providers() []P.ProxyProvider
|
||||
Proxies() []C.Proxy
|
||||
Now() string
|
||||
Touch()
|
||||
|
||||
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error)
|
||||
}
|
||||
|
||||
var _ ProxyGroup = (*Fallback)(nil)
|
||||
var _ ProxyGroup = (*LoadBalance)(nil)
|
||||
var _ ProxyGroup = (*URLTest)(nil)
|
||||
var _ ProxyGroup = (*Selector)(nil)
|
||||
|
||||
type SelectAble interface {
|
||||
Set(string) error
|
||||
ForceSet(name string)
|
||||
|
||||
@@ -139,11 +139,6 @@ type ProxyAdapter interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Group interface {
|
||||
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error)
|
||||
Touch()
|
||||
}
|
||||
|
||||
type DelayHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/metacubex/fswatch v0.1.1
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9
|
||||
github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128
|
||||
github.com/metacubex/quic-go v0.55.1-0.20251203073212-6940cac967c2
|
||||
github.com/metacubex/randv2 v0.2.0
|
||||
github.com/metacubex/restls-client-go v0.1.7
|
||||
github.com/metacubex/sing v0.5.6
|
||||
@@ -43,7 +43,7 @@ require (
|
||||
github.com/mroth/weightedrand/v2 v2.1.0
|
||||
github.com/openacid/low v0.1.21
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
|
||||
github.com/saba-futai/sudoku v0.0.1-g
|
||||
github.com/saba-futai/sudoku v0.0.1-i
|
||||
github.com/sagernet/cors v1.2.1
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
||||
github.com/samber/lo v1.52.0
|
||||
|
||||
@@ -112,8 +112,8 @@ github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9 h1:7m3tRPrLpKOLOv
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128 h1:I1uvJl206/HbkzEAZpLgGkZgUveOZb+P+6oTUj7dN+o=
|
||||
github.com/metacubex/quic-go v0.55.1-0.20251024060151-bd465f127128/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c=
|
||||
github.com/metacubex/quic-go v0.55.1-0.20251203073212-6940cac967c2 h1:21KrRBqF5en0yXwwb5Vpptbeiiu3p7gD0G+RqNYvsvw=
|
||||
github.com/metacubex/quic-go v0.55.1-0.20251203073212-6940cac967c2/go.mod h1:1lktQFtCD17FZliVypbrDHwbsFSsmz2xz2TRXydvB5c=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
||||
@@ -171,8 +171,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/saba-futai/sudoku v0.0.1-g h1:4q6OuAA6COaRW+CgoQtdim5AUPzzm0uOkvbYpJnOaBE=
|
||||
github.com/saba-futai/sudoku v0.0.1-g/go.mod h1:2ZRzRwz93cS2K/o2yOG4CPJEltcvk5y6vbvUmjftGU0=
|
||||
github.com/saba-futai/sudoku v0.0.1-i h1:t6H875LSceXaEEwho84GU9OoLa4ieoBo3v+dxpFf4wc=
|
||||
github.com/saba-futai/sudoku v0.0.1-i/go.mod h1:FNtEAA44TSMvHI94o1kri/itbjvSMm1qCrbd0e6MTZY=
|
||||
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
||||
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
|
||||
@@ -31,7 +31,7 @@ func groupRouter() http.Handler {
|
||||
func getGroups(w http.ResponseWriter, r *http.Request) {
|
||||
var gs []C.Proxy
|
||||
for _, p := range tunnel.Proxies() {
|
||||
if _, ok := p.Adapter().(C.Group); ok {
|
||||
if _, ok := p.Adapter().(outboundgroup.ProxyGroup); ok {
|
||||
gs = append(gs, p)
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func getGroups(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func getGroup(w http.ResponseWriter, r *http.Request) {
|
||||
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
||||
if _, ok := proxy.Adapter().(C.Group); ok {
|
||||
if _, ok := proxy.Adapter().(outboundgroup.ProxyGroup); ok {
|
||||
render.JSON(w, r, proxy)
|
||||
return
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func getGroup(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func getGroupDelay(w http.ResponseWriter, r *http.Request) {
|
||||
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
||||
group, ok := proxy.Adapter().(C.Group)
|
||||
group, ok := proxy.Adapter().(outboundgroup.ProxyGroup)
|
||||
if !ok {
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, ErrNotFound)
|
||||
|
||||
@@ -47,8 +47,10 @@ func SetEmbedMode(embed bool) {
|
||||
}
|
||||
|
||||
type Traffic struct {
|
||||
Up int64 `json:"up"`
|
||||
Down int64 `json:"down"`
|
||||
Up int64 `json:"up"`
|
||||
Down int64 `json:"down"`
|
||||
UpTotal int64 `json:"upTotal"`
|
||||
DownTotal int64 `json:"downTotal"`
|
||||
}
|
||||
|
||||
type Memory struct {
|
||||
@@ -380,9 +382,12 @@ func traffic(w http.ResponseWriter, r *http.Request) {
|
||||
for range tick.C {
|
||||
buf.Reset()
|
||||
up, down := t.Now()
|
||||
upTotal, downTotal := t.Total()
|
||||
if err := json.NewEncoder(buf).Encode(Traffic{
|
||||
Up: up,
|
||||
Down: down,
|
||||
Up: up,
|
||||
Down: down,
|
||||
UpTotal: upTotal,
|
||||
DownTotal: downTotal,
|
||||
}); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
@@ -10,7 +12,9 @@ import (
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
"github.com/metacubex/mihomo/transport/sudoku"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
@@ -43,19 +47,74 @@ func (l *Listener) Close() error {
|
||||
}
|
||||
|
||||
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
tunnelConn, target, err := apis.ServerHandshake(conn, &l.protoConf)
|
||||
session, err := sudoku.ServerHandshake(conn, &l.protoConf)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
targetAddr := socks5.ParseAddr(target)
|
||||
if targetAddr == nil {
|
||||
_ = tunnelConn.Close()
|
||||
return
|
||||
switch session.Type {
|
||||
case sudoku.SessionTypeUoT:
|
||||
l.handleUoTSession(session.Conn, tunnel, additions...)
|
||||
default:
|
||||
targetAddr := socks5.ParseAddr(session.Target)
|
||||
if targetAddr == nil {
|
||||
_ = session.Conn.Close()
|
||||
return
|
||||
}
|
||||
tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, session.Conn, C.SUDOKU, additions...))
|
||||
}
|
||||
}
|
||||
|
||||
tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, tunnelConn, C.SUDOKU, additions...))
|
||||
func (l *Listener) handleUoTSession(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
writer := sudoku.NewUoTPacketConn(conn)
|
||||
remoteAddr := conn.RemoteAddr()
|
||||
|
||||
for {
|
||||
addrStr, payload, err := sudoku.ReadDatagram(conn)
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
log.Debugln("[Sudoku][UoT] session closed: %v", err)
|
||||
}
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
target := socks5.ParseAddr(addrStr)
|
||||
if target == nil {
|
||||
log.Debugln("[Sudoku][UoT] drop invalid target: %s", addrStr)
|
||||
continue
|
||||
}
|
||||
|
||||
packet := &uotPacket{
|
||||
payload: payload,
|
||||
writer: writer,
|
||||
rAddr: remoteAddr,
|
||||
}
|
||||
tunnel.HandleUDPPacket(inbound.NewPacket(target, packet, C.SUDOKU, additions...))
|
||||
}
|
||||
}
|
||||
|
||||
type uotPacket struct {
|
||||
payload []byte
|
||||
writer *sudoku.UoTPacketConn
|
||||
rAddr net.Addr
|
||||
}
|
||||
|
||||
func (p *uotPacket) Data() []byte {
|
||||
return p.payload
|
||||
}
|
||||
|
||||
func (p *uotPacket) WriteBack(b []byte, addr net.Addr) (int, error) {
|
||||
return p.writer.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
func (p *uotPacket) Drop() {
|
||||
p.payload = nil
|
||||
}
|
||||
|
||||
func (p *uotPacket) LocalAddr() net.Addr {
|
||||
return p.rAddr
|
||||
}
|
||||
|
||||
func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
|
||||
|
||||
91
clash-meta/transport/sudoku/address.go
Normal file
91
clash-meta/transport/sudoku/address.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func EncodeAddress(rawAddr string) ([]byte, error) {
|
||||
host, portStr, err := net.SplitHostPort(rawAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
portInt, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
buf = append(buf, 0x01) // IPv4
|
||||
buf = append(buf, ip4...)
|
||||
} else {
|
||||
buf = append(buf, 0x04) // IPv6
|
||||
buf = append(buf, ip...)
|
||||
}
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return nil, fmt.Errorf("domain too long")
|
||||
}
|
||||
buf = append(buf, 0x03) // domain
|
||||
buf = append(buf, byte(len(host)))
|
||||
buf = append(buf, host...)
|
||||
}
|
||||
|
||||
var portBytes [2]byte
|
||||
binary.BigEndian.PutUint16(portBytes[:], uint16(portInt))
|
||||
buf = append(buf, portBytes[:]...)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func DecodeAddress(r io.Reader) (string, error) {
|
||||
var atyp [1]byte
|
||||
if _, err := io.ReadFull(r, atyp[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch atyp[0] {
|
||||
case 0x01: // IPv4
|
||||
var ipBuf [net.IPv4len]byte
|
||||
if _, err := io.ReadFull(r, ipBuf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var portBuf [2]byte
|
||||
if _, err := io.ReadFull(r, portBuf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil
|
||||
case 0x04: // IPv6
|
||||
var ipBuf [net.IPv6len]byte
|
||||
if _, err := io.ReadFull(r, ipBuf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var portBuf [2]byte
|
||||
if _, err := io.ReadFull(r, portBuf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return net.JoinHostPort(net.IP(ipBuf[:]).String(), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil
|
||||
case 0x03: // domain
|
||||
var lengthBuf [1]byte
|
||||
if _, err := io.ReadFull(r, lengthBuf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
l := int(lengthBuf[0])
|
||||
hostBuf := make([]byte, l)
|
||||
if _, err := io.ReadFull(r, hostBuf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var portBuf [2]byte
|
||||
if _, err := io.ReadFull(r, portBuf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return net.JoinHostPort(string(hostBuf), fmt.Sprint(binary.BigEndian.Uint16(portBuf[:]))), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown address type: %d", atyp[0])
|
||||
}
|
||||
}
|
||||
147
clash-meta/transport/sudoku/handshake.go
Normal file
147
clash-meta/transport/sudoku/handshake.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
"github.com/saba-futai/sudoku/pkg/crypto"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/httpmask"
|
||||
"github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type SessionType int
|
||||
|
||||
const (
|
||||
SessionTypeTCP SessionType = iota
|
||||
SessionTypeUoT
|
||||
)
|
||||
|
||||
type ServerSession struct {
|
||||
Conn net.Conn
|
||||
Type SessionType
|
||||
Target string
|
||||
}
|
||||
|
||||
type bufferedConn struct {
|
||||
net.Conn
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
func (bc *bufferedConn) Read(p []byte) (int, error) {
|
||||
return bc.r.Read(p)
|
||||
}
|
||||
|
||||
type preBufferedConn struct {
|
||||
net.Conn
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (p *preBufferedConn) Read(b []byte) (int, error) {
|
||||
if len(p.buf) > 0 {
|
||||
n := copy(b, p.buf)
|
||||
p.buf = p.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
if p.Conn == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return p.Conn.Read(b)
|
||||
}
|
||||
|
||||
func absInt64(v int64) int64 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
|
||||
func ServerHandshake(rawConn net.Conn, cfg *apis.ProtocolConfig) (*ServerSession, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config is required")
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second
|
||||
if handshakeTimeout <= 0 {
|
||||
handshakeTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
bufReader := bufio.NewReader(rawConn)
|
||||
if !cfg.DisableHTTPMask {
|
||||
if peek, _ := bufReader.Peek(4); len(peek) == 4 && string(peek) == "POST" {
|
||||
if _, err := httpmask.ConsumeHeader(bufReader); err != nil {
|
||||
return nil, fmt.Errorf("invalid http header: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout))
|
||||
bConn := &bufferedConn{
|
||||
Conn: rawConn,
|
||||
r: bufReader,
|
||||
}
|
||||
sConn := sudoku.NewConn(bConn, cfg.Table, cfg.PaddingMin, cfg.PaddingMax, true)
|
||||
cConn, err := crypto.NewAEADConn(sConn, cfg.Key, cfg.AEADMethod)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("crypto setup failed: %w", err)
|
||||
}
|
||||
|
||||
var handshakeBuf [16]byte
|
||||
if _, err := io.ReadFull(cConn, handshakeBuf[:]); err != nil {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("read handshake failed: %w", err)
|
||||
}
|
||||
|
||||
ts := int64(binary.BigEndian.Uint64(handshakeBuf[:8]))
|
||||
if absInt64(time.Now().Unix()-ts) > 60 {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("timestamp skew detected")
|
||||
}
|
||||
|
||||
sConn.StopRecording()
|
||||
|
||||
firstByte := make([]byte, 1)
|
||||
if _, err := io.ReadFull(cConn, firstByte); err != nil {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("read first byte failed: %w", err)
|
||||
}
|
||||
|
||||
if firstByte[0] == UoTMagicByte {
|
||||
version := make([]byte, 1)
|
||||
if _, err := io.ReadFull(cConn, version); err != nil {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("read uot version failed: %w", err)
|
||||
}
|
||||
if version[0] != uotVersion {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("unsupported uot version: %d", version[0])
|
||||
}
|
||||
rawConn.SetReadDeadline(time.Time{})
|
||||
return &ServerSession{Conn: cConn, Type: SessionTypeUoT}, nil
|
||||
}
|
||||
|
||||
prefixed := &preBufferedConn{Conn: cConn, buf: firstByte}
|
||||
target, err := DecodeAddress(prefixed)
|
||||
if err != nil {
|
||||
cConn.Close()
|
||||
return nil, fmt.Errorf("read target address failed: %w", err)
|
||||
}
|
||||
|
||||
rawConn.SetReadDeadline(time.Time{})
|
||||
log.Debugln("[Sudoku] incoming TCP session target: %s", target)
|
||||
return &ServerSession{
|
||||
Conn: prefixed,
|
||||
Type: SessionTypeTCP,
|
||||
Target: target,
|
||||
}, nil
|
||||
}
|
||||
158
clash-meta/transport/sudoku/uot.go
Normal file
158
clash-meta/transport/sudoku/uot.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
const (
|
||||
UoTMagicByte byte = 0xEE
|
||||
uotVersion = 0x01
|
||||
maxUoTPayload = 64 * 1024
|
||||
)
|
||||
|
||||
// WritePreface writes the UDP-over-TCP marker and version.
|
||||
func WritePreface(w io.Writer) error {
|
||||
_, err := w.Write([]byte{UoTMagicByte, uotVersion})
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteDatagram sends a single UDP datagram frame over a reliable stream.
|
||||
func WriteDatagram(w io.Writer, addr string, payload []byte) error {
|
||||
addrBuf, err := EncodeAddress(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode address: %w", err)
|
||||
}
|
||||
|
||||
if addrLen := len(addrBuf); addrLen == 0 || addrLen > maxUoTPayload {
|
||||
return fmt.Errorf("address too long: %d", len(addrBuf))
|
||||
}
|
||||
if payloadLen := len(payload); payloadLen > maxUoTPayload {
|
||||
return fmt.Errorf("payload too large: %d", payloadLen)
|
||||
}
|
||||
|
||||
var header [4]byte
|
||||
binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf)))
|
||||
binary.BigEndian.PutUint16(header[2:], uint16(len(payload)))
|
||||
|
||||
if _, err := w.Write(header[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(addrBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadDatagram parses a single UDP datagram frame from the reliable stream.
|
||||
func ReadDatagram(r io.Reader) (string, []byte, error) {
|
||||
var header [4]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
addrLen := int(binary.BigEndian.Uint16(header[:2]))
|
||||
payloadLen := int(binary.BigEndian.Uint16(header[2:]))
|
||||
|
||||
if addrLen <= 0 || addrLen > maxUoTPayload {
|
||||
return "", nil, fmt.Errorf("invalid address length: %d", addrLen)
|
||||
}
|
||||
if payloadLen < 0 || payloadLen > maxUoTPayload {
|
||||
return "", nil, fmt.Errorf("invalid payload length: %d", payloadLen)
|
||||
}
|
||||
|
||||
addrBuf := make([]byte, addrLen)
|
||||
if _, err := io.ReadFull(r, addrBuf); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
addr, err := DecodeAddress(bytes.NewReader(addrBuf))
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("decode address: %w", err)
|
||||
}
|
||||
|
||||
payload := make([]byte, payloadLen)
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return addr, payload, nil
|
||||
}
|
||||
|
||||
// UoTPacketConn adapts a net.Conn with the Sudoku UoT framing to net.PacketConn.
|
||||
type UoTPacketConn struct {
|
||||
conn net.Conn
|
||||
writeMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewUoTPacketConn(conn net.Conn) *UoTPacketConn {
|
||||
return &UoTPacketConn{conn: conn}
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
for {
|
||||
addrStr, payload, err := ReadDatagram(c.conn)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if len(payload) > len(p) {
|
||||
return 0, nil, io.ErrShortBuffer
|
||||
}
|
||||
|
||||
host, port, _ := net.SplitHostPort(addrStr)
|
||||
portInt, _ := strconv.ParseUint(port, 10, 16)
|
||||
ip, err := netip.ParseAddr(host)
|
||||
if err != nil { // disallow domain addr at here, just ignore
|
||||
log.Debugln("[Sudoku][UoT] discard datagram with invalid address %s: %v", addrStr, err)
|
||||
continue
|
||||
}
|
||||
udpAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip.Unmap(), uint16(portInt)))
|
||||
|
||||
copy(p, payload)
|
||||
return len(payload), udpAddr, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
if addr == nil {
|
||||
return 0, errors.New("address is nil")
|
||||
}
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
if err := WriteDatagram(c.conn, addr.String(), p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UoTPacketConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
@@ -72,6 +72,10 @@ func (m *Manager) Now() (up int64, down int64) {
|
||||
return m.uploadBlip.Load(), m.downloadBlip.Load()
|
||||
}
|
||||
|
||||
func (m *Manager) Total() (up, down int64) {
|
||||
return m.uploadTotal.Load(), m.downloadTotal.Load()
|
||||
}
|
||||
|
||||
func (m *Manager) Memory() uint64 {
|
||||
m.updateMemory()
|
||||
return m.memory
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build android && cmfa
|
||||
|
||||
package statistic
|
||||
|
||||
func (m *Manager) Total() (up, down int64) {
|
||||
return m.uploadTotal.Load(), m.downloadTotal.Load()
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 1,
|
||||
"latest": {
|
||||
"mihomo": "v1.19.17",
|
||||
"mihomo_alpha": "alpha-9a5e506",
|
||||
"mihomo_alpha": "alpha-fdb7cb1",
|
||||
"clash_rs": "v0.9.2",
|
||||
"clash_premium": "2023-09-05-gdcc8d87",
|
||||
"clash_rs_alpha": "0.9.2-alpha+sha.e1f8fbb"
|
||||
@@ -69,5 +69,5 @@
|
||||
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
|
||||
}
|
||||
},
|
||||
"updated_at": "2025-12-01T22:21:34.119Z"
|
||||
"updated_at": "2025-12-02T22:21:02.066Z"
|
||||
}
|
||||
|
||||
24
clash-verge-rev/.github/workflows/alpha.yml
vendored
24
clash-verge-rev/.github/workflows/alpha.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check tag and package.json version
|
||||
id: check_tag
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete Old Alpha Tags Except Latest
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
needs: delete_old_assets
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
@@ -275,7 +275,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -305,9 +305,9 @@ jobs:
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -375,9 +375,9 @@ jobs:
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -491,7 +491,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
@@ -503,9 +503,9 @@ jobs:
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
130
clash-verge-rev/.github/workflows/autobuild.yml
vendored
130
clash-verge-rev/.github/workflows/autobuild.yml
vendored
@@ -31,22 +31,22 @@ jobs:
|
||||
if: ${{ needs.check_commit.outputs.should_run == 'true' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
run: bash ./scripts/extract_update_logs.sh
|
||||
shell: bash
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -77,20 +77,20 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
body_path: release.txt
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
|
||||
clean_old_assets:
|
||||
name: Clean Old Release Assets
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -156,11 +156,13 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
prefix-key: "v1-rust"
|
||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
@@ -177,24 +179,24 @@ jobs:
|
||||
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
@@ -218,12 +220,12 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
# APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
# APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tagName: ${{ env.TAG_NAME }}
|
||||
releaseName: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
@@ -251,7 +253,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -265,30 +267,32 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
prefix-key: "v1-rust"
|
||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
@@ -385,8 +389,8 @@ jobs:
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
target/${{ matrix.target }}/release/bundle/rpm/*.rpm
|
||||
|
||||
autobuild-x86-arm-windows_webview2:
|
||||
name: Autobuild x86 and ARM Windows with WebView2
|
||||
@@ -405,7 +409,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
@@ -413,30 +417,32 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
cache-all-crates: true
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
prefix-key: "v1-rust"
|
||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v4.2.0
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache pnpm store
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
@@ -475,19 +481,19 @@ jobs:
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
@@ -500,7 +506,7 @@ jobs:
|
||||
name: "Clash Verge Rev ${{ env.TAG_CHANNEL }}"
|
||||
prerelease: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
files: target/${{ matrix.target }}/release/bundle/nsis/*setup*
|
||||
|
||||
- name: Portable Bundle
|
||||
run: pnpm portable-fixed-webview2 ${{ matrix.target }} --${{ env.TAG_NAME }}
|
||||
@@ -519,7 +525,7 @@ jobs:
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
@@ -527,11 +533,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
@@ -566,20 +572,20 @@ jobs:
|
||||
|
||||
### Windows (不再支持Win7)
|
||||
#### 正常版本(推荐)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup_windows.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup_windows.exe)
|
||||
- [64位(常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64-setup.exe) | [ARM64(不常用)](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64-setup.exe)
|
||||
|
||||
#### 内置Webview2版(体积较大,仅在企业版系统或无法安装webview2时使用)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_fixed_webview2-setup.exe) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64_fixed_webview2-setup.exe)
|
||||
|
||||
### macOS
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64_darwin.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64_darwin.dmg)
|
||||
- [Apple M芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_aarch64.dmg) | [Intel芯片](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_x64.dmg)
|
||||
|
||||
### Linux
|
||||
#### DEB包(Debian系) 使用 apt ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||
|
||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||
|
||||
### FAQ
|
||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
autobuild_version: ${{ steps.check.outputs.autobuild_version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
echo "🔍 Finding last commit with Tauri-related changes..."
|
||||
|
||||
# Define patterns for Tauri-related files
|
||||
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
|
||||
TAURI_PATTERNS="src/ src-tauri/src src-tauri/Cargo.toml Cargo.lock src-tauri/tauri.*.conf.json src-tauri/build.rs src-tauri/capabilities"
|
||||
|
||||
# Get the last commit that changed any of these patterns (excluding build artifacts)
|
||||
LAST_TAURI_COMMIT=""
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
needs: check_current_version
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Clean old assets from release
|
||||
env:
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -41,9 +41,9 @@ jobs:
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
49
clash-verge-rev/.github/workflows/dev.yml
vendored
49
clash-verge-rev/.github/workflows/dev.yml
vendored
@@ -70,20 +70,22 @@ jobs:
|
||||
|
||||
- name: Checkout Repository
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: dtolnay/rust-toolchain@1.91.0
|
||||
|
||||
- name: Rust Cache
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: false
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: "v1-rust"
|
||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
shared-key: autobuild-shared
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
|
||||
@@ -99,11 +101,20 @@ jobs:
|
||||
|
||||
- name: Install Node
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "24.11.1"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Pnpm Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: "pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Pnpm install and check
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
@@ -130,36 +141,36 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
# APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
# APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
# APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
# APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
|
||||
|
||||
- name: Upload Artifacts (macOS)
|
||||
if: matrix.os == 'macos-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||
path: target/${{ matrix.target }}/release/bundle/dmg/*.dmg
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Windows)
|
||||
if: matrix.os == 'windows-latest' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
|
||||
path: target/${{ matrix.target }}/release/bundle/nsis/*.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04' && github.event.inputs[matrix.input] == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.target }}
|
||||
path: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
if-no-files-found: error
|
||||
|
||||
75
clash-verge-rev/.github/workflows/frontend-check.yml
vendored
Normal file
75
clash-verge-rev/.github/workflows/frontend-check.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Frontend Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HUSKY: 0
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check frontend changes
|
||||
id: check_frontend
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
frontend:
|
||||
- 'src/**'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'eslint.config.ts'
|
||||
- 'tsconfig.json'
|
||||
- 'vite.config.*'
|
||||
|
||||
- name: Skip if no frontend changes
|
||||
if: steps.check_frontend.outputs.frontend != 'true'
|
||||
run: echo "No frontend changes, skipping frontend checks."
|
||||
|
||||
- name: Install pnpm
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
with:
|
||||
node-version: "24.11.1"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}"
|
||||
restore-keys: |
|
||||
pnpm-shared-stable-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
|
||||
- name: Run Prettier check
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run ESLint
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run TypeScript typecheck
|
||||
if: steps.check_frontend.outputs.frontend == 'true'
|
||||
run: pnpm typecheck
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
echo "Manual trigger detected: skipping changes check and running clippy."
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -58,11 +58,13 @@ jobs:
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
save-if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
prefix-key: "v1-rust"
|
||||
key: "rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}"
|
||||
workspaces: |
|
||||
. -> target
|
||||
cache-all-crates: true
|
||||
save-if: false
|
||||
shared-key: autobuild-${{ runner.os }}-${{ matrix.target }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
cache-workspace-crates: true
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
|
||||
52
clash-verge-rev/.github/workflows/release.yml
vendored
52
clash-verge-rev/.github/workflows/release.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
needs: [release, release-for-linux-arm, release-for-fixed-webview2]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -186,9 +186,9 @@ jobs:
|
||||
echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
|
||||
- name: Tauri build
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.5.23
|
||||
uses: tauri-apps/tauri-action@v0.6.0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -240,7 +240,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -255,9 +255,9 @@ jobs:
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -364,7 +364,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
@@ -376,9 +376,9 @@ jobs:
|
||||
save-if: false
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
- name: Tauri build
|
||||
id: build
|
||||
# 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本
|
||||
uses: tauri-apps/tauri-action@v0.5.23
|
||||
uses: tauri-apps/tauri-action@v0.6.0
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -412,19 +412,19 @@ jobs:
|
||||
|
||||
- name: Rename
|
||||
run: |
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
|
||||
Rename-Item $file.FullName $newName
|
||||
}
|
||||
|
||||
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
$files = Get-ChildItem ".\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe.sig"
|
||||
foreach ($file in $files) {
|
||||
$newName = $file.Name -replace "-setup\.exe\.sig$", "_fixed_webview2-setup.exe.sig"
|
||||
Rename-Item $file.FullName $newName
|
||||
@@ -452,12 +452,12 @@ jobs:
|
||||
needs: [update_tag]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -478,12 +478,12 @@ jobs:
|
||||
needs: [update_tag]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
needs: [update_tag, release-update]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get Version
|
||||
@@ -535,7 +535,7 @@ jobs:
|
||||
]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch UPDATE logs
|
||||
id: fetch_update_logs
|
||||
@@ -543,9 +543,9 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check Rust changes
|
||||
id: check_rust
|
||||
@@ -39,42 +39,6 @@ jobs:
|
||||
if: steps.check_rust.outputs.rust == 'true'
|
||||
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check Web changes
|
||||
id: check_web
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
web:
|
||||
- 'src/**'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- '**/*.json'
|
||||
|
||||
- name: Skip if no Web changes
|
||||
if: steps.check_web.outputs.web != 'true'
|
||||
run: echo "No web changes, skipping prettier."
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- run: corepack enable
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
- run: pnpm install --frozen-lockfile
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
- run: pnpm format:check
|
||||
if: steps.check_web.outputs.web == 'true'
|
||||
|
||||
# taplo:
|
||||
# name: taplo (.toml files)
|
||||
# runs-on: ubuntu-latest
|
||||
12
clash-verge-rev/.github/workflows/updater.yml
vendored
12
clash-verge-rev/.github/workflows/updater.yml
vendored
@@ -10,12 +10,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -34,12 +34,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "24.11.1"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
@@ -40,4 +40,3 @@ else
|
||||
fi
|
||||
|
||||
echo "[pre-push] All checks passed."
|
||||
exit 0
|
||||
|
||||
@@ -1,151 +1,132 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
|
||||
Thank you for your interest in contributing to **Clash Verge Rev**! This guide provides instructions to help you set up your development environment and start contributing effectively.
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
We welcome translations and improvements to existing locales. Please follow the detailed guidelines in [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md) for instructions on extracting strings, file naming conventions, testing translations, and submitting translation PRs.
|
||||
We welcome translations and improvements to existing locales. For details on contributing translations, please see [CONTRIBUTING_i18n.md](docs/CONTRIBUTING_i18n.md).
|
||||
|
||||
## Development Setup
|
||||
|
||||
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
|
||||
Before contributing, you need to set up your development environment. Follow the steps below carefully.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/start/prerequisites/) to install them on your system.
|
||||
1. **Install Rust and Node.js**
|
||||
Our project requires both Rust and Node.js. Follow the official installation instructions [here](https://tauri.app/start/prerequisites/).
|
||||
|
||||
### Setup for Windows Users
|
||||
### Windows Users
|
||||
|
||||
> [!NOTE]
|
||||
> **If you are using a Windows ARM device, you additionally need to install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the environment variable.**
|
||||
>
|
||||
> Because the `ring` crate is compiled based on `clang` under Windows ARM.
|
||||
> [!NOTE]
|
||||
> **Windows ARM users must also install [LLVM](https://github.com/llvm/llvm-project/releases) (including clang) and set the corresponding environment variables.**
|
||||
> The `ring` crate depends on `clang` when building on Windows ARM.
|
||||
|
||||
If you're a Windows user, you may need to perform some additional steps:
|
||||
Additional steps for Windows:
|
||||
|
||||
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
|
||||
- The gnu `patch` tool should be installed
|
||||
- Ensure Rust and Node.js are added to your system `PATH`.
|
||||
|
||||
When you setup `Rust` environment, Only use toolchain with `Windows MSVC` , to change settings follow command:
|
||||
- Install the GNU `patch` tool.
|
||||
|
||||
```shell
|
||||
- Use the MSVC toolchain for Rust:
|
||||
|
||||
```bash
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
rustup set default-host x86_64-pc-windows-msvc
|
||||
```
|
||||
|
||||
### Install Node.js Package
|
||||
### Install Node.js Package Manager
|
||||
|
||||
After installing Rust and Node.js, install the necessary Node.js and Node Package Manager:
|
||||
Enable `corepack`:
|
||||
|
||||
```shell
|
||||
npm install pnpm -g
|
||||
```bash
|
||||
corepack enable
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
### Install Project Dependencies
|
||||
|
||||
Install node packages
|
||||
Node.js dependencies:
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Install apt packages ONLY for Ubuntu
|
||||
Ubuntu-only system packages:
|
||||
|
||||
```shell
|
||||
apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
```bash
|
||||
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
```
|
||||
|
||||
### Download the Mihomo Core Binary
|
||||
### Download the Mihomo Core Binary (Automatic)
|
||||
|
||||
You have two options for downloading the clash binary:
|
||||
|
||||
- Automatically download it via the provided script:
|
||||
|
||||
```shell
|
||||
pnpm run prebuild
|
||||
# Use '--force' or '-f' to update both the Mihomo core version
|
||||
# and the Clash Verge Rev service version to the latest available.
|
||||
pnpm run prebuild --force
|
||||
```
|
||||
|
||||
- Manually download it from the [Mihomo release](https://github.com/MetaCubeX/mihomo/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
|
||||
```bash
|
||||
pnpm run prebuild
|
||||
pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries
|
||||
```
|
||||
|
||||
### Run the Development Server
|
||||
|
||||
To run the development server, use the following command:
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
# If an app instance already exists, use a different command
|
||||
pnpm dev:diff
|
||||
# To using tauri built-in dev tool
|
||||
pnpm dev:tauri
|
||||
```bash
|
||||
pnpm dev # Standard
|
||||
pnpm dev:diff # If an app instance already exists
|
||||
pnpm dev:tauri # Run Tauri development mode
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
|
||||
To build this project:
|
||||
Standard build:
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
For a faster build, use the following command
|
||||
Fast build for testing:
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pnpm build:fast
|
||||
```
|
||||
|
||||
This uses Rust's fast-release profile which significantly reduces compilation time by disabling optimization and LTO. The resulting binary will be larger and less performant than the standard build, but it's useful for testing changes quickly.
|
||||
### Clean Build
|
||||
|
||||
The `Artifacts` will display in the `log` in the Terminal.
|
||||
|
||||
### Build clean
|
||||
|
||||
To clean rust build:
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pnpm clean
|
||||
```
|
||||
|
||||
### Portable Version (Windows Only)
|
||||
|
||||
To package portable version after the build:
|
||||
|
||||
```shell
|
||||
```bash
|
||||
pnpm portable
|
||||
```
|
||||
|
||||
## Contributing Your Changes
|
||||
|
||||
#### Before commit your changes
|
||||
### Before Committing
|
||||
|
||||
If you changed the rust code, it's recommanded to execute code style formatting and quailty checks.
|
||||
|
||||
1. Code quailty checks
|
||||
**Code quality checks:**
|
||||
|
||||
```bash
|
||||
# For rust backend
|
||||
$ clash-verge-rev: pnpm clippy
|
||||
# For frontend (not yet).
|
||||
# Rust backend
|
||||
cargo clippy-all
|
||||
# Frontend
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
2. Code style formatting
|
||||
**Code formatting:**
|
||||
|
||||
```bash
|
||||
# For rust backend
|
||||
$ clash-verge-rev: cd src-tauri
|
||||
$ clash-verge-rev/src-tauri: cargo fmt
|
||||
# For frontend
|
||||
$ clash-verge-rev: pnpm format:check
|
||||
$ clash-verge-rev: pnpm format
|
||||
# Rust backend
|
||||
cargo fmt
|
||||
# Frontend
|
||||
pnpm format
|
||||
```
|
||||
|
||||
Once you have made your changes:
|
||||
### Submitting Your Changes
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Commit your changes with clear and concise commit messages.
|
||||
4. Push your branch to your fork and submit a pull request to our repository.
|
||||
|
||||
We appreciate your contributions and look forward to your active participation in our project!
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
|
||||
3. Commit your changes with clear messages.
|
||||
|
||||
4. Push your branch and submit a pull request.
|
||||
|
||||
We appreciate your contributions and look forward to your participation!
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
134
clash-verge-rev/Cargo.toml
Normal file
134
clash-verge-rev/Cargo.toml
Normal file
@@ -0,0 +1,134 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"src-tauri",
|
||||
"crates/clash-verge-draft",
|
||||
"crates/clash-verge-logging",
|
||||
"crates/clash-verge-signal",
|
||||
"crates/tauri-plugin-clash-verge-sysinfo",
|
||||
"crates/clash-verge-types",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = "thin"
|
||||
opt-level = 3
|
||||
debug = false
|
||||
strip = true
|
||||
overflow-checks = false
|
||||
rpath = false
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
codegen-units = 64
|
||||
opt-level = 0
|
||||
debug = true
|
||||
strip = "none"
|
||||
overflow-checks = true
|
||||
lto = false
|
||||
rpath = false
|
||||
|
||||
[profile.fast-release]
|
||||
inherits = "release"
|
||||
codegen-units = 64
|
||||
incremental = true
|
||||
lto = false
|
||||
opt-level = 0
|
||||
debug = true
|
||||
strip = false
|
||||
|
||||
[workspace.dependencies]
|
||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||
clash-verge-logging = { path = "crates/clash-verge-logging" }
|
||||
clash-verge-signal = { path = "crates/clash-verge-signal" }
|
||||
clash-verge-types = { path = "crates/clash-verge-types" }
|
||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||
|
||||
tauri = { version = "2.9.4" }
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
anyhow = "1.0.100"
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
tokio = { version = "1.48.0", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
flexi_logger = "0.31.7"
|
||||
log = "0.4.29"
|
||||
|
||||
smartstring = { version = "1.0.1" }
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
|
||||
serde = { version = "1.0.228" }
|
||||
serde_json = { version = "1.0.145" }
|
||||
serde_yaml_ng = { version = "0.10.0" }
|
||||
|
||||
# *** For Windows platform only ***
|
||||
deelevate = "0.2.0"
|
||||
# *********************************
|
||||
|
||||
[workspace.lints.clippy]
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
todo = "warn"
|
||||
dbg_macro = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
rc_clone_in_vec_init = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_const_arrays = "warn"
|
||||
async_yields_async = "deny"
|
||||
mutex_atomic = "deny"
|
||||
mutex_integer = "deny"
|
||||
rc_mutex = "deny"
|
||||
unused_async = "deny"
|
||||
await_holding_lock = "deny"
|
||||
large_futures = "deny"
|
||||
future_not_send = "deny"
|
||||
redundant_else = "deny"
|
||||
needless_continue = "deny"
|
||||
needless_raw_string_hashes = "deny"
|
||||
or_fun_call = "deny"
|
||||
cognitive_complexity = "deny"
|
||||
useless_let_if_seq = "deny"
|
||||
use_self = "deny"
|
||||
tuple_array_conversions = "deny"
|
||||
trait_duplication_in_bounds = "deny"
|
||||
suspicious_operation_groupings = "deny"
|
||||
string_lit_as_bytes = "deny"
|
||||
significant_drop_tightening = "deny"
|
||||
significant_drop_in_scrutinee = "deny"
|
||||
redundant_clone = "deny"
|
||||
# option_if_let_else = "deny" // 过于激进,暂时不开启
|
||||
needless_pass_by_ref_mut = "deny"
|
||||
needless_collect = "deny"
|
||||
missing_const_for_fn = "deny"
|
||||
iter_with_drain = "deny"
|
||||
iter_on_single_items = "deny"
|
||||
iter_on_empty_collections = "deny"
|
||||
# fallible_impl_from = "deny" // 过于激进,暂时不开启
|
||||
equatable_if_let = "deny"
|
||||
collection_is_never_read = "deny"
|
||||
branches_sharing_code = "deny"
|
||||
pathbuf_init_then_push = "deny"
|
||||
option_as_ref_cloned = "deny"
|
||||
large_types_passed_by_value = "deny"
|
||||
# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启
|
||||
expl_impl_clone_on_copy = "deny"
|
||||
copy_iterator = "deny"
|
||||
cloned_instead_of_copied = "deny"
|
||||
# self_only_used_in_recursion = "deny" // Since 1.92.0
|
||||
unnecessary_self_imports = "deny"
|
||||
unused_trait_names = "deny"
|
||||
wildcard_imports = "deny"
|
||||
unnecessary_wraps = "deny"
|
||||
@@ -1,29 +1,63 @@
|
||||
## v2.4.4
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.17**
|
||||
|
||||
> [!WARNING]
|
||||
> Apple 公证服务故障,临时暂停 macOS 签名
|
||||
> macOS 跳过签名,终端执行 `sudo xattr -rd com.apple.quarantine /Applications/Clash\ Verge.app/`
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
- Linux 无法切换 TUN 堆栈
|
||||
- macOS service 启动项显示名称(试验性修改)
|
||||
- macOS 非预期 Tproxy 端口设置
|
||||
- 流量图缩放异常
|
||||
- PAC 自动代理脚本内容无法动态调整
|
||||
- 兼容从旧版服务模式升级
|
||||
- Monaco 编辑器的行数上限
|
||||
- 已删除节点在手动分组中导致配置无法加载
|
||||
- 仪表盘与托盘状态不同步
|
||||
- 修复重启或退出应用,关闭系统时无法记忆用户行为
|
||||
- 彻底修复 macOS 连接页面显示异常
|
||||
- windows 端监听关机信号失败
|
||||
- 修复代理按钮和高亮状态不同步
|
||||
- 修复侧边栏可能的未能正确跳转
|
||||
- 修复解锁测试部分地区图标编码不正确
|
||||
- 修复 IP 检测切页后强制刷新,改为仅在必要时更新
|
||||
- 修复在搜索框输入不完整正则直接崩溃
|
||||
- 修复创建窗口时在非简体中文环境或深色主题下的短暂闪烁
|
||||
- 修复更新时加载进度条异常
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.16**
|
||||
- 支持连接页面各个项目的排序
|
||||
- 实现可选的自动备份
|
||||
- 连接页面支持查看已关闭的连接(最近最多 500 个已关闭连接)
|
||||
- 日志页面支持按时间倒序
|
||||
- 增加「重新激活订阅」的全局快捷键
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||
|
||||
- 网络请求改为使用 rustls,提升 TLS 兼容性
|
||||
- rustls 避免因服务器证书链配置问题或较新 TLS 要求导致订阅无法导入
|
||||
- 替换前端信息编辑组件,提供更好性能
|
||||
- 优化后端内存和性能表现
|
||||
- 防止退出时可能的禁用 TUN 失败
|
||||
- i18n 支持
|
||||
- 优化备份设置布局
|
||||
- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停
|
||||
- 性能优化系统状态获取
|
||||
- 优化托盘菜单当前订阅检测逻辑
|
||||
- 优化连接页面表格渲染
|
||||
- 优化链式代理 UI 反馈
|
||||
- 优化重启应用的资源清理逻辑
|
||||
- 优化前端数据刷新
|
||||
- 优化流量采样和数据处理
|
||||
- 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
17
clash-verge-rev/crates/clash-verge-draft/Cargo.toml
Normal file
17
clash-verge-rev/crates/clash-verge-draft/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "clash-verge-draft"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bench]]
|
||||
name = "draft_bench"
|
||||
path = "bench/benche_me.rs"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
parking_lot = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -3,17 +3,20 @@ use std::hint::black_box;
|
||||
use std::process;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use app_lib::config::IVerge;
|
||||
use app_lib::utils::Draft as DraftNew;
|
||||
use clash_verge_draft::Draft;
|
||||
|
||||
/// 创建测试数据
|
||||
fn make_draft() -> DraftNew<IVerge> {
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct IVerge {
|
||||
enable_auto_launch: Option<bool>,
|
||||
enable_tun_mode: Option<bool>,
|
||||
}
|
||||
|
||||
fn make_draft() -> Draft<IVerge> {
|
||||
let verge = IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
DraftNew::new(verge)
|
||||
Draft::new(verge)
|
||||
}
|
||||
|
||||
pub fn bench_draft(c: &mut Criterion) {
|
||||
102
clash-verge-rev/crates/clash-verge-draft/src/lib.rs
Normal file
102
clash-verge-rev/crates/clash-verge-draft/src/lib.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type SharedBox<T> = Arc<Box<T>>;
|
||||
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
||||
|
||||
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
||||
// (committed_snapshot, optional_draft_snapshot)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone> {
|
||||
inner: Arc<RwLock<DraftInner<T>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone> Draft<T> {
|
||||
#[inline]
|
||||
pub fn new(data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
|
||||
}
|
||||
}
|
||||
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
||||
#[inline]
|
||||
pub fn data_arc(&self) -> SharedBox<T> {
|
||||
let guard = self.inner.read();
|
||||
Arc::clone(&guard.0)
|
||||
}
|
||||
|
||||
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
||||
/// 这也是零拷贝:只 clone Arc,不 clone T
|
||||
#[inline]
|
||||
pub fn latest_arc(&self) -> SharedBox<T> {
|
||||
let guard = self.inner.read();
|
||||
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
|
||||
}
|
||||
|
||||
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
|
||||
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
|
||||
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
|
||||
#[inline]
|
||||
pub fn edit_draft<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut T) -> R,
|
||||
{
|
||||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||||
let mut guard = self.inner.write();
|
||||
let mut draft_arc = if guard.1.is_none() {
|
||||
Arc::clone(&guard.0)
|
||||
} else {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
guard.1.take().unwrap()
|
||||
};
|
||||
drop(guard);
|
||||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||||
// 对 Box<T> 解引用得到 &mut T
|
||||
let result = f(&mut **boxed);
|
||||
// 恢复修改后的草稿 Arc
|
||||
self.inner.write().1 = Some(draft_arc);
|
||||
result
|
||||
}
|
||||
|
||||
/// 将草稿提交到已提交位置(替换),并清除草稿
|
||||
#[inline]
|
||||
pub fn apply(&self) {
|
||||
let mut guard = self.inner.write();
|
||||
if let Some(d) = guard.1.take() {
|
||||
guard.0 = d;
|
||||
}
|
||||
}
|
||||
|
||||
/// 丢弃草稿(如果存在)
|
||||
#[inline]
|
||||
pub fn discard(&self) {
|
||||
let mut guard = self.inner.write();
|
||||
guard.1 = None;
|
||||
}
|
||||
|
||||
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
|
||||
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
|
||||
#[inline]
|
||||
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: FnOnce(Box<T>) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
||||
{
|
||||
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
||||
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
||||
let local: Box<T> = {
|
||||
let guard = self.inner.read();
|
||||
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
||||
(*guard.0).clone()
|
||||
};
|
||||
|
||||
let (new_local, res) = f(local).await?;
|
||||
|
||||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||||
self.inner.write().0 = Arc::new(new_local);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
@@ -1,110 +1,7 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type SharedBox<T> = Arc<Box<T>>;
|
||||
type DraftInner<T> = (SharedBox<T>, Option<SharedBox<T>>);
|
||||
|
||||
/// Draft 管理:committed 与 optional draft 都以 Arc<Box<T>> 存储,
|
||||
// (committed_snapshot, optional_draft_snapshot)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone> {
|
||||
inner: Arc<RwLock<DraftInner<T>>>,
|
||||
}
|
||||
|
||||
impl<T: Clone> Draft<T> {
|
||||
#[inline]
|
||||
pub fn new(data: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))),
|
||||
}
|
||||
}
|
||||
/// 以 Arc<Box<T>> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc)
|
||||
#[inline]
|
||||
pub fn data_arc(&self) -> SharedBox<T> {
|
||||
let guard = self.inner.read();
|
||||
Arc::clone(&guard.0)
|
||||
}
|
||||
|
||||
/// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照
|
||||
/// 这也是零拷贝:只 clone Arc,不 clone T
|
||||
#[inline]
|
||||
pub fn latest_arc(&self) -> SharedBox<T> {
|
||||
let guard = self.inner.read();
|
||||
guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0))
|
||||
}
|
||||
|
||||
/// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T)
|
||||
/// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T;
|
||||
/// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。
|
||||
#[inline]
|
||||
pub fn edit_draft<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut T) -> R,
|
||||
{
|
||||
// 先获得写锁以创建或取出草稿 Arc 的可变引用位置
|
||||
let mut guard = self.inner.write();
|
||||
let mut draft_arc = if guard.1.is_none() {
|
||||
Arc::clone(&guard.0)
|
||||
} else {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
guard.1.take().unwrap()
|
||||
};
|
||||
drop(guard);
|
||||
// Arc::make_mut: 如果只有一个引用则返回可变引用;否则会克隆底层 Box<T>(要求 T: Clone)
|
||||
let boxed = Arc::make_mut(&mut draft_arc); // &mut Box<T>
|
||||
// 对 Box<T> 解引用得到 &mut T
|
||||
let result = f(&mut **boxed);
|
||||
// 恢复修改后的草稿 Arc
|
||||
self.inner.write().1 = Some(draft_arc);
|
||||
result
|
||||
}
|
||||
|
||||
/// 将草稿提交到已提交位置(替换),并清除草稿
|
||||
#[inline]
|
||||
pub fn apply(&self) {
|
||||
let mut guard = self.inner.write();
|
||||
if let Some(d) = guard.1.take() {
|
||||
guard.0 = d;
|
||||
}
|
||||
}
|
||||
|
||||
/// 丢弃草稿(如果存在)
|
||||
#[inline]
|
||||
pub fn discard(&self) {
|
||||
let mut guard = self.inner.write();
|
||||
guard.1 = None;
|
||||
}
|
||||
|
||||
/// 异步地以拥有 Box<T> 的方式修改已提交数据:将克隆一次已提交数据到本地,
|
||||
/// 异步闭包返回新的 Box<T>(替换已提交数据)和业务返回值 R。
|
||||
#[inline]
|
||||
pub async fn with_data_modify<F, Fut, R>(&self, f: F) -> Result<R, anyhow::Error>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
F: FnOnce(Box<T>) -> Fut + Send,
|
||||
Fut: std::future::Future<Output = Result<(Box<T>, R), anyhow::Error>> + Send,
|
||||
{
|
||||
// 读取已提交快照(cheap Arc clone, 然后得到 Box<T> 所有权 via clone)
|
||||
// 注意:为了让闭包接收 Box<T> 所有权,我们需要 clone 底层 T(不可避免)
|
||||
let local: Box<T> = {
|
||||
let guard = self.inner.read();
|
||||
// 将 Arc<Box<T>> 的 Box<T> clone 出来(会调用 T: Clone)
|
||||
(*guard.0).clone()
|
||||
};
|
||||
|
||||
let (new_local, res) = f(local).await?;
|
||||
|
||||
// 将新的 Box<T> 放到已提交位置(包进 Arc)
|
||||
self.inner.write().0 = Arc::new(new_local);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::anyhow;
|
||||
use clash_verge_draft::Draft;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
14
clash-verge-rev/crates/clash-verge-logging/Cargo.toml
Normal file
14
clash-verge-rev/crates/clash-verge-logging/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "clash-verge-logging"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
flexi_logger = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tauri-dev = []
|
||||
@@ -34,6 +34,7 @@ pub enum Type {
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Cmd => write!(f, "[Cmd]"),
|
||||
@@ -58,44 +59,6 @@ impl fmt::Display for Type {
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($result: expr) => {
|
||||
log::error!(target: "app", "{}", $result);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_err {
|
||||
($result: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
};
|
||||
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(_) = $result {
|
||||
log::error!(target: "app", "{}", $err_str);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
macro_rules! wrap_err {
|
||||
// Case 1: Future<Result<T, E>>
|
||||
($stat:expr, async) => {{
|
||||
match $stat.await {
|
||||
Ok(a) => Ok::<_, ::anyhow::Error>(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err);
|
||||
Err(::anyhow::Error::msg(err.to_string()))
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! logging {
|
||||
// 不带 print 参数的版本(默认不打印)
|
||||
@@ -119,6 +82,7 @@ macro_rules! logging_error {
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_sidecar_log(
|
||||
writer: MutexGuard<'_, FileLogWriter>,
|
||||
now: &mut DeferredNow,
|
||||
@@ -158,6 +122,7 @@ impl<'a> NoModuleFilter<'a> {
|
||||
|
||||
#[cfg(not(feature = "tauri-dev"))]
|
||||
impl<'a> LogLineFilter for NoModuleFilter<'a> {
|
||||
#[inline]
|
||||
fn write(
|
||||
&self,
|
||||
now: &mut DeferredNow,
|
||||
26
clash-verge-rev/crates/clash-verge-signal/Cargo.toml
Normal file
26
clash-verge-rev/crates/clash-verge-signal/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "clash-verge-signal"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clash-verge-logging = { workspace = true }
|
||||
log = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook = "0.3.18"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tauri = { workspace = true }
|
||||
windows-sys = { version = "0.61.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
35
clash-verge-rev/crates/clash-verge-signal/src/lib.rs
Normal file
35
clash-verge-rev/crates/clash-verge-signal/src/lib.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
pub(crate) static RUNTIME: OnceLock<Option<tokio::runtime::Runtime>> = OnceLock::new();
|
||||
|
||||
pub fn register<F, Fut>(#[cfg(windows)] app_handle: &tauri::AppHandle, f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future + Send + 'static,
|
||||
{
|
||||
RUNTIME.get_or_init(|| match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => Some(rt),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::System,
|
||||
"register shutdown signal failed, create tokio runtime error: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(unix)]
|
||||
unix::register(f);
|
||||
|
||||
#[cfg(windows)]
|
||||
windows::register(app_handle, f);
|
||||
}
|
||||
54
clash-verge-rev/crates/clash-verge-signal/src/unix.rs
Normal file
54
clash-verge-rev/crates/clash-verge-signal/src/unix.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use signal_hook::{
|
||||
consts::{SIGHUP, SIGINT, SIGTERM},
|
||||
iterator::Signals,
|
||||
low_level,
|
||||
};
|
||||
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
|
||||
use crate::RUNTIME;
|
||||
|
||||
pub fn register<F, Fut>(f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future + Send + 'static,
|
||||
{
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.spawn(async move {
|
||||
let signals = [SIGTERM, SIGINT, SIGHUP];
|
||||
|
||||
let mut sigs = match Signals::new(signals) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
logging!(error, Type::System, "注册信号处理器失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for signal in &mut sigs {
|
||||
let signal_to_str = |signal| match signal {
|
||||
SIGTERM => "SIGTERM",
|
||||
SIGINT => "SIGINT",
|
||||
SIGHUP => "SIGHUP",
|
||||
_ => "UNKNOWN",
|
||||
};
|
||||
|
||||
logging!(info, Type::System, "捕获到信号 {}", signal_to_str(signal));
|
||||
|
||||
f().await;
|
||||
|
||||
logging_error!(
|
||||
Type::System,
|
||||
"信号 {:?} 默认处理失败",
|
||||
low_level::emulate_default_handler(signal)
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"register shutdown signal failed, RUNTIME is not available"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use tauri::Manager as _;
|
||||
use std::{future::Future, pin::Pin, sync::OnceLock};
|
||||
|
||||
use tauri::{AppHandle, Manager as _};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{HWND, LPARAM, LRESULT, WPARAM},
|
||||
UI::WindowsAndMessaging::{
|
||||
@@ -8,12 +10,19 @@ use windows_sys::Win32::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{core::handle, feat, logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use crate::RUNTIME;
|
||||
|
||||
// code refer to:
|
||||
// global-hotkey (https://github.com/tauri-apps/global-hotkey)
|
||||
// Global Shortcut (https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut)
|
||||
|
||||
type ShutdownHandler =
|
||||
Box<dyn Fn() -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send + Sync>;
|
||||
|
||||
static SHUTDOWN_HANDLER: OnceLock<ShutdownHandler> = OnceLock::new();
|
||||
|
||||
struct ShutdownState {
|
||||
hwnd: HWND,
|
||||
}
|
||||
@@ -48,11 +57,27 @@ unsafe extern "system" fn shutdown_proc(
|
||||
);
|
||||
}
|
||||
WM_ENDSESSION => {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
logging!(info, Type::System, "Session ended, system shutting down.");
|
||||
feat::clean_async().await;
|
||||
logging!(info, Type::System, "resolved reset finished");
|
||||
});
|
||||
if let Some(handler) = SHUTDOWN_HANDLER.get() {
|
||||
if let Some(Some(rt)) = RUNTIME.get() {
|
||||
rt.block_on(async {
|
||||
logging!(info, Type::System, "Session ended, system shutting down.");
|
||||
handler().await;
|
||||
logging!(info, Type::System, "resolved reset finished");
|
||||
});
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"handle shutdown signal failed, RUNTIME is not available"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logging!(
|
||||
error,
|
||||
Type::System,
|
||||
"WM_ENDSESSION received but no shutdown handler is registered"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -80,8 +105,18 @@ fn get_instance_handle() -> windows_sys::Win32::Foundation::HMODULE {
|
||||
unsafe { &__ImageBase as *const _ as _ }
|
||||
}
|
||||
|
||||
pub fn register() {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
pub fn register<F, Fut>(app_handle: &AppHandle, f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: Future + Send + 'static,
|
||||
{
|
||||
let _ = SHUTDOWN_HANDLER.set(Box::new(move || {
|
||||
let fut = (f)();
|
||||
Box::pin(async move {
|
||||
fut.await;
|
||||
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
|
||||
}));
|
||||
|
||||
let class_name = encode_wide("global_shutdown_app");
|
||||
unsafe {
|
||||
let hinstance = get_instance_handle();
|
||||
13
clash-verge-rev/crates/clash-verge-types/Cargo.toml
Normal file
13
clash-verge-rev/crates/clash-verge-types/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "clash-verge-types"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
clash-verge-rev/crates/clash-verge-types/src/lib.rs
Normal file
1
clash-verge-rev/crates/clash-verge-types/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod runtime;
|
||||
152
clash-verge-rev/crates/clash-verge-types/src/runtime.rs
Normal file
152
clash-verge-rev/crates/clash-verge-types/src/runtime.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
const PATCH_CONFIG_INNER: [&str; 4] = ["allow-lan", "ipv6", "log-level", "unified-delay"];
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct IRuntime {
|
||||
pub config: Option<Mapping>,
|
||||
// 记录在订阅中(包括merge和script生成的)出现过的keys
|
||||
// 这些keys不一定都生效
|
||||
pub exists_keys: HashSet<String>,
|
||||
// TODO 或许可以用 FixMap 来存储以提升效率
|
||||
pub chain_logs: HashMap<String, Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl IRuntime {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 这里只更改 allow-lan | ipv6 | log-level | tun
|
||||
#[inline]
|
||||
pub fn patch_config(&mut self, patch: &Mapping) {
|
||||
let config = if let Some(config) = self.config.as_mut() {
|
||||
config
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
for key in PATCH_CONFIG_INNER.iter() {
|
||||
if let Some(value) = patch.get(key) {
|
||||
config.insert((*key).into(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let patch_tun = patch.get("tun");
|
||||
if let Some(patch_tun_value) = patch_tun {
|
||||
let mut tun = config
|
||||
.get("tun")
|
||||
.and_then(|val| val.as_mapping())
|
||||
.cloned()
|
||||
.unwrap_or_else(Mapping::new);
|
||||
|
||||
if let Some(patch_tun_mapping) = patch_tun_value.as_mapping() {
|
||||
for key in use_keys(patch_tun_mapping) {
|
||||
if let Some(value) = patch_tun_mapping.get(key.as_str()) {
|
||||
tun.insert(Value::from(key.as_str()), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.insert("tun".into(), Value::from(tun));
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新链式代理配置
|
||||
///
|
||||
/// 该函数更新 `proxies` 和 `proxy-groups` 配置,并处理链式代理的修改或(传入 None )删除。
|
||||
///
|
||||
/// 配置示例:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "proxies": [
|
||||
/// {
|
||||
/// "name": "入口节点",
|
||||
/// "type": "xxx",
|
||||
/// "server": "xxx",
|
||||
/// "port": "xxx",
|
||||
/// "ports": "xxx",
|
||||
/// "password": "xxx",
|
||||
/// "skip-cert-verify": "xxx"
|
||||
/// },
|
||||
/// {
|
||||
/// "name": "hop_node_1_xxxx",
|
||||
/// "type": "xxx",
|
||||
/// "server": "xxx",
|
||||
/// "port": "xxx",
|
||||
/// "ports": "xxx",
|
||||
/// "password": "xxx",
|
||||
/// "skip-cert-verify": "xxx",
|
||||
/// "dialer-proxy": "入口节点"
|
||||
/// },
|
||||
/// {
|
||||
/// "name": "出口节点",
|
||||
/// "type": "xxx",
|
||||
/// "server": "xxx",
|
||||
/// "port": "xxx",
|
||||
/// "ports": "xxx",
|
||||
/// "password": "xxx",
|
||||
/// "skip-cert-verify": "xxx",
|
||||
/// "dialer-proxy": "hop_node_1_xxxx"
|
||||
/// }
|
||||
/// ],
|
||||
/// "proxy-groups": [
|
||||
/// {
|
||||
/// "name": "proxy_chain",
|
||||
/// "type": "select",
|
||||
/// "proxies": ["出口节点"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
|
||||
let config = if let Some(config) = self.config.as_mut() {
|
||||
config
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
|
||||
proxies.iter_mut().for_each(|proxy| {
|
||||
if let Some(proxy) = proxy.as_mapping_mut()
|
||||
&& proxy.get("dialer-proxy").is_some()
|
||||
{
|
||||
proxy.remove("dialer-proxy");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config
|
||||
&& let Some(Value::Sequence(proxies)) = config.get_mut("proxies")
|
||||
{
|
||||
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
|
||||
if let Some(Value::Mapping(proxy)) = proxies
|
||||
.iter_mut()
|
||||
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
|
||||
&& i != 0
|
||||
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
|
||||
{
|
||||
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO 完整迁移 enhance 行为后移除
|
||||
#[inline]
|
||||
fn use_keys<'a>(config: &'a Mapping) -> impl Iterator<Item = String> + 'a {
|
||||
config
|
||||
.iter()
|
||||
.filter_map(|(key, _)| key.as_str())
|
||||
.map(|s: &str| {
|
||||
let mut s: String = s.into();
|
||||
s.make_ascii_lowercase();
|
||||
s
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "tauri-plugin-clash-verge-sysinfo"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
sysinfo = { version = "0.37.2", features = ["network", "system"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc = "0.2.178"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,39 @@
|
||||
use parking_lot::RwLock;
|
||||
use tauri::{AppHandle, Runtime, State, command};
|
||||
use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
|
||||
|
||||
use crate::Platform;
|
||||
|
||||
// TODO 迁移,让新的结构体允许通过 tauri command 正确使用 structure.field 方式获取信息
|
||||
#[command]
|
||||
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<String, Error> {
|
||||
Ok(state.inner().read().to_string())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[command]
|
||||
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
|
||||
Ok(state
|
||||
.inner()
|
||||
.read()
|
||||
.appinfo
|
||||
.app_startup_time
|
||||
.elapsed()
|
||||
.as_millis())
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[command]
|
||||
pub fn app_is_admin(state: State<'_, RwLock<Platform>>) -> Result<bool, Error> {
|
||||
Ok(state.inner().read().appinfo.app_is_admin)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn export_diagnostic_info<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
state: State<'_, RwLock<Platform>>,
|
||||
) -> Result<(), Error> {
|
||||
let info = state.inner().read().to_string();
|
||||
let clipboard = app_handle.clipboard();
|
||||
clipboard.write_text(info)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
pub mod commands;
|
||||
|
||||
#[cfg(windows)]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use parking_lot::RwLock;
|
||||
use sysinfo::{Networks, System};
|
||||
use tauri::{
|
||||
Manager as _, Runtime,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
};
|
||||
|
||||
pub struct SysInfo {
|
||||
system_name: String,
|
||||
system_version: String,
|
||||
system_kernel_version: String,
|
||||
system_arch: String,
|
||||
}
|
||||
|
||||
impl Default for SysInfo {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let system_name = System::name().unwrap_or_else(|| "Null".into());
|
||||
let system_version = System::long_os_version().unwrap_or_else(|| "Null".into());
|
||||
let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into());
|
||||
let system_arch = System::cpu_arch();
|
||||
Self {
|
||||
system_name,
|
||||
system_version,
|
||||
system_kernel_version,
|
||||
system_arch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppInfo {
|
||||
app_version: String,
|
||||
app_core_mode: String,
|
||||
pub app_startup_time: Instant,
|
||||
pub app_is_admin: bool,
|
||||
}
|
||||
|
||||
impl Default for AppInfo {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let app_version = "0.0.0".into();
|
||||
let app_core_mode = "NotRunning".into();
|
||||
let app_is_admin = false;
|
||||
let app_startup_time = Instant::now();
|
||||
Self {
|
||||
app_version,
|
||||
app_core_mode,
|
||||
app_startup_time,
|
||||
app_is_admin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Platform {
|
||||
pub sysinfo: SysInfo,
|
||||
pub appinfo: AppInfo,
|
||||
}
|
||||
|
||||
impl Debug for Platform {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Platform")
|
||||
.field("system_name", &self.sysinfo.system_name)
|
||||
.field("system_version", &self.sysinfo.system_version)
|
||||
.field("system_kernel_version", &self.sysinfo.system_kernel_version)
|
||||
.field("system_arch", &self.sysinfo.system_arch)
|
||||
.field("app_version", &self.appinfo.app_version)
|
||||
.field("app_core_mode", &self.appinfo.app_core_mode)
|
||||
.field("app_is_admin", &self.appinfo.app_is_admin)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Platform {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"System Name: {}\nSystem Version: {}\nSystem kernel Version: {}\nSystem Arch: {}\nVerge Version: {}\nRunning Mode: {}\nIs Admin: {}",
|
||||
self.sysinfo.system_name,
|
||||
self.sysinfo.system_version,
|
||||
self.sysinfo.system_kernel_version,
|
||||
self.sysinfo.system_arch,
|
||||
self.appinfo.app_version,
|
||||
self.appinfo.app_core_mode,
|
||||
self.appinfo.app_is_admin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
#[inline]
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_binary_admin() -> bool {
|
||||
#[cfg(not(windows))]
|
||||
unsafe {
|
||||
libc::geteuid() == 0
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn list_network_interfaces() -> Vec<String> {
|
||||
let mut networks = Networks::new();
|
||||
networks.refresh(false);
|
||||
networks.keys().map(|name| name.to_owned()).collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_app_core_mode<R: Runtime>(app: &tauri::AppHandle<R>, mode: impl Into<String>) {
|
||||
let platform_spec = app.state::<RwLock<Platform>>();
|
||||
let mut spec = platform_spec.write();
|
||||
spec.appinfo.app_core_mode = mode.into();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_current_app_handle_admin<R: Runtime>(app: &tauri::AppHandle<R>) -> bool {
|
||||
let platform_spec = app.state::<RwLock<Platform>>();
|
||||
let spec = platform_spec.read();
|
||||
spec.appinfo.app_is_admin
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::<R>::new("clash_verge_sysinfo")
|
||||
// TODO 现在 crate 还不是真正的 tauri 插件,必须由主 lib 自行注册
|
||||
// TODO 从 clash-verge 中迁移获取系统信息的 commnand 并实现优雅 structure.field 访问
|
||||
// .invoke_handler(tauri::generate_handler![
|
||||
// commands::get_system_info,
|
||||
// commands::get_app_uptime,
|
||||
// commands::app_is_admin,
|
||||
// commands::export_diagnostic_info,
|
||||
// ])
|
||||
.setup(move |app, _api| {
|
||||
let app_version = app.package_info().version.to_string();
|
||||
let is_admin = is_binary_admin();
|
||||
|
||||
let mut platform_spec = Platform::new();
|
||||
platform_spec.appinfo.app_version = app_version;
|
||||
platform_spec.appinfo.app_is_admin = is_admin;
|
||||
|
||||
app.manage(RwLock::new(platform_spec));
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
@@ -24,16 +24,13 @@
|
||||
"release:autobuild": "pnpm release-version autobuild",
|
||||
"release:deploytest": "pnpm release-version deploytest",
|
||||
"publish-version": "node scripts/publish-version.mjs",
|
||||
"fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml",
|
||||
"clippy": "cargo clippy --all-features --all-targets --manifest-path ./src-tauri/Cargo.toml",
|
||||
"lint": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache src",
|
||||
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -46,8 +43,9 @@
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.18.0",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
@@ -55,39 +53,37 @@
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "2.3.3",
|
||||
"@tauri-apps/plugin-updater": "2.9.0",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ahooks": "^3.9.6",
|
||||
"axios": "^1.13.2",
|
||||
"dayjs": "1.11.19",
|
||||
"foxact": "^0.2.49",
|
||||
"i18next": "^25.6.2",
|
||||
"i18next": "^25.7.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"json-schema": "^0.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-yaml": "^5.4.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "16.3.3",
|
||||
"react-hook-form": "^7.67.0",
|
||||
"react-i18next": "16.3.5",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^7.9.6",
|
||||
"react-virtuoso": "^4.14.1",
|
||||
"swr": "^2.3.6",
|
||||
"react-router": "^7.10.0",
|
||||
"react-virtuoso": "^4.16.1",
|
||||
"swr": "^2.3.7",
|
||||
"tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#main",
|
||||
"types-pac": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^6.0.1",
|
||||
"@eslint-react/eslint-plugin": "^2.3.5",
|
||||
"@eslint-react/eslint-plugin": "^2.3.11",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tauri-apps/cli": "2.9.4",
|
||||
"@tauri-apps/cli": "2.9.5",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "19.2.4",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
@@ -103,36 +99,31 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"glob": "^13.0.0",
|
||||
"globals": "^16.5.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.6.1",
|
||||
"lint-staged": "^16.2.6",
|
||||
"meta-json-schema": "^1.19.16",
|
||||
"lint-staged": "^16.2.7",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.94.0",
|
||||
"prettier": "^3.7.3",
|
||||
"sass": "^1.94.2",
|
||||
"tar": "^7.5.2",
|
||||
"terser": "^5.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-monaco-editor-esm": "^2.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vitest": "^4.0.9"
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.6",
|
||||
"vite-plugin-svgr": "^4.5.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
"eslint --fix --max-warnings=0",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{css,scss,json,md}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.13.2"
|
||||
"packageManager": "pnpm@10.22.0"
|
||||
}
|
||||
|
||||
1537
clash-verge-rev/pnpm-lock.yaml
generated
1537
clash-verge-rev/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
clash-verge-rev/pnpm-workspace.yaml
Normal file
7
clash-verge-rev/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
onlyBuiltDependencies:
|
||||
- "@parcel/watcher"
|
||||
- "@swc/core"
|
||||
- core-js
|
||||
- es5-ext
|
||||
- esbuild
|
||||
- unrs-resolver
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard"],
|
||||
"baseBranches": ["dev"],
|
||||
"enabledManagers": ["cargo", "npm"],
|
||||
"enabledManagers": ["cargo", "npm", "github-actions"],
|
||||
"labels": ["dependencies"],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
|
||||
@@ -6,56 +6,73 @@ authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
rust-version = "1.91"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = ["clash_verge_logger/color"]
|
||||
tauri-dev = ["clash-verge-logging/tauri-dev"]
|
||||
tokio-trace = ["console-subscriber"]
|
||||
clippy = ["tauri/test"]
|
||||
tracing = []
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.2", features = [] }
|
||||
tauri-build = { version = "2.5.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
anyhow = "1.0.100"
|
||||
open = "5.3.2"
|
||||
log = "0.4.28"
|
||||
dunce = "1.0.5"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.42"
|
||||
sysinfo = { version = "0.37.2", features = ["network", "system"] }
|
||||
boa_engine = "0.21.0"
|
||||
serde_json = "1.0.145"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||
percent-encoding = "2.3.2"
|
||||
tokio = { version = "1.48.0", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
"sync",
|
||||
] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
|
||||
regex = "1.12.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
|
||||
tauri = { version = "2.9.3", features = [
|
||||
clash-verge-draft = { workspace = true }
|
||||
clash-verge-logging = { workspace = true }
|
||||
clash-verge-signal = { workspace = true }
|
||||
clash-verge-types = { workspace = true }
|
||||
tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = { workspace = true }
|
||||
tauri = { workspace = true, features = [
|
||||
"protocol-asset",
|
||||
"devtools",
|
||||
"tray-icon",
|
||||
"image-ico",
|
||||
"image-png",
|
||||
] }
|
||||
parking_lot = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
flexi_logger = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml_ng = { workspace = true }
|
||||
smartstring = { workspace = true, features = ["serde"] }
|
||||
warp = { version = "0.4.2", features = ["server"] }
|
||||
open = "5.3.3"
|
||||
dunce = "1.0.5"
|
||||
nanoid = "0.4"
|
||||
chrono = "0.4.42"
|
||||
boa_engine = "0.21.0"
|
||||
once_cell = { version = "1.21.3", features = ["parking_lot"] }
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
percent-encoding = "2.3.2"
|
||||
reqwest = { version = "0.12.24", features = ["json", "cookies", "rustls-tls"] }
|
||||
regex = "1.12.2"
|
||||
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", features = [
|
||||
"guard",
|
||||
] }
|
||||
network-interface = { version = "2.0.3", features = ["serde"] }
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-deep-link = "2.4.5"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
zip = "6.0.0"
|
||||
@@ -65,30 +82,28 @@ base64 = "0.22.1"
|
||||
getrandom = "0.3.4"
|
||||
futures = "0.3.31"
|
||||
sys-locale = "0.3.2"
|
||||
libc = "0.2.177"
|
||||
gethostname = "1.1.0"
|
||||
scopeguard = "1.2.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tokio-stream = "0.1.17"
|
||||
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||
compact_str = { version = "0.9.0", features = ["serde"] }
|
||||
tauri-plugin-http = "2.5.4"
|
||||
flexi_logger = "0.31.7"
|
||||
console-subscriber = { version = "0.5.0", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
async-trait = "0.1.89"
|
||||
smartstring = { version = "1.0.1", features = ["serde"] }
|
||||
clash_verge_service_ipc = { version = "2.0.21", features = [
|
||||
"client",
|
||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||
arc-swap = "1.7.1"
|
||||
rust-i18n = "3.1.5"
|
||||
rust_iso3166 = "0.1.14"
|
||||
dark-light = "2.0.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
deelevate = { workspace = true }
|
||||
runas = "=1.2.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = "0.55.0"
|
||||
winapi = { version = "0.3.9", features = [
|
||||
"winbase",
|
||||
@@ -103,154 +118,14 @@ winapi = { version = "0.3.9", features = [
|
||||
"winhttp",
|
||||
"winreg",
|
||||
] }
|
||||
windows-sys = { version = "0.61.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook = "0.3.18"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2.5.1"
|
||||
tauri-plugin-global-shortcut = "2.3.1"
|
||||
tauri-plugin-updater = "2.9.0"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = ["clash_verge_logger/color"]
|
||||
tauri-dev = []
|
||||
tokio-trace = ["console-subscriber"]
|
||||
clippy = ["tauri/test"]
|
||||
tracing = []
|
||||
|
||||
[[bench]]
|
||||
name = "draft_benchmark"
|
||||
path = "benches/draft_benchmark.rs"
|
||||
harness = false
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
lto = "thin"
|
||||
opt-level = 3
|
||||
debug = false
|
||||
strip = true
|
||||
overflow-checks = false
|
||||
rpath = false
|
||||
|
||||
[profile.dev]
|
||||
incremental = true
|
||||
codegen-units = 64
|
||||
opt-level = 0
|
||||
debug = true
|
||||
strip = "none"
|
||||
overflow-checks = true
|
||||
lto = false
|
||||
rpath = false
|
||||
|
||||
[profile.fast-release]
|
||||
inherits = "release"
|
||||
codegen-units = 64
|
||||
incremental = true
|
||||
lto = false
|
||||
opt-level = 0
|
||||
debug = true
|
||||
strip = false
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
criterion = { workspace = true }
|
||||
|
||||
[lints.clippy]
|
||||
# Core categories - most important for code safety and correctness
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
|
||||
# Critical safety lints - warn for now due to extensive existing usage
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
|
||||
# Development quality lints
|
||||
todo = "warn"
|
||||
dbg_macro = "warn"
|
||||
|
||||
# 我们期望所有输出方式通过 logging 模块进行统一管理
|
||||
# print_stdout = "deny"
|
||||
# print_stderr = "deny"
|
||||
|
||||
# Performance lints for proxy application
|
||||
clone_on_ref_ptr = "warn"
|
||||
rc_clone_in_vec_init = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_const_arrays = "warn"
|
||||
|
||||
# Security lints
|
||||
#integer_division = "warn"
|
||||
#lossy_float_literal = "warn"
|
||||
#default_numeric_fallback = "warn"
|
||||
|
||||
# Mutex and async lints - strict control
|
||||
async_yields_async = "deny" # Prevents missing await in async blocks
|
||||
mutex_atomic = "deny" # Use atomics instead of Mutex<bool/int>
|
||||
mutex_integer = "deny" # Use AtomicInt instead of Mutex<int>
|
||||
rc_mutex = "deny" # Single-threaded Rc with Mutex is wrong
|
||||
unused_async = "deny" # Too many false positives in Tauri/framework code
|
||||
await_holding_lock = "deny"
|
||||
large_futures = "deny"
|
||||
future_not_send = "deny"
|
||||
|
||||
# Common style improvements
|
||||
redundant_else = "deny" # Too many in existing code
|
||||
needless_continue = "deny" # Too many in existing code
|
||||
needless_raw_string_hashes = "deny" # Too many in existing code
|
||||
|
||||
# Disable noisy categories for existing codebase but keep them available
|
||||
#style = { level = "allow", priority = -1 }
|
||||
#complexity = { level = "allow", priority = -1 }
|
||||
#perf = { level = "allow", priority = -1 }
|
||||
#pedantic = { level = "allow", priority = -1 }
|
||||
#nursery = { level = "allow", priority = -1 }
|
||||
#restriction = { level = "allow", priority = -1 }
|
||||
|
||||
or_fun_call = "deny"
|
||||
cognitive_complexity = "deny"
|
||||
useless_let_if_seq = "deny"
|
||||
use_self = "deny"
|
||||
tuple_array_conversions = "deny"
|
||||
trait_duplication_in_bounds = "deny"
|
||||
suspicious_operation_groupings = "deny"
|
||||
string_lit_as_bytes = "deny"
|
||||
significant_drop_tightening = "deny"
|
||||
significant_drop_in_scrutinee = "deny"
|
||||
redundant_clone = "deny"
|
||||
# option_if_let_else = "deny" // 过于激进,暂时不开启
|
||||
needless_pass_by_ref_mut = "deny"
|
||||
needless_collect = "deny"
|
||||
missing_const_for_fn = "deny"
|
||||
iter_with_drain = "deny"
|
||||
iter_on_single_items = "deny"
|
||||
iter_on_empty_collections = "deny"
|
||||
# fallible_impl_from = "deny" // 过于激进,暂时不开启
|
||||
equatable_if_let = "deny"
|
||||
collection_is_never_read = "deny"
|
||||
branches_sharing_code = "deny"
|
||||
pathbuf_init_then_push = "deny"
|
||||
option_as_ref_cloned = "deny"
|
||||
large_types_passed_by_value = "deny"
|
||||
# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启
|
||||
expl_impl_clone_on_copy = "deny"
|
||||
copy_iterator = "deny"
|
||||
cloned_instead_of_copied = "deny"
|
||||
# self_only_used_in_recursion = "deny" // Since 1.92.0
|
||||
unnecessary_self_imports = "deny"
|
||||
unused_trait_names = "deny"
|
||||
wildcard_imports = "deny"
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: 경량 모드
|
||||
body: 경량 모드에 진입했습니다.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: 곧 종료
|
||||
body: Clash Verge가 곧 종료됩니다.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: Lightweight Mode
|
||||
body: Entered lightweight mode.
|
||||
profilesReactivated:
|
||||
title: Profiles
|
||||
body: Profile Reactivated.
|
||||
appQuit:
|
||||
title: About to Exit
|
||||
body: Clash Verge is about to exit.
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: 轻量模式
|
||||
body: 已进入轻量模式。
|
||||
profilesReactivated:
|
||||
title: 订阅
|
||||
body: 订阅已激活。
|
||||
appQuit:
|
||||
title: 即将退出
|
||||
body: Clash Verge 即将退出。
|
||||
|
||||
@@ -15,6 +15,9 @@ notifications:
|
||||
lightweightModeEntered:
|
||||
title: 輕量模式
|
||||
body: 已進入輕量模式。
|
||||
profilesReactivated:
|
||||
title: 訂閱
|
||||
body: 訂閱已啟用。
|
||||
appQuit:
|
||||
title: 即將退出
|
||||
body: Clash Verge 即將退出。
|
||||
|
||||
@@ -3,12 +3,10 @@ use crate::core::sysopt::Sysopt;
|
||||
use crate::utils::resolve::ui::{self, UiReadyStage};
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
feat, logging,
|
||||
utils::{
|
||||
dirs::{self, PathBufExec as _},
|
||||
logging::Type,
|
||||
},
|
||||
feat,
|
||||
utils::dirs::{self, PathBufExec as _},
|
||||
};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
use std::path::Path;
|
||||
use tauri::{AppHandle, Manager as _};
|
||||
@@ -84,8 +82,8 @@ pub async fn restart_app() -> CmdResult<()> {
|
||||
|
||||
/// 获取便携版标识
|
||||
#[tauri::command]
|
||||
pub fn get_portable_flag() -> CmdResult<bool> {
|
||||
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
|
||||
pub fn get_portable_flag() -> bool {
|
||||
*dirs::PORTABLE_FLAG.get().unwrap_or(&false)
|
||||
}
|
||||
|
||||
/// 获取应用目录
|
||||
@@ -241,16 +239,14 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<Stri
|
||||
|
||||
/// 通知UI已准备就绪
|
||||
#[tauri::command]
|
||||
pub fn notify_ui_ready() -> CmdResult<()> {
|
||||
pub fn notify_ui_ready() {
|
||||
logging!(info, Type::Cmd, "前端UI已准备就绪");
|
||||
ui::mark_ui_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// UI加载阶段
|
||||
#[tauri::command]
|
||||
pub fn update_ui_stage(stage: UiReadyStage) -> CmdResult<()> {
|
||||
pub fn update_ui_stage(stage: UiReadyStage) {
|
||||
logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage);
|
||||
ui::update_ui_ready_stage(stage);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::CmdResult;
|
||||
use crate::feat;
|
||||
use crate::utils::dirs;
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
@@ -6,7 +7,7 @@ use crate::{
|
||||
constants,
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
};
|
||||
use crate::{feat, logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use compact_str::CompactString;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
@@ -28,7 +29,7 @@ pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
/// 修改Clash配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
feat::patch_clash(payload).await.stringify_err()
|
||||
feat::patch_clash(&payload).await.stringify_err()
|
||||
}
|
||||
|
||||
/// 修改Clash模式
|
||||
@@ -45,6 +46,11 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
|
||||
|
||||
match CoreManager::global().change_core(&clash_core).await {
|
||||
Ok(_) => {
|
||||
logging_error!(
|
||||
Type::Core,
|
||||
Config::profiles().await.latest_arc().save_file().await
|
||||
);
|
||||
|
||||
// 切换内核后重启内核
|
||||
match CoreManager::global().restart_core().await {
|
||||
Ok(_) => {
|
||||
@@ -88,6 +94,10 @@ pub async fn start_core() -> CmdResult {
|
||||
/// 关闭核心
|
||||
#[tauri::command]
|
||||
pub async fn stop_core() -> CmdResult {
|
||||
logging_error!(
|
||||
Type::Core,
|
||||
Config::profiles().await.latest_arc().save_file().await
|
||||
);
|
||||
let result = CoreManager::global().stop_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -98,6 +108,10 @@ pub async fn stop_core() -> CmdResult {
|
||||
/// 重启核心
|
||||
#[tauri::command]
|
||||
pub async fn restart_core() -> CmdResult {
|
||||
logging_error!(
|
||||
Type::Core,
|
||||
Config::profiles().await.latest_arc().save_file().await
|
||||
);
|
||||
let result = CoreManager::global().restart_core().await.stringify_err();
|
||||
if result.is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
@@ -170,7 +184,7 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||
|
||||
// 应用DNS配置到运行时配置
|
||||
Config::runtime().await.edit_draft(|d| {
|
||||
d.patch_config(patch);
|
||||
d.patch_config(&patch);
|
||||
});
|
||||
|
||||
// 重新生成配置
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
@@ -12,6 +11,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
let cookie_store = Arc::new(Jar::default());
|
||||
|
||||
let client_with_cookies = match Client::builder()
|
||||
.use_rustls_tls()
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
.cookie_provider(Arc::clone(&cookie_store))
|
||||
.build() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
@@ -4,7 +4,7 @@ use reqwest::Client;
|
||||
use tauri::command;
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
mod bahamut;
|
||||
mod bilibili;
|
||||
@@ -42,6 +42,7 @@ pub async fn get_unlock_items() -> Result<Vec<UnlockItem>, String> {
|
||||
#[command]
|
||||
pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
let client = match Client::builder()
|
||||
.use_rustls_tls()
|
||||
.user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 13] = [
|
||||
"ChatGPT Web",
|
||||
"Claude",
|
||||
"Gemini",
|
||||
"Youtube Premium",
|
||||
"YouTube Premium",
|
||||
"Bahamut Anime",
|
||||
"Netflix",
|
||||
"Disney+",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::Local;
|
||||
use rust_iso3166;
|
||||
|
||||
pub fn get_local_date_string() -> String {
|
||||
let now = Local::now();
|
||||
@@ -6,16 +7,70 @@ pub fn get_local_date_string() -> String {
|
||||
}
|
||||
|
||||
pub fn country_code_to_emoji(country_code: &str) -> String {
|
||||
let country_code = country_code.to_uppercase();
|
||||
if country_code.len() < 2 {
|
||||
return String::new();
|
||||
}
|
||||
let uc = country_code.to_ascii_uppercase();
|
||||
|
||||
let bytes = country_code.as_bytes();
|
||||
// 长度校验:仅允许 2 或 3
|
||||
match uc.len() {
|
||||
2 => {
|
||||
// 校验是否是合法 alpha2
|
||||
if rust_iso3166::from_alpha2(&uc).is_none() {
|
||||
return String::new();
|
||||
}
|
||||
alpha2_to_emoji(&uc)
|
||||
}
|
||||
3 => {
|
||||
// 转换并校验 alpha3
|
||||
match rust_iso3166::from_alpha3(&uc) {
|
||||
Some(c) => {
|
||||
let alpha2 = c.alpha2.to_ascii_uppercase();
|
||||
alpha2_to_emoji(&alpha2)
|
||||
}
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha2_to_emoji(alpha2: &str) -> String {
|
||||
let bytes = alpha2.as_bytes();
|
||||
let c1 = 0x1F1E6 + (bytes[0] as u32) - ('A' as u32);
|
||||
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
||||
|
||||
char::from_u32(c1)
|
||||
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||
.and_then(|x| char::from_u32(c2).map(|y| format!("{x}{y}")))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::country_code_to_emoji;
|
||||
|
||||
#[test]
|
||||
fn country_code_to_emoji_iso2() {
|
||||
assert_eq!(country_code_to_emoji("CN"), "🇨🇳");
|
||||
assert_eq!(country_code_to_emoji("us"), "🇺🇸");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_code_to_emoji_iso3() {
|
||||
assert_eq!(country_code_to_emoji("CHN"), "🇨🇳");
|
||||
assert_eq!(country_code_to_emoji("USA"), "🇺🇸");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_code_to_emoji_invalid() {
|
||||
assert_eq!(country_code_to_emoji("XXX"), "");
|
||||
assert_eq!(country_code_to_emoji("ZZ"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_code_to_emoji_short() {
|
||||
assert_eq!(country_code_to_emoji("C"), "");
|
||||
assert_eq!(country_code_to_emoji(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn country_code_to_emoji_long() {
|
||||
assert_eq!(country_code_to_emoji("CNAAA"), "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
@@ -42,14 +42,14 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
name: "YouTube Premium".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
name: "YouTube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
@@ -57,7 +57,7 @@ pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Youtube Premium".to_string(),
|
||||
name: "YouTube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
use super::CmdResult;
|
||||
use crate::cmd::StringifyErr as _;
|
||||
use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery};
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use gethostname::gethostname;
|
||||
use network_interface::NetworkInterface;
|
||||
use serde_yaml_ng::Mapping;
|
||||
use sysproxy::{Autoproxy, Sysproxy};
|
||||
use tauri_plugin_clash_verge_sysinfo;
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
logging!(debug, Type::Network, "异步获取系统代理配置");
|
||||
|
||||
let current = AsyncProxyQuery::get_system_proxy().await;
|
||||
let sys_proxy = Sysproxy::get_system_proxy().stringify_err()?;
|
||||
let Sysproxy {
|
||||
ref host,
|
||||
ref bypass,
|
||||
ref port,
|
||||
ref enable,
|
||||
} = sys_proxy;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
map.insert("enable".into(), (*enable).into());
|
||||
map.insert("server".into(), format!("{}:{}", host, port).into());
|
||||
map.insert("bypass".into(), bypass.as_str().into());
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回系统代理配置: enable={}, {}:{}",
|
||||
current.enable,
|
||||
current.host,
|
||||
current.port
|
||||
sys_proxy.enable,
|
||||
sys_proxy.host,
|
||||
sys_proxy.port
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
@@ -35,37 +39,31 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
/// 获取自动代理配置
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
||||
logging!(debug, Type::Network, "开始获取自动代理配置(事件驱动)");
|
||||
|
||||
let proxy_manager = EventDrivenProxyManager::global();
|
||||
|
||||
let current = proxy_manager.get_auto_proxy_cached().await;
|
||||
// 异步请求更新,立即返回缓存数据
|
||||
AsyncHandler::spawn(move || async move {
|
||||
let _ = proxy_manager.get_auto_proxy_async().await;
|
||||
});
|
||||
let auto_proxy = Autoproxy::get_auto_proxy().stringify_err()?;
|
||||
let Autoproxy {
|
||||
ref enable,
|
||||
ref url,
|
||||
} = auto_proxy;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert("url".into(), current.url.clone().into());
|
||||
map.insert("enable".into(), (*enable).into());
|
||||
map.insert("url".into(), url.as_str().into());
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"返回自动代理配置(缓存): enable={}, url={}",
|
||||
current.enable,
|
||||
current.url
|
||||
auto_proxy.enable,
|
||||
auto_proxy.url
|
||||
);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 获取系统主机名
|
||||
#[tauri::command]
|
||||
pub fn get_system_hostname() -> CmdResult<String> {
|
||||
use gethostname::gethostname;
|
||||
|
||||
pub fn get_system_hostname() -> String {
|
||||
// 获取系统主机名,处理可能的非UTF-8字符
|
||||
let hostname = match gethostname().into_string() {
|
||||
match gethostname().into_string() {
|
||||
Ok(name) => name,
|
||||
Err(os_string) => {
|
||||
// 对于包含非UTF-8的主机名,使用调试格式化
|
||||
@@ -73,21 +71,13 @@ pub fn get_system_hostname() -> CmdResult<String> {
|
||||
// 去掉可能存在的引号
|
||||
fallback.trim_matches('"').to_string()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取网络接口列表
|
||||
#[tauri::command]
|
||||
pub fn get_network_interfaces() -> Vec<String> {
|
||||
use sysinfo::Networks;
|
||||
let mut result = Vec::new();
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
for (interface_name, _) in &networks {
|
||||
result.push(interface_name.clone());
|
||||
}
|
||||
result
|
||||
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
|
||||
}
|
||||
|
||||
/// 获取网络接口详细信息
|
||||
|
||||
@@ -10,12 +10,14 @@ use crate::{
|
||||
profiles_append_item_safe,
|
||||
},
|
||||
core::{CoreManager, handle, timer::Timer, tray::Tray},
|
||||
feat, logging,
|
||||
feat,
|
||||
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||
process::AsyncHandler,
|
||||
ret_err,
|
||||
utils::{dirs, help, logging::Type},
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use clash_verge_draft::SharedBox;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use scopeguard::defer;
|
||||
use smartstring::alias::String;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -24,10 +26,10 @@ use std::time::Duration;
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
pub async fn get_profiles() -> CmdResult<SharedBox<IProfiles>> {
|
||||
logging!(debug, Type::Cmd, "获取配置文件列表");
|
||||
let draft = Config::profiles().await;
|
||||
let data = (**draft.data_arc()).clone();
|
||||
let data = draft.data_arc();
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
@@ -35,14 +37,29 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
#[tauri::command]
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
match feat::enhance_profiles().await {
|
||||
Ok(_) => {}
|
||||
Ok((true, _)) => {
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
Ok((false, msg)) => {
|
||||
let message: String = if msg.is_empty() {
|
||||
"Failed to reactivate profiles".into()
|
||||
} else {
|
||||
msg
|
||||
};
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"Reactivate profiles command failed validation: {}",
|
||||
message.as_str()
|
||||
);
|
||||
Err(message)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Cmd, "{}", e);
|
||||
return Err(e.to_string().into());
|
||||
Err(e.to_string().into())
|
||||
}
|
||||
}
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导入配置文件
|
||||
@@ -76,7 +93,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
return Err(format!("导入订阅失败: {}", e).into());
|
||||
}
|
||||
}
|
||||
// 立即发送配置变更通知
|
||||
|
||||
if let Some(uid) = &item.uid {
|
||||
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
@@ -87,6 +104,7 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
||||
if let Some(uid) = uid_clone {
|
||||
// 延迟发送,确保文件已完全写入
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
|
||||
@@ -119,9 +137,9 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||
Ok(_) => {
|
||||
// 发送配置变更通知
|
||||
if let Some(uid) = &item.uid {
|
||||
if let Some(uid) = item.uid.clone() {
|
||||
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
|
||||
handle::Handle::notify_profile_changed(uid.clone());
|
||||
handle::Handle::notify_profile_changed(uid);
|
||||
}
|
||||
Config::profiles().await.apply();
|
||||
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
|
||||
@@ -477,7 +495,7 @@ pub async fn view_profile(index: String) -> CmdResult {
|
||||
.get_item(&index)
|
||||
.stringify_err()?
|
||||
.file
|
||||
.clone()
|
||||
.as_ref()
|
||||
.ok_or("the file field is null")?;
|
||||
|
||||
let path = dirs::app_profiles_dir()
|
||||
@@ -497,7 +515,11 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles().await;
|
||||
let profiles_ref = profiles.latest_arc();
|
||||
PrfItem {
|
||||
file: profiles_ref.get_item(&index).stringify_err()?.file.clone(),
|
||||
file: profiles_ref
|
||||
.get_item(&index)
|
||||
.stringify_err()?
|
||||
.file
|
||||
.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::CmdResult;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
// TODO: 前端通过 emit 发送更新事件, tray 监听更新事件
|
||||
/// 同步托盘和GUI的代理选择状态
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
config::{Config, ConfigType},
|
||||
core::CoreManager,
|
||||
log_err,
|
||||
};
|
||||
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use clash_verge_logging::{Type, logging_error};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// 获取运行时配置
|
||||
#[tauri::command]
|
||||
@@ -35,7 +31,7 @@ pub async fn get_runtime_yaml() -> CmdResult<String> {
|
||||
|
||||
/// 获取运行时存在的键
|
||||
#[tauri::command]
|
||||
pub async fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
pub async fn get_runtime_exists() -> CmdResult<HashSet<String>> {
|
||||
Ok(Config::runtime().await.latest_arc().exists_keys.clone())
|
||||
}
|
||||
|
||||
@@ -104,14 +100,9 @@ pub async fn update_proxy_chain_config_in_runtime(
|
||||
{
|
||||
let runtime = Config::runtime().await;
|
||||
runtime.edit_draft(|d| d.update_proxy_chain_config(proxy_chain_config));
|
||||
runtime.apply();
|
||||
// 我们需要在 CoreManager 中验证并应用配置,这里不应该直接调用 runtime.apply()
|
||||
}
|
||||
|
||||
// 生成新的运行配置文件并通知 Clash 核心重新加载
|
||||
let run_path = Config::generate_file(ConfigType::Run)
|
||||
.await
|
||||
.stringify_err()?;
|
||||
log_err!(CoreManager::global().put_configs_force(run_path).await);
|
||||
logging_error!(Type::Core, CoreManager::global().update_config().await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::{
|
||||
cmd::StringifyErr as _,
|
||||
config::{Config, PrfItem},
|
||||
core::{CoreManager, handle, validate::CoreConfigValidator},
|
||||
logging,
|
||||
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||
utils::{dirs, logging::Type},
|
||||
utils::dirs,
|
||||
};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
use tokio::fs;
|
||||
|
||||
|
||||
@@ -1,64 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{CoreManager, handle, manager::RunningMode},
|
||||
logging,
|
||||
module::sysinfo::PlatformSpecification,
|
||||
utils::logging::Type,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use once_cell::sync::Lazy;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt as _;
|
||||
use tokio::time::Instant;
|
||||
|
||||
// 存储应用启动时间的全局变量
|
||||
static APP_START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| unsafe { libc::geteuid() } == 0);
|
||||
#[cfg(target_os = "windows")]
|
||||
static APPS_RUN_AS_ADMIN: Lazy<bool> = Lazy::new(|| {
|
||||
Token::with_current_process()
|
||||
.and_then(|token| token.privilege_level())
|
||||
.map(|level| level != PrivilegeLevel::NotPrivileged)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_diagnostic_info() -> CmdResult<()> {
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
let cliboard = app_handle.clipboard();
|
||||
if cliboard.write_text(info).is_err() {
|
||||
logging!(error, Type::System, "Failed to write to clipboard");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_info() -> CmdResult<String> {
|
||||
let sysinfo = PlatformSpecification::new_sync();
|
||||
let info = format!("{sysinfo:?}");
|
||||
Ok(info)
|
||||
}
|
||||
use crate::core::{CoreManager, manager::RunningMode};
|
||||
|
||||
/// 获取当前内核运行模式
|
||||
#[tauri::command]
|
||||
pub async fn get_running_mode() -> Result<Arc<RunningMode>, String> {
|
||||
Ok(CoreManager::global().get_running_mode())
|
||||
}
|
||||
|
||||
/// 获取应用的运行时间(毫秒)
|
||||
#[tauri::command]
|
||||
pub fn get_app_uptime() -> CmdResult<u128> {
|
||||
Ok(APP_START_TIME.elapsed().as_millis())
|
||||
}
|
||||
|
||||
/// 检查应用是否以管理员身份运行
|
||||
#[tauri::command]
|
||||
pub fn is_admin() -> CmdResult<bool> {
|
||||
Ok(*APPS_RUN_AS_ADMIN)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ mod platform {
|
||||
mod platform {
|
||||
use super::CmdResult;
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
pub const fn invoke_uwp_tool() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use super::CmdResult;
|
||||
use crate::{
|
||||
core::{handle, validate::CoreConfigValidator},
|
||||
logging,
|
||||
utils::logging::Type,
|
||||
};
|
||||
use crate::core::{handle, validate::CoreConfigValidator};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use smartstring::alias::String;
|
||||
|
||||
/// 发送脚本验证通知消息
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::CmdResult;
|
||||
use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox};
|
||||
use crate::{cmd::StringifyErr as _, config::IVerge, feat};
|
||||
use clash_verge_draft::SharedBox;
|
||||
|
||||
/// 获取Verge配置
|
||||
#[tauri::command]
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::config::Config;
|
||||
use crate::constants::{network, tun as tun_const};
|
||||
use crate::utils::dirs::{ipc_path, path_to_str};
|
||||
use crate::utils::{dirs, help};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::{
|
||||
@@ -16,7 +16,6 @@ pub struct IClashTemp(pub Mapping);
|
||||
|
||||
impl IClashTemp {
|
||||
pub async fn new() -> Self {
|
||||
let template = Self::template();
|
||||
let clash_path_result = dirs::clash_path();
|
||||
let map_result = if let Ok(path) = clash_path_result {
|
||||
help::read_mapping(&path).await
|
||||
@@ -26,24 +25,26 @@ impl IClashTemp {
|
||||
|
||||
match map_result {
|
||||
Ok(mut map) => {
|
||||
template.0.keys().for_each(|key| {
|
||||
if !map.contains_key(key)
|
||||
&& let Some(value) = template.0.get(key)
|
||||
{
|
||||
map.insert(key.clone(), value.clone());
|
||||
let template_map = Self::template().0;
|
||||
for (key, value) in template_map.into_iter() {
|
||||
if !map.contains_key(&key) {
|
||||
map.insert(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确保 secret 字段存在且不为空
|
||||
if let Some(Value::String(s)) = map.get_mut("secret")
|
||||
if let Some(val) = map.get_mut("secret")
|
||||
&& let Value::String(s) = val
|
||||
&& s.is_empty()
|
||||
{
|
||||
*s = "set-your-secret".into();
|
||||
}
|
||||
|
||||
Self(Self::guard(map))
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "{err}");
|
||||
template
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,9 +145,9 @@ impl IClashTemp {
|
||||
config
|
||||
}
|
||||
|
||||
pub fn patch_config(&mut self, patch: Mapping) {
|
||||
for (key, value) in patch.into_iter() {
|
||||
self.0.insert(key, value);
|
||||
pub fn patch_config(&mut self, patch: &Mapping) {
|
||||
for (key, value) in patch.iter() {
|
||||
self.0.insert(key.to_owned(), value.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use super::{IClashTemp, IProfiles, IVerge};
|
||||
use crate::{
|
||||
cmd,
|
||||
config::{PrfItem, profiles_append_item_safe},
|
||||
constants::{files, timing},
|
||||
core::{CoreManager, handle, service, tray, validate::CoreConfigValidator},
|
||||
enhance, logging, logging_error,
|
||||
utils::{Draft, dirs, help, logging::Type},
|
||||
core::{
|
||||
CoreManager,
|
||||
handle::{self, Handle},
|
||||
service, tray,
|
||||
validate::CoreConfigValidator,
|
||||
},
|
||||
enhance,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use backoff::{Error as BackoffError, ExponentialBackoff};
|
||||
use clash_verge_draft::Draft;
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
use clash_verge_types::runtime::IRuntime;
|
||||
use smartstring::alias::String;
|
||||
use std::path::PathBuf;
|
||||
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -57,9 +66,10 @@ impl Config {
|
||||
Self::ensure_default_profile_items().await?;
|
||||
|
||||
// init Tun mode
|
||||
if !cmd::system::is_admin().unwrap_or_default()
|
||||
&& service::is_service_available().await.is_err()
|
||||
{
|
||||
let handle = Handle::app_handle();
|
||||
let is_admin = is_current_app_handle_admin(handle);
|
||||
let is_service_available = service::is_service_available().await.is_ok();
|
||||
if !is_admin && !is_service_available {
|
||||
let verge = Self::verge().await;
|
||||
verge.edit_draft(|d| {
|
||||
d.enable_tun_mode = Some(false);
|
||||
@@ -200,6 +210,32 @@ impl Config {
|
||||
logging!(error, Type::Setup, "Config init verification failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 升级草稿为正式数据,并写入文件。避免用户行为丢失。
|
||||
// 仅在应用退出、重启、关机监听事件启用
|
||||
pub async fn apply_all_and_save_file() {
|
||||
logging!(info, Type::Config, "save all draft data");
|
||||
let save_clash_task = AsyncHandler::spawn(|| async {
|
||||
let clash = Self::clash().await;
|
||||
clash.apply();
|
||||
logging_error!(Type::Config, clash.data_arc().save_config().await);
|
||||
});
|
||||
|
||||
let save_verge_task = AsyncHandler::spawn(|| async {
|
||||
let verge = Self::verge().await;
|
||||
verge.apply();
|
||||
logging_error!(Type::Config, verge.data_arc().save_file().await);
|
||||
});
|
||||
|
||||
let save_profiles_task = AsyncHandler::spawn(|| async {
|
||||
let profiles = Self::profiles().await;
|
||||
profiles.apply();
|
||||
logging_error!(Type::Config, profiles.data_arc().save_file().await);
|
||||
});
|
||||
|
||||
let _ = tokio::join!(save_clash_task, save_verge_task, save_profiles_task);
|
||||
logging!(info, Type::Config, "save all draft data finished");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -4,10 +4,9 @@ mod config;
|
||||
mod encrypt;
|
||||
mod prfitem;
|
||||
pub mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, runtime::*, verge::*};
|
||||
pub use self::{clash::*, config::*, encrypt::*, prfitem::*, profiles::*, verge::*};
|
||||
|
||||
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||
|
||||
@@ -555,32 +555,33 @@ impl PrfItem {
|
||||
|
||||
impl PrfItem {
|
||||
/// 获取current指向的订阅的merge
|
||||
pub fn current_merge(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.clone())
|
||||
pub fn current_merge(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.merge.as_ref())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的script
|
||||
pub fn current_script(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.script.clone())
|
||||
pub fn current_script(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.script.as_ref())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的rules
|
||||
pub fn current_rules(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.clone())
|
||||
pub fn current_rules(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.rules.as_ref())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的proxies
|
||||
pub fn current_proxies(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.clone())
|
||||
pub fn current_proxies(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.proxies.as_ref())
|
||||
}
|
||||
|
||||
/// 获取current指向的订阅的groups
|
||||
pub fn current_groups(&self) -> Option<String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.clone())
|
||||
pub fn current_groups(&self) -> Option<&String> {
|
||||
self.option.as_ref().and_then(|o| o.groups.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
// 向前兼容,默认为订阅启用自动更新
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
const fn default_allow_auto_update() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::utils::{
|
||||
dirs::{self, PathBufExec as _},
|
||||
help,
|
||||
};
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::Mapping;
|
||||
use smartstring::alias::String;
|
||||
@@ -21,6 +21,12 @@ pub struct IProfiles {
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
}
|
||||
|
||||
pub struct IProfilePreview<'a> {
|
||||
pub uid: &'a String,
|
||||
pub name: &'a String,
|
||||
pub is_current: bool,
|
||||
}
|
||||
|
||||
/// 清理结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CleanupResult {
|
||||
@@ -367,14 +373,20 @@ impl IProfiles {
|
||||
self.current.as_ref() == Some(index)
|
||||
}
|
||||
|
||||
/// 获取所有的profiles(uid,名称)
|
||||
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(&String, &String)>> {
|
||||
/// 获取所有的profiles(uid,名称, 是否为 current)
|
||||
pub fn profiles_preview(&self) -> Option<Vec<IProfilePreview<'_>>> {
|
||||
self.items.as_ref().map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let (Some(uid), Some(name)) = (e.uid.as_ref(), e.name.as_ref()) {
|
||||
Some((uid, name))
|
||||
let is_current = self.is_current_profile_index(uid);
|
||||
let preview = IProfilePreview {
|
||||
uid,
|
||||
name,
|
||||
is_current,
|
||||
};
|
||||
Some(preview)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use crate::enhance::field::use_keys;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use smartstring::alias::String;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IRuntime {
|
||||
pub config: Option<Mapping>,
|
||||
// 记录在订阅中(包括merge和script生成的)出现过的keys
|
||||
// 这些keys不一定都生效
|
||||
pub exists_keys: Vec<String>,
|
||||
pub chain_logs: HashMap<String, Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl IRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 这里只更改 allow-lan | ipv6 | log-level | tun
|
||||
pub fn patch_config(&mut self, patch: Mapping) {
|
||||
if let Some(config) = self.config.as_mut() {
|
||||
["allow-lan", "ipv6", "log-level", "unified-delay"]
|
||||
.into_iter()
|
||||
.for_each(|key| {
|
||||
if let Some(value) = patch.get(key).to_owned() {
|
||||
config.insert(key.into(), value.clone());
|
||||
}
|
||||
});
|
||||
|
||||
let patch_tun = patch.get("tun");
|
||||
if patch_tun.is_some() {
|
||||
let tun = config.get("tun");
|
||||
let mut tun: Mapping = tun.map_or_else(Mapping::new, |val| {
|
||||
val.as_mapping().cloned().unwrap_or_else(Mapping::new)
|
||||
});
|
||||
let patch_tun = patch_tun.map_or_else(Mapping::new, |val| {
|
||||
val.as_mapping().cloned().unwrap_or_else(Mapping::new)
|
||||
});
|
||||
use_keys(&patch_tun).into_iter().for_each(|key| {
|
||||
if let Some(value) = patch_tun.get(key.as_str()) {
|
||||
tun.insert(Value::from(key.as_str()), value.clone());
|
||||
}
|
||||
});
|
||||
|
||||
config.insert("tun".into(), Value::from(tun));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//跟新链式代理配置文件
|
||||
/// {
|
||||
/// "proxies":[
|
||||
/// {
|
||||
/// name : 入口节点,
|
||||
/// type: xxx
|
||||
/// server: xxx
|
||||
/// port: xxx
|
||||
/// ports: xxx
|
||||
/// password: xxx
|
||||
/// skip-cert-verify: xxx,
|
||||
/// },
|
||||
/// {
|
||||
/// name : hop_node_1_xxxx,
|
||||
/// type: xxx
|
||||
/// server: xxx
|
||||
/// port: xxx
|
||||
/// ports: xxx
|
||||
/// password: xxx
|
||||
/// skip-cert-verify: xxx,
|
||||
/// dialer-proxy : "入口节点"
|
||||
/// },
|
||||
/// {
|
||||
/// name : 出口节点,
|
||||
/// type: xxx
|
||||
/// server: xxx
|
||||
/// port: xxx
|
||||
/// ports: xxx
|
||||
/// password: xxx
|
||||
/// skip-cert-verify: xxx,
|
||||
/// dialer-proxy : "hop_node_1_xxxx"
|
||||
/// }
|
||||
/// ],
|
||||
/// "proxy-groups" : [
|
||||
/// {
|
||||
/// name : "proxy_chain",
|
||||
/// type: "select",
|
||||
/// proxies ["出口节点"]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
///
|
||||
/// 传入none 为删除
|
||||
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
|
||||
if let Some(config) = self.config.as_mut() {
|
||||
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
|
||||
proxies.iter_mut().for_each(|proxy| {
|
||||
if let Some(proxy) = proxy.as_mapping_mut()
|
||||
&& proxy.get("dialer-proxy").is_some()
|
||||
{
|
||||
proxy.remove("dialer-proxy");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config
|
||||
&& let Some(Value::Sequence(proxies)) = config.get_mut("proxies")
|
||||
{
|
||||
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
|
||||
if let Some(Value::Mapping(proxy)) = proxies
|
||||
.iter_mut()
|
||||
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
|
||||
&& i != 0
|
||||
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
|
||||
{
|
||||
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::config::Config;
|
||||
use crate::{
|
||||
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted},
|
||||
logging,
|
||||
utils::{dirs, help, i18n, logging::Type},
|
||||
utils::{dirs, help, i18n},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smartstring::alias::String;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod network {
|
||||
pub const DEFAULT_PROXY_HOST: &str = "127.0.0.1";
|
||||
pub const DEFAULT_EXTERNAL_CONTROLLER: &str = "127.0.0.1:9097";
|
||||
|
||||
pub mod ports {
|
||||
@@ -20,23 +19,10 @@ pub mod network {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bypass {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT: &str = "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const DEFAULT: &str =
|
||||
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const DEFAULT: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
|
||||
}
|
||||
|
||||
pub mod timing {
|
||||
use super::Duration;
|
||||
|
||||
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(500);
|
||||
pub const CONFIG_RELOAD_DELAY: Duration = Duration::from_millis(300);
|
||||
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
|
||||
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
|
||||
pub const ERROR_BATCH_DELAY: Duration = Duration::from_millis(300);
|
||||
@@ -58,15 +44,6 @@ pub mod files {
|
||||
pub const WINDOW_STATE: &str = "window_state.json";
|
||||
}
|
||||
|
||||
pub mod error_patterns {
|
||||
pub const CONNECTION_ERRORS: &[&str] = &[
|
||||
"Failed to create connection",
|
||||
"The system cannot find the file specified",
|
||||
"operation timed out",
|
||||
"connection refused",
|
||||
];
|
||||
}
|
||||
|
||||
pub mod tun {
|
||||
pub const DEFAULT_STACK: &str = "gvisor";
|
||||
|
||||
|
||||
@@ -1,565 +0,0 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::process::AsyncHandler;
|
||||
use crate::{logging, utils::logging::Type};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use anyhow::anyhow;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AsyncAutoproxy {
|
||||
pub enable: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AsyncSysproxy {
|
||||
pub enable: bool,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub bypass: String,
|
||||
}
|
||||
|
||||
impl Default for AsyncSysproxy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable: false,
|
||||
host: "127.0.0.1".into(),
|
||||
port: 7897,
|
||||
bypass: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsyncProxyQuery;
|
||||
|
||||
impl AsyncProxyQuery {
|
||||
/// 异步获取自动代理配置(PAC)
|
||||
pub async fn get_auto_proxy() -> AsyncAutoproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"异步获取自动代理成功: enable={}, url={}",
|
||||
proxy.enable,
|
||||
proxy.url
|
||||
);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取自动代理失败: {e}");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取自动代理超时");
|
||||
AsyncAutoproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 异步获取系统代理配置
|
||||
pub async fn get_system_proxy() -> AsyncSysproxy {
|
||||
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
|
||||
Ok(Ok(proxy)) => {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"异步获取系统代理成功: enable={}, {}:{}",
|
||||
proxy.enable,
|
||||
proxy.host,
|
||||
proxy.port
|
||||
);
|
||||
proxy
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取系统代理失败: {e}");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
Err(_) => {
|
||||
logging!(warn, Type::Network, "Warning: 异步获取系统代理超时");
|
||||
AsyncSysproxy::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// Windows: 从注册表读取PAC配置
|
||||
AsyncHandler::spawn_blocking(move || -> Result<AsyncAutoproxy> {
|
||||
Self::get_pac_config_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_pac_config_from_registry() -> Result<AsyncAutoproxy> {
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
|
||||
|
||||
unsafe {
|
||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||
.encode_utf16()
|
||||
.collect::<Vec<u16>>();
|
||||
|
||||
let mut hkey: HKEY = ptr::null_mut();
|
||||
let result =
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
logging!(debug, Type::Network, "无法打开注册表项");
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
// 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用)
|
||||
let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut url_buffer = vec![0u16; 1024];
|
||||
let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD;
|
||||
let mut url_value_type: DWORD = 0;
|
||||
|
||||
let url_query_result = RegQueryValueExW(
|
||||
hkey,
|
||||
auto_config_url_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut url_value_type,
|
||||
url_buffer.as_mut_ptr() as *mut u8,
|
||||
&mut url_buffer_size,
|
||||
);
|
||||
|
||||
let mut pac_url = String::new();
|
||||
if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 {
|
||||
let end_pos = url_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(url_buffer.len());
|
||||
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
||||
logging!(debug, Type::Network, "从注册表读取到PAC URL: {pac_url}");
|
||||
}
|
||||
|
||||
// 2. 检查自动检测设置是否启用
|
||||
let auto_detect_name = "AutoDetect\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut auto_detect: DWORD = 0;
|
||||
let mut detect_buffer_size: DWORD = 4;
|
||||
let mut detect_value_type: DWORD = 0;
|
||||
|
||||
let detect_query_result = RegQueryValueExW(
|
||||
hkey,
|
||||
auto_detect_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut detect_value_type,
|
||||
&mut auto_detect as *mut DWORD as *mut u8,
|
||||
&mut detect_buffer_size,
|
||||
);
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
// PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用
|
||||
let pac_enabled = !pac_url.is_empty()
|
||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||
|
||||
if pac_enabled {
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}"
|
||||
);
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".into();
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
})
|
||||
} else {
|
||||
logging!(debug, Type::Network, "PAC配置未启用");
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// macOS: 使用 scutil --proxy 命令
|
||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(AsyncAutoproxy::default());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
crate::logging!(
|
||||
debug,
|
||||
crate::utils::logging::Type::Network,
|
||||
"scutil output: {stdout}"
|
||||
);
|
||||
|
||||
let mut pac_enabled = false;
|
||||
let mut pac_url = String::new();
|
||||
|
||||
// 解析 scutil 输出
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.contains("ProxyAutoConfigEnable") && line.contains("1") {
|
||||
pac_enabled = true;
|
||||
} else if line.contains("ProxyAutoConfigURLString") {
|
||||
// 正确解析包含冒号的URL
|
||||
// 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac"
|
||||
if let Some(colon_pos) = line.find(" : ") {
|
||||
pac_url = line[colon_pos + 3..].trim().into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::logging!(
|
||||
debug,
|
||||
crate::utils::logging::Type::Network,
|
||||
"解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}"
|
||||
);
|
||||
|
||||
Ok(AsyncAutoproxy {
|
||||
enable: pac_enabled && !pac_url.is_empty(),
|
||||
url: pac_url,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_auto_proxy_impl() -> Result<AsyncAutoproxy> {
|
||||
// Linux: 检查环境变量和GNOME设置
|
||||
|
||||
// 首先检查环境变量
|
||||
if let Ok(auto_proxy) = std::env::var("auto_proxy")
|
||||
&& !auto_proxy.is_empty()
|
||||
{
|
||||
return Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: auto_proxy,
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||
let output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(output) = output
|
||||
&& output.status.success()
|
||||
{
|
||||
let mode: String = String::from_utf8_lossy(&output.stdout).trim().into();
|
||||
if mode.contains("auto") {
|
||||
// 获取 PAC URL
|
||||
let pac_output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "autoconfig-url"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(pac_output) = pac_output
|
||||
&& pac_output.status.success()
|
||||
{
|
||||
let pac_url: String = String::from_utf8_lossy(&pac_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.into();
|
||||
|
||||
if !pac_url.is_empty() {
|
||||
return Ok(AsyncAutoproxy {
|
||||
enable: true,
|
||||
url: pac_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncAutoproxy::default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
// Windows: 使用注册表直接读取代理设置
|
||||
AsyncHandler::spawn_blocking(move || -> Result<AsyncSysproxy> {
|
||||
Self::get_system_proxy_from_registry()
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_system_proxy_from_registry() -> Result<AsyncSysproxy> {
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, HKEY};
|
||||
use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ};
|
||||
use winapi::um::winreg::{HKEY_CURRENT_USER, RegCloseKey, RegOpenKeyExW, RegQueryValueExW};
|
||||
|
||||
unsafe {
|
||||
let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0"
|
||||
.encode_utf16()
|
||||
.collect::<Vec<u16>>();
|
||||
|
||||
let mut hkey: HKEY = ptr::null_mut();
|
||||
let result =
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
|
||||
|
||||
if result != 0 {
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
// 检查代理是否启用
|
||||
let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut proxy_enable: DWORD = 0;
|
||||
let mut buffer_size: DWORD = 4;
|
||||
let mut value_type: DWORD = 0;
|
||||
|
||||
let enable_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_enable_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut value_type,
|
||||
&mut proxy_enable as *mut DWORD as *mut u8,
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 {
|
||||
RegCloseKey(hkey);
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
// 读取代理服务器设置
|
||||
let proxy_server_name = "ProxyServer\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut buffer = vec![0u16; 1024];
|
||||
let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD;
|
||||
let mut value_type: DWORD = 0;
|
||||
|
||||
let server_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_server_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut value_type,
|
||||
buffer.as_mut_ptr() as *mut u8,
|
||||
&mut buffer_size,
|
||||
);
|
||||
|
||||
let proxy_server = if server_result == 0 && value_type == REG_SZ && buffer_size > 0 {
|
||||
let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
|
||||
String::from_utf16_lossy(&buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// 读取代理绕过列表
|
||||
let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::<Vec<u16>>();
|
||||
let mut bypass_buffer = vec![0u16; 1024];
|
||||
let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD;
|
||||
let mut bypass_value_type: DWORD = 0;
|
||||
|
||||
let override_result = RegQueryValueExW(
|
||||
hkey,
|
||||
proxy_override_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut bypass_value_type,
|
||||
bypass_buffer.as_mut_ptr() as *mut u8,
|
||||
&mut bypass_buffer_size,
|
||||
);
|
||||
|
||||
let bypass_list =
|
||||
if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 {
|
||||
let end_pos = bypass_buffer
|
||||
.iter()
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(bypass_buffer.len());
|
||||
String::from_utf16_lossy(&bypass_buffer[..end_pos])
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
RegCloseKey(hkey);
|
||||
|
||||
if !proxy_server.is_empty() {
|
||||
// 解析服务器地址和端口
|
||||
let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') {
|
||||
let host = proxy_server[..colon_pos].into();
|
||||
let port = proxy_server[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
logging!(
|
||||
debug,
|
||||
Type::Network,
|
||||
"从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}"
|
||||
);
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: bypass_list,
|
||||
})
|
||||
} else {
|
||||
Ok(AsyncSysproxy::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
let output = Command::new("scutil").args(["--proxy"]).output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(AsyncSysproxy::default());
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
logging!(debug, Type::Network, "scutil proxy output: {stdout}");
|
||||
|
||||
let mut http_enabled = false;
|
||||
let mut http_host = String::new();
|
||||
let mut http_port = 8080u16;
|
||||
let mut exceptions: Vec<String> = Vec::new();
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.contains("HTTPEnable") && line.contains("1") {
|
||||
http_enabled = true;
|
||||
} else if line.contains("HTTPProxy") && !line.contains("Port") {
|
||||
if let Some(host_part) = line.split(':').nth(1) {
|
||||
http_host = host_part.trim().into();
|
||||
}
|
||||
} else if line.contains("HTTPPort") {
|
||||
if let Some(port_part) = line.split(':').nth(1)
|
||||
&& let Ok(port) = port_part.trim().parse::<u16>()
|
||||
{
|
||||
http_port = port;
|
||||
}
|
||||
} else if line.contains("ExceptionsList") {
|
||||
// 解析异常列表
|
||||
if let Some(list_part) = line.split(':').nth(1) {
|
||||
let list = list_part.trim();
|
||||
if !list.is_empty() {
|
||||
exceptions.push(list.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: http_enabled && !http_host.is_empty(),
|
||||
host: http_host,
|
||||
port: http_port,
|
||||
bypass: exceptions.join(","),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_system_proxy_impl() -> Result<AsyncSysproxy> {
|
||||
// Linux: 检查环境变量和桌面环境设置
|
||||
|
||||
// 首先检查环境变量
|
||||
if let Ok(http_proxy) = std::env::var("http_proxy")
|
||||
&& let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy)
|
||||
{
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
|
||||
if let Ok(https_proxy) = std::env::var("https_proxy")
|
||||
&& let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy)
|
||||
{
|
||||
return Ok(proxy_info);
|
||||
}
|
||||
|
||||
// 尝试使用 gsettings 获取 GNOME 代理设置
|
||||
let mode_output = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy", "mode"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(mode_output) = mode_output
|
||||
&& mode_output.status.success()
|
||||
{
|
||||
let mode: String = String::from_utf8_lossy(&mode_output.stdout).trim().into();
|
||||
if mode.contains("manual") {
|
||||
// 获取HTTP代理设置
|
||||
let host_result = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy.http", "host"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let port_result = Command::new("gsettings")
|
||||
.args(["get", "org.gnome.system.proxy.http", "port"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let (Ok(host_output), Ok(port_output)) = (host_result, port_result)
|
||||
&& host_output.status.success()
|
||||
&& port_output.status.success()
|
||||
{
|
||||
let host: String = String::from_utf8_lossy(&host_output.stdout)
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.into();
|
||||
|
||||
let port = String::from_utf8_lossy(&port_output.stdout)
|
||||
.trim()
|
||||
.parse::<u16>()
|
||||
.unwrap_or(8080);
|
||||
|
||||
if !host.is_empty() {
|
||||
return Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy::default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn parse_proxy_url(proxy_url: &str) -> Result<AsyncSysproxy> {
|
||||
// 解析形如 "http://proxy.example.com:8080" 的URL
|
||||
let url = proxy_url.trim();
|
||||
|
||||
// 移除协议前缀
|
||||
let url = if let Some(stripped) = url.strip_prefix("http://") {
|
||||
stripped
|
||||
} else if let Some(stripped) = url.strip_prefix("https://") {
|
||||
stripped
|
||||
} else {
|
||||
url
|
||||
};
|
||||
|
||||
// 解析主机和端口
|
||||
let (host, port) = if let Some(colon_pos) = url.rfind(':') {
|
||||
let host: String = url[..colon_pos].into();
|
||||
let port = url[colon_pos + 1..].parse::<u16>().unwrap_or(8080);
|
||||
(host, port)
|
||||
} else {
|
||||
(url.into(), 8080)
|
||||
};
|
||||
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("无效的代理URL"));
|
||||
}
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
host,
|
||||
port,
|
||||
bypass: std::env::var("no_proxy").unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user