Update On Mon Sep 9 20:33:15 CEST 2024

This commit is contained in:
github-action[bot]
2024-09-09 20:33:16 +02:00
parent 41d6e6b854
commit 4e8b852803
189 changed files with 25751 additions and 2987 deletions

1
.github/update.log vendored
View File

@@ -758,3 +758,4 @@ Update On Thu Sep 5 20:35:23 CEST 2024
Update On Fri Sep 6 20:34:58 CEST 2024 Update On Fri Sep 6 20:34:58 CEST 2024
Update On Sat Sep 7 20:31:47 CEST 2024 Update On Sat Sep 7 20:31:47 CEST 2024
Update On Sun Sep 8 20:32:53 CEST 2024 Update On Sun Sep 8 20:32:53 CEST 2024
Update On Mon Sep 9 20:33:04 CEST 2024

View File

@@ -98,6 +98,10 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies return pp.proxies
} }
func (pp *proxySetProvider) Count() int {
return len(pp.proxies)
}
func (pp *proxySetProvider) Touch() { func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch() pp.healthCheck.touch()
} }
@@ -126,7 +130,7 @@ func (pp *proxySetProvider) getSubscriptionInfo() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel() defer cancel()
resp, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), resp, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil, pp.Vehicle().Proxy()) http.MethodGet, nil, nil, pp.Vehicle().Proxy())
if err != nil { if err != nil {
return return
} }
@@ -267,6 +271,10 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies return cp.proxies
} }
func (cp *compatibleProvider) Count() int {
return len(cp.proxies)
}
func (cp *compatibleProvider) Touch() { func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch() cp.healthCheck.touch()
} }

View File

@@ -84,7 +84,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
if cfg.addrReuse { if cfg.addrReuse {
addrReuseToListenConfig(lc) addrReuseToListenConfig(lc)
} }
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CFMA) if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
socketHookToListenConfig(lc) socketHookToListenConfig(lc)
} else { } else {
if cfg.interfaceName != "" { if cfg.interfaceName != "" {
@@ -148,7 +148,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
setMultiPathTCP(dialer) setMultiPathTCP(dialer)
} }
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CFMA) if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
socketHookToToDialer(dialer) socketHookToToDialer(dialer)
} else { } else {
if opt.interfaceName != "" { if opt.interfaceName != "" {

View File

@@ -7,11 +7,11 @@ import (
) )
// SocketControl // SocketControl
// never change type traits because it's used in CFMA // never change type traits because it's used in CMFA
type SocketControl func(network, address string, conn syscall.RawConn) error type SocketControl func(network, address string, conn syscall.RawConn) error
// DefaultSocketHook // DefaultSocketHook
// never change type traits because it's used in CFMA // never change type traits because it's used in CMFA
var DefaultSocketHook SocketControl var DefaultSocketHook SocketControl
func socketHookToToDialer(dialer *net.Dialer) { func socketHookToToDialer(dialer *net.Dialer) {

View File

@@ -63,13 +63,13 @@ func TestPool_Basic(t *testing.T) {
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last) bar, exist := pool.LookBack(last)
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.Equal(t, pool.Lookup("foo.com"), netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5}))
assert.True(t, exist) assert.True(t, exist)
assert.Equal(t, bar, "bar.com") assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1})) assert.Equal(t, pool.Gateway(), netip.AddrFrom4([4]byte{192, 168, 0, 1}))
assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15})) assert.Equal(t, pool.Broadcast(), netip.AddrFrom4([4]byte{192, 168, 0, 15}))
assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5}))) assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5})))
assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6}))) assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6})))
@@ -91,13 +91,13 @@ func TestPool_BasicV6(t *testing.T) {
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last) bar, exist := pool.LookBack(last)
assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) assert.Equal(t, first, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")) assert.Equal(t, pool.Lookup("foo.com"), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")) assert.Equal(t, last, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))
assert.True(t, exist) assert.True(t, exist)
assert.Equal(t, bar, "bar.com") assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801")) assert.Equal(t, pool.Gateway(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff")) assert.Equal(t, pool.Broadcast(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
assert.Equal(t, pool.IPNet().String(), ipnet.String()) assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))) assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")))
assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806"))) assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806")))
@@ -143,8 +143,8 @@ func TestPool_CycleUsed(t *testing.T) {
} }
baz := pool.Lookup("baz.com") baz := pool.Lookup("baz.com")
next := pool.Lookup("foo.com") next := pool.Lookup("foo.com")
assert.True(t, foo == baz) assert.Equal(t, foo, baz)
assert.True(t, next == bar) assert.Equal(t, next, bar)
} }
} }
@@ -201,7 +201,7 @@ func TestPool_MaxCacheSize(t *testing.T) {
pool.Lookup("baz.com") pool.Lookup("baz.com")
next := pool.Lookup("foo.com") next := pool.Lookup("foo.com")
assert.False(t, first == next) assert.NotEqual(t, first, next)
} }
func TestPool_DoubleMapping(t *testing.T) { func TestPool_DoubleMapping(t *testing.T) {
@@ -231,7 +231,7 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazExist) assert.False(t, bazExist)
assert.True(t, barExist) assert.True(t, barExist)
assert.False(t, bazIP == newBazIP) assert.NotEqual(t, bazIP, newBazIP)
} }
func TestPool_Clone(t *testing.T) { func TestPool_Clone(t *testing.T) {
@@ -243,8 +243,8 @@ func TestPool_Clone(t *testing.T) {
first := pool.Lookup("foo.com") first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com") last := pool.Lookup("bar.com")
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4})) assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5})) assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5}))
newPool, _ := New(Options{ newPool, _ := New(Options{
IPNet: ipnet, IPNet: ipnet,
@@ -289,13 +289,13 @@ func TestPool_FlushFileCache(t *testing.T) {
baz := pool.Lookup("foo.com") baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com") nero := pool.Lookup("foo.com")
assert.True(t, foo == fox) assert.Equal(t, foo, fox)
assert.True(t, foo == next) assert.Equal(t, foo, next)
assert.False(t, foo == baz) assert.NotEqual(t, foo, baz)
assert.True(t, bar == bax) assert.Equal(t, bar, bax)
assert.True(t, bar == baz) assert.Equal(t, bar, baz)
assert.False(t, bar == next) assert.NotEqual(t, bar, next)
assert.True(t, baz == nero) assert.Equal(t, baz, nero)
} }
} }
@@ -318,11 +318,11 @@ func TestPool_FlushMemoryCache(t *testing.T) {
baz := pool.Lookup("foo.com") baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com") nero := pool.Lookup("foo.com")
assert.True(t, foo == fox) assert.Equal(t, foo, fox)
assert.True(t, foo == next) assert.Equal(t, foo, next)
assert.False(t, foo == baz) assert.NotEqual(t, foo, baz)
assert.True(t, bar == bax) assert.Equal(t, bar, bax)
assert.True(t, bar == baz) assert.Equal(t, bar, baz)
assert.False(t, bar == next) assert.NotEqual(t, bar, next)
assert.True(t, baz == nero) assert.Equal(t, baz, nero)
} }

View File

@@ -6,8 +6,10 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"sync"
"time" "time"
"github.com/metacubex/mihomo/common/atomic"
mihomoHttp "github.com/metacubex/mihomo/component/http" mihomoHttp "github.com/metacubex/mihomo/component/http"
"github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/mmdb"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@@ -18,12 +20,79 @@ var (
initGeoSite bool initGeoSite bool
initGeoIP int initGeoIP int
initASN bool initASN bool
initGeoSiteMutex sync.Mutex
initGeoIPMutex sync.Mutex
initASNMutex sync.Mutex
geoIpEnable atomic.Bool
geoSiteEnable atomic.Bool
asnEnable atomic.Bool
geoIpUrl string
mmdbUrl string
geoSiteUrl string
asnUrl string
) )
func GeoIpUrl() string {
return geoIpUrl
}
func SetGeoIpUrl(url string) {
geoIpUrl = url
}
func MmdbUrl() string {
return mmdbUrl
}
func SetMmdbUrl(url string) {
mmdbUrl = url
}
func GeoSiteUrl() string {
return geoSiteUrl
}
func SetGeoSiteUrl(url string) {
geoSiteUrl = url
}
func ASNUrl() string {
return asnUrl
}
func SetASNUrl(url string) {
asnUrl = url
}
func downloadToPath(url string, path string) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func InitGeoSite() error { func InitGeoSite() error {
geoSiteEnable.Store(true)
initGeoSiteMutex.Lock()
defer initGeoSiteMutex.Unlock()
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Can't find GeoSite.dat, start download") log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadGeoSite(C.Path.GeoSite()); err != nil { if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
} }
log.Infoln("Download GeoSite.dat finish") log.Infoln("Download GeoSite.dat finish")
@@ -35,7 +104,7 @@ func InitGeoSite() error {
if err := os.Remove(C.Path.GeoSite()); err != nil { if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error()) return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
} }
if err := downloadGeoSite(C.Path.GeoSite()); err != nil { if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
} }
} }
@@ -44,49 +113,14 @@ func InitGeoSite() error {
return nil return nil
} }
func downloadGeoSite(path string) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, C.GeoSiteUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func downloadGeoIP(path string) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, C.GeoIpUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func InitGeoIP() error { func InitGeoIP() error {
if C.GeodataMode { geoIpEnable.Store(true)
initGeoIPMutex.Lock()
defer initGeoIPMutex.Unlock()
if GeodataMode() {
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
log.Infoln("Can't find GeoIP.dat, start download") log.Infoln("Can't find GeoIP.dat, start download")
if err := downloadGeoIP(C.Path.GeoIP()); err != nil { if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
} }
log.Infoln("Download GeoIP.dat finish") log.Infoln("Download GeoIP.dat finish")
@@ -99,7 +133,7 @@ func InitGeoIP() error {
if err := os.Remove(C.Path.GeoIP()); err != nil { if err := os.Remove(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error()) return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error())
} }
if err := downloadGeoIP(C.Path.GeoIP()); err != nil { if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error()) return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
} }
} }
@@ -110,7 +144,7 @@ func InitGeoIP() error {
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Infoln("Can't find MMDB, start download") log.Infoln("Can't find MMDB, start download")
if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil { if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error()) return fmt.Errorf("can't download MMDB: %s", err.Error())
} }
} }
@@ -121,7 +155,7 @@ func InitGeoIP() error {
if err := os.Remove(C.Path.MMDB()); err != nil { if err := os.Remove(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
} }
if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil { if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error()) return fmt.Errorf("can't download MMDB: %s", err.Error())
} }
} }
@@ -131,9 +165,12 @@ func InitGeoIP() error {
} }
func InitASN() error { func InitASN() error {
asnEnable.Store(true)
initASNMutex.Lock()
defer initASNMutex.Unlock()
if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) {
log.Infoln("Can't find ASN.mmdb, start download") log.Infoln("Can't find ASN.mmdb, start download")
if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil {
return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) return fmt.Errorf("can't download ASN.mmdb: %s", err.Error())
} }
log.Infoln("Download ASN.mmdb finish") log.Infoln("Download ASN.mmdb finish")
@@ -145,7 +182,7 @@ func InitASN() error {
if err := os.Remove(C.Path.ASN()); err != nil { if err := os.Remove(C.Path.ASN()); err != nil {
return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) return fmt.Errorf("can't remove invalid ASN: %s", err.Error())
} }
if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil {
return fmt.Errorf("can't download ASN: %s", err.Error()) return fmt.Errorf("can't download ASN: %s", err.Error())
} }
} }
@@ -153,3 +190,15 @@ func InitASN() error {
} }
return nil return nil
} }
func GeoIpEnable() bool {
return geoIpEnable.Load()
}
func GeoSiteEnable() bool {
return geoSiteEnable.Load()
}
func ASNEnable() bool {
return asnEnable.Load()
}

View File

@@ -13,8 +13,6 @@ import (
var ( var (
geoMode bool geoMode bool
AutoUpdate bool
UpdateInterval int
geoLoaderName = "memconservative" geoLoaderName = "memconservative"
geoSiteMatcher = "succinct" geoSiteMatcher = "succinct"
) )
@@ -25,14 +23,6 @@ func GeodataMode() bool {
return geoMode return geoMode
} }
func GeoAutoUpdate() bool {
return AutoUpdate
}
func GeoUpdateInterval() int {
return UpdateInterval
}
func LoaderName() string { func LoaderName() string {
return geoLoaderName return geoLoaderName
} }
@@ -44,12 +34,6 @@ func SiteMatcherName() string {
func SetGeodataMode(newGeodataMode bool) { func SetGeodataMode(newGeodataMode bool) {
geoMode = newGeodataMode geoMode = newGeodataMode
} }
func SetGeoAutoUpdate(newAutoUpdate bool) {
AutoUpdate = newAutoUpdate
}
func SetGeoUpdateInterval(newGeoUpdateInterval int) {
UpdateInterval = newGeoUpdateInterval
}
func SetLoader(newLoader string) { func SetLoader(newLoader string) {
if newLoader == "memc" { if newLoader == "memc" {
@@ -209,8 +193,11 @@ func LoadGeoIPMatcher(country string) (router.IPMatcher, error) {
return matcher, nil return matcher, nil
} }
func ClearCache() { func ClearGeoSiteCache() {
loadGeoSiteMatcherListSF.Reset() loadGeoSiteMatcherListSF.Reset()
loadGeoSiteMatcherSF.Reset() loadGeoSiteMatcherSF.Reset()
}
func ClearGeoIPCache() {
loadGeoIPMatcherSF.Reset() loadGeoIPMatcherSF.Reset()
} }

View File

@@ -12,10 +12,21 @@ import (
"time" "time"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/mihomo/listener/inner"
) )
var (
ua string
)
func UA() string {
return ua
}
func SetUA(UA string) {
ua = UA
}
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) { func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
return HttpRequestWithProxy(ctx, url, method, header, body, "") return HttpRequestWithProxy(ctx, url, method, header, body, "")
} }
@@ -35,7 +46,7 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st
} }
if _, ok := header["User-Agent"]; !ok { if _, ok := header["User-Agent"]; !ok {
req.Header.Set("User-Agent", C.UA) req.Header.Set("User-Agent", UA())
} }
if err != nil { if err != nil {

View File

@@ -1,15 +1,9 @@
package mmdb package mmdb
import ( import (
"context"
"io"
"net/http"
"os"
"sync" "sync"
"time"
mihomoOnce "github.com/metacubex/mihomo/common/once" mihomoOnce "github.com/metacubex/mihomo/common/once"
mihomoHttp "github.com/metacubex/mihomo/component/http"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
@@ -25,26 +19,26 @@ const (
) )
var ( var (
IPreader IPReader ipReader IPReader
ASNreader ASNReader asnReader ASNReader
IPonce sync.Once ipOnce sync.Once
ASNonce sync.Once asnOnce sync.Once
) )
func LoadFromBytes(buffer []byte) { func LoadFromBytes(buffer []byte) {
IPonce.Do(func() { ipOnce.Do(func() {
mmdb, err := maxminddb.FromBytes(buffer) mmdb, err := maxminddb.FromBytes(buffer)
if err != nil { if err != nil {
log.Fatalln("Can't load mmdb: %s", err.Error()) log.Fatalln("Can't load mmdb: %s", err.Error())
} }
IPreader = IPReader{Reader: mmdb} ipReader = IPReader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType { switch mmdb.Metadata.DatabaseType {
case "sing-geoip": case "sing-geoip":
IPreader.databaseType = typeSing ipReader.databaseType = typeSing
case "Meta-geoip0": case "Meta-geoip0":
IPreader.databaseType = typeMetaV0 ipReader.databaseType = typeMetaV0
default: default:
IPreader.databaseType = typeMaxmind ipReader.databaseType = typeMaxmind
} }
}) })
} }
@@ -58,83 +52,45 @@ func Verify(path string) bool {
} }
func IPInstance() IPReader { func IPInstance() IPReader {
IPonce.Do(func() { ipOnce.Do(func() {
mmdbPath := C.Path.MMDB() mmdbPath := C.Path.MMDB()
log.Infoln("Load MMDB file: %s", mmdbPath) log.Infoln("Load MMDB file: %s", mmdbPath)
mmdb, err := maxminddb.Open(mmdbPath) mmdb, err := maxminddb.Open(mmdbPath)
if err != nil { if err != nil {
log.Fatalln("Can't load MMDB: %s", err.Error()) log.Fatalln("Can't load MMDB: %s", err.Error())
} }
IPreader = IPReader{Reader: mmdb} ipReader = IPReader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType { switch mmdb.Metadata.DatabaseType {
case "sing-geoip": case "sing-geoip":
IPreader.databaseType = typeSing ipReader.databaseType = typeSing
case "Meta-geoip0": case "Meta-geoip0":
IPreader.databaseType = typeMetaV0 ipReader.databaseType = typeMetaV0
default: default:
IPreader.databaseType = typeMaxmind ipReader.databaseType = typeMaxmind
} }
}) })
return IPreader return ipReader
}
func DownloadMMDB(path string) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, C.MmdbUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
} }
func ASNInstance() ASNReader { func ASNInstance() ASNReader {
ASNonce.Do(func() { asnOnce.Do(func() {
ASNPath := C.Path.ASN() ASNPath := C.Path.ASN()
log.Infoln("Load ASN file: %s", ASNPath) log.Infoln("Load ASN file: %s", ASNPath)
asn, err := maxminddb.Open(ASNPath) asn, err := maxminddb.Open(ASNPath)
if err != nil { if err != nil {
log.Fatalln("Can't load ASN: %s", err.Error()) log.Fatalln("Can't load ASN: %s", err.Error())
} }
ASNreader = ASNReader{Reader: asn} asnReader = ASNReader{Reader: asn}
}) })
return ASNreader return asnReader
}
func DownloadASN(path string) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, C.ASNUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
} }
func ReloadIP() { func ReloadIP() {
mihomoOnce.Reset(&IPonce) mihomoOnce.Reset(&ipOnce)
} }
func ReloadASN() { func ReloadASN() {
mihomoOnce.Reset(&ASNonce) mihomoOnce.Reset(&asnOnce)
} }

View File

@@ -8,11 +8,11 @@ func InstallOverride(override *maxminddb.Reader) {
newReader := IPReader{Reader: override} newReader := IPReader{Reader: override}
switch override.Metadata.DatabaseType { switch override.Metadata.DatabaseType {
case "sing-geoip": case "sing-geoip":
IPreader.databaseType = typeSing ipReader.databaseType = typeSing
case "Meta-geoip0": case "Meta-geoip0":
IPreader.databaseType = typeMetaV0 ipReader.databaseType = typeMetaV0
default: default:
IPreader.databaseType = typeMaxmind ipReader.databaseType = typeMaxmind
} }
IPreader = newReader ipReader = newReader
} }

View File

