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 Sat Sep 7 20:31:47 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
}
func (pp *proxySetProvider) Count() int {
return len(pp.proxies)
}
func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
@@ -126,7 +130,7 @@ func (pp *proxySetProvider) getSubscriptionInfo() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
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 {
return
}
@@ -267,6 +271,10 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Count() int {
return len(cp.proxies)
}
func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}

View File

@@ -84,7 +84,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
if cfg.addrReuse {
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)
} else {
if cfg.interfaceName != "" {
@@ -148,7 +148,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
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)
} else {
if opt.interfaceName != "" {

View File

@@ -7,11 +7,11 @@ import (
)
// 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
// DefaultSocketHook
// never change type traits because it's used in CFMA
// never change type traits because it's used in CMFA
var DefaultSocketHook SocketControl
func socketHookToToDialer(dialer *net.Dialer) {

View File

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

View File

@@ -6,8 +6,10 @@ import (
"io"
"net/http"
"os"
"sync"
"time"
"github.com/metacubex/mihomo/common/atomic"
mihomoHttp "github.com/metacubex/mihomo/component/http"
"github.com/metacubex/mihomo/component/mmdb"
C "github.com/metacubex/mihomo/constant"
@@ -18,12 +20,79 @@ var (
initGeoSite bool
initGeoIP int
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 {
geoSiteEnable.Store(true)
initGeoSiteMutex.Lock()
defer initGeoSiteMutex.Unlock()
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
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())
}
log.Infoln("Download GeoSite.dat finish")
@@ -35,7 +104,7 @@ func InitGeoSite() error {
if err := os.Remove(C.Path.GeoSite()); err != nil {
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())
}
}
@@ -44,49 +113,14 @@ func InitGeoSite() error {
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 {
if C.GeodataMode {
geoIpEnable.Store(true)
initGeoIPMutex.Lock()
defer initGeoIPMutex.Unlock()
if GeodataMode() {
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
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())
}
log.Infoln("Download GeoIP.dat finish")
@@ -99,7 +133,7 @@ func InitGeoIP() error {
if err := os.Remove(C.Path.GeoIP()); err != nil {
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())
}
}
@@ -110,7 +144,7 @@ func InitGeoIP() error {
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
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())
}
}
@@ -121,7 +155,7 @@ func InitGeoIP() error {
if err := os.Remove(C.Path.MMDB()); err != nil {
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())
}
}
@@ -131,9 +165,12 @@ func InitGeoIP() error {
}
func InitASN() error {
asnEnable.Store(true)
initASNMutex.Lock()
defer initASNMutex.Unlock()
if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) {
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())
}
log.Infoln("Download ASN.mmdb finish")
@@ -145,7 +182,7 @@ func InitASN() error {
if err := os.Remove(C.Path.ASN()); err != nil {
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())
}
}
@@ -153,3 +190,15 @@ func InitASN() error {
}
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 (
geoMode bool
AutoUpdate bool
UpdateInterval int
geoLoaderName = "memconservative"
geoSiteMatcher = "succinct"
)
@@ -25,14 +23,6 @@ func GeodataMode() bool {
return geoMode
}
func GeoAutoUpdate() bool {
return AutoUpdate
}
func GeoUpdateInterval() int {
return UpdateInterval
}
func LoaderName() string {
return geoLoaderName
}
@@ -44,12 +34,6 @@ func SiteMatcherName() string {
func SetGeodataMode(newGeodataMode bool) {
geoMode = newGeodataMode
}
func SetGeoAutoUpdate(newAutoUpdate bool) {
AutoUpdate = newAutoUpdate
}
func SetGeoUpdateInterval(newGeoUpdateInterval int) {
UpdateInterval = newGeoUpdateInterval
}
func SetLoader(newLoader string) {
if newLoader == "memc" {
@@ -209,8 +193,11 @@ func LoadGeoIPMatcher(country string) (router.IPMatcher, error) {
return matcher, nil
}
func ClearCache() {
func ClearGeoSiteCache() {
loadGeoSiteMatcherListSF.Reset()
loadGeoSiteMatcherSF.Reset()
}
func ClearGeoIPCache() {
loadGeoIPMatcherSF.Reset()
}

View File

@@ -12,10 +12,21 @@ import (
"time"
"github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"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) {
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 {
req.Header.Set("User-Agent", C.UA)
req.Header.Set("User-Agent", UA())
}
if err != nil {

View File

@@ -1,15 +1,9 @@
package mmdb
import (
"context"
"io"
"net/http"
"os"
"sync"
"time"
mihomoOnce "github.com/metacubex/mihomo/common/once"
mihomoHttp "github.com/metacubex/mihomo/component/http"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
@@ -25,26 +19,26 @@ const (
)
var (
IPreader IPReader
ASNreader ASNReader
IPonce sync.Once
ASNonce sync.Once
ipReader IPReader
asnReader ASNReader
ipOnce sync.Once
asnOnce sync.Once
)
func LoadFromBytes(buffer []byte) {
IPonce.Do(func() {
ipOnce.Do(func() {
mmdb, err := maxminddb.FromBytes(buffer)
if err != nil {
log.Fatalln("Can't load mmdb: %s", err.Error())
}
IPreader = IPReader{Reader: mmdb}
ipReader = IPReader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType {
case "sing-geoip":
IPreader.databaseType = typeSing
ipReader.databaseType = typeSing
case "Meta-geoip0":
IPreader.databaseType = typeMetaV0
ipReader.databaseType = typeMetaV0
default:
IPreader.databaseType = typeMaxmind
ipReader.databaseType = typeMaxmind
}
})
}
@@ -58,83 +52,45 @@ func Verify(path string) bool {
}
func IPInstance() IPReader {
IPonce.Do(func() {
ipOnce.Do(func() {
mmdbPath := C.Path.MMDB()
log.Infoln("Load MMDB file: %s", mmdbPath)
mmdb, err := maxminddb.Open(mmdbPath)
if err != nil {
log.Fatalln("Can't load MMDB: %s", err.Error())
}
IPreader = IPReader{Reader: mmdb}
ipReader = IPReader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType {
case "sing-geoip":
IPreader.databaseType = typeSing
ipReader.databaseType = typeSing
case "Meta-geoip0":
IPreader.databaseType = typeMetaV0
ipReader.databaseType = typeMetaV0
default:
IPreader.databaseType = typeMaxmind
ipReader.databaseType = typeMaxmind
}
})
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
return ipReader
}
func ASNInstance() ASNReader {
ASNonce.Do(func() {
asnOnce.Do(func() {
ASNPath := C.Path.ASN()
log.Infoln("Load ASN file: %s", ASNPath)
asn, err := maxminddb.Open(ASNPath)
if err != nil {
log.Fatalln("Can't load ASN: %s", err.Error())
}
ASNreader = ASNReader{Reader: asn}
asnReader = ASNReader{Reader: asn}
})
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
return asnReader
}
func ReloadIP() {
mihomoOnce.Reset(&IPonce)
mihomoOnce.Reset(&ipOnce)
}
func ReloadASN() {
mihomoOnce.Reset(&ASNonce)
mihomoOnce.Reset(&asnOnce)
}

View File

@@ -8,11 +8,11 @@ func InstallOverride(override *maxminddb.Reader) {
newReader := IPReader{Reader: override}
switch override.Metadata.DatabaseType {
case "sing-geoip":
IPreader.databaseType = typeSing
ipReader.databaseType = typeSing
case "Meta-geoip0":
IPreader.databaseType = typeMetaV0
ipReader.databaseType = typeMetaV0
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
// 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)
// DefaultPackageNameResolver
// never change type traits because it's used in CFMA
// never change type traits because it's used in CMFA
var DefaultPackageNameResolver PackageNameResolver
func FindPackageName(metadata *C.Metadata) (string, error) {

View File

@@ -237,7 +237,7 @@ const MaxPackageFileSize = 32 * 1024 * 1024
func downloadPackageFile() (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
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 {
return fmt.Errorf("http request failed: %w", err)
}
@@ -418,7 +418,7 @@ func copyFile(src, dst string) error {
func getLatestVersion() (version string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
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 {
return "", fmt.Errorf("get Latest Version fail: %w", err)
}

View File

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

View File

@@ -9,7 +9,6 @@ import (
"time"
mihomoHttp "github.com/metacubex/mihomo/component/http"
C "github.com/metacubex/mihomo/constant"
"golang.org/x/exp/constraints"
)
@@ -17,7 +16,7 @@ import (
func downloadForBytes(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
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 {
return nil, err
}

View File

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

View File

@@ -955,12 +955,12 @@ rule-providers:
# 对于behavior=domain:
# - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml 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:
# - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml 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
url: "url"

View File

@@ -20,7 +20,7 @@ require (
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
github.com/metacubex/chacha v0.1.0
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/sing-quic v0.0.0-20240827003841-cd97758ed8b4
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/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/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58 h1:T6OxROLZBr9SOQxN5TzUslv81hEREy/dEgaUKVjaG7U=
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 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
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/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
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/dialer"
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/profile"
"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) {
mux.Lock()
defer mux.Unlock()
log.SetLevel(cfg.General.LogLevel)
tunnel.OnSuspend()
@@ -115,8 +117,6 @@ func ApplyConfig(cfg *config.Config, force bool) {
tunnel.OnRunning()
hcCompatibleProvider(cfg.Providers)
initExternalUI()
log.SetLevel(cfg.General.LogLevel)
}
func initInnerTcp() {
@@ -157,13 +157,13 @@ func GetGeneral() *config.General {
Interface: dialer.DefaultInterface.Load(),
RoutingMark: int(dialer.DefaultRoutingMark.Load()),
GeoXUrl: config.GeoXUrl{
GeoIp: C.GeoIpUrl,
Mmdb: C.MmdbUrl,
ASN: C.ASNUrl,
GeoSite: C.GeoSiteUrl,
GeoIp: G.GeoIpUrl(),
Mmdb: G.MmdbUrl(),
ASN: G.ASNUrl(),
GeoSite: G.GeoSiteUrl(),
},
GeoAutoUpdate: G.GeoAutoUpdate(),
GeoUpdateInterval: G.GeoUpdateInterval(),
GeoAutoUpdate: updater.GeoAutoUpdate(),
GeoUpdateInterval: updater.GeoUpdateInterval(),
GeodataMode: G.GeodataMode(),
GeodataLoader: G.LoaderName(),
GeositeMatcher: G.SiteMatcherName(),
@@ -171,7 +171,7 @@ func GetGeneral() *config.General {
FindProcessMode: tunnel.FindProcessMode(),
Sniffing: tunnel.IsSniffing(),
GlobalClientFingerprint: tlsC.GetGlobalFingerprint(),
GlobalUA: C.UA,
GlobalUA: mihomoHttp.UA(),
}
return general

View File

@@ -37,7 +37,7 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
}
// 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) {
getAuth := authStore.Authenticator
if !authenticate {

View File

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

View File

@@ -46,7 +46,7 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) {
return g.isLan(ip), g.adapter
}
if C.GeodataMode {
if geodata.GeodataMode() {
if g.isSourceIP {
if slices.Contains(metadata.SrcGeoIP, g.country) {
return true, g.adapter
@@ -102,7 +102,7 @@ func (g *GEOIP) MatchIp(ip netip.Addr) bool {
return g.isLan(ip)
}
if C.GeodataMode {
if geodata.GeodataMode() {
matcher, err := g.getIPMatcher()
if err != nil {
return false
@@ -124,7 +124,7 @@ func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool {
return false
}
if C.GeodataMode {
if geodata.GeodataMode() {
matcher, err := g.getIPMatcher()
if err != nil {
return false
@@ -170,7 +170,7 @@ func (g *GEOIP) GetCountry() string {
}
func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) {
if C.GeodataMode {
if geodata.GeodataMode() {
return g.getIPMatcher()
}
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) {
if err := geodata.InitGeoIP(); err != nil {
log.Errorln("can't initial GeoIP: %s", err)
return nil, err
}
country = strings.ToLower(country)
geoip := &GEOIP{
@@ -206,11 +202,17 @@ func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP,
noResolveIP: noResolveIP,
isSourceIP: isSrc,
}
if !C.GeodataMode || country == "lan" {
if country == "lan" {
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
if err != nil {
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) {
C.ASNEnable = true
if err := geodata.InitASN(); err != nil {
log.Errorln("can't initial ASN: %s", err)
return nil, err

View File

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

View File

@@ -62,7 +62,7 @@ jobs:
- name: Setup Toolchain
run: |
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
with:
workspaces: "./backend/"
@@ -97,8 +97,8 @@ jobs:
if: ${{ inputs.aarch64 == false }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
with:
tagName: ${{ inputs.tag }}
@@ -113,8 +113,8 @@ jobs:
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: 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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
NODE_OPTIONS: "--max_old_space_size=4096"
with:
@@ -125,8 +125,8 @@ jobs:
env:
TAG_NAME: ${{ inputs.tag }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
NODE_OPTIONS: "--max_old_space_size=4096"
run: |

View File

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

View File

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

View File

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

View File

@@ -8,3 +8,4 @@ pnpm-lock.yaml
*.wxs
frontend/nyanpasu/src/router.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]
resolver = "2"
members = ["tauri", "webview2-com-bridge", "boa_utils"]
members = ["tauri", "boa_utils"]
[workspace.package]
repository = "https://github.com/keiko233/clash-nyanpasu.git"
@@ -11,7 +11,7 @@ authors = ["zzzgydi", "keiko233"]
[workspace.dependencies]
thiserror = "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]
panic = "unwind"

View File

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

View File

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

View File

@@ -9,8 +9,12 @@ default-run = "clash-nyanpasu"
edition = { workspace = true }
build = "build.rs"
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "1", features = [] }
tauri-build = { version = "2.0.0-rc", features = [] }
serde = "1"
simd-json = "0.13"
chrono = "0.4"
@@ -46,26 +50,10 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
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"
mime = "0.3"
bincode = "1"
bytes = { version = "1", features = ["serde"] }
wry = { version = "0.24.6" }
semver = "1.0"
zip = "2.0.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"] }
rust-i18n = "3"
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"] }
tracing = { workspace = true }
tracing-attributes = "0.1"
@@ -96,7 +88,6 @@ tracing-log = { version = "0.2" }
tracing-appender = { version = "0.2", features = ["parking_lot"] }
base64 = "0.22"
single-instance = "0.3.3"
tauri-plugin-deep-link = { path = "../tauri-plugin-deep-link", version = "0.1.2" }
uuid = "1.7.0"
image = "0.25.0"
fast_image_resize = "4"
@@ -138,12 +129,33 @@ mlua = { version = "0.9", features = [
] }
enumflags2 = "0.7"
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]
openssl = { version = "0.10", features = ["vendored"] }
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"
cocoa = "0.26.0"
objc = "0.2.7"
[target.'cfg(windows)'.dependencies]
@@ -153,34 +165,8 @@ windows-sys = { version = "0.59", features = [
"Win32_System_LibraryLoader",
"Win32_System_SystemInformation",
] }
webview2-com-bridge = { path = "../webview2-com-bridge" }
[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",
]
windows-core = "0.58.0"
webview2-com = "0.33"
[features]
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",
"package": {
"version": "1.6.2"
},
"tauri": {
"$schema": "../../../node_modules/@tauri-apps/cli/config.schema.json",
"version": "2.0.0",
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK",
"endpoints": [
"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",

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());
#[cfg(target_os = "windows")]
pub static IS_PORTABLE: Lazy<bool> = Lazy::new(|| {
if cfg!(windows) {
let dir = crate::utils::dirs::app_install_dir().unwrap();

View File

@@ -29,7 +29,7 @@ use std::{
},
time::Duration,
};
use tauri::api::process::Command;
use tokio::time::sleep;
use tracing_attributes::instrument;
@@ -414,17 +414,18 @@ impl CoreManager {
let app_dir = dirs::app_data_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?;
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])
.output()?;
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() {
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}");
}
@@ -462,11 +463,12 @@ impl CoreManager {
let tun_device_ip = Config::clash().clone().latest().get_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()])
.spawn()?;
let event = rx.recv().await;
log::debug!(target: "app", "{event:?}");
.output()
.await?;
log::debug!(target: "app", "set system dns: {:?}", output);
}
}
// FIXME: 重构服务模式
@@ -553,9 +555,10 @@ impl CoreManager {
if enable_tun {
log::debug!(target: "app", "try to set system dns");
match Command::new("networksetup")
match tokio::process::Command::new("networksetup")
.args(["-setdnsservers", "Wi-Fi", "Empty"])
.output()
.await
{
Ok(_) => return Ok(()),
Err(err) => {

View File

@@ -5,7 +5,7 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::{AppHandle, Manager, Window};
use tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry};
#[derive(Debug, Default, Clone)]
pub struct Handle {
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
@@ -42,11 +42,11 @@ impl 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
.lock()
.as_ref()
.and_then(|a| a.get_window("main"))
.and_then(|a| a.get_webview_window("main"))
}
pub fn refresh_clash() {
@@ -81,11 +81,12 @@ impl Handle {
}
pub fn update_systray() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
}
Tray::update_systray(app_handle.as_ref().unwrap())?;
// let app_handle = Self::global().app_handle.lock();
// if app_handle.is_none() {
// bail!("update_systray unhandled error");
// }
// Tray::update_systray(app_handle.as_ref().unwrap())?;
Handle::emit("update_systray", ())?;
Ok(())
}
@@ -98,4 +99,14 @@ impl Handle {
Tray::update_part(app_handle.as_ref().unwrap())?;
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 parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use tauri::{AppHandle, GlobalShortcutManager};
use wry::application::accelerator::Accelerator;
use tauri::AppHandle;
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
@@ -64,24 +65,20 @@ impl Hotkey {
fn check_key(hotkey: &str) -> Result<()> {
// fix #287
// tauri的这几个方法全部有Result expect会panic先检测一遍避免挂了
if hotkey.parse::<Accelerator>().is_err() {
if hotkey.parse::<Shortcut>().is_err() {
bail!("invalid hotkey `{hotkey}`");
}
Ok(())
}
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = self.app_handle.lock();
if app_handle.is_none() {
bail!("failed to get the hotkey manager");
}
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
bail!("app handle is none");
}
let manager = app_handle.as_ref().unwrap().global_shortcut();
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let mut manager = self.get_manager()?;
if manager.is_registered(hotkey)? {
if manager.is_registered(hotkey) {
manager.unregister(hotkey)?;
}
@@ -97,17 +94,25 @@ impl Hotkey {
"toggle_tun_mode" => feat::toggle_tun_mode,
"enable_tun_mode" => feat::enable_tun_mode,
"disable_tun_mode" => feat::disable_tun_mode,
_ => 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}");
Ok(())
}
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}");
Ok(())
}
@@ -189,8 +194,12 @@ impl Hotkey {
impl Drop for Hotkey {
fn drop(&mut self) {
if let Ok(mut manager) = self.get_manager() {
let _ = manager.unregister_all();
let app_handle = self.app_handle.lock();
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(())
}
#[tracing::instrument]
pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>> {
let mut cmd = tokio::process::Command::new(SERVICE_PATH.as_path());
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)?;
tracing::debug!("service status: {}", status);
tracing::trace!("service status: {}", 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::{
config::{nyanpasu::ClashCore, Config},
feat, ipc,
feat, ipc, log_err,
utils::{help, resolve},
};
use anyhow::Result;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use rust_i18n::t;
use tauri::{
AppHandle, CustomMenuItem, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu,
menu::{Menu, MenuBuilder, MenuEvent, MenuItemBuilder, SubmenuBuilder},
tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent},
AppHandle, Manager, Runtime,
};
use tracing_attributes::instrument;
@@ -15,12 +20,130 @@ pub mod icon;
pub mod proxies;
pub use self::icon::on_scale_factor_changed;
use self::proxies::SystemTrayMenuProxiesExt;
mod utils;
struct TrayState<R: Runtime> {
menu: Mutex<Menu<R>>,
}
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 {
#[instrument(skip(_app_handle))]
pub fn tray_menu(_app_handle: &AppHandle) -> SystemTrayMenu {
#[instrument(skip(app_handle))]
pub fn tray_menu<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Menu<R>> {
let version = env!("NYANPASU_VERSION");
let core = {
*Config::verge()
@@ -29,72 +152,147 @@ impl Tray {
.as_ref()
.unwrap_or(&ClashCore::default())
};
let mut menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", t!("tray.dashboard")))
.setup_proxies() // Setup the proxies menu
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("rule_mode", t!("tray.rule_mode")))
.add_item(CustomMenuItem::new("global_mode", t!("tray.global_mode")))
.add_item(CustomMenuItem::new("direct_mode", t!("tray.direct_mode")));
let mut menu = MenuBuilder::new(app_handle)
.text("open_window", t!("tray.dashboard"))
.setup_proxies(app_handle)? // Setup the proxies menu
.separator()
.check("rule_mode", t!("tray.rule_mode"))
.check("global_mode", t!("tray.global_mode"))
.check("direct_mode", t!("tray.direct_mode"));
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)
.add_item(CustomMenuItem::new("system_proxy", t!("tray.system_proxy")))
.add_item(CustomMenuItem::new("tun_mode", t!("tray.tun_mode")))
.add_item(CustomMenuItem::new("copy_env_sh", t!("tray.copy_env.sh")))
.add_item(CustomMenuItem::new("copy_env_cmd", t!("tray.copy_env.cmd")))
.add_item(CustomMenuItem::new("copy_env_ps", t!("tray.copy_env.ps")))
.add_submenu(SystemTraySubmenu::new(
t!("tray.open_dir.menu"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_app_config_dir",
t!("tray.open_dir.app_config_dir"),
))
.add_item(CustomMenuItem::new(
"open_app_data_dir",
t!("tray.open_dir.app_data_dir"),
))
.add_item(CustomMenuItem::new(
"open_core_dir",
t!("tray.open_dir.core_dir"),
))
.add_item(CustomMenuItem::new(
"open_logs_dir",
t!("tray.open_dir.log_dir"),
)),
))
.add_submenu(SystemTraySubmenu::new(
t!("tray.more.menu"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"restart_clash",
t!("tray.more.restart_clash"),
))
.add_item(CustomMenuItem::new(
"restart_app",
t!("tray.more.restart_app"),
))
.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"))
menu = menu
.separator()
.check("system_proxy", t!("tray.system_proxy"))
.check("tun_mode", t!("tray.tun_mode"))
.separator()
.text("copy_env_sh", t!("tray.copy_env.sh"))
.text("copy_env_cmd", t!("tray.copy_env.cmd"))
.text("copy_env_ps", t!("tray.copy_env.ps"))
.item(
&SubmenuBuilder::new(app_handle, t!("tray.open_dir.menu"))
.text("open_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"))
.text("open_logs_dir", t!("tray.open_dir.log_dir"))
.build()?,
)
.item(
&SubmenuBuilder::new(app_handle, t!("tray.more.menu"))
.text("restart_clash", t!("tray.more.restart_clash"))
.text("restart_app", t!("tray.more.restart_app"))
.item(
&MenuItemBuilder::new(format!("Version {}", version))
.id("app_version")
.enabled(false)
.build(app_handle)?,
)
.build()?,
)
.separator()
.item(
&MenuItemBuilder::new(t!("tray.quit"))
.id("quit")
.accelerator("CmdOrControl+Q")
.build(app_handle)?,
);
Ok(menu.build()?)
}
#[instrument(skip(app_handle))]
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
app_handle
.tray_handle()
.set_menu(Tray::tray_menu(app_handle))?;
pub fn update_systray(app_handle: &AppHandle<tauri::Wry>) -> Result<()> {
let _guard = UPDATE_SYSTRAY_MUTEX.lock();
let tray_id = get_tray_id();
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)?;
Ok(())
}
#[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 core = {
*Config::verge()
@@ -103,54 +301,25 @@ impl Tray {
.as_ref()
.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 _ = tray.get_item("rule_mode").set_title(t!("tray.rule_mode"));
let _ = tray
.get_item("global_mode")
.set_title(t!("tray.global_mode"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("tray.direct_mode"));
let _ = menu
.get("rule_mode")
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "rule").ok());
let _ = menu
.get("global_mode")
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "global").ok());
let _ = menu
.get("direct_mode")
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "direct").ok());
if core == ClashCore::ClashPremium {
let _ = tray
.get_item("script_mode")
.set_title(t!("tray.script_mode"));
}
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 _ = menu
.get("script_mode")
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "script").ok());
}
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;
@@ -174,44 +343,16 @@ impl Tray {
TrayIcon::Normal
};
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")]
{
match system_proxy {
true => {
let _ = tray
.get_item("system_proxy")
.set_title(utils::selected_title(t!("tray.system_proxy")));
}
false => {
let _ = tray
.get_item("system_proxy")
.set_title(t!("tray.system_proxy"));
}
}
let _ = menu
.get("system_proxy")
.and_then(|item| item.as_check_menuitem()?.set_checked(system_proxy).ok());
let _ = menu
.get("tun_mode")
.and_then(|item| item.as_check_menuitem()?.set_checked(tun_mode).ok());
match tun_mode {
true => {
let _ = tray
.get_item("tun_mode")
.set_title(utils::selected_title(t!("tray.tun_mode")));
}
false => {
let _ = tray.get_item("tun_mode").set_title(t!("tray.tun_mode"));
}
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = tray.get_item("system_proxy").set_selected(system_proxy);
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"));
@@ -219,22 +360,34 @@ impl Tray {
map
};
let _ = tray.set_tooltip(&format!(
#[cfg(not(target_os = "linux"))]
{
let _ = tray.set_tooltip(Some(&format!(
"{}: {}\n{}: {}",
t!("tray.system_proxy"),
switch_map[&system_proxy],
t!("tray.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(())
}
#[instrument(skip(app_handle, event))]
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
pub fn on_menu_item_event(app_handle: &AppHandle, event: MenuEvent) {
let id = event.id().0.as_str();
match id {
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode.into());
@@ -243,11 +396,11 @@ impl Tray {
"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"),
"copy_env_sh" => feat::copy_clash_env(app_handle, "sh"),
#[cfg(target_os = "windows")]
"copy_env_cmd" => feat::copy_clash_env("cmd"),
"copy_env_cmd" => feat::copy_clash_env(app_handle, "cmd"),
#[cfg(target_os = "windows")]
"copy_env_ps" => feat::copy_clash_env("ps"),
"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()),
@@ -258,14 +411,18 @@ impl Tray {
help::quit_application(app_handle);
}
_ => {
proxies::on_system_tray_event(&id);
proxies::on_system_tray_event(id);
}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
resolve::create_window(app_handle);
}
_ => {}
}
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 base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _};
use indexmap::IndexMap;
use tauri::SystemTrayMenu;
use tauri::{menu::MenuBuilder, AppHandle, Manager, Runtime};
use tracing::{debug, error, warn};
use tracing_attributes::instrument;
@@ -41,8 +40,9 @@ async fn loop_task() {
}
type GroupName = String;
type FromProxy = String;
type ToProxy = String;
type ProxyName = String;
type FromProxy = ProxyName;
type ToProxy = ProxyName;
type ProxySelectAction = (GroupName, FromProxy, ToProxy);
#[derive(PartialEq)]
enum TrayUpdateType {
@@ -179,8 +179,9 @@ pub async fn proxies_updated_receiver() {
match diff_proxies(&tray_proxies_holder, &current_tray_proxies) {
TrayUpdateType::Full => {
debug!("should do full update");
tray_proxies_holder = current_tray_proxies;
match Handle::update_systray() {
match Handle::emit("update_systray", ()) {
Ok(_) => {
debug!("update systray success");
}
@@ -211,92 +212,130 @@ pub fn setup_proxies() {
}
mod platform_impl {
use std::sync::atomic::AtomicBool;
use super::{ProxySelectAction, TrayProxyItem};
use super::{GroupName, ProxyName, ProxySelectAction, TrayProxyItem};
use crate::{
config::nyanpasu::ProxiesSelectorMode,
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 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;
pub fn generate_group_selector(group_name: &str, group: &TrayProxyItem) -> SystemTraySubmenu {
let mut group_menu = SystemTrayMenu::new();
for item in group.all.iter() {
let mut sub_item = CustomMenuItem::new(
format!(
"select_proxy_{}_{}",
base64_standard.encode(group_name),
base64_standard.encode(item)
),
item.clone(),
// It store a map of proxy nodes like "GROUP_PROXY" -> ID
// TODO: use Cow<str> instead of String
pub(super) static ITEM_IDS: Lazy<Mutex<BiMap<(GroupName, ProxyName), usize>>> =
Lazy::new(|| Mutex::new(BiMap::new()));
pub fn generate_group_selector<R: Runtime>(
app_handle: &AppHandle<R>,
group_name: &str,
group: &TrayProxyItem,
) -> 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 now == item.as_str() {
#[cfg(target_os = "linux")]
{
sub_item.title = super::super::utils::selected_title(item);
}
#[cfg(not(target_os = "linux"))]
{
sub_item = sub_item.selected();
}
sub_item_builder = sub_item_builder.checked(true);
}
}
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(
menu: &SystemTrayMenu,
pub fn generate_selectors<R: Runtime>(
app_handle: &AppHandle<R>,
proxies: &super::TrayProxies,
) -> SystemTrayMenu {
let mut menu = menu.to_owned();
) -> anyhow::Result<Vec<MenuItemKind<R>>> {
let mut items = Vec::new();
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() {
let group_menu = generate_group_selector(group, item);
menu = menu.add_submenu(group_menu);
let group_menu = generate_group_selector(app_handle, group, item)?;
items.push(MenuItemKind::Submenu(group_menu));
}
menu
Ok(items)
}
pub fn setup_tray(menu: &mut SystemTrayMenu) -> SystemTrayMenu {
let mut parent_menu = menu.to_owned();
pub fn setup_tray<'m, R: Runtime, M: Manager<R>>(
app_handle: &AppHandle<R>,
mut menu: MenuBuilder<'m, R, M>,
) -> anyhow::Result<MenuBuilder<'m, R, M>> {
let selector_mode = crate::config::Config::verge()
.latest()
.clash_tray_selector
.unwrap_or_default();
let mut menu = match selector_mode {
ProxiesSelectorMode::Hidden => return parent_menu,
ProxiesSelectorMode::Normal => {
parent_menu = parent_menu.add_native_item(SystemTrayMenuItem::Separator);
parent_menu.clone()
}
ProxiesSelectorMode::Submenu => SystemTrayMenu::new(),
menu = match selector_mode {
ProxiesSelectorMode::Hidden => return Ok(menu),
ProxiesSelectorMode::Normal => menu.separator(),
ProxiesSelectorMode::Submenu => menu,
};
let proxies = ProxiesGuard::global().read().inner().to_owned();
let mode = crate::utils::config::get_current_clash_mode();
let tray_proxies = super::to_tray_proxies(mode.as_str(), &proxies);
menu = generate_selectors(&menu, &tray_proxies);
if selector_mode == ProxiesSelectorMode::Submenu {
parent_menu =
parent_menu.add_submenu(SystemTraySubmenu::new(t!("tray.select_proxies"), menu));
parent_menu
} else {
menu
let items = generate_selectors::<R>(app_handle, &tray_proxies)?;
match selector_mode {
ProxiesSelectorMode::Normal => {
for item in items {
menu = menu.item(&item);
}
}
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);
@@ -306,84 +345,175 @@ mod platform_impl {
warn!("tray item update is in progress, skip this update");
return;
}
let tray = Handle::global()
.app_handle
.lock()
let app_handle = Handle::global().app_handle.lock();
let tray_state = app_handle
.as_ref()
.unwrap()
.tray_handle();
.state::<crate::core::tray::TrayState<tauri::Wry>>();
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 {
tracing::debug!("update selected proxies: {:?}", action);
let from = format!(
"select_proxy_{}_{}",
base64_standard.encode(&action.0),
base64_standard.encode(&action.1)
);
let to = format!(
"select_proxy_{}_{}",
base64_standard.encode(&action.0),
base64_standard.encode(&action.2)
);
// #[cfg(not(target_os = "linux"))]
// {
// tracing::debug!("update selected proxies: {:?}", action);
// let from_id = match item_ids.get_by_left(&(action.0.clone(), action.1.clone())) {
// Some(id) => *id,
// None => {
// warn!("from item not found: {:?}", action);
// continue;
// }
// };
// 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) => {
#[cfg(not(target_os = "linux"))]
{
let _ = item.set_selected(false);
}
#[cfg(target_os = "linux")]
{
let _ = item.set_title(action.1.clone());
}
let _ = item.set_checked(false);
}
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) => {
#[cfg(not(target_os = "linux"))]
{
let _ = item.set_selected(true);
}
#[cfg(target_os = "linux")]
{
let _ = item.set_title(super::super::utils::selected_title(&action.2));
}
let _ = item.set_checked(true);
}
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);
}
}
}
pub trait SystemTrayMenuProxiesExt {
fn setup_proxies(&mut self) -> Self;
pub trait SystemTrayMenuProxiesExt<R: Runtime> {
fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self>
where
Self: Sized;
}
impl SystemTrayMenuProxiesExt for SystemTrayMenu {
fn setup_proxies(&mut self) -> Self {
platform_impl::setup_tray(self)
impl<'m, R: Runtime, M: Manager<R>> SystemTrayMenuProxiesExt<R> for MenuBuilder<'m, R, M> {
fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self> {
platform_impl::setup_tray(app_handle, self)
}
}
#[instrument]
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
}
let parts: Vec<&str> = event.split('_').collect();
if parts.len() != 4 {
return; // bypass invalid event
let node_id = event.split('_').last().unwrap(); // safe to unwrap
let node_id = match node_id.parse::<usize>() {
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 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);
tauri::async_runtime::block_on(async move {
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 {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);

View File

@@ -14,7 +14,8 @@ use anyhow::{bail, Result};
use handle::Message;
use nyanpasu_ipc::api::status::CoreState;
use serde_yaml::{Mapping, Value};
use wry::application::clipboard::Clipboard;
use tauri::AppHandle;
use tauri_plugin_clipboard_manager::ClipboardExt;
// 打开面板
#[allow(unused)]
@@ -400,7 +401,7 @@ async fn update_core_config() -> Result<()> {
}
/// 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 http_proxy = format!("http://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 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 {
"sh" => clipboard.write_text(sh),
"cmd" => clipboard.write_text(cmd),
"ps" => clipboard.write_text(ps),
"sh" => {
if let Err(e) = clipboard.write_text(sh) {
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}"),
}
}

View File

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

View File

@@ -60,8 +60,11 @@ pub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {
let mut core = HashMap::new();
for c in CoreType::get_supported_cores() {
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
.args(if matches!(c, CoreType::Clash(ClashCoreType::ClashRust)) {
["-V"]
@@ -70,9 +73,10 @@ pub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {
})
.output()
.expect("failed to execute sidecar command");
let stdout = String::from_utf8_lossy(&output.stdout);
core.insert(
Cow::Borrowed(name),
Cow::Owned(output.stdout.replace("\n\n", " ").trim().to_owned()),
Cow::Owned(stdout.replace("\n\n", " ").trim().to_owned()),
);
}
Ok(EnvInfo {

View File

@@ -1,4 +1,4 @@
use rfd::{MessageButtons, MessageDialog, MessageLevel};
use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
use rust_i18n::t;
pub fn panic_dialog(msg: &str) {
@@ -12,12 +12,15 @@ pub fn panic_dialog(msg: &str) {
}
pub fn migrate_dialog(msg: &str) -> bool {
matches!(
MessageDialog::new()
.set_level(MessageLevel::Warning)
.set_title("Clash Nyanpasu Migration")
.set_buttons(MessageButtons::YesNo)
.set_description(msg)
.show()
.show(),
MessageDialogResult::Yes
)
}
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 once_cell::sync::Lazy;
use std::{borrow::Cow, fs, path::PathBuf};
use tauri::{
api::path::{home_dir, resource_dir},
Env,
};
use tauri::{utils::platform::resource_dir, Env};
#[cfg(not(feature = "verge-dev"))]
const PREVIOUS_APP_NAME: &str = "clash-verge";
@@ -109,7 +106,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")]
{
if !get_portable_flag() {
Ok(home_dir()
Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to check old app home dir"))?
.join(".config")
.join(PREVIOUS_APP_NAME))
@@ -120,7 +117,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
}
#[cfg(not(target_os = "windows"))]
Ok(home_dir()
Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join(".config")
.join(PREVIOUS_APP_NAME))
@@ -133,7 +130,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
)]
pub fn app_home_dir() -> Result<PathBuf> {
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"))?
.join(".config")
.join(APP_NAME));
@@ -147,7 +144,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
if let Some(reg_app_dir) = 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"))?
.join(".config")
.join(APP_NAME));
@@ -156,7 +153,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
}
#[cfg(not(target_os = "windows"))]
Ok(home_dir()
Ok(dirs::home_dir()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join(".config")
.join(APP_NAME))
@@ -168,7 +165,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
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");
return Ok(res_dir);
};
@@ -315,6 +312,28 @@ fn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> {
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 {
#[test]
fn test_dir_placeholder() {

View File

@@ -15,16 +15,13 @@ use std::{
path::PathBuf,
str::FromStr,
};
use tauri::{
api::{
process::current_binary,
shell::{open, Program},
},
AppHandle, Manager,
};
use tauri::{process::current_binary, AppHandle, Manager};
use tauri_plugin_shell::ShellExt;
use tracing::{debug, warn};
use tracing_attributes::instrument;
use crate::trace_err;
/// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
if !path.exists() {
@@ -105,20 +102,27 @@ pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
#[cfg(not(target_os = "macos"))]
let code = "code";
let _ = match Program::from_str(code) {
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
let shell = app.shell();
trace_err!(
match which::which(code) {
Ok(_) => crate::utils::open::with(path, code),
Err(err) => {
log::error!(target: "app", "Can't find VScode `{err}`");
// default open
open(&app.shell_scope(), path.to_string_lossy(), None)
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(())
}
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 {
@@ -215,10 +219,12 @@ pub fn get_max_scale_factor() -> f64 {
}
#[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);
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))]
@@ -263,9 +269,9 @@ macro_rules! log_err {
}
};
($result: expr, $err_str: expr) => {
if let Err(_) = $result {
log::error!(target: "app", "{}", $err_str);
($result: expr, $label: expr) => {
if let Err(err) = $result {
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)
}
}
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,
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);
@@ -85,7 +86,7 @@ pub fn find_unused_port() -> Result<u16> {
pub fn resolve_setup(app: &mut App) {
#[cfg(target_os = "macos")]
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");
reset_window_open_counter();
#[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_service());
@@ -134,20 +135,31 @@ pub fn resolve_setup(app: &mut App) {
log_err!(CoreManager::global().init());
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
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 };
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_sysproxy());
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
log_err!(JobsManager::global_register());
@@ -165,8 +177,10 @@ pub fn resolve_reset() {
}
/// create main window
#[tracing_attributes::instrument(skip(app_handle))]
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 {
trace_err!(window.unminimize(), "set win unminimize");
trace_err!(window.show(), "set win visible");
@@ -183,10 +197,11 @@ pub fn create_window(app_handle: &AppHandle) {
.unwrap_or(&false)
};
let mut builder = tauri::window::WindowBuilder::new(
tracing::debug!("create main window...");
let mut builder = tauri::WebviewWindowBuilder::new(
app_handle,
"main".to_string(),
tauri::WindowUrl::App("/".into()),
tauri::WebviewUrl::App("/".into()),
)
.title("Clash Nyanpasu")
.fullscreen(false)
@@ -221,6 +236,7 @@ pub fn create_window(app_handle: &AppHandle) {
.decorations(false)
.transparent(true)
.visible(false)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.build();
#[cfg(target_os = "macos")]
let win_res = builder
@@ -229,11 +245,15 @@ pub fn create_window(app_handle: &AppHandle) {
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build();
#[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")]
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) => {
let window_id: cocoa::base::id = raw_window as _;
set_window_controls_pos(window_id, 26.0, 26.0);
@@ -247,8 +267,6 @@ pub fn create_window(app_handle: &AppHandle) {
match win_res {
Ok(win) => {
use tauri::{PhysicalPosition, PhysicalSize};
#[cfg(windows)]
use window_shadows::set_shadow;
if win_state.is_some() {
let state = win_state.as_ref().unwrap();
@@ -273,7 +291,7 @@ pub fn create_window(app_handle: &AppHandle) {
}
}
#[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");
let center = (|| -> Result<bool> {
let center;
@@ -310,7 +328,9 @@ pub fn create_window(app_handle: &AppHandle) {
#[cfg(debug_assertions)]
{
win.open_devtools();
if let Some(webview_window) = win.get_webview_window("main") {
webview_window.open_devtools();
}
}
#[cfg(target_os = "macos")]
@@ -329,17 +349,23 @@ pub fn create_window(app_handle: &AppHandle) {
}
Err(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")]
{
use webview2_com_bridge::{
webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6,
windows::core::Interface,
};
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6;
use windows_core::Interface;
app_handle
.get_window("main")
.get_webview_window("main")
.unwrap()
.with_webview(|webview| unsafe {
let settings = webview
@@ -358,7 +384,7 @@ pub fn create_window(app_handle: &AppHandle) {
/// close main window
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");
reset_window_open_counter()
}
@@ -366,12 +392,12 @@ pub fn close_window(app_handle: &AppHandle) {
/// is window open
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<()> {
let win = app_handle
.get_window("main")
.get_webview_window("main")
.ok_or(anyhow::anyhow!("failed to get window"))?;
let current_monitor = win.current_monitor()?;
let verge = Config::verge();
@@ -412,21 +438,23 @@ pub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<(
/// resolve core version
// 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();
log::debug!(target: "app", "check config in `{core}`");
let cmd = match core_type {
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()?;
log::debug!(target: "app", "get core version: {:?}", out);
let out = cmd.output().await?;
if !out.status.success() {
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 {
log::debug!(target: "app", "check item: {}", item);
if item.starts_with('v')

View File

@@ -1,23 +1,29 @@
{
"package": {
"productName": "Clash Nyanpasu",
"version": "1.6.1"
},
"build": {
"distDir": "./tmp/dist",
"devPath": "http://localhost:3000/",
"beforeDevCommand": "pnpm run web:dev",
"beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd)"
},
"tauri": {
"systemTray": {
"iconPath": "icons/win-tray-icon.png",
"iconAsTemplate": true
},
"$schema": "../../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
"targets": "all",
"identifier": "moe.elaina.clash.nyanpasu",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"silent": true,
"type": "embedBootstrapper"
},
"wix": {
"language": ["zh-CN", "en-US", "ru-RU"],
"template": "./templates/installer.wxs",
"fragmentPaths": ["./templates/cleanup.wxs"]
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English", "Russian"],
"template": "./templates/installer.nsi",
"installMode": "both"
}
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -37,9 +43,6 @@
"category": "DeveloperTool",
"shortDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
"longDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
"deb": {
"depends": ["openssl"]
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "11.0",
@@ -47,72 +50,37 @@
"signingIdentity": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"silent": true,
"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"
}
"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": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK",
"endpoints": [
"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://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update.json"
],
"dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK"
},
"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
]
}
},
"app": {
"windows": [],
"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
!insertmacro CheckNyanpasuProcess "Clash Nyanpasu.exe" "1"
; !insertmacro CheckNyanpasuProcess "clash-verge-service.exe" "2"
!insertmacro CheckNyanpasuProcess "clash.exe" "3"
!insertmacro CheckNyanpasuProcess "clash-rs.exe" "4"
!insertmacro CheckNyanpasuProcess "mihomo.exe" "5"
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "6"
!insertmacro CheckNyanpasuProcess "clash-nyanpasu.exe" "2"
; !insertmacro CheckNyanpasuProcess "clash-verge-service.exe" "3"
!insertmacro CheckNyanpasuProcess "clash.exe" "4"
!insertmacro CheckNyanpasuProcess "clash-rs.exe" "5"
!insertmacro CheckNyanpasuProcess "mihomo.exe" "6"
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "7"
!macroend
; 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"
},
"dependencies": {
"@tauri-apps/api": "1.6.0",
"@tauri-apps/api": "2.0.0-rc.4",
"ahooks": "3.8.1",
"ofetch": "1.3.4",
"react": "18.3.1",

View File

@@ -1,5 +1,5 @@
import { IPSBResponse } from "@/openapi";
import { invoke } from "@tauri-apps/api/tauri";
import { invoke } from "@tauri-apps/api/core";
import { ManifestVersion } from "./core";
import {
ClashConfig,
@@ -259,3 +259,7 @@ export const isAppImage = async () => {
export const getServiceInstallPrompt = async () => {
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",
"@nyanpasu/interface": "workspace:^",
"@nyanpasu/ui": "workspace:^",
"@tauri-apps/api": "1.6.0",
"@tauri-apps/api": "2.0.0-rc.4",
"@types/json-schema": "7.0.15",
"ahooks": "3.8.1",
"allotment": "1.20.2",
@@ -52,13 +52,21 @@
"@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.12.0",
"@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-dom": "18.3.0",
"@vitejs/plugin-react": "4.3.1",
"@vitejs/plugin-react-swc": "3.7.0",
"clsx": "2.1.1",
"meta-json-schema": "libnyanpasu/meta-json-schema#main",
"meta-json-schema": "1.18.8",
"monaco-yaml": "5.2.2",
"nanoid": "5.0.7",
"sass": "1.78.0",

View File

@@ -2,7 +2,7 @@ import { Allotment } from "allotment";
import getSystem from "@/utils/get-system";
import { alpha, useTheme } from "@mui/material";
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 { ReactNode } from "react";
import { LayoutControl } from "../layout/layout-control";
@@ -10,6 +10,8 @@ import styles from "./app-container.module.scss";
import AppDrawer from "./app-drawer";
import DrawerContent from "./drawer-content";
const appWindow = getCurrentWebviewWindow();
const OS = getSystem();
export const AppContainer = ({

View File

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

View File

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

View File

@@ -7,7 +7,9 @@ import { themeMode as themeModeAtom } from "@/store";
import { alpha, darken, lighten, Theme, useColorScheme } from "@mui/material";
import { useNyanpasu } from "@nyanpasu/interface";
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 root = document.documentElement;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ export const UpdateProxiesProviders = () => {
const handleProviderUpdate = useLockFn(async () => {
if (!getProxiesProviders.data) {
message(`No Providers.`, {
type: "info",
kind: "info",
title: t("Info"),
});
@@ -35,7 +35,7 @@ export const UpdateProxiesProviders = () => {
);
} catch (e) {
message(`Update all failed.\n${String(e)}`, {
type: "error",
kind: "error",
title: t("Error"),
});
} 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 Clash from "@/assets/image/core/clash.png";
import { formatError } from "@/utils";
import { message } from "@/utils/notification";
import parseTraffic from "@/utils/parse-traffic";
import FiberManualRecord from "@mui/icons-material/FiberManualRecord";
import Update from "@mui/icons-material/Update";
@@ -22,7 +23,6 @@ import {
useNyanpasu,
} from "@nyanpasu/interface";
import { cleanDeepClickEvent, cn } from "@nyanpasu/ui";
import { message } from "@tauri-apps/api/dialog";
export const getImage = (core: ClashCore) => {
switch (core) {
@@ -175,12 +175,12 @@ export const ClashCoreItem = ({
getClashCore.mutate();
message(`Successfully update core ${data.name}`, {
type: "info",
kind: "info",
title: t("Success"),
});
} catch (e) {
message(`Update failed. ${formatError(e)}`, {
type: "error",
kind: "error",
title: t("Error"),
});
} finally {

View File

@@ -11,7 +11,7 @@ import {
setTrayIcon as setTrayIconCall,
} from "@nyanpasu/interface";
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" }) {
const { t } = useTranslation();
@@ -53,7 +53,7 @@ function TrayIconItem({ mode }: { mode: "system_proxy" | "tun" | "normal" }) {
return await setTrayIconCall(mode, selected);
} catch (e) {
message(formatError(e), {
type: "error",
kind: "error",
});
} finally {
setTs(Date.now());

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import {
setCustomAppDir,
} from "@nyanpasu/interface";
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";
export const SettingNyanpasuPath = () => {
@@ -36,7 +36,7 @@ export const SettingNyanpasuPath = () => {
if (Array.isArray(selected)) {
message(t("Multiple directories are not supported"), {
title: t("Error"),
type: "error",
kind: "error",
});
return;
@@ -46,7 +46,7 @@ export const SettingNyanpasuPath = () => {
message(t("App directory changed successfully"), {
title: t("Success"),
type: "error",
kind: "error",
});
await sleep(1000);
@@ -55,7 +55,7 @@ export const SettingNyanpasuPath = () => {
} catch (e) {
message(`Migration failed! ${JSON.stringify(e)}`, {
title: t("Error"),
type: "error",
kind: "error",
});
}
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,44 +1,62 @@
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useSetAtom } from "jotai";
import { lazy, Suspense } from "react";
import { lazy, Suspense, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { UpdaterIgnoredAtom } from "@/store/updater";
import { formatError } from "@/utils";
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 { relaunch } from "@tauri-apps/api/process";
import { open as openThat } from "@tauri-apps/api/shell";
import { installUpdate, type UpdateManifest } from "@tauri-apps/api/updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
import styles from "./updater-dialog.module.scss";
const Markdown = lazy(() => import("react-markdown"));
export interface UpdaterDialogProps extends Omit<BaseDialogProps, "title"> {
manifest: UpdateManifest;
update: Update;
}
export default function UpdaterDialog({
open,
manifest,
update,
onClose,
...others
}: UpdaterDialogProps) {
const { t } = useTranslation();
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 () => {
try {
// 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.
// You could use this step to display another confirmation dialog.
await relaunch();
} catch (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")}
open={open}
onClose={() => {
setUpdaterIgnore(manifest.version); // TODO: control this behavior
setUpdaterIgnore(update.version); // TODO: control this behavior
onClose?.();
}}
onOk={handleUpdate}
@@ -64,9 +82,9 @@ export default function UpdaterDialog({
>
<div className="flex items-center justify-between px-2 py-2">
<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">
{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",
)}
</span>
@@ -76,7 +94,7 @@ export default function UpdaterDialog({
size="small"
onClick={() => {
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>
</Suspense>
</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>
</BaseDialog>
);

View File

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

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