@@ -23,11 +23,11 @@ func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, str
} }
// PackageNameResolver // PackageNameResolver
// never change type traits because it's used in CFMA // never change type traits because it's used in CMFA
type PackageNameResolver func(metadata *C.Metadata) (string, error) type PackageNameResolver func(metadata *C.Metadata) (string, error)
// DefaultPackageNameResolver // DefaultPackageNameResolver
// never change type traits because it's used in CFMA // never change type traits because it's used in CMFA
var DefaultPackageNameResolver PackageNameResolver var DefaultPackageNameResolver PackageNameResolver
func FindPackageName(metadata *C.Metadata) (string, error) { func FindPackageName(metadata *C.Metadata) (string, error) {

View File

@@ -237,7 +237,7 @@ const MaxPackageFileSize = 32 * 1024 * 1024
func downloadPackageFile() (err error) { func downloadPackageFile() (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel() defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil)
if err != nil { if err != nil {
return fmt.Errorf("http request failed: %w", err) return fmt.Errorf("http request failed: %w", err)
} }
@@ -418,7 +418,7 @@ func copyFile(src, dst string) error {
func getLatestVersion() (version string, err error) { func getLatestVersion() (version string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("get Latest Version fail: %w", err) return "", fmt.Errorf("get Latest Version fail: %w", err)
} }

View File

@@ -1,6 +1,7 @@
package updater package updater
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -8,6 +9,7 @@ import (
"time" "time"
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/batch"
"github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/geodata"
_ "github.com/metacubex/mihomo/component/geodata/standard" _ "github.com/metacubex/mihomo/component/geodata/standard"
"github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/mmdb"
@@ -18,69 +20,87 @@ import (
) )
var ( var (
UpdatingGeo atomic.Bool autoUpdate bool
updateInterval int
updatingGeo atomic.Bool
) )
func updateGeoDatabases() error { func GeoAutoUpdate() bool {
defer runtime.GC() return autoUpdate
geoLoader, err := geodata.GetGeoDataLoader("standard") }
func GeoUpdateInterval() int {
return updateInterval
}
func SetGeoAutoUpdate(newAutoUpdate bool) {
autoUpdate = newAutoUpdate
}
func SetGeoUpdateInterval(newGeoUpdateInterval int) {
updateInterval = newGeoUpdateInterval
}
func UpdateMMDB() (err error) {
defer mmdb.ReloadIP()
data, err := downloadForBytes(geodata.MmdbUrl())
if err != nil { if err != nil {
return err return fmt.Errorf("can't download MMDB database file: %w", err)
}
instance, err := maxminddb.FromBytes(data)
if err != nil {
return fmt.Errorf("invalid MMDB database file: %s", err)
}
_ = instance.Close()
mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file
if err = saveFile(data, C.Path.MMDB()); err != nil {
return fmt.Errorf("can't save MMDB database file: %w", err)
}
return nil
}
func UpdateASN() (err error) {
defer mmdb.ReloadASN()
data, err := downloadForBytes(geodata.ASNUrl())
if err != nil {
return fmt.Errorf("can't download ASN database file: %w", err)
} }
if C.GeodataMode { instance, err := maxminddb.FromBytes(data)
data, err := downloadForBytes(C.GeoIpUrl) if err != nil {
if err != nil { return fmt.Errorf("invalid ASN database file: %s", err)
return fmt.Errorf("can't download GeoIP database file: %w", err)
}
if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil {
return fmt.Errorf("invalid GeoIP database file: %s", err)
}
if err = saveFile(data, C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't save GeoIP database file: %w", err)
}
} else {
defer mmdb.ReloadIP()
data, err := downloadForBytes(C.MmdbUrl)
if err != nil {
return fmt.Errorf("can't download MMDB database file: %w", err)
}
instance, err := maxminddb.FromBytes(data)
if err != nil {
return fmt.Errorf("invalid MMDB database file: %s", err)
}
_ = instance.Close()
mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file
if err = saveFile(data, C.Path.MMDB()); err != nil {
return fmt.Errorf("can't save MMDB database file: %w", err)
}
} }
_ = instance.Close()
if C.ASNEnable { mmdb.ASNInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file
defer mmdb.ReloadASN() if err = saveFile(data, C.Path.ASN()); err != nil {
data, err := downloadForBytes(C.ASNUrl) return fmt.Errorf("can't save ASN database file: %w", err)
if err != nil {
return fmt.Errorf("can't download ASN database file: %w", err)
}
instance, err := maxminddb.FromBytes(data)
if err != nil {
return fmt.Errorf("invalid ASN database file: %s", err)
}
_ = instance.Close()
mmdb.ASNInstance().Reader.Close()
if err = saveFile(data, C.Path.ASN()); err != nil {
return fmt.Errorf("can't save ASN database file: %w", err)
}
} }
return nil
}
data, err := downloadForBytes(C.GeoSiteUrl) func UpdateGeoIp() (err error) {
defer geodata.ClearGeoIPCache()
geoLoader, err := geodata.GetGeoDataLoader("standard")
data, err := downloadForBytes(geodata.GeoIpUrl())
if err != nil {
return fmt.Errorf("can't download GeoIP database file: %w", err)
}
if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil {
return fmt.Errorf("invalid GeoIP database file: %s", err)
}
if err = saveFile(data, C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't save GeoIP database file: %w", err)
}
return nil
}
func UpdateGeoSite() (err error) {
defer geodata.ClearGeoSiteCache()
geoLoader, err := geodata.GetGeoDataLoader("standard")
data, err := downloadForBytes(geodata.GeoSiteUrl())
if err != nil { if err != nil {
return fmt.Errorf("can't download GeoSite database file: %w", err) return fmt.Errorf("can't download GeoSite database file: %w", err)
} }
@@ -92,8 +112,45 @@ func updateGeoDatabases() error {
if err = saveFile(data, C.Path.GeoSite()); err != nil { if err = saveFile(data, C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't save GeoSite database file: %w", err) return fmt.Errorf("can't save GeoSite database file: %w", err)
} }
return nil
}
geodata.ClearCache() func updateGeoDatabases() error {
defer runtime.GC()
b, _ := batch.New[interface{}](context.Background())
if geodata.GeoIpEnable() {
if geodata.GeodataMode() {
b.Go("UpdateGeoIp", func() (_ interface{}, err error) {
err = UpdateGeoIp()
return
})
} else {
b.Go("UpdateMMDB", func() (_ interface{}, err error) {
err = UpdateMMDB()
return
})
}
}
if geodata.ASNEnable() {
b.Go("UpdateASN", func() (_ interface{}, err error) {
err = UpdateASN()
return
})
}
if geodata.GeoSiteEnable() {
b.Go("UpdateGeoSite", func() (_ interface{}, err error) {
err = UpdateGeoSite()
return
})
}
if e := b.Wait(); e != nil {
return e.Err
}
return nil return nil
} }
@@ -103,12 +160,12 @@ var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip")
func UpdateGeoDatabases() error { func UpdateGeoDatabases() error {
log.Infoln("[GEO] Start updating GEO database") log.Infoln("[GEO] Start updating GEO database")
if UpdatingGeo.Load() { if updatingGeo.Load() {
return ErrGetDatabaseUpdateSkip return ErrGetDatabaseUpdateSkip
} }
UpdatingGeo.Store(true) updatingGeo.Store(true)
defer UpdatingGeo.Store(false) defer updatingGeo.Store(false)
log.Infoln("[GEO] Updating GEO database") log.Infoln("[GEO] Updating GEO database")
@@ -122,7 +179,7 @@ func UpdateGeoDatabases() error {
func getUpdateTime() (err error, time time.Time) { func getUpdateTime() (err error, time time.Time) {
var fileInfo os.FileInfo var fileInfo os.FileInfo
if C.GeodataMode { if geodata.GeodataMode() {
fileInfo, err = os.Stat(C.Path.GeoIP()) fileInfo, err = os.Stat(C.Path.GeoIP())
if err != nil { if err != nil {
return err, time return err, time
@@ -138,13 +195,13 @@ func getUpdateTime() (err error, time time.Time) {
} }
func RegisterGeoUpdater() { func RegisterGeoUpdater() {
if C.GeoUpdateInterval <= 0 { if updateInterval <= 0 {
log.Errorln("[GEO] Invalid update interval: %d", C.GeoUpdateInterval) log.Errorln("[GEO] Invalid update interval: %d", updateInterval)
return return
} }
go func() { go func() {
ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour) ticker := time.NewTicker(time.Duration(updateInterval) * time.Hour)
defer ticker.Stop() defer ticker.Stop()
err, lastUpdate := getUpdateTime() err, lastUpdate := getUpdateTime()
@@ -154,8 +211,8 @@ func RegisterGeoUpdater() {
} }
log.Infoln("[GEO] last update time %s", lastUpdate) log.Infoln("[GEO] last update time %s", lastUpdate)
if lastUpdate.Add(time.Duration(C.GeoUpdateInterval) * time.Hour).Before(time.Now()) { if lastUpdate.Add(time.Duration(updateInterval) * time.Hour).Before(time.Now()) {
log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(C.GeoUpdateInterval)*time.Hour) log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(updateInterval)*time.Hour)
if err := UpdateGeoDatabases(); err != nil { if err := UpdateGeoDatabases(); err != nil {
log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) log.Errorln("[GEO] Failed to update GEO database: %s", err.Error())
return return
@@ -163,7 +220,7 @@ func RegisterGeoUpdater() {
} }
for range ticker.C { for range ticker.C {
log.Infoln("[GEO] updating database every %d hours", C.GeoUpdateInterval) log.Infoln("[GEO] updating database every %d hours", updateInterval)
if err := UpdateGeoDatabases(); err != nil { if err := UpdateGeoDatabases(); err != nil {
log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) log.Errorln("[GEO] Failed to update GEO database: %s", err.Error())
} }

View File

@@ -9,7 +9,6 @@ import (
"time" "time"
mihomoHttp "github.com/metacubex/mihomo/component/http" mihomoHttp "github.com/metacubex/mihomo/component/http"
C "github.com/metacubex/mihomo/constant"
"golang.org/x/exp/constraints" "golang.org/x/exp/constraints"
) )
@@ -17,7 +16,7 @@ import (
func downloadForBytes(url string) ([]byte, error) { func downloadForBytes(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel() defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -22,6 +22,7 @@ import (
"github.com/metacubex/mihomo/component/cidr" "github.com/metacubex/mihomo/component/cidr"
"github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/fakeip"
"github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/geodata"
mihomoHttp "github.com/metacubex/mihomo/component/http"
P "github.com/metacubex/mihomo/component/process" P "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/sniffer" "github.com/metacubex/mihomo/component/sniffer"
@@ -433,7 +434,7 @@ func DefaultRawConfig() *RawConfig {
Mode: T.Rule, Mode: T.Rule,
GeoAutoUpdate: false, GeoAutoUpdate: false,
GeoUpdateInterval: 24, GeoUpdateInterval: 24,
GeodataMode: C.GeodataMode, GeodataMode: geodata.GeodataMode(),
GeodataLoader: "memconservative", GeodataLoader: "memconservative",
UnifiedDelay: false, UnifiedDelay: false,
Authentication: []string{}, Authentication: []string{},
@@ -681,19 +682,16 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
func parseGeneral(cfg *RawConfig) (*General, error) { func parseGeneral(cfg *RawConfig) (*General, error) {
updater.SetGeoAutoUpdate(cfg.GeoAutoUpdate)
updater.SetGeoUpdateInterval(cfg.GeoUpdateInterval)
geodata.SetGeodataMode(cfg.GeodataMode) geodata.SetGeodataMode(cfg.GeodataMode)
geodata.SetGeoAutoUpdate(cfg.GeoAutoUpdate)
geodata.SetGeoUpdateInterval(cfg.GeoUpdateInterval)
geodata.SetLoader(cfg.GeodataLoader) geodata.SetLoader(cfg.GeodataLoader)
geodata.SetSiteMatcher(cfg.GeositeMatcher) geodata.SetSiteMatcher(cfg.GeositeMatcher)
C.GeoAutoUpdate = cfg.GeoAutoUpdate geodata.SetGeoIpUrl(cfg.GeoXUrl.GeoIp)
C.GeoUpdateInterval = cfg.GeoUpdateInterval geodata.SetGeoSiteUrl(cfg.GeoXUrl.GeoSite)
C.GeoIpUrl = cfg.GeoXUrl.GeoIp geodata.SetMmdbUrl(cfg.GeoXUrl.Mmdb)
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite geodata.SetASNUrl(cfg.GeoXUrl.ASN)
C.MmdbUrl = cfg.GeoXUrl.Mmdb mihomoHttp.SetUA(cfg.GlobalUA)
C.ASNUrl = cfg.GeoXUrl.ASN
C.GeodataMode = cfg.GeodataMode
C.UA = cfg.GlobalUA
if cfg.KeepAliveIdle != 0 { if cfg.KeepAliveIdle != 0 {
N.KeepAliveIdle = time.Duration(cfg.KeepAliveIdle) * time.Second N.KeepAliveIdle = time.Duration(cfg.KeepAliveIdle) * time.Second

View File

@@ -1,12 +0,0 @@
package constant
var (
ASNEnable bool
GeodataMode bool
GeoAutoUpdate bool
GeoUpdateInterval int
GeoIpUrl string
MmdbUrl string
GeoSiteUrl string
ASNUrl string
)

View File

@@ -1,5 +0,0 @@
package constant
var (
UA string
)

View File

@@ -71,6 +71,7 @@ type Provider interface {
type ProxyProvider interface { type ProxyProvider interface {
Provider Provider
Proxies() []constant.Proxy Proxies() []constant.Proxy
Count() int
// Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies. // Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
// Commonly used in DialContext and DialPacketConn // Commonly used in DialContext and DialPacketConn
Touch() Touch()

View File

@@ -955,12 +955,12 @@ rule-providers:
# 对于behavior=domain: # 对于behavior=domain:
# - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式 # - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式
# - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式 # - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式
# - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式暂不支持转换回ymal格式 # - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式暂不支持转换回yaml格式
# #
# 对于behavior=ipcidr: # 对于behavior=ipcidr:
# - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式 # - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式
# - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式 # - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式
# - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式暂不支持转换回ymal格式 # - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式暂不支持转换回yaml格式
# #
type: http type: http
url: "url" url: "url"

View File

@@ -20,7 +20,7 @@ require (
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
github.com/metacubex/chacha v0.1.0 github.com/metacubex/chacha v0.1.0
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58 github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4
github.com/metacubex/sing-shadowsocks v0.2.8 github.com/metacubex/sing-shadowsocks v0.2.8

View File

@@ -104,8 +104,8 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc= github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw= github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58 h1:T6OxROLZBr9SOQxN5TzUslv81hEREy/dEgaUKVjaG7U= github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU= github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA= github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=

View File

@@ -17,6 +17,7 @@ import (
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
G "github.com/metacubex/mihomo/component/geodata" G "github.com/metacubex/mihomo/component/geodata"
mihomoHttp "github.com/metacubex/mihomo/component/http"
"github.com/metacubex/mihomo/component/iface" "github.com/metacubex/mihomo/component/iface"
"github.com/metacubex/mihomo/component/profile" "github.com/metacubex/mihomo/component/profile"
"github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/profile/cachefile"
@@ -81,6 +82,7 @@ func ParseWithBytes(buf []byte) (*config.Config, error) {
func ApplyConfig(cfg *config.Config, force bool) { func ApplyConfig(cfg *config.Config, force bool) {
mux.Lock() mux.Lock()
defer mux.Unlock() defer mux.Unlock()
log.SetLevel(cfg.General.LogLevel)
tunnel.OnSuspend() tunnel.OnSuspend()
@@ -115,8 +117,6 @@ func ApplyConfig(cfg *config.Config, force bool) {
tunnel.OnRunning() tunnel.OnRunning()
hcCompatibleProvider(cfg.Providers) hcCompatibleProvider(cfg.Providers)
initExternalUI() initExternalUI()
log.SetLevel(cfg.General.LogLevel)
} }
func initInnerTcp() { func initInnerTcp() {
@@ -157,13 +157,13 @@ func GetGeneral() *config.General {
Interface: dialer.DefaultInterface.Load(), Interface: dialer.DefaultInterface.Load(),
RoutingMark: int(dialer.DefaultRoutingMark.Load()), RoutingMark: int(dialer.DefaultRoutingMark.Load()),
GeoXUrl: config.GeoXUrl{ GeoXUrl: config.GeoXUrl{
GeoIp: C.GeoIpUrl, GeoIp: G.GeoIpUrl(),
Mmdb: C.MmdbUrl, Mmdb: G.MmdbUrl(),
ASN: C.ASNUrl, ASN: G.ASNUrl(),
GeoSite: C.GeoSiteUrl, GeoSite: G.GeoSiteUrl(),
}, },
GeoAutoUpdate: G.GeoAutoUpdate(), GeoAutoUpdate: updater.GeoAutoUpdate(),
GeoUpdateInterval: G.GeoUpdateInterval(), GeoUpdateInterval: updater.GeoUpdateInterval(),
GeodataMode: G.GeodataMode(), GeodataMode: G.GeodataMode(),
GeodataLoader: G.LoaderName(), GeodataLoader: G.LoaderName(),
GeositeMatcher: G.SiteMatcherName(), GeositeMatcher: G.SiteMatcherName(),
@@ -171,7 +171,7 @@ func GetGeneral() *config.General {
FindProcessMode: tunnel.FindProcessMode(), FindProcessMode: tunnel.FindProcessMode(),
Sniffing: tunnel.IsSniffing(), Sniffing: tunnel.IsSniffing(),
GlobalClientFingerprint: tlsC.GetGlobalFingerprint(), GlobalClientFingerprint: tlsC.GetGlobalFingerprint(),
GlobalUA: C.UA, GlobalUA: mihomoHttp.UA(),
} }
return general return general

View File

@@ -37,7 +37,7 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
} }
// NewWithAuthenticate // NewWithAuthenticate
// never change type traits because it's used in CFMA // never change type traits because it's used in CMFA
func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) {
getAuth := authStore.Authenticator getAuth := authStore.Authenticator
if !authenticate { if !authenticate {

View File

@@ -11,6 +11,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@@ -78,7 +79,7 @@ func main() {
} }
if geodataMode { if geodataMode {
C.GeodataMode = true geodata.SetGeodataMode(true)
} }
if configString != "" { if configString != "" {
@@ -140,7 +141,7 @@ func main() {
log.Fatalln("Parse config error: %s", err.Error()) log.Fatalln("Parse config error: %s", err.Error())
} }
if C.GeoAutoUpdate { if updater.GeoAutoUpdate() {
updater.RegisterGeoUpdater() updater.RegisterGeoUpdater()
} }

View File

@@ -46,7 +46,7 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) {
return g.isLan(ip), g.adapter return g.isLan(ip), g.adapter
} }
if C.GeodataMode { if geodata.GeodataMode() {
if g.isSourceIP { if g.isSourceIP {
if slices.Contains(metadata.SrcGeoIP, g.country) { if slices.Contains(metadata.SrcGeoIP, g.country) {
return true, g.adapter return true, g.adapter
@@ -102,7 +102,7 @@ func (g *GEOIP) MatchIp(ip netip.Addr) bool {
return g.isLan(ip) return g.isLan(ip)
} }
if C.GeodataMode { if geodata.GeodataMode() {
matcher, err := g.getIPMatcher() matcher, err := g.getIPMatcher()
if err != nil { if err != nil {
return false return false
@@ -124,7 +124,7 @@ func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool {
return false return false
} }
if C.GeodataMode { if geodata.GeodataMode() {
matcher, err := g.getIPMatcher() matcher, err := g.getIPMatcher()
if err != nil { if err != nil {
return false return false
@@ -170,7 +170,7 @@ func (g *GEOIP) GetCountry() string {
} }
func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) { func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) {
if C.GeodataMode { if geodata.GeodataMode() {
return g.getIPMatcher() return g.getIPMatcher()
} }
return nil, errors.New("not geodata mode") return nil, errors.New("not geodata mode")
@@ -193,10 +193,6 @@ func (g *GEOIP) GetRecodeSize() int {
} }
func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) { func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) {
if err := geodata.InitGeoIP(); err != nil {
log.Errorln("can't initial GeoIP: %s", err)
return nil, err
}
country = strings.ToLower(country) country = strings.ToLower(country)
geoip := &GEOIP{ geoip := &GEOIP{
@@ -206,11 +202,17 @@ func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP,
noResolveIP: noResolveIP, noResolveIP: noResolveIP,
isSourceIP: isSrc, isSourceIP: isSrc,
} }
if !C.GeodataMode || country == "lan" {
if country == "lan" {
return geoip, nil return geoip, nil
} }
if C.GeodataMode { if err := geodata.InitGeoIP(); err != nil {
log.Errorln("can't initial GeoIP: %s", err)
return nil, err
}
if geodata.GeodataMode() {
geoIPMatcher, err := geoip.getIPMatcher() // test load geoIPMatcher, err := geoip.getIPMatcher() // test load
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -63,7 +63,6 @@ func (a *ASN) GetASN() string {
} }
func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error) { func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error) {
C.ASNEnable = true
if err := geodata.InitASN(); err != nil { if err := geodata.InitASN(); err != nil {
log.Errorln("can't initial ASN: %s", err) log.Errorln("can't initial ASN: %s", err)
return nil, err return nil, err

View File

@@ -35,7 +35,9 @@ jobs:
sudo apt-get install -y sudo apt-get install -y
libgtk-3-dev libgtk-3-dev
libayatana-appindicator3-dev libayatana-appindicator3-dev
libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev
librsvg2-dev
libxdo-dev
webkit2gtk-driver webkit2gtk-driver
xvfb xvfb
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2

View File

@@ -62,7 +62,7 @@ jobs:
- name: Setup Toolchain - name: Setup Toolchain
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl sudo apt-get install -y libwebkit2gtk-4.1-dev libxdo-dev libappindicator3-dev librsvg2-dev patchelf openssl
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: "./backend/" workspaces: "./backend/"
@@ -97,8 +97,8 @@ jobs:
if: ${{ inputs.aarch64 == false }} if: ${{ inputs.aarch64 == false }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
with: with:
tagName: ${{ inputs.tag }} tagName: ${{ inputs.tag }}
@@ -113,8 +113,8 @@ jobs:
shell: bash shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc

View File

@@ -107,8 +107,8 @@ jobs:
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
with: with:
@@ -125,8 +125,8 @@ jobs:
env: env:
TAG_NAME: ${{ inputs.tag }} TAG_NAME: ${{ inputs.tag }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
NODE_OPTIONS: "--max_old_space_size=4096" NODE_OPTIONS: "--max_old_space_size=4096"
run: | run: |

View File

@@ -85,8 +85,8 @@ jobs:
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
with: with:
tagName: ${{ inputs.tag }} tagName: ${{ inputs.tag }}
@@ -104,7 +104,7 @@ jobs:
env: env:
TAG_NAME: ${{ inputs.tag }} TAG_NAME: ${{ inputs.tag }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }} NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
VITE_WIN_PORTABLE: 1 VITE_WIN_PORTABLE: 1

View File

@@ -50,8 +50,8 @@ jobs:
env: env:
TAG_NAME: dev TAG_NAME: dev
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: | run: |
pnpm build --target aarch64-apple-darwin pnpm build --target aarch64-apple-darwin
pnpm upload:osx-aarch64 pnpm upload:osx-aarch64

View File

@@ -36,15 +36,15 @@ jobs:
tag: "pre-release" tag: "pre-release"
secrets: inherit secrets: inherit
linux_aarch64_build: # linux_aarch64_build:
name: Linux aarch64 Build # name: Linux aarch64 Build
uses: ./.github/workflows/deps-build-linux.yaml # uses: ./.github/workflows/deps-build-linux.yaml
needs: [delete_current_releases] # needs: [delete_current_releases]
with: # with:
nightly: true # nightly: true
tag: "pre-release" # tag: "pre-release"
aarch64: true # aarch64: true
secrets: inherit # secrets: inherit
macos_amd64_build: macos_amd64_build:
name: macOS amd64 Build name: macOS amd64 Build
@@ -68,11 +68,10 @@ jobs:
update_tag: update_tag:
name: Update tag name: Update tag
needs: needs: [
[
windows_build, windows_build,
linux_amd64_build, linux_amd64_build,
linux_aarch64_build, # linux_aarch64_build,
macos_amd64_build, macos_amd64_build,
macos_aarch64_build, macos_aarch64_build,
] ]

View File

@@ -8,3 +8,4 @@ pnpm-lock.yaml
*.wxs *.wxs
frontend/nyanpasu/src/router.ts frontend/nyanpasu/src/router.ts
frontend/nyanpasu/auto-imports.d.ts frontend/nyanpasu/auto-imports.d.ts
backend/tauri/gen/schemas/

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["tauri", "webview2-com-bridge", "boa_utils"] members = ["tauri", "boa_utils"]
[workspace.package] [workspace.package]
repository = "https://github.com/keiko233/clash-nyanpasu.git" repository = "https://github.com/keiko233/clash-nyanpasu.git"
@@ -11,7 +11,7 @@ authors = ["zzzgydi", "keiko233"]
[workspace.dependencies] [workspace.dependencies]
thiserror = "1" thiserror = "1"
tracing = "0.1" tracing = "0.1"
boa_engine = "0.19" boa_engine = { version = "0.19", git = "https://github.com/boa-dev/boa.git", branch = "backport-0.19-fixes" }
[profile.release] [profile.release]
panic = "unwind" panic = "unwind"

View File

@@ -3,8 +3,8 @@ image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge"
pre-build = [ pre-build = [
"dpkg --add-architecture $CROSS_DEB_ARCH", "dpkg --add-architecture $CROSS_DEB_ARCH",
"""apt-get update && apt-get -y install \ """apt-get update && apt-get -y install \
libwebkit2gtk-4.0-dev:$CROSS_DEB_ARCH \ libwebkit2gtk-4.1-dev:$CROSS_DEB_ARCH \
libgtk-3-dev:$CROSS_DEB_ARCH \ libxdo-dev:$CROSS_DEB_ARCH \
libayatana-appindicator3-dev:$CROSS_DEB_ARCH \ libayatana-appindicator3-dev:$CROSS_DEB_ARCH \
librsvg2-dev:$CROSS_DEB_ARCH \ librsvg2-dev:$CROSS_DEB_ARCH \
libpango1.0-dev:$CROSS_DEB_ARCH \ libpango1.0-dev:$CROSS_DEB_ARCH \

View File

@@ -9,8 +9,8 @@ authors.workspace = true
[dependencies] [dependencies]
rustc-hash = { version = "2", features = ["std"] } rustc-hash = { version = "2", features = ["std"] }
boa_engine.workspace = true boa_engine.workspace = true
boa_gc = "0.19" boa_gc = { version = "0.19", git = "https://github.com/boa-dev/boa.git", branch = "backport-0.19-fixes" }
boa_parser = "0.19" boa_parser = { version = "0.19", git = "https://github.com/boa-dev/boa.git", branch = "backport-0.19-fixes" }
isahc = "1.7" isahc = "1.7"
futures-util = "0.3" futures-util = "0.3"
smol = "2" smol = "2"

View File

@@ -9,8 +9,12 @@ default-run = "clash-nyanpasu"
edition = { workspace = true } edition = { workspace = true }
build = "build.rs" build = "build.rs"
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "1", features = [] } tauri-build = { version = "2.0.0-rc", features = [] }
serde = "1" serde = "1"
simd-json = "0.13" simd-json = "0.13"
chrono = "0.4" chrono = "0.4"
@@ -46,26 +50,10 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
relative-path = "1.9" relative-path = "1.9"
tauri = { version = "1.5.4", features = [
"dialog-all",
"updater",
"fs-all",
"clipboard-all",
"os-all",
"global-shortcut-all",
"notification-all",
"process-all",
"shell-all",
"system-tray",
"window-all",
] }
window-vibrancy = { version = "0.5.0" }
window-shadows = { version = "0.2.2" }
axum = "0.7" axum = "0.7"
mime = "0.3" mime = "0.3"
bincode = "1" bincode = "1"
bytes = { version = "1", features = ["serde"] } bytes = { version = "1", features = ["serde"] }
wry = { version = "0.24.6" }
semver = "1.0" semver = "1.0"
zip = "2.0.0" zip = "2.0.0"
zip-extensions = "0.8.0" zip-extensions = "0.8.0"
@@ -81,7 +69,11 @@ runas = { git = "https://github.com/libnyanpasu/rust-runas.git" }
backon = { version = "1.0.1", features = ["tokio-sleep"] } backon = { version = "1.0.1", features = ["tokio-sleep"] }
rust-i18n = "3" rust-i18n = "3"
adler = "1.0.2" adler = "1.0.2"
rfd = "0.10" # should bump to v0.14 when clarify why the rfd v0.10 from tauri breaks build rfd = { version = "0.14", default-features = false, features = [
"tokio",
"gtk3",
"common-controls-v6",
] }
indexmap = { version = "2.2.3", features = ["serde"] } indexmap = { version = "2.2.3", features = ["serde"] }
tracing = { workspace = true } tracing = { workspace = true }
tracing-attributes = "0.1" tracing-attributes = "0.1"
@@ -96,7 +88,6 @@ tracing-log = { version = "0.2" }
tracing-appender = { version = "0.2", features = ["parking_lot"] } tracing-appender = { version = "0.2", features = ["parking_lot"] }
base64 = "0.22" base64 = "0.22"
single-instance = "0.3.3" single-instance = "0.3.3"
tauri-plugin-deep-link = { path = "../tauri-plugin-deep-link", version = "0.1.2" }
uuid = "1.7.0" uuid = "1.7.0"
image = "0.25.0" image = "0.25.0"
fast_image_resize = "4" fast_image_resize = "4"
@@ -138,12 +129,33 @@ mlua = { version = "0.9", features = [
] } ] }
enumflags2 = "0.7" enumflags2 = "0.7"
sha2 = "0.10" sha2 = "0.10"
bimap = "0.6.3"
# Tauri Dependencies
tauri = { version = "2.0.0-rc", features = [
"tray-icon",
"image-png",
"image-ico",
] }
tauri-plugin-deep-link = { path = "../tauri-plugin-deep-link", version = "0.1.2" }
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
window-vibrancy = { version = "0.5.2" }
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies] [target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0" cocoa = "0.26.0"
objc = "0.2.7" objc = "0.2.7"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
@@ -153,34 +165,8 @@ windows-sys = { version = "0.59", features = [
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_SystemInformation", "Win32_System_SystemInformation",
] } ] }
webview2-com-bridge = { path = "../webview2-com-bridge" } windows-core = "0.58.0"
webview2-com = "0.33"
[target.'cfg(windows)'.dependencies.tauri]
version = "1.5.4"
features = [
"global-shortcut-all",
"icon-png",
"process-all",
"dialog-all",
"shell-all",
"system-tray",
"updater",
"window-all",
]
[target.'cfg(linux)'.dependencies.tauri]
version = "1.5.4"
features = [
"global-shortcut-all",
"process-all",
"dialog-all",
"shell-all",
"system-tray",
"updater",
"window-all",
"native-tls-vendored",
"reqwest-native-tls-vendored",
]
[features] [features]
default = ["custom-protocol", "default-meta"] default = ["custom-protocol", "default-meta"]

View File

@@ -0,0 +1,5 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"permissions": ["updater:default"]
}

View File

@@ -0,0 +1,76 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-file",
"fs:allow-write-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"core:window:allow-create",
"core:window:allow-center",
"core:window:allow-request-user-attention",
"core:window:allow-set-resizable",
"core:window:allow-set-maximizable",
"core:window:allow-set-minimizable",
"core:window:allow-set-closable",
"core:window:allow-set-title",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-close",
"core:window:allow-set-decorations",
"core:window:allow-set-always-on-top",
"core:window:allow-set-content-protected",
"core:window:allow-set-size",
"core:window:allow-set-min-size",
"core:window:allow-set-max-size",
"core:window:allow-set-position",
"core:window:allow-set-fullscreen",
"core:window:allow-set-focus",
"core:window:allow-set-icon",
"core:window:allow-set-skip-taskbar",
"core:window:allow-set-cursor-grab",
"core:window:allow-set-cursor-visible",
"core:window:allow-set-cursor-icon",
"core:window:allow-set-cursor-position",
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-start-dragging",
"core:webview:allow-print",
"shell:allow-execute",
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
"notification:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-register-all",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"process:allow-restart",
"process:allow-exit",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
{ {
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/1.x/tooling/cli/schema.json", "$schema": "../../../node_modules/@tauri-apps/cli/config.schema.json",
"package": { "version": "2.0.0",
"version": "1.6.2" "plugins": {
},
"tauri": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK",
"endpoints": [ "endpoints": [
"https://mirror.ghproxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json", "https://mirror.ghproxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json",
"https://gh-proxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json", "https://gh-proxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json",

View File

@@ -31,6 +31,7 @@ pub static BUILD_INFO: Lazy<BuildInfo> = Lazy::new(|| BuildInfo {
pub static IS_APPIMAGE: Lazy<bool> = Lazy::new(|| std::env::var("APPIMAGE").is_ok()); pub static IS_APPIMAGE: Lazy<bool> = Lazy::new(|| std::env::var("APPIMAGE").is_ok());
#[cfg(target_os = "windows")]
pub static IS_PORTABLE: Lazy<bool> = Lazy::new(|| { pub static IS_PORTABLE: Lazy<bool> = Lazy::new(|| {
if cfg!(windows) { if cfg!(windows) {
let dir = crate::utils::dirs::app_install_dir().unwrap(); let dir = crate::utils::dirs::app_install_dir().unwrap();

View File

@@ -29,7 +29,7 @@ use std::{
}, },
time::Duration, time::Duration,
}; };
use tauri::api::process::Command;
use tokio::time::sleep; use tokio::time::sleep;
use tracing_attributes::instrument; use tracing_attributes::instrument;
@@ -414,17 +414,18 @@ impl CoreManager {
let app_dir = dirs::app_data_dir()?; let app_dir = dirs::app_data_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?; let app_dir = dirs::path_to_str(&app_dir)?;
log::debug!(target: "app", "check config in `{clash_core}`"); log::debug!(target: "app", "check config in `{clash_core}`");
let output = Command::new_sidecar(clash_core)? let output = std::process::Command::new(dirs::get_data_or_sidecar_path(&clash_core)?)
.args(["-t", "-d", app_dir, "-f", config_path]) .args(["-t", "-d", app_dir, "-f", config_path])
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
let error = api::parse_check_output(output.stdout.clone()); let stdout = String::from_utf8_lossy(&output.stdout);
let error = api::parse_check_output(stdout.to_string());
let error = match !error.is_empty() { let error = match !error.is_empty() {
true => error, true => error,
false => output.stdout.clone(), false => stdout.to_string(),
}; };
Logger::global().set_log(output.stdout); Logger::global().set_log(stdout.to_string());
bail!("{error}"); bail!("{error}");
} }
@@ -462,11 +463,12 @@ impl CoreManager {
let tun_device_ip = Config::clash().clone().latest().get_tun_device_ip(); let tun_device_ip = Config::clash().clone().latest().get_tun_device_ip();
// 执行 networksetup -setdnsservers Wi-Fi $tun_device_ip // 执行 networksetup -setdnsservers Wi-Fi $tun_device_ip
let (mut rx, _) = Command::new("networksetup") let output = tokio::process::Command::new("networksetup")
.args(["-setdnsservers", "Wi-Fi", tun_device_ip.as_str()]) .args(["-setdnsservers", "Wi-Fi", tun_device_ip.as_str()])
.spawn()?; .output()
let event = rx.recv().await; .await?;
log::debug!(target: "app", "{event:?}");
log::debug!(target: "app", "set system dns: {:?}", output);
} }
} }
// FIXME: 重构服务模式 // FIXME: 重构服务模式
@@ -553,9 +555,10 @@ impl CoreManager {
if enable_tun { if enable_tun {
log::debug!(target: "app", "try to set system dns"); log::debug!(target: "app", "try to set system dns");
match Command::new("networksetup") match tokio::process::Command::new("networksetup")
.args(["-setdnsservers", "Wi-Fi", "Empty"]) .args(["-setdnsservers", "Wi-Fi", "Empty"])
.output() .output()
.await
{ {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(err) => { Err(err) => {

View File

@@ -5,7 +5,7 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use tauri::{AppHandle, Manager, Window}; use tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Handle { pub struct Handle {
pub app_handle: Arc<Mutex<Option<AppHandle>>>, pub app_handle: Arc<Mutex<Option<AppHandle>>>,
@@ -42,11 +42,11 @@ impl Handle {
*self.app_handle.lock() = Some(app_handle); *self.app_handle.lock() = Some(app_handle);
} }
pub fn get_window(&self) -> Option<Window> { pub fn get_window(&self) -> Option<WebviewWindow<Wry>> {
self.app_handle self.app_handle
.lock() .lock()
.as_ref() .as_ref()
.and_then(|a| a.get_window("main")) .and_then(|a| a.get_webview_window("main"))
} }
pub fn refresh_clash() { pub fn refresh_clash() {
@@ -81,11 +81,12 @@ impl Handle {
} }
pub fn update_systray() -> Result<()> { pub fn update_systray() -> Result<()> {
let app_handle = Self::global().app_handle.lock(); // let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() { // if app_handle.is_none() {
bail!("update_systray unhandled error"); // bail!("update_systray unhandled error");
} // }
Tray::update_systray(app_handle.as_ref().unwrap())?; // Tray::update_systray(app_handle.as_ref().unwrap())?;
Handle::emit("update_systray", ())?;
Ok(()) Ok(())
} }
@@ -98,4 +99,14 @@ impl Handle {
Tray::update_part(app_handle.as_ref().unwrap())?; Tray::update_part(app_handle.as_ref().unwrap())?;
Ok(()) Ok(())
} }
pub fn emit<S: Serialize + Clone>(event: &str, payload: S) -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("app_handle is not exist");
}
app_handle.as_ref().unwrap().emit(event, payload)?;
Ok(())
}
} }

View File

@@ -3,8 +3,9 @@ use anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tauri::{AppHandle, GlobalShortcutManager}; use tauri::AppHandle;
use wry::application::accelerator::Accelerator;
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
pub struct Hotkey { pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置 current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
@@ -64,24 +65,20 @@ impl Hotkey {
fn check_key(hotkey: &str) -> Result<()> { fn check_key(hotkey: &str) -> Result<()> {
// fix #287 // fix #287
// tauri的这几个方法全部有Result expect会panic先检测一遍避免挂了 // tauri的这几个方法全部有Result expect会panic先检测一遍避免挂了
if hotkey.parse::<Accelerator>().is_err() { if hotkey.parse::<Shortcut>().is_err() {
bail!("invalid hotkey `{hotkey}`"); bail!("invalid hotkey `{hotkey}`");
} }
Ok(()) Ok(())
} }
fn get_manager(&self) -> Result<impl GlobalShortcutManager> { fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = self.app_handle.lock(); let app_handle = self.app_handle.lock();
if app_handle.is_none() { if app_handle.is_none() {
bail!("failed to get the hotkey manager"); bail!("app handle is none");
} }
Ok(app_handle.as_ref().unwrap().global_shortcut_manager()) let manager = app_handle.as_ref().unwrap().global_shortcut();
}
fn register(&self, hotkey: &str, func: &str) -> Result<()> { if manager.is_registered(hotkey) {
let mut manager = self.get_manager()?;
if manager.is_registered(hotkey)? {
manager.unregister(hotkey)?; manager.unregister(hotkey)?;
} }
@@ -97,17 +94,25 @@ impl Hotkey {
"toggle_tun_mode" => feat::toggle_tun_mode, "toggle_tun_mode" => feat::toggle_tun_mode,
"enable_tun_mode" => feat::enable_tun_mode, "enable_tun_mode" => feat::enable_tun_mode,
"disable_tun_mode" => feat::disable_tun_mode, "disable_tun_mode" => feat::disable_tun_mode,
_ => bail!("invalid function \"{func}\""), _ => bail!("invalid function \"{func}\""),
}; };
manager.register(hotkey, f)?; manager.on_shortcut(hotkey, move |_app_handle, _hotkey, _ev| {
f();
})?;
log::info!(target: "app", "register hotkey {hotkey} {func}"); log::info!(target: "app", "register hotkey {hotkey} {func}");
Ok(()) Ok(())
} }
fn unregister(&self, hotkey: &str) -> Result<()> { fn unregister(&self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(hotkey)?; let app_handle = self.app_handle.lock();
if app_handle.is_none() {
bail!("app handle is none");
}
let manager = app_handle.as_ref().unwrap().global_shortcut();
manager.unregister(hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}"); log::info!(target: "app", "unregister hotkey {hotkey}");
Ok(()) Ok(())
} }
@@ -189,8 +194,12 @@ impl Hotkey {
impl Drop for Hotkey { impl Drop for Hotkey {
fn drop(&mut self) { fn drop(&mut self) {
if let Ok(mut manager) = self.get_manager() { let app_handle = self.app_handle.lock();
let _ = manager.unregister_all(); if let Some(app_handle) = app_handle.as_ref() {
let manager = app_handle.global_shortcut();
if let Ok(()) = manager.unregister_all() {
log::info!(target: "app", "unregister all hotkeys");
}
} }
} }
} }

View File

@@ -216,6 +216,7 @@ pub async fn restart_service() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tracing::instrument]
pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>> { pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>> {
let mut cmd = tokio::process::Command::new(SERVICE_PATH.as_path()); let mut cmd = tokio::process::Command::new(SERVICE_PATH.as_path());
cmd.args(["status", "--json"]); cmd.args(["status", "--json"]);
@@ -239,6 +240,6 @@ pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>>
); );
} }
let mut status = String::from_utf8(output.stdout)?; let mut status = String::from_utf8(output.stdout)?;
tracing::debug!("service status: {}", status); tracing::trace!("service status: {}", status);
Ok(unsafe { simd_json::serde::from_str(&mut status)? }) Ok(unsafe { simd_json::serde::from_str(&mut status)? })
} }

View File

@@ -1,13 +1,18 @@
use std::{borrow::Cow, sync::atomic::AtomicU16};
use crate::{ use crate::{
config::{nyanpasu::ClashCore, Config}, config::{nyanpasu::ClashCore, Config},
feat, ipc, feat, ipc, log_err,
utils::{help, resolve}, utils::{help, resolve},
}; };
use anyhow::Result; use anyhow::Result;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use rust_i18n::t; use rust_i18n::t;
use tauri::{ use tauri::{
AppHandle, CustomMenuItem, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, menu::{Menu, MenuBuilder, MenuEvent, MenuItemBuilder, SubmenuBuilder},
SystemTraySubmenu, tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent},
AppHandle, Manager, Runtime,
}; };
use tracing_attributes::instrument; use tracing_attributes::instrument;
@@ -15,12 +20,130 @@ pub mod icon;
pub mod proxies; pub mod proxies;
pub use self::icon::on_scale_factor_changed; pub use self::icon::on_scale_factor_changed;
use self::proxies::SystemTrayMenuProxiesExt; use self::proxies::SystemTrayMenuProxiesExt;
mod utils;
struct TrayState<R: Runtime> {
menu: Mutex<Menu<R>>,
}
pub struct Tray {} pub struct Tray {}
static UPDATE_SYSTRAY_MUTEX: Lazy<parking_lot::Mutex<()>> =
Lazy::new(|| parking_lot::Mutex::new(()));
const TRAY_ID: &str = "main-tray";
#[cfg(target_os = "linux")]
static LINUX_TRAY_ID: AtomicU16 = AtomicU16::new(0);
// #[cfg(target_os = "linux")]
// fn bump_tray_id() -> Cow<'static, str> {
// let id = LINUX_TRAY_ID.fetch_add(1, std::sync::atomic::Ordering::Release) + 1;
// Cow::Owned(format!("{}-{}", TRAY_ID, id))
// }
#[inline]
fn get_tray_id<'n>() -> Cow<'n, str> {
#[cfg(target_os = "linux")]
{
let id = LINUX_TRAY_ID.load(std::sync::atomic::Ordering::Acquire);
Cow::Owned(format!("{}-{}", TRAY_ID, id))
}
#[cfg(not(target_os = "linux"))]
{
Cow::Borrowed(TRAY_ID)
}
}
// fn dummy_print_submenu<R: Runtime>(submenu: &Submenu<R>) {
// for item in submenu.items().unwrap() {
// tracing::debug!("item: {:#?}", item.id());
// match item {
// tauri::menu::MenuItemKind::MenuItem(item) => {
// tracing::debug!(
// "item: {:#?}, type: MenuItem, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// tauri::menu::MenuItemKind::Submenu(submenu) => {
// tracing::debug!(
// "item: {:#?}, type: Submenu, text: {:#?}",
// submenu.id(),
// submenu.text()
// );
// dummy_print_submenu(&submenu);
// }
// tauri::menu::MenuItemKind::Predefined(item) => {
// tracing::debug!(
// "item: {:#?}, type: Predefined, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// tauri::menu::MenuItemKind::Check(item) => {
// tracing::debug!(
// "item: {:#?}, type: Check, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// tauri::menu::MenuItemKind::Icon(item) => {
// tracing::debug!(
// "item: {:#?}, type: Icon, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// }
// }
// }
// fn dummy_print_menu<R: Runtime>(menu: &Menu<R>) {
// for item in menu.items().unwrap() {
// tracing::debug!("item: {:#?}", item.id());
// match item {
// tauri::menu::MenuItemKind::MenuItem(item) => {
// tracing::debug!(
// "item: {:#?}, type: MenuItem, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// tauri::menu::MenuItemKind::Submenu(submenu) => {
// tracing::debug!(
// "item: {:#?}, type: Submenu, text: {:#?}",
// submenu.id(),
// submenu.text()
// );
// dummy_print_submenu(&submenu);
// }
// tauri::menu::MenuItemKind::Predefined(item) => {
// tracing::debug!(
// "item: {:#?}, type: Predefined, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// tauri::menu::MenuItemKind::Check(item) => {
// tracing::debug!(
// "item: {:#?}, type: Check, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// tauri::menu::MenuItemKind::Icon(item) => {
// tracing::debug!(
// "item: {:#?}, type: Icon, text: {:#?}",
// item.id(),
// item.text()
// );
// }
// }
// }
// }
impl Tray { impl Tray {
#[instrument(skip(_app_handle))] #[instrument(skip(app_handle))]
pub fn tray_menu(_app_handle: &AppHandle) -> SystemTrayMenu { pub fn tray_menu<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Menu<R>> {
let version = env!("NYANPASU_VERSION"); let version = env!("NYANPASU_VERSION");
let core = { let core = {
*Config::verge() *Config::verge()
@@ -29,72 +152,147 @@ impl Tray {
.as_ref() .as_ref()
.unwrap_or(&ClashCore::default()) .unwrap_or(&ClashCore::default())
}; };
let mut menu = SystemTrayMenu::new() let mut menu = MenuBuilder::new(app_handle)
.add_item(CustomMenuItem::new("open_window", t!("tray.dashboard"))) .text("open_window", t!("tray.dashboard"))
.setup_proxies() // Setup the proxies menu .setup_proxies(app_handle)? // Setup the proxies menu
.add_native_item(SystemTrayMenuItem::Separator) .separator()
.add_item(CustomMenuItem::new("rule_mode", t!("tray.rule_mode"))) .check("rule_mode", t!("tray.rule_mode"))
.add_item(CustomMenuItem::new("global_mode", t!("tray.global_mode"))) .check("global_mode", t!("tray.global_mode"))
.add_item(CustomMenuItem::new("direct_mode", t!("tray.direct_mode"))); .check("direct_mode", t!("tray.direct_mode"));
if core == ClashCore::ClashPremium { if core == ClashCore::ClashPremium {
menu = menu.add_item(CustomMenuItem::new("script_mode", t!("tray.script_mode"))) menu = menu.check("script_mode", t!("tray.script_mode"));
} }
menu.add_native_item(SystemTrayMenuItem::Separator) menu = menu
.add_item(CustomMenuItem::new("system_proxy", t!("tray.system_proxy"))) .separator()
.add_item(CustomMenuItem::new("tun_mode", t!("tray.tun_mode"))) .check("system_proxy", t!("tray.system_proxy"))
.add_item(CustomMenuItem::new("copy_env_sh", t!("tray.copy_env.sh"))) .check("tun_mode", t!("tray.tun_mode"))
.add_item(CustomMenuItem::new("copy_env_cmd", t!("tray.copy_env.cmd"))) .separator()
.add_item(CustomMenuItem::new("copy_env_ps", t!("tray.copy_env.ps"))) .text("copy_env_sh", t!("tray.copy_env.sh"))
.add_submenu(SystemTraySubmenu::new( .text("copy_env_cmd", t!("tray.copy_env.cmd"))
t!("tray.open_dir.menu"), .text("copy_env_ps", t!("tray.copy_env.ps"))
SystemTrayMenu::new() .item(
.add_item(CustomMenuItem::new( &SubmenuBuilder::new(app_handle, t!("tray.open_dir.menu"))
"open_app_config_dir", .text("open_app_config_dir", t!("tray.open_dir.app_config_dir"))
t!("tray.open_dir.app_config_dir"), .text("open_app_data_dir", t!("tray.open_dir.app_data_dir"))
)) .text("open_core_dir", t!("tray.open_dir.core_dir"))
.add_item(CustomMenuItem::new( .text("open_logs_dir", t!("tray.open_dir.log_dir"))
"open_app_data_dir", .build()?,
t!("tray.open_dir.app_data_dir"), )
)) .item(
.add_item(CustomMenuItem::new( &SubmenuBuilder::new(app_handle, t!("tray.more.menu"))
"open_core_dir", .text("restart_clash", t!("tray.more.restart_clash"))
t!("tray.open_dir.core_dir"), .text("restart_app", t!("tray.more.restart_app"))
)) .item(
.add_item(CustomMenuItem::new( &MenuItemBuilder::new(format!("Version {}", version))
"open_logs_dir", .id("app_version")
t!("tray.open_dir.log_dir"), .enabled(false)
)), .build(app_handle)?,
)) )
.add_submenu(SystemTraySubmenu::new( .build()?,
t!("tray.more.menu"), )
SystemTrayMenu::new() .separator()
.add_item(CustomMenuItem::new( .item(
"restart_clash", &MenuItemBuilder::new(t!("tray.quit"))
t!("tray.more.restart_clash"), .id("quit")
)) .accelerator("CmdOrControl+Q")
.add_item(CustomMenuItem::new( .build(app_handle)?,
"restart_app", );
t!("tray.more.restart_app"),
)) Ok(menu.build()?)
.add_item(
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", t!("tray.quit")).accelerator("CmdOrControl+Q"))
} }
#[instrument(skip(app_handle))] #[instrument(skip(app_handle))]
pub fn update_systray(app_handle: &AppHandle) -> Result<()> { pub fn update_systray(app_handle: &AppHandle<tauri::Wry>) -> Result<()> {
app_handle let _guard = UPDATE_SYSTRAY_MUTEX.lock();
.tray_handle() let tray_id = get_tray_id();
.set_menu(Tray::tray_menu(app_handle))?; let tray = {
// if cfg!(target_os = "linux") {
// tracing::debug!("removing tray by id: {}", tray_id);
// let mut tray = app_handle.remove_tray_by_id(tray_id.as_ref());
// tray.take(); // Drop the tray
// tray_id = bump_tray_id();
// tracing::debug!("bumped tray id to: {}", tray_id);
// }
app_handle.tray_by_id(tray_id.as_ref())
};
let menu = Tray::tray_menu(app_handle)?;
let tray = match tray {
None => {
let mut builder = TrayIconBuilder::with_id(tray_id);
#[cfg(any(windows, target_os = "linux"))]
{
builder = builder.icon(tauri::image::Image::from_bytes(&icon::get_icon(
&icon::TrayIcon::Normal,
))?);
}
#[cfg(target_os = "macos")]
{
builder = builder
.icon(tauri::image::Image::from_bytes(include_bytes!(
"../../../icons/tray-icon.png"
))?)
.icon_as_template(true);
}
builder
.menu(&menu)
.on_menu_event(|app, event| {
Tray::on_menu_item_event(app, event);
})
.on_tray_icon_event(|tray_icon, event| {
Tray::on_system_tray_event(tray_icon, event);
})
.build(app_handle)?
}
Some(tray) => {
// This is a workaround for linux tray menu update. Due to the api disallow set_menu again
// and recreate tray icon will cause buggy tray. No icon and no menu.
// So this block is a dirty inheritance of the menu items from the previous tray menu.
if cfg!(target_os = "linux") {
let state = app_handle.state::<TrayState<tauri::Wry>>();
let previous_menu = state.menu.lock();
if let Ok(items) = previous_menu.items() {
tracing::debug!("removing previous tray menu items");
for item in items {
log_err!(previous_menu.remove(&item), "failed to remove menu item");
}
}
// migrate the menu items
if let Ok(items) = menu.items() {
tracing::debug!("migrating new tray menu items");
for item in items {
log_err!(previous_menu.append(&item), "failed to append menu item");
}
}
} else {
tray.set_menu(Some(menu.clone()))?;
}
tray
}
};
tray.set_visible(true)?;
{
match app_handle.try_state::<TrayState<tauri::Wry>>() {
Some(state) if cfg!(not(target_os = "linux")) => {
tracing::debug!("replacing previous tray menu");
*state.menu.lock() = menu;
}
None => {
tracing::debug!("creating new tray menu");
app_handle.manage(TrayState {
menu: Mutex::new(menu),
});
}
_ => {}
}
}
tracing::debug!("full update tray finished");
Tray::update_part(app_handle)?; Tray::update_part(app_handle)?;
Ok(()) Ok(())
} }
#[instrument(skip(app_handle))] #[instrument(skip(app_handle))]
pub fn update_part(app_handle: &AppHandle) -> Result<()> { pub fn update_part<R: Runtime>(app_handle: &AppHandle<R>) -> Result<()> {
let mode = crate::utils::config::get_current_clash_mode(); let mode = crate::utils::config::get_current_clash_mode();
let core = { let core = {
*Config::verge() *Config::verge()
@@ -103,54 +301,25 @@ impl Tray {
.as_ref() .as_ref()
.unwrap_or(&ClashCore::default()) .unwrap_or(&ClashCore::default())
}; };
let tray = app_handle.tray_handle(); let tray_id = get_tray_id();
tracing::debug!("updating tray part: {}", tray_id);
let tray = app_handle.tray_by_id(tray_id.as_ref()).unwrap();
let state = app_handle.state::<TrayState<R>>();
let menu = state.menu.lock();
#[cfg(target_os = "linux")] let _ = menu
{ .get("rule_mode")
let _ = tray.get_item("rule_mode").set_title(t!("tray.rule_mode")); .and_then(|item| item.as_check_menuitem()?.set_checked(mode == "rule").ok());
let _ = tray let _ = menu
.get_item("global_mode") .get("global_mode")
.set_title(t!("tray.global_mode")); .and_then(|item| item.as_check_menuitem()?.set_checked(mode == "global").ok());
let _ = tray let _ = menu
.get_item("direct_mode") .get("direct_mode")
.set_title(t!("tray.direct_mode")); .and_then(|item| item.as_check_menuitem()?.set_checked(mode == "direct").ok());
if core == ClashCore::ClashPremium { if core == ClashCore::ClashPremium {
let _ = tray let _ = menu
.get_item("script_mode") .get("script_mode")
.set_title(t!("tray.script_mode")); .and_then(|item| item.as_check_menuitem()?.set_checked(mode == "script").ok());
}
match mode.as_str() {
"rule" => {
let _ = tray
.get_item("rule_mode")
.set_title(utils::selected_title(t!("tray.rule_mode")));
}
"global" => {
let _ = tray
.get_item("global_mode")
.set_title(utils::selected_title(t!("tray.global_mode")));
}
"direct" => {
let _ = tray
.get_item("direct_mode")
.set_title(utils::selected_title(t!("tray.direct_mode")));
}
"script" => {
let _ = tray
.get_item("script_mode")
.set_title(utils::selected_title(t!("tray.script_mode")));
}
_ => {}
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
let _ = tray.get_item("global_mode").set_selected(mode == "global");
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
if core == ClashCore::ClashPremium {
let _ = tray.get_item("script_mode").set_selected(mode == "script");
}
} }
let (system_proxy, tun_mode) = { let (system_proxy, tun_mode) = {
@@ -162,7 +331,7 @@ impl Tray {
) )
}; };
#[cfg(target_os = "windows")] #[cfg(any(target_os = "windows", target_os = "linux"))]
{ {
use icon::TrayIcon; use icon::TrayIcon;
@@ -174,98 +343,86 @@ impl Tray {
TrayIcon::Normal TrayIcon::Normal
}; };
let icon = icon::get_icon(&mode); let icon = icon::get_icon(&mode);
let _ = tray.set_icon(tauri::Icon::Raw(icon)); let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon)?));
} }
#[cfg(target_os = "linux")] let _ = menu
{ .get("system_proxy")
match system_proxy { .and_then(|item| item.as_check_menuitem()?.set_checked(system_proxy).ok());
true => { let _ = menu
let _ = tray .get("tun_mode")
.get_item("system_proxy") .and_then(|item| item.as_check_menuitem()?.set_checked(tun_mode).ok());
.set_title(utils::selected_title(t!("tray.system_proxy")));
}
false => {
let _ = tray
.get_item("system_proxy")
.set_title(t!("tray.system_proxy"));
}
}
match tun_mode { let switch_map = {
true => { let mut map = std::collections::HashMap::new();
let _ = tray map.insert(true, t!("tray.proxy_action.on"));
.get_item("tun_mode") map.insert(false, t!("tray.proxy_action.off"));
.set_title(utils::selected_title(t!("tray.tun_mode"))); map
} };
false => {
let _ = tray.get_item("tun_mode").set_title(t!("tray.tun_mode"));
}
}
}
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
let _ = tray.get_item("system_proxy").set_selected(system_proxy); let _ = tray.set_tooltip(Some(&format!(
let _ = tray.get_item("tun_mode").set_selected(tun_mode);
}
#[cfg(not(target_os = "linux"))]
{
let switch_map = {
let mut map = std::collections::HashMap::new();
map.insert(true, t!("tray.proxy_action.on"));
map.insert(false, t!("tray.proxy_action.off"));
map
};
let _ = tray.set_tooltip(&format!(
"{}: {}\n{}: {}", "{}: {}\n{}: {}",
t!("tray.system_proxy"), t!("tray.system_proxy"),
switch_map[&system_proxy], switch_map[&system_proxy],
t!("tray.tun_mode"), t!("tray.tun_mode"),
switch_map[&tun_mode] switch_map[&tun_mode]
)); )));
}
#[cfg(target_os = "linux")]
{
let _ = tray.set_title(Some(&format!(
"{}: {}\n{}: {}",
t!("tray.system_proxy"),
switch_map[&system_proxy],
t!("tray.tun_mode"),
switch_map[&tun_mode]
)));
} }
Ok(()) Ok(())
} }
#[instrument(skip(app_handle, event))] #[instrument(skip(app_handle, event))]
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) { pub fn on_menu_item_event(app_handle: &AppHandle, event: MenuEvent) {
match event { let id = event.id().0.as_str();
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { match id {
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => { mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
let mode = &mode[0..mode.len() - 5]; let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode.into()); feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env_sh" => feat::copy_clash_env("sh"),
#[cfg(target_os = "windows")]
"copy_env_cmd" => feat::copy_clash_env("cmd"),
#[cfg(target_os = "windows")]
"copy_env_ps" => feat::copy_clash_env("ps"),
"open_app_config_dir" => crate::log_err!(ipc::open_app_config_dir()),
"open_app_data_dir" => crate::log_err!(ipc::open_app_data_dir()),
"open_core_dir" => crate::log_err!(ipc::open_core_dir()),
"open_logs_dir" => crate::log_err!(ipc::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => help::restart_application(app_handle),
"quit" => {
help::quit_application(app_handle);
}
_ => {
proxies::on_system_tray_event(&id);
}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
resolve::create_window(app_handle);
} }
_ => {}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env_sh" => feat::copy_clash_env(app_handle, "sh"),
#[cfg(target_os = "windows")]
"copy_env_cmd" => feat::copy_clash_env(app_handle, "cmd"),
#[cfg(target_os = "windows")]
"copy_env_ps" => feat::copy_clash_env(app_handle, "ps"),
"open_app_config_dir" => crate::log_err!(ipc::open_app_config_dir()),
"open_app_data_dir" => crate::log_err!(ipc::open_app_data_dir()),
"open_core_dir" => crate::log_err!(ipc::open_core_dir()),
"open_logs_dir" => crate::log_err!(ipc::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => help::restart_application(app_handle),
"quit" => {
help::quit_application(app_handle);
}
_ => {
proxies::on_system_tray_event(id);
}
}
}
pub fn on_system_tray_event(tray_icon: &TrayIcon, event: TrayIconEvent) {
if let TrayIconEvent::Click {
button: MouseButton::Left,
..
} = event
{
resolve::create_window(tray_icon.app_handle());
} }
} }
} }

View File

@@ -6,9 +6,8 @@ use crate::{
}, },
}; };
use anyhow::Context; use anyhow::Context;
use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _};
use indexmap::IndexMap; use indexmap::IndexMap;
use tauri::SystemTrayMenu; use tauri::{menu::MenuBuilder, AppHandle, Manager, Runtime};
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use tracing_attributes::instrument; use tracing_attributes::instrument;
@@ -41,8 +40,9 @@ async fn loop_task() {
} }
type GroupName = String; type GroupName = String;
type FromProxy = String; type ProxyName = String;
type ToProxy = String; type FromProxy = ProxyName;
type ToProxy = ProxyName;
type ProxySelectAction = (GroupName, FromProxy, ToProxy); type ProxySelectAction = (GroupName, FromProxy, ToProxy);
#[derive(PartialEq)] #[derive(PartialEq)]
enum TrayUpdateType { enum TrayUpdateType {
@@ -179,8 +179,9 @@ pub async fn proxies_updated_receiver() {
match diff_proxies(&tray_proxies_holder, &current_tray_proxies) { match diff_proxies(&tray_proxies_holder, &current_tray_proxies) {
TrayUpdateType::Full => { TrayUpdateType::Full => {
debug!("should do full update"); debug!("should do full update");
tray_proxies_holder = current_tray_proxies; tray_proxies_holder = current_tray_proxies;
match Handle::update_systray() { match Handle::emit("update_systray", ()) {
Ok(_) => { Ok(_) => {
debug!("update systray success"); debug!("update systray success");
} }
@@ -211,91 +212,129 @@ pub fn setup_proxies() {
} }
mod platform_impl { mod platform_impl {
use std::sync::atomic::AtomicBool; use super::{GroupName, ProxyName, ProxySelectAction, TrayProxyItem};
use super::{ProxySelectAction, TrayProxyItem};
use crate::{ use crate::{
config::nyanpasu::ProxiesSelectorMode, config::nyanpasu::ProxiesSelectorMode,
core::{clash::proxies::ProxiesGuard, handle::Handle}, core::{clash::proxies::ProxiesGuard, handle::Handle},
}; };
use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _}; use bimap::BiMap;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use rust_i18n::t; use rust_i18n::t;
use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu}; use std::sync::atomic::AtomicBool;
use tauri::{
menu::{
CheckMenuItemBuilder, IsMenuItem, Menu, MenuBuilder, MenuItemBuilder, MenuItemKind,
Submenu, SubmenuBuilder,
},
AppHandle, Manager, Runtime,
};
use tracing::warn; use tracing::warn;
pub fn generate_group_selector(group_name: &str, group: &TrayProxyItem) -> SystemTraySubmenu { // It store a map of proxy nodes like "GROUP_PROXY" -> ID
let mut group_menu = SystemTrayMenu::new(); // TODO: use Cow<str> instead of String
for item in group.all.iter() { pub(super) static ITEM_IDS: Lazy<Mutex<BiMap<(GroupName, ProxyName), usize>>> =
let mut sub_item = CustomMenuItem::new( Lazy::new(|| Mutex::new(BiMap::new()));
format!(
"select_proxy_{}_{}", pub fn generate_group_selector<R: Runtime>(
base64_standard.encode(group_name), app_handle: &AppHandle<R>,
base64_standard.encode(item) group_name: &str,
), group: &TrayProxyItem,
item.clone(), ) -> anyhow::Result<Submenu<R>> {
let mut item_ids = ITEM_IDS.lock();
let mut group_menu = SubmenuBuilder::new(app_handle, group_name);
if group.all.is_empty() {
group_menu = group_menu.item(
&MenuItemBuilder::new(t!("tray.no_proxies"))
.enabled(false)
.build(app_handle)?,
); );
return Ok(group_menu.build()?);
}
for item in group.all.iter() {
let key = (group_name.to_string(), item.to_string());
let id = item_ids.len();
item_ids.insert(key, id);
let mut sub_item_builder = CheckMenuItemBuilder::new(item.clone())
.id(format!("proxy_node_{}", id))
.checked(false);
if let Some(now) = group.current.clone() { if let Some(now) = group.current.clone() {
if now == item.as_str() { if now == item.as_str() {
#[cfg(target_os = "linux")] sub_item_builder = sub_item_builder.checked(true);
{
sub_item.title = super::super::utils::selected_title(item);
}
#[cfg(not(target_os = "linux"))]
{
sub_item = sub_item.selected();
}
} }
} }
if !matches!(group.r#type.as_str(), "Selector" | "Fallback") { if !matches!(group.r#type.as_str(), "Selector" | "Fallback") {
sub_item = sub_item.disabled(); sub_item_builder = sub_item_builder.enabled(false);
} }
group_menu = group_menu.add_item(sub_item); group_menu = group_menu.item(&sub_item_builder.build(app_handle)?);
} }
SystemTraySubmenu::new(group_name.to_string(), group_menu) Ok(group_menu.build()?)
} }
pub fn generate_selectors( pub fn generate_selectors<R: Runtime>(
menu: &SystemTrayMenu, app_handle: &AppHandle<R>,
proxies: &super::TrayProxies, proxies: &super::TrayProxies,
) -> SystemTrayMenu { ) -> anyhow::Result<Vec<MenuItemKind<R>>> {
let mut menu = menu.to_owned(); let mut items = Vec::new();
if proxies.is_empty() { if proxies.is_empty() {
return menu.add_item(CustomMenuItem::new("no_proxies", "No Proxies")); items.push(MenuItemKind::MenuItem(
MenuItemBuilder::new("No Proxies")
.id("no_proxies")
.enabled(false)
.build(app_handle)?,
));
return Ok(items);
}
{
let mut item_ids = ITEM_IDS.lock();
item_ids.clear(); // clear the item ids
} }
for (group, item) in proxies.iter() { for (group, item) in proxies.iter() {
let group_menu = generate_group_selector(group, item); let group_menu = generate_group_selector(app_handle, group, item)?;
menu = menu.add_submenu(group_menu); items.push(MenuItemKind::Submenu(group_menu));
} }
menu Ok(items)
} }
pub fn setup_tray(menu: &mut SystemTrayMenu) -> SystemTrayMenu { pub fn setup_tray<'m, R: Runtime, M: Manager<R>>(
let mut parent_menu = menu.to_owned(); app_handle: &AppHandle<R>,
mut menu: MenuBuilder<'m, R, M>,
) -> anyhow::Result<MenuBuilder<'m, R, M>> {
let selector_mode = crate::config::Config::verge() let selector_mode = crate::config::Config::verge()
.latest() .latest()
.clash_tray_selector .clash_tray_selector
.unwrap_or_default(); .unwrap_or_default();
let mut menu = match selector_mode { menu = match selector_mode {
ProxiesSelectorMode::Hidden => return parent_menu, ProxiesSelectorMode::Hidden => return Ok(menu),
ProxiesSelectorMode::Normal => { ProxiesSelectorMode::Normal => menu.separator(),
parent_menu = parent_menu.add_native_item(SystemTrayMenuItem::Separator); ProxiesSelectorMode::Submenu => menu,
parent_menu.clone()
}
ProxiesSelectorMode::Submenu => SystemTrayMenu::new(),
}; };
let proxies = ProxiesGuard::global().read().inner().to_owned(); let proxies = ProxiesGuard::global().read().inner().to_owned();
let mode = crate::utils::config::get_current_clash_mode(); let mode = crate::utils::config::get_current_clash_mode();
let tray_proxies = super::to_tray_proxies(mode.as_str(), &proxies); let tray_proxies = super::to_tray_proxies(mode.as_str(), &proxies);
menu = generate_selectors(&menu, &tray_proxies); let items = generate_selectors::<R>(app_handle, &tray_proxies)?;
if selector_mode == ProxiesSelectorMode::Submenu { match selector_mode {
parent_menu = ProxiesSelectorMode::Normal => {
parent_menu.add_submenu(SystemTraySubmenu::new(t!("tray.select_proxies"), menu)); for item in items {
parent_menu menu = menu.item(&item);
} else { }
menu }
ProxiesSelectorMode::Submenu => {
let mut submenu = SubmenuBuilder::with_id(
app_handle,
"select_proxies",
t!("tray.select_proxies"),
);
for item in items {
submenu = submenu.item(&item);
}
menu = menu.item(&submenu.build()?);
}
_ => {}
} }
Ok(menu)
} }
static TRAY_ITEM_UPDATE_BARRIER: AtomicBool = AtomicBool::new(false); static TRAY_ITEM_UPDATE_BARRIER: AtomicBool = AtomicBool::new(false);
@@ -306,84 +345,175 @@ mod platform_impl {
warn!("tray item update is in progress, skip this update"); warn!("tray item update is in progress, skip this update");
return; return;
} }
let tray = Handle::global() let app_handle = Handle::global().app_handle.lock();
.app_handle let tray_state = app_handle
.lock()
.as_ref() .as_ref()
.unwrap() .unwrap()
.tray_handle(); .state::<crate::core::tray::TrayState<tauri::Wry>>();
TRAY_ITEM_UPDATE_BARRIER.store(true, std::sync::atomic::Ordering::Release); TRAY_ITEM_UPDATE_BARRIER.store(true, std::sync::atomic::Ordering::Release);
let menu = tray_state.menu.lock();
let item_ids = ITEM_IDS.lock();
for action in actions { for action in actions {
tracing::debug!("update selected proxies: {:?}", action); // #[cfg(not(target_os = "linux"))]
let from = format!( // {
"select_proxy_{}_{}", // tracing::debug!("update selected proxies: {:?}", action);
base64_standard.encode(&action.0), // let from_id = match item_ids.get_by_left(&(action.0.clone(), action.1.clone())) {
base64_standard.encode(&action.1) // Some(id) => *id,
); // None => {
let to = format!( // warn!("from item not found: {:?}", action);
"select_proxy_{}_{}", // continue;
base64_standard.encode(&action.0), // }
base64_standard.encode(&action.2) // };
); // let from_id = format!("proxy_node_{}", from_id);
match tray.try_get_item(&from) { // let to_id = match item_ids.get_by_left(&(action.0.clone(), action.2.clone())) {
// Some(id) => *id,
// None => {
// warn!("to item not found: {:?}", action);
// continue;
// }
// };
// let to_id = format!("proxy_node_{}", to_id);
// match menu.get(&from_id) {
// Some(item) => match item.kind() {
// MenuItemKind::Check(item) => {
// if item.is_checked().is_ok_and(|x| x) {
// let _ = item.set_checked(false);
// }
// }
// MenuItemKind::MenuItem(item) => {
// let _ = item.set_text(action.1.clone());
// }
// _ => {
// warn!("failed to deselect, item is not a check item: {}", from_id);
// }
// },
// None => {
// warn!("failed to deselect, item not found: {}", from_id);
// }
// }
// match menu.get(&to_id) {
// Some(item) => match item.kind() {
// MenuItemKind::Check(item) => {
// if item.is_checked().is_ok_and(|x| !x) {
// let _ = item.set_checked(true);
// }
// }
// MenuItemKind::MenuItem(item) => {
// let _ = item.set_text(action.2.clone());
// }
// _ => {
// warn!("failed to select, item is not a check item: {}", to_id);
// }
// },
// None => {
// warn!("failed to select, item not found: {}", to_id);
// }
// }
// }
// }
// here is a fucking workaround for id getter
#[inline]
fn find_check_item<R: Runtime>(
menu: &Menu<R>,
group: GroupName,
proxy: ProxyName,
) -> Option<tauri::menu::CheckMenuItem<R>> {
menu.items()
.ok()
.and_then(|items| {
items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Submenu(submenu) if submenu.text().is_ok_and(|text| text == group) || submenu.id() == "select_proxies"))
})
.and_then(|submenu| {
let submenu = submenu.as_submenu_unchecked();
if submenu.id() == "select_proxies" {
submenu.items().ok().and_then(|items| {
items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Submenu(submenu) if submenu.text().is_ok_and(|text| text == group)))
})
.and_then(|submenu| {
submenu.as_submenu_unchecked().items().ok()
})
} else {
submenu.items().ok()
}
})
.and_then(|items| {
items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Check(item) if item.text().is_ok_and(|text| text == proxy)))
}).map(|item| item.as_check_menuitem_unchecked().clone())
}
let from_item = find_check_item(&menu, actions[0].0.clone(), actions[0].1.clone());
match from_item {
Some(item) => { Some(item) => {
#[cfg(not(target_os = "linux"))] let _ = item.set_checked(false);
{
let _ = item.set_selected(false);
}
#[cfg(target_os = "linux")]
{
let _ = item.set_title(action.1.clone());
}
} }
None => { None => {
warn!("failed to deselect, item not found: {}", from); warn!(
"failed to deselect, item not found: {} {}",
actions[0].0, actions[0].1
);
} }
} }
match tray.try_get_item(&to) {
let to_item = find_check_item(&menu, actions[0].0.clone(), actions[0].2.clone());
match to_item {
Some(item) => { Some(item) => {
#[cfg(not(target_os = "linux"))] let _ = item.set_checked(true);
{
let _ = item.set_selected(true);
}
#[cfg(target_os = "linux")]
{
let _ = item.set_title(super::super::utils::selected_title(&action.2));
}
} }
None => { None => {
warn!("failed to select, item not found: {}", to); warn!(
"failed to select, item not found: {} {}",
actions[0].0, actions[0].2
);
} }
} }
TRAY_ITEM_UPDATE_BARRIER.store(false, std::sync::atomic::Ordering::Release);
} }
TRAY_ITEM_UPDATE_BARRIER.store(false, std::sync::atomic::Ordering::Release);
} }
} }
pub trait SystemTrayMenuProxiesExt { pub trait SystemTrayMenuProxiesExt<R: Runtime> {
fn setup_proxies(&mut self) -> Self; fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self>
where
Self: Sized;
} }
impl SystemTrayMenuProxiesExt for SystemTrayMenu { impl<'m, R: Runtime, M: Manager<R>> SystemTrayMenuProxiesExt<R> for MenuBuilder<'m, R, M> {
fn setup_proxies(&mut self) -> Self { fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self> {
platform_impl::setup_tray(self) platform_impl::setup_tray(app_handle, self)
} }
} }
#[instrument] #[instrument]
pub fn on_system_tray_event(event: &str) { pub fn on_system_tray_event(event: &str) {
if !event.starts_with("select_proxy_") { if !event.starts_with("proxy_node_") {
return; // bypass non-select event return; // bypass non-select event
} }
let parts: Vec<&str> = event.split('_').collect(); let node_id = event.split('_').last().unwrap(); // safe to unwrap
if parts.len() != 4 { let node_id = match node_id.parse::<usize>() {
return; // bypass invalid event Ok(id) => id,
} Err(e) => {
error!("parse node id failed: {:?}", e);
return;
}
};
let (group, name) = {
let map = platform_impl::ITEM_IDS.lock();
let item = map.get_by_right(&node_id);
match item {
Some((group, name)) => (group.clone(), name.clone()),
None => {
error!("node id not found: {}", node_id);
return;
}
}
};
let wrapper = move || -> anyhow::Result<()> { let wrapper = move || -> anyhow::Result<()> {
let group = String::from_utf8(base64_standard.decode(parts[2])?)?;
let name = String::from_utf8(base64_standard.decode(parts[3])?)?;
tracing::debug!("received select proxy event: {} {}", group, name); tracing::debug!("received select proxy event: {} {}", group, name);
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
ProxiesGuard::global() ProxiesGuard::global()

View File

@@ -1,3 +0,0 @@
pub(super) fn selected_title(s: impl AsRef<str>) -> String {
format!("{}", s.as_ref())
}

View File

@@ -22,7 +22,7 @@ macro_rules! append {
}; };
} }
#[tracing::instrument] #[tracing_attributes::instrument(skip(config))]
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun"); let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key); let tun_val = config.get(&tun_key);

View File

@@ -14,7 +14,8 @@ use anyhow::{bail, Result};
use handle::Message; use handle::Message;
use nyanpasu_ipc::api::status::CoreState; use nyanpasu_ipc::api::status::CoreState;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use wry::application::clipboard::Clipboard; use tauri::AppHandle;
use tauri_plugin_clipboard_manager::ClipboardExt;
// 打开面板 // 打开面板
#[allow(unused)] #[allow(unused)]
@@ -400,7 +401,7 @@ async fn update_core_config() -> Result<()> {
} }
/// copy env variable /// copy env variable
pub fn copy_clash_env(option: &str) { pub fn copy_clash_env(app_handle: &AppHandle, option: &str) {
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7890) }; let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7890) };
let http_proxy = format!("http://127.0.0.1:{}", port); let http_proxy = format!("http://127.0.0.1:{}", port);
let socks5_proxy = format!("socks5://127.0.0.1:{}", port); let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
@@ -410,12 +411,24 @@ pub fn copy_clash_env(option: &str) {
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}"); let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\""); let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let mut clipboard = Clipboard::new(); let clipboard = app_handle.clipboard();
match option { match option {
"sh" => clipboard.write_text(sh), "sh" => {
"cmd" => clipboard.write_text(cmd), if let Err(e) = clipboard.write_text(sh) {
"ps" => clipboard.write_text(ps), log::error!(target: "app", "copy_clash_env failed: {e}");
}
}
"cmd" => {
if let Err(e) = clipboard.write_text(cmd) {
log::error!(target: "app", "copy_clash_env failed: {e}");
}
}
"ps" => {
if let Err(e) = clipboard.write_text(ps) {
log::error!(target: "app", "copy_clash_env failed: {e}");
}
}
_ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"), _ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"),
} }
} }

View File

@@ -20,9 +20,10 @@ use profile::item_type::ProfileItemType;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{borrow::Cow, collections::VecDeque, path::PathBuf}; use std::{borrow::Cow, collections::VecDeque, path::PathBuf};
use sysproxy::Sysproxy; use sysproxy::Sysproxy;
use tauri::AppHandle;
use tray::icon::TrayIcon; use tray::icon::TrayIcon;
use tauri::api::dialog::FileDialogBuilder; use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
type CmdResult<T = ()> = Result<T, String>; type CmdResult<T = ()> = Result<T, String>;
@@ -339,34 +340,36 @@ pub async fn fetch_latest_core_versions() -> CmdResult<ManifestVersionLatest> {
} }
#[tauri::command] #[tauri::command]
pub async fn get_core_version(core_type: nyanpasu::ClashCore) -> CmdResult<String> { pub async fn get_core_version(
match tokio::task::spawn_blocking(move || resolve::resolve_core_version(&core_type)).await { app_handle: AppHandle,
Ok(Ok(version)) => Ok(version), core_type: nyanpasu::ClashCore,
Ok(Err(err)) => Err(format!("{err}")), ) -> CmdResult<String> {
match resolve::resolve_core_version(&app_handle, &core_type).await {
Ok(version) => Ok(version),
Err(err) => Err(format!("{err}")), Err(err) => Err(format!("{err}")),
} }
} }
#[tauri::command] #[tauri::command]
pub async fn collect_logs() -> CmdResult { pub async fn collect_logs(app_handle: AppHandle) -> CmdResult {
let now = Local::now().format("%Y-%m-%d"); let now = Local::now().format("%Y-%m-%d");
let fname = format!("{}-log", now); let fname = format!("{}-log", now);
let builder = FileDialogBuilder::new(); let builder = FileDialogBuilder::new(app_handle.dialog().clone());
builder builder
.add_filter("archive files", &["zip"]) .add_filter("archive files", &["zip"])
.set_file_name(&fname) .set_file_name(&fname)
.set_title("Save log archive") .set_title("Save log archive")
.save_file(|file_path| match file_path { .save_file(|file_path| match file_path {
None => (), Some(path) if path.as_path().is_some() => {
Some(path) => { debug!("{:#?}", path);
debug!("{:#?}", path.as_os_str()); match candy::collect_logs(path.as_path().unwrap()) {
match candy::collect_logs(&path) {
Ok(_) => (), Ok(_) => (),
Err(err) => { Err(err) => {
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
} }
} }
} }
_ => (),
}); });
Ok(()) Ok(())
} }
@@ -652,3 +655,9 @@ pub async fn get_service_install_prompt() -> CmdResult<String> {
} }
Ok(prompt) Ok(prompt)
} }
#[tauri::command]
pub fn cleanup_processes(app_handle: AppHandle) -> CmdResult {
crate::utils::help::cleanup_processes(&app_handle);
Ok(())
}

View File

@@ -0,0 +1,353 @@
#![feature(auto_traits, negative_impls)]
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#[cfg(target_os = "macos")]
#[macro_use]
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
mod cmds;
mod config;
mod consts;
mod core;
mod enhance;
mod feat;
mod ipc;
mod server;
mod utils;
use crate::{
config::Config,
core::handle::Handle,
utils::{init, resolve},
};
use tauri::Emitter;
use tauri_plugin_shell::ShellExt;
use utils::resolve::{is_window_opened, reset_window_open_counter};
rust_i18n::i18n!("../../locales");
#[cfg(feature = "deadlock-detection")]
fn deadlock_detection() {
use parking_lot::deadlock;
use std::{thread, time::Duration};
use tracing::error;
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
error!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
error!("Deadlock #{}", i);
for t in threads {
error!("Thread Id {:#?}", t.thread_id());
error!("{:#?}", t.backtrace());
}
}
});
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() -> std::io::Result<()> {
// share the tauri async runtime to nyanpasu-utils
#[cfg(feature = "deadlock-detection")]
deadlock_detection();
// Should be in first place in order prevent single instance check block everything
// Custom scheme check
#[cfg(not(target_os = "macos"))]
// on macos the plugin handles this (macos doesn't use cli args for the url)
let custom_scheme = match std::env::args().nth(1) {
Some(url) => url::Url::parse(&url).ok(),
None => None,
};
#[cfg(target_os = "macos")]
let custom_scheme: Option<url::Url> = None;
if custom_scheme.is_none() {
// Parse commands
cmds::parse().unwrap();
};
#[cfg(feature = "verge-dev")]
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu.dev");
#[cfg(not(feature = "verge-dev"))]
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu");
// 单例检测
let single_instance_result = utils::init::check_singleton();
if single_instance_result
.as_ref()
.is_ok_and(|instance| instance.is_none())
{
std::process::exit(0);
}
// Use system locale as default
let locale = {
let locale = utils::help::get_system_locale();
utils::help::mapping_to_i18n_key(&locale)
};
rust_i18n::set_locale(locale);
if single_instance_result
.as_ref()
.is_ok_and(|instance| instance.is_some())
{
if let Err(e) = init::run_pending_migrations() {
utils::dialog::panic_dialog(
&format!(
"Failed to finish migration event: {}\nYou can see the detailed information at migration.log in your local data dir.\nYou're supposed to submit it as the attachment of new issue.",
e,
)
);
std::process::exit(1);
}
}
crate::log_err!(init::init_config());
// Panic Hook to show a panic dialog and save logs
let default_panic = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
use std::backtrace::{Backtrace, BacktraceStatus};
let payload = panic_info.payload();
#[allow(clippy::manual_map)]
let payload = if let Some(s) = payload.downcast_ref::<&str>() {
Some(&**s)
} else if let Some(s) = payload.downcast_ref::<String>() {
Some(s.as_str())
} else {
None
};
let location = panic_info.location().map(|l| l.to_string());
let (backtrace, note) = {
let backtrace = Backtrace::capture();
let note = (backtrace.status() == BacktraceStatus::Disabled)
.then_some("run with RUST_BACKTRACE=1 environment variable to display a backtrace");
(Some(backtrace), note)
};
tracing::error!(
panic.payload = payload,
panic.location = location,
panic.backtrace = backtrace.as_ref().map(tracing::field::display),
panic.note = note,
"A panic occurred",
);
utils::dialog::panic_dialog(&format!(
"payload: {:#?}\nlocation: {:?}\nbacktrace: {:#?}\n\nnote: {:?}",
payload, location, backtrace, note
));
// cleanup the core manager
let task = std::thread::spawn(move || {
nyanpasu_utils::runtime::block_on(async {
let _ = crate::core::CoreManager::global().stop_core().await;
});
});
let _ = task.join();
default_panic(panic_info);
}));
let verge = { Config::verge().latest().language.clone().unwrap() };
rust_i18n::set_locale(verge.as_str());
// show a dialog to print the single instance error
let _singleton = single_instance_result.unwrap().unwrap(); // hold the guard until the end of the program
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_global_shortcut::Builder::default().build())
.setup(|app| {
#[cfg(target_os = "macos")]
{
use tauri::menu::{MenuBuilder, SubmenuBuilder};
let submenu = SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.copy()
.paste()
.cut()
.select_all()
.close_window()
.quit()
.build()
.unwrap();
let menu = MenuBuilder::new(app).item(&submenu).build().unwrap();
app.set_menu(menu).unwrap();
}
resolve::resolve_setup(app);
// setup custom scheme
let handle = app.handle().clone();
// For start new app from schema
#[cfg(not(target_os = "macos"))]
if let Some(url) = custom_scheme {
log::info!(target: "app", "started with schema");
resolve::create_window(&handle.clone());
while !is_window_opened() {
log::info!(target: "app", "waiting for window open");
std::thread::sleep(std::time::Duration::from_millis(100));
}
Handle::global()
.app_handle
.lock()
.as_ref()
.unwrap()
.emit("scheme-request-received", url.clone())
.unwrap();
}
// This operation should terminate the app if app is called by custom scheme and this instance is not the primary instance
log_err!(tauri_plugin_deep_link::register(
&["clash-nyanpasu", "clash"],
move |request| {
log::info!(target: "app", "scheme request received: {:?}", &request);
resolve::create_window(&handle.clone()); // create window if not exists
while !is_window_opened() {
log::info!(target: "app", "waiting for window open");
std::thread::sleep(std::time::Duration::from_millis(100));
}
handle.emit("scheme-request-received", request).unwrap();
}
));
std::thread::spawn(move || {
nyanpasu_utils::runtime::block_on(async move {
server::run(*server::SERVER_PORT)
.await
.expect("failed to start server");
});
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
// common
ipc::get_sys_proxy,
ipc::open_app_config_dir,
ipc::open_app_data_dir,
ipc::open_logs_dir,
ipc::open_web_url,
ipc::open_core_dir,
// cmds::kill_sidecar,
ipc::restart_sidecar,
ipc::grant_permission,
// clash
ipc::get_clash_info,
ipc::get_clash_logs,
ipc::patch_clash_config,
ipc::change_clash_core,
ipc::get_runtime_config,
ipc::get_runtime_yaml,
ipc::get_runtime_exists,
ipc::get_runtime_logs,
ipc::clash_api_get_proxy_delay,
ipc::uwp::invoke_uwp_tool,
// updater
ipc::fetch_latest_core_versions,
ipc::update_core,
ipc::inspect_updater,
ipc::get_core_version,
// utils
ipc::collect_logs,
// verge
ipc::get_verge_config,
ipc::patch_verge_config,
// cmds::update_hotkeys,
// profile
ipc::get_profiles,
ipc::enhance_profiles,
ipc::patch_profiles_config,
ipc::view_profile,
ipc::patch_profile,
ipc::create_profile,
ipc::import_profile,
ipc::reorder_profile,
ipc::update_profile,
ipc::delete_profile,
ipc::read_profile_file,
ipc::save_profile_file,
ipc::save_window_size_state,
ipc::get_custom_app_dir,
ipc::set_custom_app_dir,
// service mode
ipc::service::status_service,
ipc::service::install_service,
ipc::service::uninstall_service,
ipc::service::start_service,
ipc::service::stop_service,
ipc::service::restart_service,
ipc::is_portable,
ipc::get_proxies,
ipc::select_proxy,
ipc::update_proxy_provider,
ipc::restart_application,
ipc::collect_envs,
ipc::get_server_port,
ipc::set_tray_icon,
ipc::is_tray_icon_set,
ipc::get_core_status,
ipc::url_delay_test,
ipc::get_ipsb_asn,
ipc::open_that,
ipc::is_appimage,
ipc::get_service_install_prompt,
ipc::cleanup_processes,
]);
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
app.run(|app_handle, e| match e {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
tauri::RunEvent::Exit => {
resolve::resolve_reset();
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
core::tray::on_scale_factor_changed(scale_factor);
}
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => {
log::debug!(target: "app", "window close requested");
reset_window_open_counter();
let _ = resolve::save_window_state(app_handle, true);
#[cfg(target_os = "macos")]
unsafe {
crate::utils::dock::macos::hide_dock_icon();
}
}
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
log::debug!(target: "app", "window moved or resized");
std::thread::sleep(std::time::Duration::from_nanos(1));
let _ = resolve::save_window_state(app_handle, false);
}
_ => {}
}
}
}
_ => {}
});
Ok(())
}

View File

@@ -1,318 +1,5 @@
#![feature(auto_traits, negative_impls)] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#[cfg(target_os = "macos")] fn main() {
#[macro_use] app_lib::run().unwrap();
extern crate cocoa;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
mod cmds;
mod config;
mod consts;
mod core;
mod enhance;
mod feat;
mod ipc;
mod server;
mod utils;
use crate::{
config::Config,
core::handle::Handle,
utils::{init, resolve},
};
use tauri::{api, Manager, SystemTray};
use utils::resolve::{is_window_opened, reset_window_open_counter};
rust_i18n::i18n!("../../locales");
#[cfg(feature = "deadlock-detection")]
fn deadlock_detection() {
use parking_lot::deadlock;
use std::{thread, time::Duration};
use tracing::error;
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = deadlock::check_deadlock();
if deadlocks.is_empty() {
continue;
}
error!("{} deadlocks detected", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
error!("Deadlock #{}", i);
for t in threads {
error!("Thread Id {:#?}", t.thread_id());
error!("{:#?}", t.backtrace());
}
}
});
}
fn main() -> std::io::Result<()> {
// share the tauri async runtime to nyanpasu-utils
#[cfg(feature = "deadlock-detection")]
deadlock_detection();
// Should be in first place in order prevent single instance check block everything
// Custom scheme check
#[cfg(not(target_os = "macos"))]
// on macos the plugin handles this (macos doesn't use cli args for the url)
let custom_scheme = match std::env::args().nth(1) {
Some(url) => url::Url::parse(&url).ok(),
None => None,
};
#[cfg(target_os = "macos")]
let custom_scheme: Option<url::Url> = None;
if custom_scheme.is_none() {
// Parse commands
cmds::parse().unwrap();
};
#[cfg(feature = "verge-dev")]
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu.dev");
#[cfg(not(feature = "verge-dev"))]
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu");
// 单例检测
let single_instance_result = utils::init::check_singleton();
if single_instance_result
.as_ref()
.is_ok_and(|instance| instance.is_none())
{
std::process::exit(0);
}
// Use system locale as default
let locale = {
let locale = utils::help::get_system_locale();
utils::help::mapping_to_i18n_key(&locale)
};
rust_i18n::set_locale(locale);
if single_instance_result
.as_ref()
.is_ok_and(|instance| instance.is_some())
{
if let Err(e) = init::run_pending_migrations() {
utils::dialog::panic_dialog(
&format!(
"Failed to finish migration event: {}\nYou can see the detailed information at migration.log in your local data dir.\nYou're supposed to submit it as the attachment of new issue.",
e,
)
);
std::process::exit(1);
}
}
crate::log_err!(init::init_config());
// Panic Hook to show a panic dialog and save logs
let default_panic = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
error!(format!("panic hook: {:?}", info));
utils::dialog::panic_dialog(&format!("{:?}", info));
default_panic(info);
}));
let verge = { Config::verge().latest().language.clone().unwrap() };
rust_i18n::set_locale(verge.as_str());
// show a dialog to print the single instance error
let _singleton = single_instance_result.unwrap().unwrap(); // hold the guard until the end of the program
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.system_tray(SystemTray::new())
.setup(|app| {
resolve::resolve_setup(app);
// setup custom scheme
let handle = app.handle().clone();
// For start new app from schema
#[cfg(not(target_os = "macos"))]
if let Some(url) = custom_scheme {
log::info!(target: "app", "started with schema");
resolve::create_window(&handle.clone());
while !is_window_opened() {
log::info!(target: "app", "waiting for window open");
std::thread::sleep(std::time::Duration::from_millis(100));
}
Handle::global()
.app_handle
.lock()
.as_ref()
.unwrap()
.emit_all("scheme-request-received", url.clone())
.unwrap();
}
// This operation should terminate the app if app is called by custom scheme and this instance is not the primary instance
log_err!(tauri_plugin_deep_link::register(
&["clash-nyanpasu", "clash"],
move |request| {
log::info!(target: "app", "scheme request received: {:?}", &request);
resolve::create_window(&handle.clone()); // create window if not exists
while !is_window_opened() {
log::info!(target: "app", "waiting for window open");
std::thread::sleep(std::time::Duration::from_millis(100));
}
handle.emit_all("scheme-request-received", request).unwrap();
}
));
std::thread::spawn(move || {
nyanpasu_utils::runtime::block_on(async move {
server::run(*server::SERVER_PORT)
.await
.expect("failed to start server");
});
});
Ok(())
})
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
.invoke_handler(tauri::generate_handler![
// common
ipc::get_sys_proxy,
ipc::open_app_config_dir,
ipc::open_app_data_dir,
ipc::open_logs_dir,
ipc::open_web_url,
ipc::open_core_dir,
// cmds::kill_sidecar,
ipc::restart_sidecar,
ipc::grant_permission,
// clash
ipc::get_clash_info,
ipc::get_clash_logs,
ipc::patch_clash_config,
ipc::change_clash_core,
ipc::get_runtime_config,
ipc::get_runtime_yaml,
ipc::get_runtime_exists,
ipc::get_runtime_logs,
ipc::clash_api_get_proxy_delay,
ipc::uwp::invoke_uwp_tool,
// updater
ipc::fetch_latest_core_versions,
ipc::update_core,
ipc::inspect_updater,
ipc::get_core_version,
// utils
ipc::collect_logs,
// verge
ipc::get_verge_config,
ipc::patch_verge_config,
// cmds::update_hotkeys,
// profile
ipc::get_profiles,
ipc::enhance_profiles,
ipc::patch_profiles_config,
ipc::view_profile,
ipc::patch_profile,
ipc::create_profile,
ipc::import_profile,
ipc::reorder_profile,
ipc::update_profile,
ipc::delete_profile,
ipc::read_profile_file,
ipc::save_profile_file,
ipc::save_window_size_state,
ipc::get_custom_app_dir,
ipc::set_custom_app_dir,
// service mode
ipc::service::status_service,
ipc::service::install_service,
ipc::service::uninstall_service,
ipc::service::start_service,
ipc::service::stop_service,
ipc::service::restart_service,
ipc::is_portable,
ipc::get_proxies,
ipc::select_proxy,
ipc::update_proxy_provider,
ipc::restart_application,
ipc::collect_envs,
ipc::get_server_port,
ipc::set_tray_icon,
ipc::is_tray_icon_set,
ipc::get_core_status,
ipc::url_delay_test,
ipc::get_ipsb_asn,
ipc::open_that,
ipc::is_appimage,
ipc::get_service_install_prompt,
]);
#[cfg(target_os = "macos")]
{
use tauri::{Menu, MenuItem, Submenu};
builder = builder.menu(
Menu::new().add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::CloseWindow)
.add_native_item(MenuItem::Quit),
)),
);
}
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
app.run(|app_handle, e| match e {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
tauri::RunEvent::Exit => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
resolve::resolve_reset();
nyanpasu_utils::runtime::block_on(async {
if let Err(e) = crate::core::CoreManager::global().stop_core().await {
log::error!(target: "app", "failed to stop core while dispatch updater: {}", e);
}
});
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
core::tray::on_scale_factor_changed(scale_factor);
}
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => {
// log::info!(target: "app", "window close requested");
reset_window_open_counter();
let _ = resolve::save_window_state(app_handle, true);
#[cfg(target_os = "macos")]
unsafe {
crate::utils::dock::macos::hide_dock_icon();
}
}
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
// log::info!(target: "app", "window moved or resized");
std::thread::sleep(std::time::Duration::from_nanos(1));
let _ = resolve::save_window_state(app_handle, false);
}
_ => {}
}
}
}
_ => {}
});
Ok(())
} }

View File

@@ -60,8 +60,11 @@ pub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {
let mut core = HashMap::new(); let mut core = HashMap::new();
for c in CoreType::get_supported_cores() { for c in CoreType::get_supported_cores() {
let name: &str = c.as_ref(); let name: &str = c.as_ref();
let command = tauri::api::process::Command::new_sidecar(name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; let mut command = std::process::Command::new(
super::dirs::get_data_or_sidecar_path(name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?,
);
let output = command let output = command
.args(if matches!(c, CoreType::Clash(ClashCoreType::ClashRust)) { .args(if matches!(c, CoreType::Clash(ClashCoreType::ClashRust)) {
["-V"] ["-V"]
@@ -70,9 +73,10 @@ pub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {
}) })
.output() .output()
.expect("failed to execute sidecar command"); .expect("failed to execute sidecar command");
let stdout = String::from_utf8_lossy(&output.stdout);
core.insert( core.insert(
Cow::Borrowed(name), Cow::Borrowed(name),
Cow::Owned(output.stdout.replace("\n\n", " ").trim().to_owned()), Cow::Owned(stdout.replace("\n\n", " ").trim().to_owned()),
); );
} }
Ok(EnvInfo { Ok(EnvInfo {

View File

@@ -1,4 +1,4 @@
use rfd::{MessageButtons, MessageDialog, MessageLevel}; use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
use rust_i18n::t; use rust_i18n::t;
pub fn panic_dialog(msg: &str) { pub fn panic_dialog(msg: &str) {
@@ -12,12 +12,15 @@ pub fn panic_dialog(msg: &str) {
} }
pub fn migrate_dialog(msg: &str) -> bool { pub fn migrate_dialog(msg: &str) -> bool {
MessageDialog::new() matches!(
.set_level(MessageLevel::Warning) MessageDialog::new()
.set_title("Clash Nyanpasu Migration") .set_level(MessageLevel::Warning)
.set_buttons(MessageButtons::YesNo) .set_title("Clash Nyanpasu Migration")
.set_description(msg) .set_buttons(MessageButtons::YesNo)
.show() .set_description(msg)
.show(),
MessageDialogResult::Yes
)
} }
pub fn error_dialog(msg: String) { pub fn error_dialog(msg: String) {

View File

@@ -3,10 +3,7 @@ use anyhow::Result;
use nyanpasu_utils::dirs::{suggest_config_dir, suggest_data_dir}; use nyanpasu_utils::dirs::{suggest_config_dir, suggest_data_dir};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{borrow::Cow, fs, path::PathBuf}; use std::{borrow::Cow, fs, path::PathBuf};
use tauri::{ use tauri::{utils::platform::resource_dir, Env};
api::path::{home_dir, resource_dir},
Env,
};
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
const PREVIOUS_APP_NAME: &str = "clash-verge"; const PREVIOUS_APP_NAME: &str = "clash-verge";
@@ -109,7 +106,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
if !get_portable_flag() { if !get_portable_flag() {
Ok(home_dir() Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to check old app home dir"))? .ok_or(anyhow::anyhow!("failed to check old app home dir"))?
.join(".config") .join(".config")
.join(PREVIOUS_APP_NAME)) .join(PREVIOUS_APP_NAME))
@@ -120,7 +117,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
Ok(home_dir() Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))? .ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join(".config") .join(".config")
.join(PREVIOUS_APP_NAME)) .join(PREVIOUS_APP_NAME))
@@ -133,7 +130,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
)] )]
pub fn app_home_dir() -> Result<PathBuf> { pub fn app_home_dir() -> Result<PathBuf> {
if cfg!(feature = "verge-dev") { if cfg!(feature = "verge-dev") {
return Ok(home_dir() return Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))? .ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join(".config") .join(".config")
.join(APP_NAME)); .join(APP_NAME));
@@ -147,7 +144,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
if let Some(reg_app_dir) = reg_app_dir { if let Some(reg_app_dir) = reg_app_dir {
return Ok(reg_app_dir); return Ok(reg_app_dir);
} }
return Ok(home_dir() return Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to get app home dir"))? .ok_or(anyhow::anyhow!("failed to get app home dir"))?
.join(".config") .join(".config")
.join(APP_NAME)); .join(APP_NAME));
@@ -156,7 +153,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
Ok(home_dir() Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))? .ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join(".config") .join(".config")
.join(APP_NAME)) .join(APP_NAME))
@@ -168,7 +165,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
let app_handle = handle.app_handle.lock(); let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() { if let Some(app_handle) = app_handle.as_ref() {
let res_dir = resource_dir(app_handle.package_info(), &Env::default()) let res_dir = resource_dir(app_handle.package_info(), &Env::default())
.ok_or(anyhow::anyhow!("failed to get the resource dir"))? .map_err(|_| anyhow::anyhow!("failed to get the resource dir"))?
.join("resources"); .join("resources");
return Ok(res_dir); return Ok(res_dir);
}; };
@@ -315,6 +312,28 @@ fn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> {
Ok(()) Ok(())
} }
pub fn get_data_or_sidecar_path(binary_name: impl AsRef<str>) -> Result<PathBuf> {
let binary_name = binary_name.as_ref();
let data_dir = app_data_dir()?;
let path = data_dir.join(if cfg!(windows) && !binary_name.ends_with(".exe") {
format!("{}.exe", binary_name)
} else {
binary_name.to_string()
});
if path.exists() {
return Ok(data_dir);
}
let install_dir = app_install_dir()?;
let path = install_dir.join(if cfg!(windows) && !binary_name.ends_with(".exe") {
format!("{}.exe", binary_name)
} else {
binary_name.to_string()
});
Ok(path)
}
mod test { mod test {
#[test] #[test]
fn test_dir_placeholder() { fn test_dir_placeholder() {

View File

@@ -15,16 +15,13 @@ use std::{
path::PathBuf, path::PathBuf,
str::FromStr, str::FromStr,
}; };
use tauri::{ use tauri::{process::current_binary, AppHandle, Manager};
api::{ use tauri_plugin_shell::ShellExt;
process::current_binary,
shell::{open, Program},
},
AppHandle, Manager,
};
use tracing::{debug, warn}; use tracing::{debug, warn};
use tracing_attributes::instrument; use tracing_attributes::instrument;
use crate::trace_err;
/// read data from yaml as struct T /// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> { pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
if !path.exists() { if !path.exists() {
@@ -105,20 +102,27 @@ pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
let code = "code"; let code = "code";
let _ = match Program::from_str(code) { let shell = app.shell();
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
Err(err) => { trace_err!(
log::error!(target: "app", "Can't find VScode `{err}`"); match which::which(code) {
// default open Ok(_) => crate::utils::open::with(path, code),
open(&app.shell_scope(), path.to_string_lossy(), None) Err(err) => {
} log::error!(target: "app", "Can't find VScode `{err}`");
}; // default open
shell
.open(path.to_string_lossy().to_string(), None)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
}
},
"Can't open file"
);
Ok(()) Ok(())
} }
pub fn get_system_locale() -> String { pub fn get_system_locale() -> String {
tauri::api::os::locale().unwrap_or("en-US".to_string()) tauri_plugin_os::locale().unwrap_or("en-US".to_string())
} }
pub fn mapping_to_i18n_key(locale_key: &str) -> &'static str { pub fn mapping_to_i18n_key(locale_key: &str) -> &'static str {
@@ -215,10 +219,12 @@ pub fn get_max_scale_factor() -> f64 {
} }
#[instrument(skip(app_handle))] #[instrument(skip(app_handle))]
fn cleanup_processes(app_handle: &AppHandle) { pub fn cleanup_processes(app_handle: &AppHandle) {
let _ = super::resolve::save_window_state(app_handle, true); let _ = super::resolve::save_window_state(app_handle, true);
super::resolve::resolve_reset(); super::resolve::resolve_reset();
tauri::api::process::kill_children(); let _ = nyanpasu_utils::runtime::block_on(async move {
crate::core::CoreManager::global().stop_core().await
});
} }
#[instrument(skip(app_handle))] #[instrument(skip(app_handle))]
@@ -263,9 +269,9 @@ macro_rules! log_err {
} }
}; };
($result: expr, $err_str: expr) => { ($result: expr, $label: expr) => {
if let Err(_) = $result { if let Err(err) = $result {
log::error!(target: "app", "{}", $err_str); log::error!(target: "app", "{}: {:#?}", $label, err);
} }
}; };
} }

View File

@@ -12,3 +12,16 @@ pub fn that<T: AsRef<OsStr>>(path: T) -> std::io::Result<()> {
open::that(path) open::that(path)
} }
} }
pub fn with<T: AsRef<OsStr>>(path: T, program: &str) -> std::io::Result<()> {
// A dirty workaround for AppImage
if std::env::var("APPIMAGE").is_ok() {
std::process::Command::new(program)
.arg(path)
.env_remove("LD_LIBRARY_PATH")
.status()?;
Ok(())
} else {
open::with(path, program)
}
}

View File

@@ -18,7 +18,8 @@ use std::{
net::TcpListener, net::TcpListener,
sync::atomic::{AtomicU16, Ordering}, sync::atomic::{AtomicU16, Ordering},
}; };
use tauri::{api::process::Command, async_runtime::block_on, App, AppHandle, Manager}; use tauri::{async_runtime::block_on, App, AppHandle, Emitter, Listener, Manager};
use tauri_plugin_shell::ShellExt;
static OPEN_WINDOWS_COUNTER: AtomicU16 = AtomicU16::new(0); static OPEN_WINDOWS_COUNTER: AtomicU16 = AtomicU16::new(0);
@@ -85,7 +86,7 @@ pub fn find_unused_port() -> Result<u16> {
pub fn resolve_setup(app: &mut App) { pub fn resolve_setup(app: &mut App) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory); app.set_activation_policy(tauri::ActivationPolicy::Accessory);
app.listen_global("react_app_mounted", move |_| { app.listen("react_app_mounted", move |_| {
tracing::debug!("Frontend React App is mounted, reset open window counter"); tracing::debug!("Frontend React App is mounted, reset open window counter");
reset_window_open_counter(); reset_window_open_counter();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -94,7 +95,7 @@ pub fn resolve_setup(app: &mut App) {
} }
}); });
handle::Handle::global().init(app.app_handle()); handle::Handle::global().init(app.app_handle().clone());
log_err!(init::init_resources()); log_err!(init::init_resources());
log_err!(init::init_service()); log_err!(init::init_service());
@@ -134,20 +135,31 @@ pub fn resolve_setup(app: &mut App) {
log_err!(CoreManager::global().init()); log_err!(CoreManager::global().init());
log::trace!("init system tray"); log::trace!("init system tray");
#[cfg(windows)] #[cfg(any(windows, target_os = "linux"))]
tray::icon::resize_images(crate::utils::help::get_max_scale_factor()); // generate latest cache icon by current scale factor tray::icon::resize_images(crate::utils::help::get_max_scale_factor()); // generate latest cache icon by current scale factor
log_err!(tray::Tray::update_systray(&app.app_handle())); let app_handle = app.app_handle().clone();
app.listen("update_systray", move |_| {
// Fix the GTK should run on main thread issue
let app_handle_clone = app_handle.clone();
log_err!(app_handle.run_on_main_thread(move || {
log_err!(
tray::Tray::update_systray(&app_handle_clone),
"failed to update systray"
);
}));
});
log_err!(app.emit("update_systray", ()));
let silent_start = { Config::verge().data().enable_silent_start }; let silent_start = { Config::verge().data().enable_silent_start };
if !silent_start.unwrap_or(false) { if !silent_start.unwrap_or(false) {
create_window(&app.app_handle()); create_window(app.app_handle());
} }
log_err!(sysopt::Sysopt::global().init_launch()); log_err!(sysopt::Sysopt::global().init_launch());
log_err!(sysopt::Sysopt::global().init_sysproxy()); log_err!(sysopt::Sysopt::global().init_sysproxy());
log_err!(handle::Handle::update_systray_part()); log_err!(handle::Handle::update_systray_part());
log_err!(hotkey::Hotkey::global().init(app.app_handle())); log_err!(hotkey::Hotkey::global().init(app.app_handle().clone()));
// setup jobs // setup jobs
log_err!(JobsManager::global_register()); log_err!(JobsManager::global_register());
@@ -165,8 +177,10 @@ pub fn resolve_reset() {
} }
/// create main window /// create main window
#[tracing_attributes::instrument(skip(app_handle))]
pub fn create_window(app_handle: &AppHandle) { pub fn create_window(app_handle: &AppHandle) {
if let Some(window) = app_handle.get_window("main") { if let Some(window) = app_handle.get_webview_window("main") {
tracing::debug!("main window is already opened, try to show it");
if OPEN_WINDOWS_COUNTER.load(Ordering::Acquire) == 0 { if OPEN_WINDOWS_COUNTER.load(Ordering::Acquire) == 0 {
trace_err!(window.unminimize(), "set win unminimize"); trace_err!(window.unminimize(), "set win unminimize");
trace_err!(window.show(), "set win visible"); trace_err!(window.show(), "set win visible");
@@ -183,10 +197,11 @@ pub fn create_window(app_handle: &AppHandle) {
.unwrap_or(&false) .unwrap_or(&false)
}; };
let mut builder = tauri::window::WindowBuilder::new( tracing::debug!("create main window...");
let mut builder = tauri::WebviewWindowBuilder::new(
app_handle, app_handle,
"main".to_string(), "main".to_string(),
tauri::WindowUrl::App("/".into()), tauri::WebviewUrl::App("/".into()),
) )
.title("Clash Nyanpasu") .title("Clash Nyanpasu")
.fullscreen(false) .fullscreen(false)
@@ -221,6 +236,7 @@ pub fn create_window(app_handle: &AppHandle) {
.decorations(false) .decorations(false)
.transparent(true) .transparent(true)
.visible(false) .visible(false)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.build(); .build();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let win_res = builder let win_res = builder
@@ -229,11 +245,15 @@ pub fn create_window(app_handle: &AppHandle) {
.title_bar_style(tauri::TitleBarStyle::Overlay) .title_bar_style(tauri::TitleBarStyle::Overlay)
.build(); .build();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let win_res = builder.decorations(true).transparent(true).build(); let win_res = builder.decorations(true).transparent(false).build();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn set_controls_and_log_error(app_handle: &tauri::AppHandle, window_name: &str) { fn set_controls_and_log_error(app_handle: &tauri::AppHandle, window_name: &str) {
match app_handle.get_window(window_name).unwrap().ns_window() { match app_handle
.get_webview_window(window_name)
.unwrap()
.ns_window()
{
Ok(raw_window) => { Ok(raw_window) => {
let window_id: cocoa::base::id = raw_window as _; let window_id: cocoa::base::id = raw_window as _;
set_window_controls_pos(window_id, 26.0, 26.0); set_window_controls_pos(window_id, 26.0, 26.0);
@@ -247,8 +267,6 @@ pub fn create_window(app_handle: &AppHandle) {
match win_res { match win_res {
Ok(win) => { Ok(win) => {
use tauri::{PhysicalPosition, PhysicalSize}; use tauri::{PhysicalPosition, PhysicalSize};
#[cfg(windows)]
use window_shadows::set_shadow;
if win_state.is_some() { if win_state.is_some() {
let state = win_state.as_ref().unwrap(); let state = win_state.as_ref().unwrap();
@@ -273,7 +291,7 @@ pub fn create_window(app_handle: &AppHandle) {
} }
} }
#[cfg(windows)] #[cfg(windows)]
trace_err!(set_shadow(&win, true), "set win shadow"); trace_err!(win.set_shadow(true), "set win shadow");
log::trace!("try to calculate the monitor size"); log::trace!("try to calculate the monitor size");
let center = (|| -> Result<bool> { let center = (|| -> Result<bool> {
let center; let center;
@@ -310,7 +328,9 @@ pub fn create_window(app_handle: &AppHandle) {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
win.open_devtools(); if let Some(webview_window) = win.get_webview_window("main") {
webview_window.open_devtools();
}
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -329,17 +349,23 @@ pub fn create_window(app_handle: &AppHandle) {
} }
Err(err) => { Err(err) => {
log::error!(target: "app", "failed to create window, {err}"); log::error!(target: "app", "failed to create window, {err}");
if let Some(win) = app_handle.get_webview_window("main") {
// Cleanup window if failed to create, it's a workaround for tauri bug
log_err!(
win.destroy(),
"occur error when close window while failed to create"
);
}
} }
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
use webview2_com_bridge::{ use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6;
webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6, use windows_core::Interface;
windows::core::Interface,
};
app_handle app_handle
.get_window("main") .get_webview_window("main")
.unwrap() .unwrap()
.with_webview(|webview| unsafe { .with_webview(|webview| unsafe {
let settings = webview let settings = webview
@@ -358,7 +384,7 @@ pub fn create_window(app_handle: &AppHandle) {
/// close main window /// close main window
pub fn close_window(app_handle: &AppHandle) { pub fn close_window(app_handle: &AppHandle) {
if let Some(window) = app_handle.get_window("main") { if let Some(window) = app_handle.get_webview_window("main") {
trace_err!(window.close(), "close window"); trace_err!(window.close(), "close window");
reset_window_open_counter() reset_window_open_counter()
} }
@@ -366,12 +392,12 @@ pub fn close_window(app_handle: &AppHandle) {
/// is window open /// is window open
pub fn is_window_open(app_handle: &AppHandle) -> bool { pub fn is_window_open(app_handle: &AppHandle) -> bool {
app_handle.get_window("main").is_some() app_handle.get_webview_window("main").is_some()
} }
pub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<()> { pub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
let win = app_handle let win = app_handle
.get_window("main") .get_webview_window("main")
.ok_or(anyhow::anyhow!("failed to get window"))?; .ok_or(anyhow::anyhow!("failed to get window"))?;
let current_monitor = win.current_monitor()?; let current_monitor = win.current_monitor()?;
let verge = Config::verge(); let verge = Config::verge();
@@ -412,21 +438,23 @@ pub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<(
/// resolve core version /// resolve core version
// TODO: use enum instead // TODO: use enum instead
pub fn resolve_core_version(core_type: &ClashCore) -> Result<String> { pub async fn resolve_core_version(app_handle: &AppHandle, core_type: &ClashCore) -> Result<String> {
let shell = app_handle.shell();
let core = core_type.clone().to_string(); let core = core_type.clone().to_string();
log::debug!(target: "app", "check config in `{core}`"); log::debug!(target: "app", "check config in `{core}`");
let cmd = match core_type { let cmd = match core_type {
ClashCore::ClashPremium | ClashCore::Mihomo | ClashCore::MihomoAlpha => { ClashCore::ClashPremium | ClashCore::Mihomo | ClashCore::MihomoAlpha => {
Command::new_sidecar(core)?.args(["-v"]) shell.sidecar(core)?.args(["-v"])
} }
ClashCore::ClashRs => Command::new_sidecar(core)?.args(["-V"]), ClashCore::ClashRs => shell.sidecar(core)?.args(["-V"]),
}; };
let out = cmd.output()?; let out = cmd.output().await?;
log::debug!(target: "app", "get core version: {:?}", out);
if !out.status.success() { if !out.status.success() {
return Err(anyhow::anyhow!("failed to get core version")); return Err(anyhow::anyhow!("failed to get core version"));
} }
let out = out.stdout.trim().split(' ').collect::<Vec<&str>>(); let out = String::from_utf8_lossy(&out.stdout);
log::trace!(target: "app", "get core version: {:?}", out);
let out = out.trim().split(' ').collect::<Vec<&str>>();
for item in out { for item in out {
log::debug!(target: "app", "check item: {}", item); log::debug!(target: "app", "check item: {}", item);
if item.starts_with('v') if item.starts_with('v')

View File

@@ -1,118 +1,86 @@
{ {
"package": { "$schema": "../../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Clash Nyanpasu", "bundle": {
"version": "1.6.1" "active": true,
}, "targets": "all",
"build": { "windows": {
"distDir": "./tmp/dist", "certificateThumbprint": null,
"devPath": "http://localhost:3000/", "digestAlgorithm": "sha256",
"beforeDevCommand": "pnpm run web:dev", "timestampUrl": "",
"beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd)" "webviewInstallMode": {
}, "silent": true,
"tauri": { "type": "embedBootstrapper"
"systemTray": {
"iconPath": "icons/win-tray-icon.png",
"iconAsTemplate": true
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "moe.elaina.clash.nyanpasu",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": ["resources"],
"externalBin": [
"sidecar/clash",
"sidecar/mihomo",
"sidecar/mihomo-alpha",
"sidecar/clash-rs",
"sidecar/nyanpasu-service"
],
"copyright": "© 2024 Clash Nyanpasu All Rights Reserved",
"category": "DeveloperTool",
"shortDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
"longDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
"deb": {
"depends": ["openssl"]
}, },
"macOS": { "wix": {
"frameworks": [], "language": ["zh-CN", "en-US", "ru-RU"],
"minimumSystemVersion": "11.0", "template": "./templates/installer.wxs",
"exceptionDomain": "", "fragmentPaths": ["./templates/cleanup.wxs"]
"signingIdentity": null,
"entitlements": null
}, },
"windows": { "nsis": {
"certificateThumbprint": null, "displayLanguageSelector": true,
"digestAlgorithm": "sha256", "installerIcon": "icons/icon.ico",
"timestampUrl": "", "languages": ["SimpChinese", "English", "Russian"],
"webviewInstallMode": { "template": "./templates/installer.nsi",
"silent": true, "installMode": "both"
"type": "embedBootstrapper"
},
"wix": {
"language": ["zh-CN", "en-US", "ru-RU"],
"template": "./templates/installer.wxs",
"license": "../../LICENSE",
"fragmentPaths": ["./templates/cleanup.wxs"]
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English", "Russian"],
"license": "../../LICENSE",
"installMode": "both",
"template": "./templates/installer.nsi"
}
} }
}, },
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": ["resources"],
"externalBin": [
"sidecar/clash",
"sidecar/mihomo",
"sidecar/mihomo-alpha",
"sidecar/clash-rs",
"sidecar/nyanpasu-service"
],
"copyright": "© 2024 Clash Nyanpasu All Rights Reserved",
"category": "DeveloperTool",
"shortDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
"longDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
"macOS": {
"frameworks": [],
"minimumSystemVersion": "11.0",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
},
"linux": {
"deb": {
"depends": ["openssl"]
}
},
"licenseFile": "../../LICENSE",
"createUpdaterArtifacts": "v1Compatible"
},
"build": {
"beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd)",
"frontendDist": "./tmp/dist",
"beforeDevCommand": "pnpm run web:dev",
"devUrl": "http://localhost:3000/"
},
"productName": "Clash Nyanpasu",
"version": "1.6.0",
"identifier": "moe.elaina.clash.nyanpasu",
"plugins": {
"updater": { "updater": {
"active": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK",
"endpoints": [ "endpoints": [
"https://mirror.ghproxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json", "https://mirror.ghproxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json",
"https://gh-proxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json", "https://gh-proxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json",
"https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update.json" "https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update.json"
], ]
"dialog": false, }
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK" },
}, "app": {
"allowlist": {
"shell": {
"all": true
},
"window": {
"all": true
},
"process": {
"all": true
},
"globalShortcut": {
"all": true
},
"notification": {
"all": true
},
"os": {
"all": true
},
"clipboard": {
"all": true
},
"fs": {
"all": true
},
"dialog": {
"all": true
}
},
"windows": [], "windows": [],
"security": { "security": {
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' data: asset: blob: http://localhost:* https:;" "csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' data: asset: blob: http://localhost:* https:; connect-src ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:* wss://*"
} }
} }
} }

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"targets": ["nsis"]
}
}

View File

@@ -481,11 +481,12 @@ FunctionEnd
!macro CheckAllNyanpasuProcesses !macro CheckAllNyanpasuProcesses
!insertmacro CheckNyanpasuProcess "Clash Nyanpasu.exe" "1" !insertmacro CheckNyanpasuProcess "Clash Nyanpasu.exe" "1"
; !insertmacro CheckNyanpasuProcess "clash-verge-service.exe" "2" !insertmacro CheckNyanpasuProcess "clash-nyanpasu.exe" "2"
!insertmacro CheckNyanpasuProcess "clash.exe" "3" ; !insertmacro CheckNyanpasuProcess "clash-verge-service.exe" "3"
!insertmacro CheckNyanpasuProcess "clash-rs.exe" "4" !insertmacro CheckNyanpasuProcess "clash.exe" "4"
!insertmacro CheckNyanpasuProcess "mihomo.exe" "5" !insertmacro CheckNyanpasuProcess "clash-rs.exe" "5"
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "6" !insertmacro CheckNyanpasuProcess "mihomo.exe" "6"
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "7"
!macroend !macroend
; Section CheckProcesses ; Section CheckProcesses

View File

@@ -1,11 +0,0 @@
[package]
name = "webview2-com-bridge"
version = "0.1.0"
repository.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
webview2-com = "0.19"
windows = "0.39"

View File

@@ -1,2 +0,0 @@
pub use webview2_com;
pub use windows;

View File

@@ -11,7 +11,7 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "1.6.0", "@tauri-apps/api": "2.0.0-rc.4",
"ahooks": "3.8.1", "ahooks": "3.8.1",
"ofetch": "1.3.4", "ofetch": "1.3.4",
"react": "18.3.1", "react": "18.3.1",

View File

@@ -1,5 +1,5 @@
import { IPSBResponse } from "@/openapi"; import { IPSBResponse } from "@/openapi";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/core";
import { ManifestVersion } from "./core"; import { ManifestVersion } from "./core";
import { import {
ClashConfig, ClashConfig,
@@ -259,3 +259,7 @@ export const isAppImage = async () => {
export const getServiceInstallPrompt = async () => { export const getServiceInstallPrompt = async () => {
return await invoke<string>("get_service_install_prompt"); return await invoke<string>("get_service_install_prompt");
}; };
export const cleanupProcesses = async () => {
return await invoke<void>("cleanup_processes");
};

View File

@@ -22,7 +22,7 @@
"@mui/material": "5.16.7", "@mui/material": "5.16.7",
"@nyanpasu/interface": "workspace:^", "@nyanpasu/interface": "workspace:^",
"@nyanpasu/ui": "workspace:^", "@nyanpasu/ui": "workspace:^",
"@tauri-apps/api": "1.6.0", "@tauri-apps/api": "2.0.0-rc.4",
"@types/json-schema": "7.0.15", "@types/json-schema": "7.0.15",
"ahooks": "3.8.1", "ahooks": "3.8.1",
"allotment": "1.20.2", "allotment": "1.20.2",
@@ -52,13 +52,21 @@
"@csstools/normalize.css": "12.1.1", "@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.12.0", "@emotion/babel-plugin": "11.12.0",
"@emotion/react": "11.13.3", "@emotion/react": "11.13.3",
"@iconify/json": "2.2.245", "@iconify/json": "2.2.246",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
"@tauri-apps/plugin-dialog": "2.0.0-rc.1",
"@tauri-apps/plugin-fs": "2.0.0-rc.2",
"@tauri-apps/plugin-notification": "2.0.0-rc.1",
"@tauri-apps/plugin-os": "2.0.0-rc.1",
"@tauri-apps/plugin-process": "2.0.0-rc.1",
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
"@tauri-apps/plugin-updater": "2.0.0-rc.2",
"@types/react": "18.3.5", "@types/react": "18.3.5",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "4.3.1", "@vitejs/plugin-react": "4.3.1",
"@vitejs/plugin-react-swc": "3.7.0", "@vitejs/plugin-react-swc": "3.7.0",
"clsx": "2.1.1", "clsx": "2.1.1",
"meta-json-schema": "libnyanpasu/meta-json-schema#main", "meta-json-schema": "1.18.8",
"monaco-yaml": "5.2.2", "monaco-yaml": "5.2.2",
"nanoid": "5.0.7", "nanoid": "5.0.7",
"sass": "1.78.0", "sass": "1.78.0",

View File

@@ -2,7 +2,7 @@ import { Allotment } from "allotment";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { alpha, useTheme } from "@mui/material"; import { alpha, useTheme } from "@mui/material";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import { appWindow } from "@tauri-apps/api/window"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import "allotment/dist/style.css"; import "allotment/dist/style.css";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { LayoutControl } from "../layout/layout-control"; import { LayoutControl } from "../layout/layout-control";
@@ -10,6 +10,8 @@ import styles from "./app-container.module.scss";
import AppDrawer from "./app-drawer"; import AppDrawer from "./app-drawer";
import DrawerContent from "./drawer-content"; import DrawerContent from "./drawer-content";
const appWindow = getCurrentWebviewWindow();
const OS = getSystem(); const OS = getSystem();
export const AppContainer = ({ export const AppContainer = ({

View File

@@ -81,7 +81,7 @@ export const ProxyShortcuts = () => {
} catch (e) { } catch (e) {
message(`Activation failed!`, { message(`Activation failed!`, {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} finally { } finally {
} }

View File

@@ -1,6 +1,6 @@
import { useMemoizedFn } from "ahooks"; import { useMemoizedFn } from "ahooks";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { notification, NotificationType } from "@/utils/notification"; import { notification, NotificationType } from "@/utils/notification";
import { import {
CloseRounded, CloseRounded,
@@ -13,8 +13,10 @@ import {
import { alpha, Button, ButtonProps, useTheme } from "@mui/material"; import { alpha, Button, ButtonProps, useTheme } from "@mui/material";
import { save_window_size_state, useNyanpasu } from "@nyanpasu/interface"; import { save_window_size_state, useNyanpasu } from "@nyanpasu/interface";
import { cn } from "@nyanpasu/ui"; import { cn } from "@nyanpasu/ui";
import { platform, type Platform } from "@tauri-apps/api/os"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { appWindow } from "@tauri-apps/api/window"; import { platform as getPlatform } from "@tauri-apps/plugin-os";
const appWindow = getCurrentWebviewWindow();
const CtrlButton = (props: ButtonProps) => { const CtrlButton = (props: ButtonProps) => {
const { palette } = useTheme(); const { palette } = useTheme();
@@ -35,7 +37,7 @@ export const LayoutControl = ({ className }: { className?: string }) => {
const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu(); const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu();
const [isMaximized, setIsMaximized] = useState(false); const [isMaximized, setIsMaximized] = useState(false);
const [platfrom, setPlatform] = useState<Platform>("win32"); const platform = useRef(getPlatform());
const updateMaximized = async () => { const updateMaximized = async () => {
try { try {
@@ -60,11 +62,6 @@ export const LayoutControl = ({ className }: { className?: string }) => {
// Update the maximized state // Update the maximized state
updateMaximized(); updateMaximized();
// Get the platform
platform().then((platform) => {
setPlatform(() => platform);
});
// Add a resize handler to update the maximized state // Add a resize handler to update the maximized state
const resizeHandler = debounce(updateMaximized, 1000); const resizeHandler = debounce(updateMaximized, 1000);
@@ -112,7 +109,7 @@ export const LayoutControl = ({ className }: { className?: string }) => {
<CtrlButton <CtrlButton
onClick={() => { onClick={() => {
if (platfrom === "win32") { if (platform.current === "windows") {
save_window_size_state().finally(() => { save_window_size_state().finally(() => {
appWindow.close(); appWindow.close();
}); });

View File

@@ -7,7 +7,9 @@ import { themeMode as themeModeAtom } from "@/store";
import { alpha, darken, lighten, Theme, useColorScheme } from "@mui/material"; import { alpha, darken, lighten, Theme, useColorScheme } from "@mui/material";
import { useNyanpasu } from "@nyanpasu/interface"; import { useNyanpasu } from "@nyanpasu/interface";
import { cn, createMDYTheme } from "@nyanpasu/ui"; import { cn, createMDYTheme } from "@nyanpasu/ui";
import { appWindow } from "@tauri-apps/api/window"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
const appWindow = getCurrentWebviewWindow();
const applyRootStyleVar = (mode: "light" | "dark", theme: Theme) => { const applyRootStyleVar = (mode: "light" | "dark", theme: Theme) => {
const root = document.documentElement; const root = document.documentElement;

View File

@@ -54,7 +54,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
} }
} catch (e) { } catch (e) {
message(`Apply error: ${formatError(e)}`, { message(`Apply error: ${formatError(e)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} }

View File

@@ -118,7 +118,7 @@ export const ProfileDialog = ({
const onSubmit = handleSubmit(async (form) => { const onSubmit = handleSubmit(async (form) => {
if (editorHasError()) { if (editorHasError()) {
message("Please fix the error before saving", { message("Please fix the error before saving", {
type: "error", kind: "error",
}); });
return; return;
} }

View File

@@ -122,7 +122,7 @@ export const ProfileItem = memo(function ProfileItem({
: `Error setting profile: \n ${err instanceof Error ? err.message : String(err)}`, : `Error setting profile: \n ${err instanceof Error ? err.message : String(err)}`,
{ {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}, },
); );
} finally { } finally {
@@ -161,7 +161,7 @@ export const ProfileItem = memo(function ProfileItem({
} catch (err) { } catch (err) {
message(`Delete failed: \n ${JSON.stringify(err)}`, { message(`Delete failed: \n ${JSON.stringify(err)}`, {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} }
}); });

View File

@@ -15,7 +15,7 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useClash } from "@nyanpasu/interface"; import { useClash } from "@nyanpasu/interface";
import { readText } from "@tauri-apps/api/clipboard"; import { readText } from "@tauri-apps/plugin-clipboard-manager";
export const QuickImport = () => { export const QuickImport = () => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import LoadingButton from "@mui/lab/LoadingButton"; import LoadingButton from "@mui/lab/LoadingButton";
import { open } from "@tauri-apps/api/dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/api/fs"; import { readTextFile } from "@tauri-apps/plugin-fs";
const isWin = getSystem() === "windows"; const isWin = getSystem() === "windows";

View File

@@ -103,7 +103,7 @@ export const ScriptDialog = ({
const onSubmit = form.handleSubmit(async (data) => { const onSubmit = form.handleSubmit(async (data) => {
if (editorHasError()) { if (editorHasError()) {
message("Please fix the error before submitting", { message("Please fix the error before submitting", {
type: "error", kind: "error",
}); });
return; return;
} }

View File

@@ -27,7 +27,7 @@ export const ProxiesProvider = ({ provider }: ProxiesProviderProps) => {
await updateProxiesProviders(provider.name); await updateProxiesProviders(provider.name);
} catch (e) { } catch (e) {
message(`Update ${provider.name} failed.\n${String(e)}`, { message(`Update ${provider.name} failed.\n${String(e)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} finally { } finally {

View File

@@ -26,7 +26,7 @@ export default function RulesProvider({ provider }: RulesProviderProps) {
await updateRulesProviders(provider.name); await updateRulesProviders(provider.name);
} catch (e) { } catch (e) {
message(`Update ${provider.name} failed.\n${String(e)}`, { message(`Update ${provider.name} failed.\n${String(e)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} finally { } finally {

View File

@@ -16,7 +16,7 @@ export const UpdateProviders = () => {
const handleProviderUpdate = useLockFn(async () => { const handleProviderUpdate = useLockFn(async () => {
if (!getRulesProviders.data) { if (!getRulesProviders.data) {
message(`No Providers.`, { message(`No Providers.`, {
type: "info", kind: "info",
title: t("Info"), title: t("Info"),
}); });
@@ -35,7 +35,7 @@ export const UpdateProviders = () => {
); );
} catch (e) { } catch (e) {
message(`Update all failed.\n${String(e)}`, { message(`Update all failed.\n${String(e)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} finally { } finally {

View File

@@ -16,7 +16,7 @@ export const UpdateProxiesProviders = () => {
const handleProviderUpdate = useLockFn(async () => { const handleProviderUpdate = useLockFn(async () => {
if (!getProxiesProviders.data) { if (!getProxiesProviders.data) {
message(`No Providers.`, { message(`No Providers.`, {
type: "info", kind: "info",
title: t("Info"), title: t("Info"),
}); });
@@ -35,7 +35,7 @@ export const UpdateProxiesProviders = () => {
); );
} catch (e) { } catch (e) {
message(`Update all failed.\n${String(e)}`, { message(`Update all failed.\n${String(e)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} finally { } finally {

View File

@@ -6,6 +6,7 @@ import ClashRs from "@/assets/image/core/clash-rs.png";
import ClashMeta from "@/assets/image/core/clash.meta.png"; import ClashMeta from "@/assets/image/core/clash.meta.png";
import Clash from "@/assets/image/core/clash.png"; import Clash from "@/assets/image/core/clash.png";
import { formatError } from "@/utils"; import { formatError } from "@/utils";
import { message } from "@/utils/notification";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import FiberManualRecord from "@mui/icons-material/FiberManualRecord"; import FiberManualRecord from "@mui/icons-material/FiberManualRecord";
import Update from "@mui/icons-material/Update"; import Update from "@mui/icons-material/Update";
@@ -22,7 +23,6 @@ import {
useNyanpasu, useNyanpasu,
} from "@nyanpasu/interface"; } from "@nyanpasu/interface";
import { cleanDeepClickEvent, cn } from "@nyanpasu/ui"; import { cleanDeepClickEvent, cn } from "@nyanpasu/ui";
import { message } from "@tauri-apps/api/dialog";
export const getImage = (core: ClashCore) => { export const getImage = (core: ClashCore) => {
switch (core) { switch (core) {
@@ -175,12 +175,12 @@ export const ClashCoreItem = ({
getClashCore.mutate(); getClashCore.mutate();
message(`Successfully update core ${data.name}`, { message(`Successfully update core ${data.name}`, {
type: "info", kind: "info",
title: t("Success"), title: t("Success"),
}); });
} catch (e) { } catch (e) {
message(`Update failed. ${formatError(e)}`, { message(`Update failed. ${formatError(e)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} finally { } finally {

View File

@@ -11,7 +11,7 @@ import {
setTrayIcon as setTrayIconCall, setTrayIcon as setTrayIconCall,
} from "@nyanpasu/interface"; } from "@nyanpasu/interface";
import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui"; import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui";
import { open } from "@tauri-apps/api/dialog"; import { open } from "@tauri-apps/plugin-dialog";
function TrayIconItem({ mode }: { mode: "system_proxy" | "tun" | "normal" }) { function TrayIconItem({ mode }: { mode: "system_proxy" | "tun" | "normal" }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -53,7 +53,7 @@ function TrayIconItem({ mode }: { mode: "system_proxy" | "tun" | "normal" }) {
return await setTrayIconCall(mode, selected); return await setTrayIconCall(mode, selected);
} catch (e) { } catch (e) {
message(formatError(e), { message(formatError(e), {
type: "error", kind: "error",
}); });
} finally { } finally {
setTs(Date.now()); setTs(Date.now());

View File

@@ -24,7 +24,7 @@ export const SettingClashBase = () => {
} catch (e) { } catch (e) {
message(`Failed to Open UWP Tools.\n${JSON.stringify(e)}`, { message(`Failed to Open UWP Tools.\n${JSON.stringify(e)}`, {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} }
}; };

View File

@@ -27,7 +27,7 @@ export const SettingClashCore = () => {
} = useNyanpasu({ } = useNyanpasu({
onLatestCoreError: (error) => { onLatestCoreError: (error) => {
message(`Fetch latest core failed: ${formatError(error)}`, { message(`Fetch latest core failed: ${formatError(error)}`, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
}, },
@@ -57,7 +57,7 @@ export const SettingClashCore = () => {
await setClashCore(core); await setClashCore(core);
message(`Successfully switch to ${core}`, { message(`Successfully switch to ${core}`, {
type: "info", kind: "info",
title: t("Success"), title: t("Success"),
}); });
} catch (e) { } catch (e) {
@@ -66,7 +66,7 @@ export const SettingClashCore = () => {
e instanceof Error ? e.message : String(e) e instanceof Error ? e.message : String(e)
}`, }`,
{ {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}, },
); );
@@ -80,14 +80,18 @@ export const SettingClashCore = () => {
await restartSidecar(); await restartSidecar();
message(t("Successfully restart core"), { message(t("Successfully restart core"), {
type: "info", kind: "info",
title: t("Success"), title: t("Success"),
}); });
} catch (e) { } catch (e) {
message("Restart failed, please check log.", { message(
type: "error", "Restart failed, full detailed please check the log.\n\nError:" +
title: t("Error"), formatError(e),
}); {
kind: "error",
title: t("Error"),
},
);
} }
}; };
@@ -96,7 +100,7 @@ export const SettingClashCore = () => {
await getLatestCore.mutate(); await getLatestCore.mutate();
} catch (e) { } catch (e) {
message("Fetch failed, please check your internet connection.", { message("Fetch failed, please check your internet connection.", {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
} }

View File

@@ -83,7 +83,7 @@ export const SettingClashExternal = () => {
} catch (e) { } catch (e) {
message(JSON.stringify(e), { message(JSON.stringify(e), {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} finally { } finally {
setExpand(false); setExpand(false);

View File

@@ -45,12 +45,12 @@ export const SettingClashPort = () => {
} catch (e) { } catch (e) {
message(JSON.stringify(e), { message(JSON.stringify(e), {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} finally { } finally {
message(t("After restart to take effect"), { message(t("After restart to take effect"), {
title: t("Success"), title: t("Success"),
type: "info", kind: "info",
}); });
} }
}} }}

View File

@@ -14,7 +14,7 @@ import {
setCustomAppDir, setCustomAppDir,
} from "@nyanpasu/interface"; } from "@nyanpasu/interface";
import { BaseCard } from "@nyanpasu/ui"; import { BaseCard } from "@nyanpasu/ui";
import { open } from "@tauri-apps/api/dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { PaperButton } from "./modules/nyanpasu-path"; import { PaperButton } from "./modules/nyanpasu-path";
export const SettingNyanpasuPath = () => { export const SettingNyanpasuPath = () => {
@@ -36,7 +36,7 @@ export const SettingNyanpasuPath = () => {
if (Array.isArray(selected)) { if (Array.isArray(selected)) {
message(t("Multiple directories are not supported"), { message(t("Multiple directories are not supported"), {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
return; return;
@@ -46,7 +46,7 @@ export const SettingNyanpasuPath = () => {
message(t("App directory changed successfully"), { message(t("App directory changed successfully"), {
title: t("Success"), title: t("Success"),
type: "error", kind: "error",
}); });
await sleep(1000); await sleep(1000);
@@ -55,7 +55,7 @@ export const SettingNyanpasuPath = () => {
} catch (e) { } catch (e) {
message(`Migration failed! ${JSON.stringify(e)}`, { message(`Migration failed! ${JSON.stringify(e)}`, {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} }
}); });

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LogoSvg from "@/assets/image/logo.svg?react"; import LogoSvg from "@/assets/image/logo.svg?react";
import { useUpdaterPlatformSupported } from "@/hooks/use-updater"; import { useUpdaterPlatformSupported } from "@/hooks/use-updater";
import { UpdaterManifestAtom } from "@/store/updater"; import { UpdaterInstanceAtom } from "@/store/updater";
import { formatError } from "@/utils"; import { formatError } from "@/utils";
import { message } from "@/utils/notification"; import { message } from "@/utils/notification";
import LoadingButton from "@mui/lab/LoadingButton"; import LoadingButton from "@mui/lab/LoadingButton";
@@ -20,7 +20,7 @@ import {
import { useNyanpasu } from "@nyanpasu/interface"; import { useNyanpasu } from "@nyanpasu/interface";
import { BaseCard } from "@nyanpasu/ui"; import { BaseCard } from "@nyanpasu/ui";
import { version } from "@root/package.json"; import { version } from "@root/package.json";
import { checkUpdate } from "@tauri-apps/api/updater"; import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { LabelSwitch } from "./modules/clash-field"; import { LabelSwitch } from "./modules/clash-field";
const AutoCheckUpdate = () => { const AutoCheckUpdate = () => {
@@ -48,28 +48,28 @@ export const SettingNyanpasuVersion = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const setUpdaterManifest = useSetAtom(UpdaterManifestAtom); const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom);
const isPlatformSupported = useUpdaterPlatformSupported(); const isPlatformSupported = useUpdaterPlatformSupported();
const onCheckUpdate = useLockFn(async () => { const onCheckUpdate = useLockFn(async () => {
try { try {
setLoading(true); setLoading(true);
const info = await checkUpdate(); const update = await checkUpdate();
if (!info?.shouldUpdate) { if (!update?.available) {
message(t("No update available."), { message(t("No update available."), {
title: t("Info"), title: t("Info"),
type: "info", kind: "info",
}); });
} else { } else {
setUpdaterManifest(info.manifest || null); setUpdaterInstance(update || null);
} }
} catch (e) { } catch (e) {
message( message(
`Update check failed. Please verify your network connection.\n\n${formatError(e)}`, `Update check failed. Please verify your network connection.\n\n${formatError(e)}`,
{ {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}, },
); );
} finally { } finally {

View File

@@ -44,7 +44,7 @@ export const SettingSystemProxy = () => {
} catch (e) { } catch (e) {
message(`Activation failed!`, { message(`Activation failed!`, {
title: t("Error"), title: t("Error"),
type: "error", kind: "error",
}); });
} finally { } finally {
loading[key] = false; loading[key] = false;

View File

@@ -74,7 +74,7 @@ export const SettingSystemService = () => {
}: ${formatError(e)}`; }: ${formatError(e)}`;
message(errorMessage, { message(errorMessage, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
// If install failed show a prompt to user to install the service manually // If install failed show a prompt to user to install the service manually
@@ -109,7 +109,7 @@ export const SettingSystemService = () => {
: `Start failed: ${formatError(e)}`; : `Start failed: ${formatError(e)}`;
message(errorMessage, { message(errorMessage, {
type: "error", kind: "error",
title: t("Error"), title: t("Error"),
}); });
// If start failed show a prompt to user to start the service manually // If start failed show a prompt to user to start the service manually

View File

@@ -1,12 +1,12 @@
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { lazy, Suspense, useState } from "react"; import { lazy, Suspense, useState } from "react";
import { UpdaterManifestAtom } from "@/store/updater"; import { UpdaterInstanceAtom } from "@/store/updater";
const UpdaterDialog = lazy(() => import("./updater-dialog")); const UpdaterDialog = lazy(() => import("./updater-dialog"));
export const UpdaterDialogWrapper = () => { export const UpdaterDialogWrapper = () => {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const [manifest, setManifest] = useAtom(UpdaterManifestAtom); const [manifest, setManifest] = useAtom(UpdaterInstanceAtom);
if (!manifest) return null; if (!manifest) return null;
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
@@ -16,7 +16,7 @@ export const UpdaterDialogWrapper = () => {
setOpen(false); setOpen(false);
setManifest(null); setManifest(null);
}} }}
manifest={manifest} update={manifest}
/> />
</Suspense> </Suspense>
); );

View File

@@ -1,44 +1,62 @@
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { lazy, Suspense } from "react"; import { lazy, Suspense, useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UpdaterIgnoredAtom } from "@/store/updater"; import { UpdaterIgnoredAtom } from "@/store/updater";
import { formatError } from "@/utils"; import { formatError } from "@/utils";
import { message } from "@/utils/notification"; import { message } from "@/utils/notification";
import { Button } from "@mui/material"; import { Button, LinearProgress } from "@mui/material";
import { cleanupProcesses, openThat } from "@nyanpasu/interface";
import { BaseDialog, BaseDialogProps, cn } from "@nyanpasu/ui"; import { BaseDialog, BaseDialogProps, cn } from "@nyanpasu/ui";
import { relaunch } from "@tauri-apps/api/process"; import { relaunch } from "@tauri-apps/plugin-process";
import { open as openThat } from "@tauri-apps/api/shell"; import { DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
import { installUpdate, type UpdateManifest } from "@tauri-apps/api/updater";
import styles from "./updater-dialog.module.scss"; import styles from "./updater-dialog.module.scss";
const Markdown = lazy(() => import("react-markdown")); const Markdown = lazy(() => import("react-markdown"));
export interface UpdaterDialogProps extends Omit<BaseDialogProps, "title"> { export interface UpdaterDialogProps extends Omit<BaseDialogProps, "title"> {
manifest: UpdateManifest; update: Update;
} }
export default function UpdaterDialog({ export default function UpdaterDialog({
open, open,
manifest, update,
onClose, onClose,
...others ...others
}: UpdaterDialogProps) { }: UpdaterDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const setUpdaterIgnore = useSetAtom(UpdaterIgnoredAtom); const setUpdaterIgnore = useSetAtom(UpdaterIgnoredAtom);
const [contentLength, setContentLength] = useState(0);
const [contentDownloaded, setContentDownloaded] = useState(0);
const progress =
contentDownloaded && contentLength
? (contentDownloaded / contentLength) * 100
: 0;
const onDownloadEvent = useCallback((e: DownloadEvent) => {
switch (e.event) {
case "Started":
setContentLength(e.data.contentLength || 0);
break;
case "Progress":
setContentDownloaded((prev) => prev + e.data.chunkLength);
break;
}
}, []);
const handleUpdate = useLockFn(async () => { const handleUpdate = useLockFn(async () => {
try { try {
// Install the update. This will also restart the app on Windows! // Install the update. This will also restart the app on Windows!
await installUpdate(); await update.download(onDownloadEvent);
await cleanupProcesses();
// cleanup and stop core
await update.install();
// On macOS and Linux you will need to restart the app manually. // On macOS and Linux you will need to restart the app manually.
// You could use this step to display another confirmation dialog. // You could use this step to display another confirmation dialog.
await relaunch(); await relaunch();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
message(formatError(e), { type: "error", title: t("Error") }); message(formatError(e), { kind: "error", title: t("Error") });
} }
}); });
@@ -48,7 +66,7 @@ export default function UpdaterDialog({
title={t("updater.title")} title={t("updater.title")}
open={open} open={open}
onClose={() => { onClose={() => {
setUpdaterIgnore(manifest.version); // TODO: control this behavior setUpdaterIgnore(update.version); // TODO: control this behavior
onClose?.(); onClose?.();
}} }}
onOk={handleUpdate} onOk={handleUpdate}
@@ -64,9 +82,9 @@ export default function UpdaterDialog({
> >
<div className="flex items-center justify-between px-2 py-2"> <div className="flex items-center justify-between px-2 py-2">
<div className="flex gap-3"> <div className="flex gap-3">
<span className="text-xl font-bold">{manifest.version}</span> <span className="text-xl font-bold">{update.version}</span>
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500">
{dayjs(manifest.date, "YYYY-MM-DD H:mm:ss Z").format( {dayjs(update.date, "YYYY-MM-DD H:mm:ss Z").format(
"YYYY-MM-DD HH:mm:ss", "YYYY-MM-DD HH:mm:ss",
)} )}
</span> </span>
@@ -76,7 +94,7 @@ export default function UpdaterDialog({
size="small" size="small"
onClick={() => { onClick={() => {
openThat( openThat(
`https://github.com/LibNyanpasu/clash-nyanpasu/releases/tag/v${manifest.version}`, `https://github.com/LibNyanpasu/clash-nyanpasu/releases/tag/v${update.version}`,
); );
}} }}
> >
@@ -106,10 +124,22 @@ export default function UpdaterDialog({
}, },
}} }}
> >
{manifest.body || "New version available."} {update.body || "New version available."}
</Markdown> </Markdown>
</Suspense> </Suspense>
</div> </div>
{contentLength && (
<div className="mt-2 flex items-center gap-2">
<LinearProgress
className="flex-1"
variant="determinate"
value={progress}
/>
<span className="text-xs text-slate-500">
{progress.toFixed(2)}%
</span>
</div>
)}
</div> </div>
</BaseDialog> </BaseDialog>
); );

View File

@@ -1,9 +1,9 @@
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { OS } from "@/consts"; import { OS } from "@/consts";
import { UpdaterIgnoredAtom, UpdaterManifestAtom } from "@/store/updater"; import { UpdaterIgnoredAtom, UpdaterInstanceAtom } from "@/store/updater";
import { useNyanpasu } from "@nyanpasu/interface"; import { useNyanpasu } from "@nyanpasu/interface";
import { checkUpdate } from "@tauri-apps/api/updater"; import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useIsAppImage } from "./use-consts"; import { useIsAppImage } from "./use-consts";
export function useUpdaterPlatformSupported() { export function useUpdaterPlatformSupported() {
@@ -26,15 +26,15 @@ export function useUpdaterPlatformSupported() {
export default function useUpdater() { export default function useUpdater() {
const { nyanpasuConfig } = useNyanpasu(); const { nyanpasuConfig } = useNyanpasu();
const updaterIgnored = useAtomValue(UpdaterIgnoredAtom); const updaterIgnored = useAtomValue(UpdaterIgnoredAtom);
const setUpdaterManifest = useSetAtom(UpdaterManifestAtom); const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom);
const isPlatformSupported = useUpdaterPlatformSupported(); const isPlatformSupported = useUpdaterPlatformSupported();
useEffect(() => { useEffect(() => {
const run = async () => { const run = async () => {
if (nyanpasuConfig?.enable_auto_check_update && isPlatformSupported) { if (nyanpasuConfig?.enable_auto_check_update && isPlatformSupported) {
const info = await checkUpdate(); const updater = await checkUpdate();
if (info?.shouldUpdate && updaterIgnored !== info.manifest?.version) { if (updater?.available && updaterIgnored !== updater?.version) {
setUpdaterManifest(info.manifest || null); setUpdaterInstance(updater || null);
} }
} }
}; };
@@ -42,7 +42,7 @@ export default function useUpdater() {
}, [ }, [
isPlatformSupported, isPlatformSupported,
nyanpasuConfig?.enable_auto_check_update, nyanpasuConfig?.enable_auto_check_update,
setUpdaterManifest, setUpdaterInstance,
updaterIgnored, updaterIgnored,
]); ]);
} }

Some files were not shown because too many files have changed in this diff Show More