Remake desktop

Optimize change proxy

Optimize network check

Fix fallback issues

Optimize lots of details
This commit is contained in:
chen08209
2024-12-03 21:47:12 +08:00
parent 4b32a096dd
commit ece8a48181
96 changed files with 5869 additions and 2378 deletions

View File

@@ -27,25 +27,6 @@ jobs:
arch: arm64
steps:
- name: Setup Mingw64
if: startsWith(matrix.platform,'windows')
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
install: mingw-w64-x86_64-gcc
update: true
- name: Set Mingw64 Env
if: startsWith(matrix.platform,'windows')
run: |
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Check Matrix
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
with:
@@ -103,7 +84,6 @@ jobs:
path: ./dist
overwrite: true
upload:
permissions: write-all
needs: [ build ]

View File

@@ -1,12 +1,13 @@
name: change
name: changelog
on:
push:
branches:
- 'main'
tags:
- 'v*'
jobs:
changelog:
if: ${{ !contains(github.ref, '+') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -61,6 +62,5 @@ jobs:
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

32
core/action.go Normal file
View File

@@ -0,0 +1,32 @@
//go:build !cgo
package main
import (
"encoding/json"
)
func (action Action) Json() ([]byte, error) {
data, err := json.Marshal(action)
return data, err
}
func (action Action) callback(data interface{}) bool {
if conn == nil {
return false
}
sendAction := Action{
Id: action.Id,
Method: action.Method,
Data: data,
}
res, err := sendAction.Json()
if err != nil {
return false
}
_, err = conn.Write(append(res, []byte("\n")...))
if err != nil {
return false
}
return true
}

View File

@@ -1,23 +1,10 @@
package main
import "C"
import (
"context"
"core/state"
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/hub/route"
"github.com/samber/lo"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -27,53 +14,28 @@ import (
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/samber/lo"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
)
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
}
type GenerateConfigParams struct {
ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
GroupName *string `json:"group-name"`
ProxyName *string `json:"proxy-name"`
}
type TestDelayParams struct {
ProxyName string `json:"proxy-name"`
Timeout int64 `json:"timeout"`
}
type ProcessMapItem struct {
Id int64 `json:"id"`
Value string `json:"value"`
}
type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
Count int `json:"count"`
Path string `json:"path"`
UpdateAt time.Time `json:"update-at"`
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
}
var (
isRunning = false
runLock sync.Mutex
ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
type ExternalProviders []ExternalProvider
@@ -81,30 +43,9 @@ func (a ExternalProviders) Len() int { return len(a) }
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
func restartExecutable(execPath string) {
var err error
executor.Shutdown()
if runtime.GOOS == "windows" {
cmd := exec.Command(execPath, os.Args[1:]...)
log.Infoln("restarting: %q %q", execPath, os.Args[1:])
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
log.Fatalln("restarting: %s", err)
}
os.Exit(0)
}
log.Infoln("restarting: %q %q", execPath, os.Args[1:])
err = syscall.Exec(execPath, os.Args, os.Environ())
if err != nil {
log.Fatalln("restarting: %s", err)
}
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func readFile(path string) ([]byte, error) {
@@ -119,19 +60,6 @@ func readFile(path string) ([]byte, error) {
return data, err
}
func removeFile(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
err = os.Remove(absPath)
if err != nil {
return err
}
return nil
}
func getProfilePath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
@@ -262,8 +190,6 @@ func trimArr(arr []string) (r []string) {
return
}
var ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
@@ -325,20 +251,13 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
}
}
overrideRules(&targetConfig.Rule)
//if runtime.GOOS == "android" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
//} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//}
//if configParams.IsCompatible == false {
// targetConfig.ProxyProvider = make(map[string]map[string]any)
// targetConfig.RuleProvider = make(map[string]map[string]any)
// generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
//}
}
func patchConfig(general *config.General, controller *config.Controller, tls *config.TLS) {
func patchConfig() {
log.Infoln("[Apply] patch")
general := currentConfig.General
controller := currentConfig.Controller
tls := currentConfig.TLS
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
@@ -365,17 +284,15 @@ func patchConfig(general *config.General, controller *config.Controller, tls *co
})
}
var isRunning = false
var runLock sync.Mutex
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
func updateListeners(force bool) {
if !isRunning {
return
}
runLock.Lock()
defer runLock.Unlock()
stopListeners()
general := currentConfig.General
listeners := currentConfig.Listeners
if force == true {
stopListeners()
}
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -424,19 +341,22 @@ func patchSelectGroup() {
}
}
func applyConfig() error {
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
func applyConfig(rawConfig *config.RawConfig) error {
runLock.Lock()
defer runLock.Unlock()
var err error
currentConfig, err = config.ParseRawConfig(rawConfig)
if err != nil {
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
if configParams.IsPatch {
patchConfig(cfg.General, cfg.Controller, cfg.TLS)
patchConfig()
} else {
closeConnections()
handleCloseConnectionsUnLock()
runtime.GC()
hub.ApplyConfig(cfg)
hub.ApplyConfig(currentConfig)
patchSelectGroup()
}
updateListeners(cfg.General, cfg.Listeners)
updateListeners(false)
return err
}

110
core/constant.go Normal file
View File

@@ -0,0 +1,110 @@
package main
import (
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"time"
)
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
}
type GenerateConfigParams struct {
ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
GroupName *string `json:"group-name"`
ProxyName *string `json:"proxy-name"`
}
type TestDelayParams struct {
ProxyName string `json:"proxy-name"`
Timeout int64 `json:"timeout"`
}
type ProcessMapItem struct {
Id int64 `json:"id"`
Value string `json:"value"`
}
type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
Count int `json:"count"`
Path string `json:"path"`
UpdateAt time.Time `json:"update-at"`
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
}
const (
messageMethod Method = "message"
initClashMethod Method = "initClash"
getIsInitMethod Method = "getIsInit"
forceGcMethod Method = "forceGc"
shutdownMethod Method = "shutdown"
validateConfigMethod Method = "validateConfig"
updateConfigMethod Method = "updateConfig"
getProxiesMethod Method = "getProxies"
changeProxyMethod Method = "changeProxy"
getTrafficMethod Method = "getTraffic"
getTotalTrafficMethod Method = "getTotalTraffic"
resetTrafficMethod Method = "resetTraffic"
asyncTestDelayMethod Method = "asyncTestDelay"
getConnectionsMethod Method = "getConnections"
closeConnectionsMethod Method = "closeConnections"
closeConnectionMethod Method = "closeConnection"
getExternalProvidersMethod Method = "getExternalProviders"
getExternalProviderMethod Method = "getExternalProvider"
updateGeoDataMethod Method = "updateGeoData"
updateExternalProviderMethod Method = "updateExternalProvider"
sideLoadExternalProviderMethod Method = "sideLoadExternalProvider"
startLogMethod Method = "startLog"
stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener"
)
type Method string
type Action struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
}
type MessageType string
type Delay struct {
Name string `json:"name"`
Value int32 `json:"value"`
}
type Message struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
}
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
const (
LogMessage MessageType = "log"
ProtectMessage MessageType = "protect"
DelayMessage MessageType = "delay"
ProcessMessage MessageType = "process"
RequestMessage MessageType = "request"
StartedMessage MessageType = "started"
LoadedMessage MessageType = "loaded"
)

View File

@@ -1,3 +1,5 @@
//go:build cgo
package dart_bridge
/*

View File

@@ -0,0 +1,7 @@
//go:build !cgo
package dart_bridge
func SendToPort(port int64, msg string) bool {
return false
}

View File

@@ -1,20 +0,0 @@
//go:build android
package main
import "C"
import (
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/log"
"strings"
)
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
go func() {
log.Infoln("[DNS] updateDns %s", dnsList)
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
dns.FlushCacheWithDefaultResolver()
}()
}

View File

@@ -1,6 +1,6 @@
module core
go 1.21.0
go 1.21
replace github.com/metacubex/mihomo => ./Clash.Meta

View File

@@ -1 +1,434 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"runtime"
"sort"
"time"
)
var (
isInit = false
configParams = ConfigExtendedParams{}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
)
func handleInitClash(homeDirStr string) bool {
if !isInit {
constant.SetHomeDir(homeDirStr)
isInit = true
}
return isInit
}
func handleStartListener() bool {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
updateListeners(true)
return true
}
func handleStopListener() bool {
runLock.Lock()
defer runLock.Unlock()
isRunning = false
listener.StopListener()
return true
}
func handleGetIsInit() bool {
return isInit
}
func handleForceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}
func handleShutdown() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
isInit = false
return true
}
func handleValidateConfig(bytes []byte) string {
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
return err.Error()
}
return ""
}
func handleUpdateConfig(bytes []byte) string {
var params = &GenerateConfigParams{}
err := json.Unmarshal(bytes, params)
if err != nil {
return err.Error()
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
err = applyConfig(prof)
if err != nil {
return err.Error()
}
return ""
}
func handleGetProxies() string {
runLock.Lock()
defer runLock.Unlock()
data, err := json.Marshal(tunnel.ProxiesWithProviders())
if err != nil {
return ""
}
return string(data)
}
func handleChangeProxy(data string, fn func(string string)) {
runLock.Lock()
go func() {
defer runLock.Unlock()
var params = &ChangeProxyParams{}
err := json.Unmarshal([]byte(data), params)
if err != nil {
fn(err.Error())
return
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
group, ok := proxies[groupName]
if !ok {
fn("Not found group")
return
}
adapterProxy := group.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
fn("Group is not selectable")
return
}
if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName)
}
if err != nil {
fn(err.Error())
return
}
fn("")
return
}()
}
func handleGetTraffic(onlyProxy bool) string {
up, down := statistic.DefaultManager.Current(onlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return ""
}
return string(data)
}
func handleGetTotalTraffic(onlyProxy bool) string {
up, down := statistic.DefaultManager.Total(onlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return ""
}
return string(data)
}
func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
func handleAsyncTestDelay(paramsString string, fn func(string)) {
b.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
fn("")
return false, nil
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
fn("")
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
defer cancel()
proxies := tunnel.ProxiesWithProviders()
proxy := proxies[params.ProxyName]
delayData := &Delay{
Name: params.ProxyName,
}
if proxy == nil {
delayData.Value = -1
data, _ := json.Marshal(delayData)
fn(string(data))
return false, nil
}
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if err != nil || delay == 0 {
delayData.Value = -1
data, _ := json.Marshal(delayData)
fn(string(data))
return false, nil
}
delayData.Value = int32(delay)
data, _ := json.Marshal(delayData)
fn(string(data))
return false, nil
})
}
func handleGetConnections() string {
runLock.Lock()
defer runLock.Unlock()
snapshot := statistic.DefaultManager.Snapshot()
data, err := json.Marshal(snapshot)
if err != nil {
fmt.Println("Error:", err)
return ""
}
return string(data)
}
func handleCloseConnectionsUnLock() bool {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
return false
}
return true
})
return true
}
func handleCloseConnections() bool {
runLock.Lock()
defer runLock.Unlock()
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
return false
}
return true
})
return true
}
func handleCloseConnection(connectionId string) bool {
runLock.Lock()
defer runLock.Unlock()
c := statistic.DefaultManager.Get(connectionId)
if c == nil {
return false
}
_ = c.Close()
return true
}
func handleGetExternalProviders() string {
runLock.Lock()
defer runLock.Unlock()
externalProviders = getExternalProvidersRaw()
eps := make([]ExternalProvider, 0)
for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p)
if err != nil {
continue
}
eps = append(eps, *externalProvider)
}
sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps)
if err != nil {
return ""
}
return string(data)
}
func handleGetExternalProvider(externalProviderName string) string {
runLock.Lock()
defer runLock.Unlock()
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return ""
}
e, err := toExternalProvider(externalProvider)
if err != nil {
return ""
}
data, err := json.Marshal(e)
if err != nil {
return ""
}
return string(data)
}
func handleUpdateGeoData(geoType string, geoName string, fn func(value string)) {
go func() {
path := constant.Path.Resolve(geoName)
switch geoType {
case "MMDB":
err := updater.UpdateMMDBWithPath(path)
if err != nil {
fn(err.Error())
return
}
case "ASN":
err := updater.UpdateASNWithPath(path)
if err != nil {
fn(err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIpWithPath(path)
if err != nil {
fn(err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSiteWithPath(path)
if err != nil {
fn(err.Error())
return
}
}
fn("")
}()
}
func handleUpdateExternalProvider(providerName string, fn func(value string)) {
go func() {
externalProvider, exist := externalProviders[providerName]
if !exist {
fn("external provider is not exist")
return
}
err := externalProvider.Update()
if err != nil {
fn(err.Error())
return
}
fn("")
}()
}
func handleSideLoadExternalProvider(providerName string, data []byte, fn func(value string)) {
go func() {
runLock.Lock()
defer runLock.Unlock()
externalProvider, exist := externalProviders[providerName]
if !exist {
fn("external provider is not exist")
return
}
err := sideUpdateExternalProvider(externalProvider, data)
if err != nil {
fn(err.Error())
return
}
fn("")
}()
}
func handleStartLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
logSubscriber = log.Subscribe()
go func() {
for logData := range logSubscriber {
if logData.LogLevel < log.Level() {
continue
}
message := &Message{
Type: LogMessage,
Data: logData,
}
SendMessage(*message)
}
}()
}
func handleStopLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
}
func init() {
adapter.UrlTestHook = func(name string, delay uint16) {
delayData := &Delay{
Name: name,
}
if delay == 0 {
delayData.Value = -1
} else {
delayData.Value = int32(delay)
}
SendMessage(Message{
Type: DelayMessage,
Data: delayData,
})
}
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
SendMessage(Message{
Type: RequestMessage,
Data: c,
})
}
executor.DefaultProviderLoadedHook = func(providerName string) {
SendMessage(Message{
Type: LoadedMessage,
Data: providerName,
})
}
}

View File

@@ -1,3 +1,5 @@
//go:build cgo
package main
/*
@@ -5,430 +7,10 @@ package main
*/
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
)
var configParams = ConfigExtendedParams{}
var externalProviders = map[string]cp.Provider{}
var isInit = false
//export start
func start() {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
}
//export stop
func stop() {
runLock.Lock()
go func() {
defer runLock.Unlock()
isRunning = false
stopListeners()
}()
}
//export initClash
func initClash(homeDirStr *C.char) bool {
if !isInit {
constant.SetHomeDir(C.GoString(homeDirStr))
isInit = true
}
return isInit
}
//export getIsInit
func getIsInit() bool {
return isInit
}
//export restartClash
func restartClash() bool {
execPath, _ := os.Executable()
go restartExecutable(execPath)
return true
}
//export shutdownClash
func shutdownClash() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
isInit = false
return true
}
//export forceGc
func forceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
var updateLock sync.Mutex
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
state.CurrentRawConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export clearEffect
func clearEffect(s *C.char) {
id := C.GoString(s)
go func() {
_ = removeFile(getProfilePath(id))
_ = removeFile(getProfileProvidersPath(id))
}()
}
//export getProxies
func getProxies() *C.char {
data, err := json.Marshal(tunnel.ProxiesWithProviders())
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export changeProxy
func changeProxy(s *C.char) {
paramsString := C.GoString(s)
var params = &ChangeProxyParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
log.Infoln("Unmarshal ChangeProxyParams %v", err)
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
group, ok := proxies[groupName]
if !ok {
return
}
adapterProxy := group.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
return
}
if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName)
}
if err == nil {
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
}
}
//export getTraffic
func getTraffic() *C.char {
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export getTotalTraffic
func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
}
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export resetTraffic
func resetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
b.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
bridge.SendToPort(i, "")
return false, nil
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
bridge.SendToPort(i, "")
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
defer cancel()
proxies := tunnel.ProxiesWithProviders()
proxy := proxies[params.ProxyName]
delayData := &Delay{
Name: params.ProxyName,
}
if proxy == nil {
delayData.Value = -1
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return false, nil
}
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if err != nil || delay == 0 {
delayData.Value = -1
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return false, nil
}
delayData.Value = int32(delay)
data, _ := json.Marshal(delayData)
bridge.SendToPort(i, string(data))
return false, nil
})
}
//export getVersionInfo
func getVersionInfo() *C.char {
versionInfo := map[string]string{
"clashName": constant.Name,
"version": "1.18.5",
}
data, err := json.Marshal(versionInfo)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export getConnections
func getConnections() *C.char {
snapshot := statistic.DefaultManager.Snapshot()
data, err := json.Marshal(snapshot)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export closeConnections
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
return false
}
return true
})
}
//export closeConnection
func closeConnection(id *C.char) {
connectionId := C.GoString(id)
c := statistic.DefaultManager.Get(connectionId)
if c == nil {
return
}
_ = c.Close()
}
//export getExternalProviders
func getExternalProviders() *C.char {
runLock.Lock()
defer runLock.Unlock()
externalProviders = getExternalProvidersRaw()
eps := make([]ExternalProvider, 0)
for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p)
if err != nil {
continue
}
eps = append(eps, *externalProvider)
}
sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export getExternalProvider
func getExternalProvider(name *C.char) *C.char {
runLock.Lock()
defer runLock.Unlock()
externalProviderName := C.GoString(name)
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return C.CString("")
}
e, err := toExternalProvider(externalProvider)
if err != nil {
return C.CString("")
}
data, err := json.Marshal(e)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export updateGeoData
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
i := int64(port)
geoTypeString := C.GoString(geoType)
geoNameString := C.GoString(geoName)
go func() {
path := constant.Path.Resolve(geoNameString)
switch geoTypeString {
case "MMDB":
err := updater.UpdateMMDBWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := updater.UpdateASNWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIpWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSiteWithPath(path)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
}
bridge.SendToPort(i, "")
}()
}
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, port C.longlong) {
i := int64(port)
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerNameString]
if !exist {
bridge.SendToPort(i, "external provider is not exist")
return
}
err := externalProvider.Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(data))
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerNameString]
if !exist {
bridge.SendToPort(i, "external provider is not exist")
return
}
err := sideUpdateExternalProvider(externalProvider, bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api)
@@ -445,31 +27,156 @@ func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
}
func init() {
provider.HealthcheckHook = func(name string, delay uint16) {
delayData := &Delay{
Name: name,
}
if delay == 0 {
delayData.Value = -1
} else {
delayData.Value = int32(delay)
}
SendMessage(Message{
Type: DelayMessage,
Data: delayData,
})
}
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
SendMessage(Message{
Type: RequestMessage,
Data: c,
})
}
executor.DefaultProviderLoadedHook = func(providerName string) {
SendMessage(Message{
Type: LoadedMessage,
Data: providerName,
})
}
//export initClash
func initClash(homeDirStr *C.char) bool {
return handleInitClash(C.GoString(homeDirStr))
}
//export startListener
func startListener() {
handleStartListener()
}
//export stopListener
func stopListener() {
handleStopListener()
}
//export getIsInit
func getIsInit() bool {
return handleGetIsInit()
}
//export shutdownClash
func shutdownClash() bool {
return handleShutdown()
}
//export forceGc
func forceGc() {
handleForceGc()
}
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
bridge.SendToPort(i, handleValidateConfig(bytes))
}()
}
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export getProxies
func getProxies() *C.char {
return C.CString(handleGetProxies())
}
//export changeProxy
func changeProxy(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
handleChangeProxy(paramsString, func(value string) {
bridge.SendToPort(i, value)
})
}
//export getTraffic
func getTraffic(port C.int) *C.char {
onlyProxy := int(port) == 1
return C.CString(handleGetTraffic(onlyProxy))
}
//export getTotalTraffic
func getTotalTraffic(port C.int) *C.char {
onlyProxy := int(port) == 1
return C.CString(handleGetTotalTraffic(onlyProxy))
}
//export resetTraffic
func resetTraffic() {
handleResetTraffic()
}
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
handleAsyncTestDelay(paramsString, func(value string) {
bridge.SendToPort(i, value)
})
}
//export getConnections
func getConnections() *C.char {
return C.CString(handleGetConnections())
}
//export closeConnections
func closeConnections() {
handleCloseConnections()
}
//export closeConnection
func closeConnection(id *C.char) {
connectionId := C.GoString(id)
handleCloseConnection(connectionId)
}
//export getExternalProviders
func getExternalProviders() *C.char {
return C.CString(handleGetExternalProviders())
}
//export getExternalProvider
func getExternalProvider(externalProviderNameChar *C.char) *C.char {
externalProviderName := C.GoString(externalProviderNameChar)
return C.CString(handleGetExternalProvider(externalProviderName))
}
//export updateGeoData
func updateGeoData(geoTypeChar *C.char, geoNameChar *C.char, port C.longlong) {
i := int64(port)
geoType := C.GoString(geoTypeChar)
geoName := C.GoString(geoNameChar)
handleUpdateGeoData(geoType, geoName, func(value string) {
bridge.SendToPort(i, value)
})
}
//export updateExternalProvider
func updateExternalProvider(providerNameChar *C.char, port C.longlong) {
i := int64(port)
providerName := C.GoString(providerNameChar)
handleUpdateExternalProvider(providerName, func(value string) {
bridge.SendToPort(i, value)
})
}
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) {
i := int64(port)
providerName := C.GoString(providerNameChar)
data := []byte(C.GoString(dataChar))
handleSideLoadExternalProvider(providerName, data, func(value string) {
bridge.SendToPort(i, value)
})
}
//export startLog
func startLog() {
handleStartLog()
}
//export stopLog
func stopLog() {
handleStopLog()
}

270
core/lib_android.go Normal file
View File

@@ -0,0 +1,270 @@
//go:build android && cgo
package main
import "C"
import (
"core/platform"
"core/state"
t "core/tun"
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
)
type ProcessMap struct {
m sync.Map
}
type FdMap struct {
m sync.Map
}
type Fd struct {
Id int64 `json:"id"`
Value int64 `json:"value"`
}
var (
tunListener *sing_tun.Listener
fdMap FdMap
fdCounter int64 = 0
counter int64 = 0
processMap ProcessMap
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
)
func (cm *ProcessMap) Store(key int64, value string) {
cm.m.Store(key, value)
}
func (cm *ProcessMap) Load(key int64) (string, bool) {
value, ok := cm.m.Load(key)
if !ok || value == nil {
return "", false
}
return value.(string), true
}
func (cm *FdMap) Store(key int64) {
cm.m.Store(key, struct{}{})
}
func (cm *FdMap) Load(key int64) bool {
_, ok := cm.m.Load(key)
return ok
}
//export startTUN
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
if fd == 0 {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
return
}
initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
f := int(fd)
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
}()
}
//export getRunTime
func getRunTime() *C.char {
if runTime == nil {
return C.CString("")
}
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
}
//export stopTun
func stopTun() {
removeSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tunListener != nil {
_ = tunListener.Close()
}
}()
}
//export setFdMap
func setFdMap(fd C.long) {
fdInt := int64(fd)
go func() {
fdMap.Store(fdInt)
}()
}
func markSocket(fd Fd) {
SendMessage(Message{
Type: ProtectMessage,
Data: fd,
})
}
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
fdInt := int64(fd)
timeout := time.After(500 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
markSocket(Fd{
Id: id,
Value: fdInt,
})
for {
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(20 * time.Millisecond)
}
}
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
return "", process.ErrInvalidNetwork
}
id := atomic.AddInt64(&counter, 1)
timeout := time.After(200 * time.Millisecond)
SendMessage(Message{
Type: ProcessMessage,
Data: Process{
Id: id,
Metadata: metadata,
},
})
for {
select {
case <-timeout:
return "", errors.New("package resolver timeout")
default:
value, exists := processMap.Load(id)
if exists {
return value, nil
}
time.Sleep(20 * time.Millisecond)
}
}
}
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
go func() {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {
processMap.Store(processMapItem.Id, processMapItem.Value)
}
}()
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
if state.CurrentState == nil {
return C.CString("")
}
return C.CString(state.CurrentState.CurrentProfileName)
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
if err != nil {
return
}
}
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
go func() {
log.Infoln("[DNS] updateDns %s", dnsList)
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
dns.FlushCacheWithDefaultResolver()
}()
}

View File

@@ -1,38 +0,0 @@
package main
import "C"
import (
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/log"
)
var logSubscriber observable.Subscription[log.Event]
//export startLog
func startLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
logSubscriber = log.Subscribe()
go func() {
for logData := range logSubscriber {
if logData.LogLevel < log.Level() {
continue
}
message := &Message{
Type: LogMessage,
Data: logData,
}
SendMessage(*message)
}
}()
}
//export stopLog
func stopLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
}

View File

@@ -1,10 +1,17 @@
//go:build !cgo
package main
import "C"
import (
"fmt"
"os"
)
func main() {
fmt.Println("init clash")
args := os.Args
if len(args) <= 1 {
fmt.Println("Arguments error")
os.Exit(1)
}
startServer(args[1])
}

8
core/main_cgo.go Normal file
View File

@@ -0,0 +1,8 @@
//go:build cgo
package main
import "C"
func main() {
}

View File

@@ -1,77 +1,13 @@
//go:build !cgo
package main
import (
bridge "core/dart-bridge"
"encoding/json"
"github.com/metacubex/mihomo/constant"
)
var Port int64
var ServicePort int64
type MessageType string
const (
LogMessage MessageType = "log"
ProtectMessage MessageType = "protect"
DelayMessage MessageType = "delay"
ProcessMessage MessageType = "process"
RequestMessage MessageType = "request"
StartedMessage MessageType = "started"
LoadedMessage MessageType = "loaded"
)
type Delay struct {
Name string `json:"name"`
Value int32 `json:"value"`
}
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type Message struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
}
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
if handler, ok := messageHandlers[message.Type]; ok {
handler(s)
} else {
sendToPort(s)
}
}
var messageHandlers = map[MessageType]func(string) bool{
ProtectMessage: sendToServicePort,
ProcessMessage: sendToServicePort,
StartedMessage: conditionalSend,
LoadedMessage: conditionalSend,
}
func sendToPort(s string) bool {
return bridge.SendToPort(Port, s)
}
func sendToServicePort(s string) bool {
return bridge.SendToPort(ServicePort, s)
}
func conditionalSend(s string) bool {
isSuccess := sendToPort(s)
if !isSuccess {
return sendToServicePort(s)
}
return isSuccess
Action{
Method: messageMethod,
}.callback(s)
}

47
core/message_cgo.go Normal file
View File

@@ -0,0 +1,47 @@
//go:build cgo
package main
import (
bridge "core/dart-bridge"
)
var (
Port int64 = -1
ServicePort int64 = -1
)
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
if handler, ok := messageHandlers[message.Type]; ok {
handler(s)
} else {
sendToPort(s)
}
}
var messageHandlers = map[MessageType]func(string) bool{
ProtectMessage: sendToServicePort,
ProcessMessage: sendToServicePort,
StartedMessage: conditionalSend,
LoadedMessage: conditionalSend,
}
func sendToPort(s string) bool {
return bridge.SendToPort(Port, s)
}
func sendToServicePort(s string) bool {
return bridge.SendToPort(ServicePort, s)
}
func conditionalSend(s string) bool {
isSuccess := sendToPort(s)
if !isSuccess {
return sendToServicePort(s)
}
return isSuccess
}

View File

@@ -1,4 +1,4 @@
//go:build android
//go:build android && cgo
package platform

View File

@@ -1,81 +0,0 @@
//go:build android
package main
import "C"
import (
"encoding/json"
"errors"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"sync"
"sync/atomic"
"time"
)
type ProcessMap struct {
m sync.Map
}
func (cm *ProcessMap) Store(key int64, value string) {
cm.m.Store(key, value)
}
func (cm *ProcessMap) Load(key int64) (string, bool) {
value, ok := cm.m.Load(key)
if !ok || value == nil {
return "", false
}
return value.(string), true
}
var counter int64 = 0
var processMap ProcessMap
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
return "", process.ErrInvalidNetwork
}
id := atomic.AddInt64(&counter, 1)
timeout := time.After(200 * time.Millisecond)
SendMessage(Message{
Type: ProcessMessage,
Data: Process{
Id: id,
Metadata: metadata,
},
})
for {
select {
case <-timeout:
return "", errors.New("package resolver timeout")
default:
value, exists := processMap.Load(id)
if exists {
return value, nil
}
time.Sleep(20 * time.Millisecond)
}
}
}
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
go func() {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {
processMap.Store(processMapItem.Id, processMapItem.Value)
}
}()
}

162
core/server.go Normal file
View File

@@ -0,0 +1,162 @@
//go:build !cgo
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"strconv"
)
var conn net.Conn = nil
func startServer(arg string) {
_, err := strconv.Atoi(arg)
if err != nil {
conn, err = net.Dial("unix", arg)
} else {
conn, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", arg))
}
if err != nil {
panic(err.Error())
}
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
reader := bufio.NewReader(conn)
for {
data, err := reader.ReadString('\n')
if err != nil {
return
}
var action = &Action{}
err = json.Unmarshal([]byte(data), action)
if err != nil {
return
}
go handleAction(action)
}
}
func handleAction(action *Action) {
switch action.Method {
case initClashMethod:
data := action.Data.(string)
action.callback(handleInitClash(data))
return
case getIsInitMethod:
action.callback(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
return
case shutdownMethod:
action.callback(handleShutdown())
return
case validateConfigMethod:
data := []byte(action.Data.(string))
action.callback(handleValidateConfig(data))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
action.callback(handleUpdateConfig(data))
return
case getProxiesMethod:
action.callback(handleGetProxies())
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
action.callback(value)
})
return
case getTrafficMethod:
data := action.Data.(bool)
action.callback(handleGetTraffic(data))
return
case getTotalTrafficMethod:
data := action.Data.(bool)
action.callback(handleGetTotalTraffic(data))
return
case resetTrafficMethod:
handleResetTraffic()
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
action.callback(value)
})
return
case getConnectionsMethod:
action.callback(handleGetConnections())
return
case closeConnectionsMethod:
action.callback(handleCloseConnections())
return
case closeConnectionMethod:
id := action.Data.(string)
action.callback(handleCloseConnection(id))
return
case getExternalProvidersMethod:
action.callback(handleGetExternalProviders())
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
action.callback(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
action.callback(err.Error())
return
}
geoType := params["geoType"]
geoName := params["geoName"]
handleUpdateGeoData(geoType, geoName, func(value string) {
action.callback(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
action.callback(value)
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
action.callback(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
action.callback(value)
})
return
case startLogMethod:
handleStartLog()
return
case stopLogMethod:
handleStopLog()
return
case startListenerMethod:
action.callback(handleStartListener())
return
case stopListenerMethod:
action.callback(handleStopListener())
return
}
}

View File

@@ -1,47 +0,0 @@
package main
import "C"
import (
"core/state"
"encoding/json"
"fmt"
)
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
if state.CurrentState == nil {
return C.CString("")
}
return C.CString(state.CurrentState.CurrentProfileName)
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Port: state.CurrentRawConfig.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
if err != nil {
return
}
}

View File

@@ -1,13 +1,11 @@
package state
//go:build android && cgo
import "github.com/metacubex/mihomo/config"
package state
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
var CurrentRawConfig = config.DefaultRawConfig()
type AndroidVpnOptions struct {
Enable bool `json:"enable"`
Port int `json:"port"`
@@ -41,7 +39,6 @@ type AndroidVpnRawOptions struct {
type State struct {
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
OnlyProxy bool `json:"onlyProxy"`
}
var CurrentState = &State{}
@@ -55,7 +52,5 @@ func GetIpv6Address() string {
}
func GetDnsServerAddress() string {
//prefix, _ := netip.ParsePrefix(DefaultIpv4Address)
//return prefix.Addr().String()
return DefaultDnsAddress
}

View File

@@ -1,157 +0,0 @@
//go:build android
package main
import "C"
import (
"core/platform"
t "core/tun"
"errors"
"github.com/metacubex/mihomo/listener/sing_tun"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
)
var tunLock sync.Mutex
var runTime *time.Time
type FdMap struct {
m sync.Map
}
func (cm *FdMap) Store(key int64) {
cm.m.Store(key, struct{}{})
}
func (cm *FdMap) Load(key int64) bool {
_, ok := cm.m.Load(key)
return ok
}
var (
tunListener *sing_tun.Listener
fdMap FdMap
fdCounter int64 = 0
)
//export startTUN
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
if fd == 0 {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
return
}
initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
f := int(fd)
tunListener, _ = t.Start(f)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
}()
}
//export getRunTime
func getRunTime() *C.char {
if runTime == nil {
return C.CString("")
}
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
}
//export stopTun
func stopTun() {
removeSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tunListener != nil {
_ = tunListener.Close()
}
}()
}
var errBlocked = errors.New("blocked")
//export setFdMap
func setFdMap(fd C.long) {
fdInt := int64(fd)
go func() {
fdMap.Store(fdInt)
}()
}
type Fd struct {
Id int64 `json:"id"`
Value int64 `json:"value"`
}
func markSocket(fd Fd) {
SendMessage(Message{
Type: ProtectMessage,
Data: fd,
})
}
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
fdInt := int64(fd)
timeout := time.After(500 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
markSocket(Fd{
Id: id,
Value: fdInt,
})
for {
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(20 * time.Millisecond)
}
}
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

View File

@@ -1,10 +1,11 @@
//go:build android
//go:build android && cgo
package tun
import "C"
import (
"core/state"
"github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
@@ -23,7 +24,7 @@ type Props struct {
Dns6 string `json:"dns6"`
}
func Start(fd int) (*sing_tun.Listener, error) {
func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener, error) {
var prefix4 []netip.Prefix
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
if err != nil {
@@ -46,8 +47,8 @@ func Start(fd int) (*sing_tun.Listener, error) {
options := LC.Tun{
Enable: true,
Device: state.CurrentRawConfig.Tun.Device,
Stack: state.CurrentRawConfig.Tun.Stack,
Device: device,
Stack: stack,
DNSHijack: dnsHijack,
AutoRoute: false,
AutoDetectInterface: false,

View File

@@ -1,8 +1,10 @@
import 'dart:async';
import 'package:animations/animations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
@@ -245,8 +247,10 @@ class ApplicationState extends State<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
await globalState.appController.savePreferences();
super.dispose();
_cancelTimer();
await clashService?.destroy();
await globalState.appController.savePreferences();
await globalState.appController.handleExit();
super.dispose();
}
}

View File

@@ -1,3 +1,4 @@
export 'core.dart';
export 'lib.dart';
export 'message.dart';
export 'service.dart';
export 'message.dart';

View File

@@ -1,42 +1,26 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'generated/clash_ffi.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
class ClashCore {
static ClashCore? _instance;
static final receiver = ReceivePort();
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
DynamicLibrary _getClashLib() {
if (Platform.isWindows) {
return DynamicLibrary.open("libclash.dll");
}
if (Platform.isMacOS) {
return DynamicLibrary.open("libclash.dylib");
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open("libclash.so");
}
throw "Platform is not supported";
}
late ClashInterface clashInterface;
ClashCore._internal() {
lib = _getClashLib();
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
if (Platform.isAndroid) {
clashInterface = clashLib!;
} else {
clashInterface = clashService!;
}
}
factory ClashCore() {
@@ -44,67 +28,62 @@ class ClashCore {
return _instance!;
}
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
shutdown() {
clashFFI.shutdownClash();
lib.close();
}
bool get isInit => clashFFI.getIsInit() == 1;
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
Future<void> _initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
} catch (e) {
exit(0);
}
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
}) async {
await _initGeo();
final homeDirPath = await appPath.getHomeDirPath();
return await clashInterface.init(homeDirPath);
}
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
shutdown() async {
await clashInterface.shutdown();
}
Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
FutureOr<bool> get isInit => clashInterface.isInit;
FutureOr<String> validateConfig(String data) {
return clashInterface.validateConfig(data);
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await clashInterface.updateConfig(updateConfigParams);
}
Future<List<Group>> getProxiesGroups() async {
final proxiesRawString = await clashInterface.getProxies();
return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
@@ -134,256 +113,112 @@ class ClashCore {
});
}
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
return await clashInterface.changeProxy(changeProxyParams);
}
ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if (externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
Future<List<Connection>> getConnections() async {
final res = await clashInterface.getConnections();
final connectionsData = json.decode(res) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnection(String id) {
clashInterface.closeConnection(id);
}
closeConnections() {
clashInterface.closeConnections();
}
Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString =
await clashInterface.getExternalProviders();
return Isolate.run<List<ExternalProvider>>(
() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
},
);
}
Future<ExternalProvider?> getExternalProvider(
String externalProviderName) async {
final externalProvidersRawString =
await clashInterface.getExternalProvider(externalProviderName);
if (externalProvidersRawString == null) {
return null;
}
if (externalProvidersRawString.isEmpty) {
return null;
}
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
}
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
}
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
return clashInterface.sideLoadExternalProvider(
providerName: providerName, data: data);
}
Future<String> updateExternalProvider({
required String providerName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
return completer.future;
}) async {
return clashInterface.updateExternalProvider(providerName);
}
changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar);
startListener() async {
await clashInterface.startListener();
}
start() {
clashFFI.start();
stopListener() async {
await clashInterface.stopListener();
}
stop() {
clashFFI.stop();
Future<Delay> getDelay(String proxyName) async {
final data = await clashInterface.asyncTestDelay(proxyName);
return Delay.fromJson(json.decode(data));
}
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
final completer = Completer<Delay>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(Delay.fromJson(json.decode(message)));
receiver.close();
}
});
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
return completer.future;
Future<Traffic> getTraffic(bool value) async {
final trafficString = await clashInterface.getTraffic(value);
return Traffic.fromMap(json.decode(trafficString));
}
clearEffect(String profileId) {
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(profileIdChar);
malloc.free(profileIdChar);
Future<Traffic> getTotalTraffic(bool value) async {
final totalTrafficString = await clashInterface.getTotalTraffic(value);
return Traffic.fromMap(json.decode(totalTrafficString));
}
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(versionInfoRaw);
return VersionInfo.fromJson(versionInfo);
resetTraffic() {
clashInterface.resetTraffic();
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
}
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
Traffic getTraffic() {
final trafficRaw = clashFFI.getTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
Traffic getTotalTraffic() {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
void resetTraffic() {
clashFFI.resetTraffic();
}
void startLog() {
clashFFI.startLog();
startLog() {
clashInterface.startLog();
}
stopLog() {
clashFFI.stopLog();
}
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
}
updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
clashInterface.stopLog();
}
requestGc() {
clashFFI.forceGc();
}
void stopTun() {
clashFFI.stopTun();
}
void setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
void setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsData =
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
clashFFI.freeCString(connectionsDataRaw);
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
}
closeConnections() {
clashFFI.closeConnections();
clashInterface.forceGc();
}
}

View File

@@ -2362,21 +2362,46 @@ class ClashFFI {
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void start() {
return _start();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
return _initNativeApiBridge(
api,
);
}
late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('start');
late final _start = _startPtr.asFunction<void Function()>();
late final _initNativeApiBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void stop() {
return _stop();
void initMessage(
int port,
) {
return _initMessage(
port,
);
}
late final _stopPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stop');
late final _stop = _stopPtr.asFunction<void Function()>();
late final _initMessagePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
void freeCString(
ffi.Pointer<ffi.Char> s,
) {
return _freeCString(
s,
);
}
late final _freeCStringPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'freeCString');
late final _freeCString =
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
int initClash(
ffi.Pointer<ffi.Char> homeDirStr,
@@ -2392,6 +2417,22 @@ class ClashFFI {
late final _initClash =
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
void startListener() {
return _startListener();
}
late final _startListenerPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('startListener');
late final _startListener = _startListenerPtr.asFunction<void Function()>();
void stopListener() {
return _stopListener();
}
late final _stopListenerPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
int getIsInit() {
return _getIsInit();
}
@@ -2400,14 +2441,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
int restartClash() {
return _restartClash();
}
late final _restartClashPtr =
_lookup<ffi.NativeFunction<GoUint8 Function()>>('restartClash');
late final _restartClash = _restartClashPtr.asFunction<int Function()>();
int shutdownClash() {
return _shutdownClash();
}
@@ -2458,20 +2491,6 @@ class ClashFFI {
late final _updateConfig =
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void clearEffect(
ffi.Pointer<ffi.Char> s,
) {
return _clearEffect(
s,
);
}
late final _clearEffectPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'clearEffect');
late final _clearEffect =
_clearEffectPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getProxies() {
return _getProxies();
}
@@ -2484,37 +2503,48 @@ class ClashFFI {
void changeProxy(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _changeProxy(
s,
port,
);
}
late final _changeProxyPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'changeProxy');
late final _changeProxyPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('changeProxy');
late final _changeProxy =
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getTraffic() {
return _getTraffic();
ffi.Pointer<ffi.Char> getTraffic(
int port,
) {
return _getTraffic(
port,
);
}
late final _getTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'getTraffic');
late final _getTraffic =
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
ffi.Pointer<ffi.Char> getTotalTraffic() {
return _getTotalTraffic();
ffi.Pointer<ffi.Char> getTotalTraffic(
int port,
) {
return _getTotalTraffic(
port,
);
}
late final _getTotalTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'getTotalTraffic');
late final _getTotalTraffic =
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
void resetTraffic() {
return _resetTraffic();
@@ -2541,16 +2571,6 @@ class ClashFFI {
late final _asyncTestDelay = _asyncTestDelayPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getVersionInfo() {
return _getVersionInfo();
}
late final _getVersionInfoPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getVersionInfo');
late final _getVersionInfo =
_getVersionInfoPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getConnections() {
return _getConnections();
}
@@ -2595,10 +2615,10 @@ class ClashFFI {
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getExternalProvider(
ffi.Pointer<ffi.Char> name,
ffi.Pointer<ffi.Char> externalProviderNameChar,
) {
return _getExternalProvider(
name,
externalProviderNameChar,
);
}
@@ -2610,13 +2630,13 @@ class ClashFFI {
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void updateGeoData(
ffi.Pointer<ffi.Char> geoType,
ffi.Pointer<ffi.Char> geoName,
ffi.Pointer<ffi.Char> geoTypeChar,
ffi.Pointer<ffi.Char> geoNameChar,
int port,
) {
return _updateGeoData(
geoType,
geoName,
geoTypeChar,
geoNameChar,
port,
);
}
@@ -2629,11 +2649,11 @@ class ClashFFI {
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerNameChar,
int port,
) {
return _updateExternalProvider(
providerName,
providerNameChar,
port,
);
}
@@ -2646,13 +2666,13 @@ class ClashFFI {
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> data,
ffi.Pointer<ffi.Char> providerNameChar,
ffi.Pointer<ffi.Char> dataChar,
int port,
) {
return _sideLoadExternalProvider(
providerName,
data,
providerNameChar,
dataChar,
port,
);
}
@@ -2665,47 +2685,6 @@ class ClashFFI {
_sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
return _initNativeApiBridge(
api,
);
}
late final _initNativeApiBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void initMessage(
int port,
) {
return _initMessage(
port,
);
}
late final _initMessagePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
void freeCString(
ffi.Pointer<ffi.Char> s,
) {
return _freeCString(
s,
);
}
late final _freeCStringPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'freeCString');
late final _freeCString =
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startLog() {
return _startLog();
}
@@ -2722,6 +2701,51 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
void startTUN(
int fd,
int port,
) {
return _startTUN(
fd,
port,
);
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() {
return _stopTun();
}
late final _stopTunPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
void setProcessMap(
ffi.Pointer<ffi.Char> s,
) {
@@ -2769,51 +2793,6 @@ class ClashFFI {
'setState');
late final _setState =
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void startTUN(
int fd,
int port,
) {
return _startTUN(
fd,
port,
);
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() {
return _stopTun();
}
late final _stopTunPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
}
final class __mbstate_t extends ffi.Union {

59
lib/clash/interface.dart Normal file
View File

@@ -0,0 +1,59 @@
import 'dart:async';
import 'package:fl_clash/models/models.dart';
mixin ClashInterface {
FutureOr<bool> init(String homeDir);
FutureOr<void> shutdown();
FutureOr<bool> get isInit;
forceGc();
FutureOr<String> validateConfig(String data);
Future<String> asyncTestDelay(String proxyName);
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
FutureOr<String> getProxies();
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
Future<bool> startListener();
Future<bool> stopListener();
FutureOr<String> getExternalProviders();
FutureOr<String>? getExternalProvider(String externalProviderName);
Future<String> updateGeoData({
required String geoType,
required String geoName,
});
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
});
Future<String> updateExternalProvider(String providerName);
FutureOr<String> getTraffic(bool value);
FutureOr<String> getTotalTraffic(bool value);
resetTraffic();
startLog();
stopLog();
FutureOr<String> getConnections();
FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections();
}

367
lib/clash/lib.dart Normal file
View File

@@ -0,0 +1,367 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/models/models.dart';
import 'generated/clash_ffi.dart';
import 'interface.dart';
class ClashLib with ClashInterface {
static ClashLib? _instance;
final receiver = ReceivePort();
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
ClashLib._internal() {
lib = DynamicLibrary.open("libclash.so");
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashLib() {
_instance ??= ClashLib._internal();
return _instance!;
}
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
}
@override
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
@override
shutdown() async {
clashFFI.shutdownClash();
lib.close();
}
@override
bool get isInit => clashFFI.getIsInit() == 1;
@override
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
@override
String getProxies() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
return proxiesRawString;
}
@override
String getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return externalProvidersRawString;
}
@override
String getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
return externalProviderRawString;
}
@override
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
@override
Future<String> updateExternalProvider(String providerName) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
return completer.future;
}
@override
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
@override
String getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(connectionsDataRaw);
return connectionsString;
}
@override
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
return true;
}
@override
closeConnections() {
clashFFI.closeConnections();
return true;
}
@override
startListener() async {
clashFFI.startListener();
return true;
}
@override
stopListener() async {
clashFFI.stopListener();
return true;
}
@override
Future<String> asyncTestDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
return completer.future;
}
@override
String getTraffic(bool value) {
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0);
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
return trafficString;
}
@override
String getTotalTraffic(bool value) {
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
clashFFI.freeCString(trafficRaw);
return trafficRaw.cast<Utf8>().toDartString();
}
@override
void resetTraffic() {
clashFFI.resetTraffic();
}
@override
void startLog() {
clashFFI.startLog();
}
@override
stopLog() {
clashFFI.stopLog();
}
@override
forceGc() {
clashFFI.forceGc();
}
/// Android
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
}
stopTun() {
clashFFI.stopTun();
}
updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
}
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
}
final clashLib = Platform.isAndroid ? ClashLib() : null;

View File

@@ -1,42 +1,40 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart';
import 'core.dart';
class ClashMessage {
StreamSubscription? subscription;
final controller = StreamController();
ClashMessage._() {
if (subscription != null) {
subscription!.cancel();
subscription = null;
}
subscription = ClashCore.receiver.listen((message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
clashLib?.receiver.listen(controller.add);
controller.stream.listen(
(message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
}
}
}
});
},
);
}
static final ClashMessage instance = ClashMessage._();

View File

@@ -1,54 +1,414 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/core.dart';
import 'core.dart';
class ClashService with ClashInterface {
static ClashService? _instance;
class ClashService {
Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
} catch (e) {
debugPrint("$e");
exit(0);
Completer<ServerSocket> serverCompleter = Completer();
Completer<Socket> socketCompleter = Completer();
Map<String, Completer> callbackCompleterMap = {};
Process? process;
factory ClashService() {
_instance ??= ClashService._internal();
return _instance!;
}
ClashService._internal() {
_createServer();
startCore();
}
_createServer() async {
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
_handleAction(
Action.fromJson(
json.decode(data.trim()),
),
);
},
);
}
}
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
startCore() async {
if (process != null) {
await shutdown();
}
final serverSocket = await serverCompleter.future;
final arg = Platform.isWindows
? "${serverSocket.port}"
: serverSocket.address.address;
bool isSuccess = false;
if (Platform.isWindows && await system.checkIsAdmin()) {
isSuccess = await request.startCoreByHelper(arg);
}
if (isSuccess) {
return;
}
process = await Process.start(
appPath.corePath,
[
arg,
],
);
process!.stdout.listen((_) {});
}
_deleteSocketFile() async {
if (!Platform.isWindows) {
final file = File(unixSocketPath);
if (await file.exists()) {
await file.delete();
}
}
}
_destroySocket() async {
if (socketCompleter.isCompleted) {
final lastSocket = await socketCompleter.future;
await lastSocket.close();
socketCompleter = Completer();
}
}
_handleAction(Action action) {
final completer = callbackCompleterMap[action.id];
switch (action.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
completer?.complete(action.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
completer?.complete(action.data as String);
return;
case ActionMethod.message:
clashMessage.controller.add(action.data as String);
return;
case ActionMethod.forceGc:
case ActionMethod.startLog:
case ActionMethod.stopLog:
default:
return;
}
}
Future<T> _invoke<T>({
required ActionMethod method,
dynamic data,
Duration? timeout,
FutureOr<T> Function()? onTimeout,
}) async {
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final isInit = clashCore.init(homeDirPath);
return isInit;
final id = "${method.name}#${other.id}";
final socket = await socketCompleter.future;
callbackCompleterMap[id] = Completer<T>();
socket.writeln(
json.encode(
Action(
id: id,
method: method,
data: data,
),
),
);
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
timeout: timeout,
onLast: () {
callbackCompleterMap.remove(id);
},
onTimeout: onTimeout,
functionName: id,
);
}
_prueInvoke({
required ActionMethod method,
dynamic data,
}) async {
final id = "${method.name}#${other.id}";
final socket = await socketCompleter.future;
socket.writeln(
json.encode(
Action(
id: id,
method: method,
data: data,
),
),
);
}
@override
Future<bool> init(String homeDir) {
return _invoke<bool>(
method: ActionMethod.initClash,
data: homeDir,
);
}
@override
shutdown() async {
await _invoke<bool>(
method: ActionMethod.shutdown,
);
if (Platform.isWindows) {
await request.stopCoreByHelper();
}
await _destroySocket();
process?.kill();
process = null;
}
@override
Future<bool> get isInit {
return _invoke<bool>(
method: ActionMethod.getIsInit,
);
}
@override
forceGc() {
_prueInvoke(method: ActionMethod.forceGc);
}
@override
FutureOr<String> validateConfig(String data) {
return _invoke<String>(
method: ActionMethod.validateConfig,
data: data,
);
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await _invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
timeout: const Duration(seconds: 20),
);
}
@override
Future<String> getProxies() {
return _invoke<String>(
method: ActionMethod.getProxies,
);
}
@override
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
return _invoke<String>(
method: ActionMethod.changeProxy,
data: json.encode(changeProxyParams),
);
}
@override
FutureOr<String> getExternalProviders() {
return _invoke<String>(
method: ActionMethod.getExternalProviders,
);
}
@override
FutureOr<String> getExternalProvider(String externalProviderName) {
return _invoke<String>(
method: ActionMethod.getExternalProvider,
data: externalProviderName,
);
}
@override
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
return _invoke<String>(
method: ActionMethod.updateGeoData,
data: json.encode(
{
"geoType": geoType,
"geoName": geoName,
},
),
);
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
return _invoke<String>(
method: ActionMethod.sideLoadExternalProvider,
data: json.encode({
"providerName": providerName,
"data": data,
}),
);
}
@override
Future<String> updateExternalProvider(String providerName) {
return _invoke<String>(
method: ActionMethod.updateExternalProvider,
data: providerName,
);
}
@override
FutureOr<String> getConnections() {
return _invoke<String>(
method: ActionMethod.getConnections,
);
}
@override
Future<bool> closeConnections() {
return _invoke<bool>(
method: ActionMethod.closeConnections,
);
}
@override
Future<bool> closeConnection(String id) {
return _invoke<bool>(
method: ActionMethod.closeConnection,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic(bool value) {
return _invoke<String>(
method: ActionMethod.getTotalTraffic,
data: value,
);
}
@override
FutureOr<String> getTraffic(bool value) {
return _invoke<String>(
method: ActionMethod.getTraffic,
data: value,
);
}
@override
resetTraffic() {
_prueInvoke(method: ActionMethod.resetTraffic);
}
@override
startLog() {
_prueInvoke(method: ActionMethod.startLog);
}
@override
stopLog() {
_prueInvoke(method: ActionMethod.stopLog);
}
@override
Future<bool> startListener() {
return _invoke<bool>(
method: ActionMethod.startListener,
);
}
@override
stopListener() {
return _invoke<bool>(
method: ActionMethod.stopListener,
);
}
@override
Future<String> asyncTestDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
return _invoke<String>(
method: ActionMethod.asyncTestDelay,
data: json.encode(delayParams),
timeout: Duration(
milliseconds: 6000,
),
onTimeout: () {
return json.encode(
Delay(
name: proxyName,
value: -1,
),
);
},
);
}
destroy() async {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
}
}
final clashService = ClashService();
final clashService = system.isDesktop ? ClashService() : null;

View File

@@ -1,33 +1,36 @@
export 'path.dart';
export 'request.dart';
export 'preferences.dart';
export 'constant.dart';
export 'proxy.dart';
export 'other.dart';
export 'num.dart';
export 'navigation.dart';
export 'window.dart';
export 'system.dart';
export 'picker.dart';
export 'android.dart';
export 'launch.dart';
export 'protocol.dart';
export 'datetime.dart';
export 'context.dart';
export 'link.dart';
export 'text.dart';
export 'color.dart';
export 'list.dart';
export 'string.dart';
export 'app_localizations.dart';
export 'color.dart';
export 'constant.dart';
export 'context.dart';
export 'datetime.dart';
export 'function.dart';
export 'package.dart';
export 'measure.dart';
export 'windows.dart';
export 'iterable.dart';
export 'scroll.dart';
export 'icons.dart';
export 'future.dart';
export 'http.dart';
export 'icons.dart';
export 'iterable.dart';
export 'keyboard.dart';
export 'launch.dart';
export 'link.dart';
export 'list.dart';
export 'lock.dart';
export 'measure.dart';
export 'navigation.dart';
export 'navigator.dart';
export 'network.dart';
export 'navigator.dart';
export 'num.dart';
export 'other.dart';
export 'package.dart';
export 'path.dart';
export 'picker.dart';
export 'preferences.dart';
export 'protocol.dart';
export 'proxy.dart';
export 'request.dart';
export 'scroll.dart';
export 'string.dart';
export 'system.dart';
export 'text.dart';
export 'tray.dart';
export 'window.dart';
export 'windows.dart';

View File

@@ -1,15 +1,20 @@
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const helperTag = "2024125";
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
@@ -21,7 +26,7 @@ const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop
? !Platform.isMacOS
? 40
: 26
: 28
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":

View File

@@ -24,8 +24,8 @@ class DAVClient {
},
);
client.setConnectTimeout(8000);
client.setSendTimeout(8000);
client.setReceiveTimeout(8000);
client.setSendTimeout(60000);
client.setReceiveTimeout(60000);
pingCompleter.complete(_ping());
}

42
lib/common/future.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'dart:async';
import 'dart:ui';
extension CompleterExt<T> on Completer<T> {
safeFuture({
Duration? timeout,
VoidCallback? onLast,
FutureOr<T> Function()? onTimeout,
required String functionName,
}) {
final realTimeout = timeout ?? const Duration(seconds: 6);
Timer(realTimeout + Duration(milliseconds: 1000), () {
if (onLast != null) {
onLast();
}
});
return future.withTimeout(
timeout: realTimeout,
functionName: functionName,
onTimeout: onTimeout,
);
}
}
extension FutureExt<T> on Future<T> {
Future<T> withTimeout({
required Duration timeout,
required String functionName,
FutureOr<T> Function()? onTimeout,
}) {
return this.timeout(
timeout,
onTimeout: () async {
if (onTimeout != null) {
return onTimeout();
} else {
throw TimeoutException('$functionName timeout');
}
},
);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides {
@override
@@ -10,6 +11,9 @@ class FlClashHttpOverrides extends HttpOverrides {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
debugPrint("find $url");
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:fl_clash/models/models.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart';
import 'system.dart';
import 'windows.dart';
class AutoLaunch {
static AutoLaunch? _instance;
@@ -26,60 +26,16 @@ class AutoLaunch {
return await launchAtStartup.isEnabled();
}
Future<bool> get windowsIsEnable async {
final res = await Process.run(
'schtasks',
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
runInShell: true,
);
return res.stdout.toString().contains(Platform.resolvedExecutable);
}
Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable();
}
windowsDisable() async {
final res = await Process.run(
'schtasks',
[
'/Delete',
'/TN',
appName,
'/F',
],
runInShell: true,
);
return res.exitCode == 0;
}
Future<bool> windowsEnable() async {
await disable();
return await windows?.registerTask(appName) ?? false;
}
Future<bool> disable() async {
return await launchAtStartup.disable();
}
updateStatus(AutoLaunchState state) async {
final isAdminAutoLaunch = state.isAdminAutoLaunch;
final isAutoLaunch = state.isAutoLaunch;
if (Platform.isWindows && isAdminAutoLaunch) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();
if (!isEnable) {
enable();
}
} else {
windowsDisable();
}
return;
}
if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) {
enable();

30
lib/common/lock.dart Normal file
View File

@@ -0,0 +1,30 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
class SingleInstanceLock {
static SingleInstanceLock? _instance;
RandomAccessFile? _accessFile;
SingleInstanceLock._internal();
factory SingleInstanceLock() {
_instance ??= SingleInstanceLock._internal();
return _instance!;
}
Future<bool> acquire() async {
try {
final lockFilePath = await appPath.getLockFilePath();
final lockFile = File(lockFilePath);
await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write);
await _accessFile?.lock();
return true;
} catch (_) {
return false;
}
}
}
final singleInstanceLock = SingleInstanceLock();

View File

@@ -7,9 +7,9 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
class Other {
Color? getDelayColor(int? delay) {
@@ -19,6 +19,14 @@ class Other {
return const Color(0xFFC57F0A);
}
String get id {
final timestamp = DateTime.now().microsecondsSinceEpoch;
final random = Random();
final randomStr =
String.fromCharCodes(List.generate(8, (_) => random.nextInt(26) + 97));
return "$timestamp$randomStr";
}
String getDateStringLast2(int value) {
var valueRaw = "0$value";
return valueRaw.substring(
@@ -104,7 +112,7 @@ class Other {
String getTrayIconPath({
required Brightness brightness,
}) {
if(Platform.isMacOS){
if (Platform.isMacOS) {
return "assets/images/icon_white.png";
}
final suffix = Platform.isWindows ? "ico" : "png";

View File

@@ -13,34 +13,17 @@ class AppPath {
Completer<Directory> tempDir = Completer();
late String appDirPath;
// Future<Directory> _createDesktopCacheDir() async {
// final dir = Directory(path);
// if (await dir.exists()) {
// await dir.create(recursive: true);
// }
// return dir;
// }
AppPath._internal() {
appDirPath = join(dirname(Platform.resolvedExecutable));
getApplicationSupportDirectory().then((value) {
dataDir.complete(value);
});
getTemporaryDirectory().then((value){
tempDir.complete(value);
getTemporaryDirectory().then((value) {
tempDir.complete(value);
});
getDownloadsDirectory().then((value) {
downloadDir.complete(value);
});
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
// });
// } else {
// _createDesktopCacheDir().then((value) {
// cacheDir.complete(value);
// });
// }
}
factory AppPath() {
@@ -48,6 +31,23 @@ class AppPath {
return _instance!;
}
String get executableExtension {
return Platform.isWindows ? ".exe" : "";
}
String get executableDirPath {
final currentExecutablePath = Platform.resolvedExecutable;
return dirname(currentExecutablePath);
}
String get corePath {
return join(executableDirPath, "FlClashCore$executableExtension");
}
String get helperPath {
return join(executableDirPath, "$appHelperService$executableExtension");
}
Future<String> getDownloadDirPath() async {
final directory = await downloadDir.future;
return directory.path;
@@ -58,6 +58,11 @@ class AppPath {
return directory.path;
}
Future<String> getLockFilePath() async {
final directory = await dataDir.future;
return join(directory.path, "FlClash.lock");
}
Future<String> getProfilesPath() async {
final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName);
@@ -69,6 +74,12 @@ class AppPath {
return join(directory, "$id.yaml");
}
Future<String?> getProvidersPath(String? id) async {
if (id == null) return null;
final directory = await getProfilesPath();
return join(directory, "providers", id);
}
Future<String> get tempPath async {
final directory = await tempDir.future;
return directory.path;

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
@@ -79,25 +81,96 @@ class Request {
for (final source in _ipInfoSources.entries) {
try {
final response = await _dio
.get<Map<String, dynamic>>(
source.key,
cancelToken: cancelToken,
)
.timeout(
httpTimeoutDuration,
);
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.timeout(httpTimeoutDuration);
if (response.statusCode != 200 || response.data == null) {
continue;
}
return source.value(response.data!);
} catch (e) {
if (cancelToken?.isCancelled == true) {
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled";
}
continue;
debugPrint("checkIp error ===> $e");
}
}
return null;
}
Future<bool> pingHelper() async {
try {
final response = await _dio
.get(
"http://$localhost:$helperPort/ping",
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
return (response.data as String) == helperTag;
} catch (_) {
return false;
}
}
Future<bool> startCoreByHelper(String arg) async {
try {
final response = await _dio
.post(
"http://$localhost:$helperPort/start",
data: json.encode({
"path": appPath.corePath,
"arg": arg,
}),
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
final data = response.data as String;
return data.isEmpty;
} catch (_) {
return false;
}
}
Future<bool> stopCoreByHelper() async {
try {
final response = await _dio
.post(
"http://$localhost:$helperPort/stop",
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
final data = response.data as String;
return data.isEmpty;
} catch (_) {
return false;
}
}
}
final request = Request();

View File

@@ -1,11 +1,13 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/input.dart';
import 'package:flutter/services.dart';
import 'window.dart';
class System {
static System? _instance;
@@ -19,12 +21,6 @@ class System {
bool get isDesktop =>
Platform.isWindows || Platform.isMacOS || Platform.isLinux;
get isAdmin async {
if (!Platform.isWindows) return false;
final result = await Process.run('net', ['session'], runInShell: true);
return result.exitCode == 0;
}
Future<int> get version async {
final deviceInfo = await DeviceInfoPlugin().deviceInfo;
return switch (Platform.operatingSystem) {
@@ -35,6 +31,73 @@ class System {
};
}
Future<bool> checkIsAdmin() async {
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
if (Platform.isWindows) {
final result = await windows?.checkService();
return result == WindowsHelperServiceStatus.running;
} else if (Platform.isMacOS) {
final result = await Process.run('stat', ['-f', '%Su:%Sg %Sp', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:admin') && output.contains('rws')) {
return true;
}
return false;
} else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rwx')) {
return true;
}
return false;
}
return true;
}
Future<AuthorizeCode> authorizeCore() async {
final corePath = appPath.corePath.replaceAll(' ', '\\\\ ');
final isAdmin = await checkIsAdmin();
if (isAdmin) {
return AuthorizeCode.none;
}
if (Platform.isWindows) {
final result = await windows?.registerService();
if (result == true) {
return AuthorizeCode.success;
}
return AuthorizeCode.error;
} else if (Platform.isMacOS) {
final shell = 'chown root:admin $corePath; chmod +sx $corePath';
final arguments = [
"-e",
'do shell script "$shell" with administrator privileges',
];
final result = await Process.run("osascript", arguments);
if (result.exitCode != 0) {
return AuthorizeCode.error;
}
return AuthorizeCode.success;
} else if (Platform.isLinux) {
final shell = Platform.environment['SHELL'] ?? 'bash';
final password = await globalState.showCommonDialog<String>(
child: InputDialog(
title: appLocalizations.pleaseInputAdminPassword,
value: '',
),
);
final arguments = [
"-c",
'echo "$password" | sudo -S chown root:root "$corePath" && echo "$password" | sudo -S chmod +sx "$corePath"'
];
final result = await Process.run(shell, arguments);
if (result.exitCode != 0) {
return AuthorizeCode.error;
}
return AuthorizeCode.success;
}
return AuthorizeCode.error;
}
back() async {
await app?.moveTaskToBack();
await window?.hide();

193
lib/common/tray.dart Normal file
View File

@@ -0,0 +1,193 @@
import 'dart:io';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:tray_manager/tray_manager.dart';
import 'app_localizations.dart';
import 'constant.dart';
import 'other.dart';
import 'window.dart';
class Tray {
Future _updateSystemTray({
required Brightness? brightness,
bool force = false,
}) async {
if (Platform.isAndroid) {
return;
}
if (Platform.isLinux || force) {
await trayManager.destroy();
}
await trayManager.setIcon(
other.getTrayIconPath(
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
isTemplate: true,
);
if (!Platform.isLinux) {
await trayManager.setToolTip(
appName,
);
}
}
update({
required AppState appState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false,
}) async {
if (Platform.isAndroid) {
return;
}
if (!Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
onClick: (_) {
window?.show();
},
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
checked: false,
);
menuItems.add(startMenuItem);
menuItems.add(MenuItem.separator());
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == clashConfig.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (!Platform.isWindows) {
final groups = appState.currentGroups;
for (final group in groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {
subMenuItems.add(
MenuItem.checkbox(
label: proxy.name,
checked: appState.selectedMap[group.name] == proxy.name,
onClick: (_) {
final appController = globalState.appController;
appController.config.updateCurrentSelectedMap(
group.name,
proxy.name,
);
appController.changeProxy(
groupName: group.name,
proxyName: proxy.name,
);
},
),
);
}
menuItems.add(
MenuItem.submenu(
label: group.name,
submenu: Menu(
items: subMenuItems,
),
),
);
}
if (groups.isNotEmpty) {
menuItems.add(MenuItem.separator());
}
}
if (appFlowingState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: clashConfig.tun.enable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: config.networkProps.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: config.appSetting.autoLaunch,
);
final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar,
onClick: (_) async {
await _copyEnv(clashConfig.mixedPort);
},
);
menuItems.add(autoStartMenuItem);
menuItems.add(copyEnvVarMenuItem);
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
onClick: (_) async {
await globalState.appController.handleExit();
},
);
menuItems.add(exitMenuItem);
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
}
Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port";
final cmdline = Platform.isWindows
? "set \$env:all_proxy=$url"
: "export all_proxy=$url";
await Clipboard.setData(
ClipboardData(
text: cmdline,
),
);
}
}
final tray = Tray();

View File

@@ -4,12 +4,14 @@ import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';
class Window {
init(WindowProps props, int version) async {
final acquire = await singleInstanceLock.acquire();
if (!acquire) {
exit(0);
}
if (Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance([], "FlClash");
protocol.register("clash");
protocol.register("clashmeta");
protocol.register("flclash");

View File

@@ -1,7 +1,10 @@
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart';
class Windows {
@@ -51,12 +54,84 @@ class Windows {
calloc.free(argumentsPtr);
calloc.free(operationPtr);
if (result <= 32) {
debugPrint("[Windows] runas: $command $arguments resultCode:$result");
if (result < 42) {
return false;
}
return true;
}
_killProcess(int port) async {
final result = await Process.run('netstat', ['-ano']);
final lines = result.stdout.toString().trim().split('\n');
for (final line in lines) {
if (!line.contains(":$port") || !line.contains("LISTENING")) {
continue;
}
final parts = line.trim().split(RegExp(r'\s+'));
final pid = int.tryParse(parts.last);
if (pid != null) {
await Process.run('taskkill', ['/PID', pid.toString(), '/F']);
}
}
}
Future<WindowsHelperServiceStatus> checkService() async {
// final qcResult = await Process.run('sc', ['qc', appHelperService]);
// final qcOutput = qcResult.stdout.toString();
// if (qcResult.exitCode != 0 || !qcOutput.contains(appPath.helperPath)) {
// return WindowsHelperServiceStatus.none;
// }
final result = await Process.run('sc', ['query', appHelperService]);
if(result.exitCode != 0){
return WindowsHelperServiceStatus.none;
}
final output = result.stdout.toString();
if (output.contains("RUNNING") && await request.pingHelper()) {
return WindowsHelperServiceStatus.running;
}
return WindowsHelperServiceStatus.presence;
}
Future<bool> registerService() async {
final status = await checkService();
if (status == WindowsHelperServiceStatus.running) {
return true;
}
await _killProcess(helperPort);
final command = [
"/c",
if (status == WindowsHelperServiceStatus.presence) ...[
"sc",
"delete",
appHelperService,
"/force",
"&&",
],
"sc",
"create",
appHelperService,
'binPath= "${appPath.helperPath}"',
'start= auto',
"&&",
"sc",
"start",
appHelperService,
].join(" ");
final res = runas("cmd.exe", command);
await Future.delayed(
Duration(milliseconds: 300),
);
return res;
}
Future<bool> registerTask(String appName) async {
final taskXml = '''
<?xml version="1.0" encoding="UTF-16"?>

View File

@@ -5,6 +5,7 @@ import 'dart:isolate';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/archive.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -13,7 +14,6 @@ import 'package:path/path.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'clash/core.dart';
import 'common/common.dart';
import 'models/models.dart';
@@ -28,6 +28,7 @@ class AppController {
late Function addCheckIpNumDebounce;
late Function applyProfileDebounce;
late Function savePreferencesDebounce;
late Function changeProxyDebounce;
AppController(this.context) {
appState = context.read<AppState>();
@@ -43,6 +44,13 @@ class AppController {
applyProfileDebounce = debounce<Function()>(() async {
await applyProfile(isPrue: true);
});
changeProxyDebounce = debounce((String groupName, String proxyName) async {
await changeProxy(
groupName: groupName,
proxyName: proxyName,
);
await updateGroups();
});
addCheckIpNumDebounce = debounce(() {
appState.checkIpNum++;
});
@@ -51,6 +59,14 @@ class AppController {
});
}
restartCore() async {
await globalState.restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
}
updateStatus(bool isStart) async {
if (isStart) {
await globalState.handleStart();
@@ -60,23 +76,31 @@ class AppController {
updateRunTime,
updateTraffic,
];
if (!Platform.isAndroid) {
applyProfileDebounce();
final currentLastModified =
await config.getCurrentProfile()?.profileLastModified;
if (currentLastModified == null ||
globalState.lastProfileModified == null) {
addCheckIpNumDebounce();
return;
}
if (currentLastModified <= (globalState.lastProfileModified ?? 0)) {
addCheckIpNumDebounce();
return;
}
applyProfileDebounce();
} else {
await globalState.handleStop();
clashCore.resetTraffic();
await clashCore.resetTraffic();
appFlowingState.traffics = [];
appFlowingState.totalTraffic = Traffic();
appFlowingState.runTime = null;
await Future.delayed(
Duration(milliseconds: 300),
);
addCheckIpNumDebounce();
}
}
updateCoreVersionInfo() {
globalState.updateCoreVersionInfo(appState);
}
updateRunTime() {
final startTime = globalState.startTime;
if (startTime != null) {
@@ -90,6 +114,7 @@ class AppController {
updateTraffic() {
globalState.updateTraffic(
config: config,
appFlowingState: appFlowingState,
);
}
@@ -102,7 +127,7 @@ class AppController {
deleteProfile(String id) async {
config.deleteProfileById(id);
clashCore.clearEffect(id);
clearEffect(id);
if (config.currentProfileId == id) {
if (config.profiles.isNotEmpty) {
final updateId = config.profiles.first.id;
@@ -130,6 +155,7 @@ class AppController {
if (commonScaffoldState?.mounted != true) return;
await commonScaffoldState?.loadingRun(() async {
await globalState.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
@@ -213,8 +239,8 @@ class AppController {
changeProxy({
required String groupName,
required String proxyName,
}) {
globalState.changeProxy(
}) async {
await globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
@@ -234,22 +260,16 @@ class AppController {
}
handleExit() async {
await updateStatus(false);
await proxy?.stopProxy();
await savePreferences();
clashCore.shutdown();
try {
await updateStatus(false);
await clashCore.shutdown();
await clashService?.destroy();
await proxy?.stopProxy();
await savePreferences();
} catch (_) {}
system.exit();
}
updateLogStatus() {
if (config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
appFlowingState.logs = [];
}
}
autoCheckUpdate() async {
if (!config.appSetting.autoCheckUpdate) return;
final res = await request.checkForUpdate();
@@ -304,10 +324,20 @@ class AppController {
if (!isDisclaimerAccepted) {
handleExit();
}
updateLogStatus();
if (!config.appSetting.silentLaunch) {
window?.show();
}
await globalState.initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
await _initStatus();
autoUpdateProfiles();
autoCheckUpdate();
}
_initStatus() async {
if (Platform.isAndroid) {
globalState.updateStartTime();
}
@@ -316,8 +346,6 @@ class AppController {
} else {
await updateStatus(config.appSetting.autoRun);
}
autoUpdateProfiles();
autoCheckUpdate();
}
setDelay(Delay delay) {
@@ -525,6 +553,19 @@ class AppController {
'';
}
clearEffect(String profileId) async {
final profilePath = await appPath.getProfilePath(profileId);
final providersPath = await appPath.getProvidersPath(profileId);
return await Isolate.run(() async {
if (profilePath != null) {
await File(profilePath).delete(recursive: true);
}
if (providersPath != null) {
await File(providersPath).delete(recursive: true);
}
});
}
updateTun() {
clashConfig.tun = clashConfig.tun.copyWith(
enable: !clashConfig.tun.enable,
@@ -547,12 +588,6 @@ class AppController {
);
}
updateAdminAutoLaunch() {
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: !config.appSetting.adminAutoLaunch,
);
}
updateVisible() async {
final visible = await window?.isVisible();
if (visible != null && !visible) {
@@ -602,7 +637,7 @@ class AppController {
}
updateTray([bool focus = false]) async {
globalState.updateTray(
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,

View File

@@ -178,3 +178,39 @@ enum RouteMode {
bypassPrivate,
config,
}
enum ActionMethod {
message,
initClash,
getIsInit,
forceGc,
shutdown,
validateConfig,
updateConfig,
getProxies,
changeProxy,
getTraffic,
getTotalTraffic,
resetTraffic,
asyncTestDelay,
getConnections,
closeConnections,
closeConnection,
getExternalProviders,
getExternalProvider,
updateGeoData,
updateExternalProvider,
sideLoadExternalProvider,
startLog,
stopLog,
startListener,
stopListener,
}
enum AuthorizeCode { none, success, error }
enum WindowsHelperServiceStatus {
none,
presence,
running,
}

View File

@@ -60,32 +60,6 @@ class UsageSwitch extends StatelessWidget {
}
}
class AdminAutoLaunchItem extends StatelessWidget {
const AdminAutoLaunchItem({super.key});
@override
Widget build(BuildContext context) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.adminAutoLaunch,
builder: (_, adminAutoLaunch, __) {
return ListItem.switchItem(
title: Text(appLocalizations.adminAutoLaunch),
subtitle: Text(appLocalizations.adminAutoLaunchDesc),
delegate: SwitchDelegate(
value: adminAutoLaunch,
onChanged: (bool value) async {
final config = globalState.appController.config;
config.appSetting = config.appSetting.copyWith(
adminAutoLaunch: value,
);
},
),
);
},
);
}
}
class ApplicationSettingFragment extends StatelessWidget {
const ApplicationSettingFragment({super.key});
@@ -134,8 +108,6 @@ class ApplicationSettingFragment extends StatelessWidget {
);
},
),
if(Platform.isWindows)
const AdminAutoLaunchItem(),
if (system.isDesktop)
Selector<Config, bool>(
selector: (_, config) => config.appSetting.silentLaunch,

View File

@@ -27,18 +27,19 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
WidgetsBinding.instance.addPostFrameCallback((_) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
(timer) async {
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
connections: await clashCore.getConnections(),
);
},
);
@@ -66,10 +67,10 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
width: 8,
),
IconButton(
onPressed: () {
onPressed: () async {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
connections: await clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
@@ -99,10 +100,11 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
);
}
_handleBlockConnection(String id) {
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(),
);
}
@override
@@ -239,10 +241,10 @@ class ConnectionsSearchDelegate extends SearchDelegate {
);
}
_handleBlockConnection(String id) {
_handleBlockConnection(String id) async {
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
connections: await clashCore.getConnections(),
);
}

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math';
import 'package:fl_clash/common/common.dart';
@@ -68,11 +67,10 @@ class _DashboardFragmentState extends State<DashboardFragment> {
// child: const VPNSwitch(),
// ),
if (system.isDesktop) ...[
if (Platform.isWindows)
GridItem(
crossAxisCellCount: switchCount,
child: const TUNButton(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const TUNButton(),
),
GridItem(
crossAxisCellCount: switchCount,
child: const SystemProxyButton(),

View File

@@ -9,6 +9,13 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
const NetworkDetection({super.key});
@@ -17,12 +24,6 @@ class NetworkDetection extends StatefulWidget {
}
class _NetworkDetectionState extends State<NetworkDetection> {
final networkDetectionState = ValueNotifier<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
bool? _preIsStart;
Function? _checkIpDebounce;
Timer? _setTimeoutTimer;
@@ -55,17 +56,20 @@ class _NetworkDetectionState extends State<NetworkDetection> {
);
return;
}
_setTimeoutTimer = Timer(const Duration(milliseconds: 2000), () {
_clearSetTimeoutTimer();
_setTimeoutTimer = Timer(const Duration(milliseconds: 300), () {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: false,
ipInfo: null,
);
});
} catch (_) {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
} catch (e) {
if (e.toString() == "cancelled") {
networkDetectionState.value = networkDetectionState.value.copyWith(
isTesting: true,
ipInfo: null,
);
}
}
}
@@ -92,9 +96,8 @@ class _NetworkDetectionState extends State<NetworkDetection> {
}
@override
void dispose() {
dispose() {
super.dispose();
networkDetectionState.dispose();
}
String countryCodeToEmoji(String countryCode) {
@@ -156,7 +159,8 @@ class _NetworkDetectionState extends State<NetworkDetection> {
.textTheme
.titleLarge
?.copyWith(
fontFamily: FontFamily.twEmoji.value,
fontFamily:
FontFamily.twEmoji.value,
),
),
)

View File

@@ -115,11 +115,10 @@ class ProxyCard extends StatelessWidget {
groupName,
nextProxyName,
);
appController.changeProxy(
groupName: groupName,
proxyName: nextProxyName,
);
await appController.updateGroupDebounce();
await appController.changeProxyDebounce([
groupName,
nextProxyName,
]);
return;
}
globalState.showSnackBar(

View File

@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/app.dart';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
@@ -56,7 +56,7 @@ class _ProvidersState extends State<Providers> {
providerName: provider.name,
);
appState.setProvider(
clashCore.getExternalProvider(provider.name),
await clashCore.getExternalProvider(provider.name),
);
},
);
@@ -122,7 +122,7 @@ class ProviderItem extends StatelessWidget {
if (message.isNotEmpty) throw message;
});
appState.setProvider(
clashCore.getExternalProvider(provider.name),
await clashCore.getExternalProvider(provider.name),
);
});
await globalState.appController.updateGroupDebounce();
@@ -143,7 +143,7 @@ class ProviderItem extends StatelessWidget {
);
if (message.isNotEmpty) throw message;
appState.setProvider(
clashCore.getExternalProvider(provider.name),
await clashCore.getExternalProvider(provider.name),
);
if (message.isNotEmpty) throw message;
});

View File

@@ -1,4 +1,5 @@
import 'dart:math';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
@@ -10,6 +11,8 @@ import 'package:provider/provider.dart';
import 'card.dart';
import 'common.dart';
List<Proxy> currentProxies = [];
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
class ProxiesTabFragment extends StatefulWidget {
@@ -28,7 +31,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
@override
void dispose() {
super.dispose();
_tabController?.dispose();
_destroyTabController();
}
scrollToGroupSelected() {
@@ -106,6 +109,36 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
);
}
_tabControllerListener([int? index]) {
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
if (_tabController?.index == null) {
return;
}
final currentGroup = currentGroups[index ?? _tabController!.index];
currentProxies = currentGroup.all;
appController.config.updateCurrentGroupName(
currentGroup.name,
);
}
_destroyTabController() {
_tabController?.removeListener(_tabControllerListener);
_tabController?.dispose();
_tabController = null;
}
_updateTabController(int length, int index) {
final realIndex = index == -1 ? 0 : index;
_tabController ??= TabController(
length: length,
initialIndex: realIndex,
vsync: this,
);
_tabControllerListener(realIndex);
_tabController?.addListener(_tabControllerListener);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
@@ -119,8 +152,7 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
},
shouldRebuild: (prev, next) {
if (!stringListEquality.equals(prev.groupNames, next.groupNames)) {
_tabController?.dispose();
_tabController = null;
_destroyTabController();
return true;
}
return false;
@@ -129,12 +161,8 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
final index = state.groupNames.indexWhere(
(item) => item == state.currentGroupName,
);
_tabController ??= TabController(
length: state.groupNames.length,
initialIndex: index == -1 ? 0 : index,
vsync: this,
);
GroupNameKeyMap keyMap = {};
_updateTabController(state.groupNames.length, index);
final GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
return KeepScope(
@@ -167,16 +195,6 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
left: 16,
right: 16 + (value ? 16 : 0),
),
onTap: (index) {
final appController = globalState.appController;
final currentGroups =
appController.appState.currentGroups;
if (currentGroups.length > index) {
appController.config.updateCurrentGroupName(
currentGroups[index].name,
);
}
},
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
@@ -243,14 +261,13 @@ class ProxyGroupView extends StatefulWidget {
class ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final _controller = ScrollController();
List<Proxy> _lastProxies = [];
String get groupName => widget.groupName;
_delayTest(List<Proxy> proxies) async {
_delayTest() async {
if (isLock) return;
isLock = true;
await delayTest(proxies);
await delayTest(currentProxies);
isLock = false;
}
@@ -269,7 +286,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: _lastProxies,
proxies: currentProxies,
),
_controller.position.maxScrollExtent,
),
@@ -278,7 +295,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
);
}
initFab(bool isCurrent, List<Proxy> proxies) {
initFab(bool isCurrent) {
if (!isCurrent) {
return;
}
@@ -287,9 +304,7 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.floatingActionButton = DelayTestButton(
onClick: () async {
await _delayTest(
proxies,
);
await _delayTest();
},
);
});
@@ -319,11 +334,10 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
_lastProxies = sortedProxies;
return ActiveBuilder(
label: "proxies",
builder: (isCurrent, child) {
initFab(isCurrent, proxies);
initFab(isCurrent);
return child!;
},
child: Align(
@@ -381,7 +395,9 @@ class _DelayTestButtonState extends State<DelayTestButton>
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
if (mounted) {
_controller.reverse();
}
}
@override

View File

@@ -330,5 +330,7 @@
"routeMode_bypassPrivate": "Bypass private route address",
"routeMode_config": "Use config",
"routeAddress": "Route address",
"routeAddressDesc": "Config listen route address"
"routeAddressDesc": "Config listen route address",
"pleaseInputAdminPassword": "Please enter the admin password",
"copyEnvVar": "Copying environment variables"
}

View File

@@ -330,5 +330,7 @@
"routeMode_bypassPrivate": "绕过私有路由地址",
"routeMode_config": "使用配置",
"routeAddress": "路由地址",
"routeAddressDesc": "配置监听路由地址"
"routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量"
}

View File

@@ -121,6 +121,8 @@ class MessageLookup extends MessageLookupByLibrary {
"View current connections data"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage(
"Copying environment variables"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
"coreInfo": MessageLookupByLibrary.simpleMessage("Core info"),
"country": MessageLookupByLibrary.simpleMessage("Country"),
@@ -326,6 +328,8 @@ class MessageLookup extends MessageLookupByLibrary {
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
"pleaseBindWebDAV":
MessageLookupByLibrary.simpleMessage("Please bind WebDAV"),
"pleaseInputAdminPassword": MessageLookupByLibrary.simpleMessage(
"Please enter the admin password"),
"pleaseUploadFile":
MessageLookupByLibrary.simpleMessage("Please upload file"),
"pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage(

View File

@@ -98,6 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"copyEnvVar": MessageLookupByLibrary.simpleMessage("复制环境变量"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
"coreInfo": MessageLookupByLibrary.simpleMessage("内核信息"),
"country": MessageLookupByLibrary.simpleMessage("区域"),
@@ -259,6 +260,8 @@ class MessageLookup extends MessageLookupByLibrary {
"passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"),
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
"pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"),
"pleaseInputAdminPassword":
MessageLookupByLibrary.simpleMessage("请输入管理员密码"),
"pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"),
"pleaseUploadValidQrcode":
MessageLookupByLibrary.simpleMessage("请上传有效的二维码"),

View File

@@ -3369,6 +3369,26 @@ class AppLocalizations {
args: [],
);
}
/// `Please enter the admin password`
String get pleaseInputAdminPassword {
return Intl.message(
'Please enter the admin password',
name: 'pleaseInputAdminPassword',
desc: '',
args: [],
);
}
/// `Copying environment variables`
String get copyEnvVar {
return Intl.message(
'Copying environment variables',
name: 'copyEnvVar',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -8,14 +8,15 @@ import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'application.dart';
import 'common/common.dart';
import 'l10n/l10n.dart';
import 'models/models.dart';
import 'common/common.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
clashCore.initMessage();
clashLib?.initMessage();
globalState.packageInfo = await PackageInfo.fromPlatform();
final version = await system.version;
final config = await preferences.getConfig() ?? Config();
@@ -36,17 +37,12 @@ Future<void> main() async {
openLogs: config.appSetting.openLogs,
hasProxies: false,
);
globalState.updateTray(
tray.update(
appState: appState,
appFlowingState: appFlowingState,
config: config,
clashConfig: clashConfig,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
HttpOverrides.global = FlClashHttpOverrides();
runAppWithPreferences(
const Application(),
@@ -69,39 +65,66 @@ Future<void> vpnService() async {
other.getLocaleForString(config.appSetting.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
final appState = AppState(
mode: clashConfig.mode,
selectedMap: config.currentSelectedMap,
version: version,
);
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await app?.tip(appLocalizations.startVpn);
globalState
.updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: false,
)
.then(
(_) async {
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic(config: config);
globalState.updateFunctionLists = [
() {
globalState.updateTraffic(config: config);
}
];
},
);
vpn?.setServiceMessageHandler(
ServiceMessageHandler(
onProtect: (Fd fd) async {
await vpn?.setProtect(fd.value);
clashCore.setFdMap(fd.id);
clashLib?.setFdMap(fd.id);
},
onProcess: (Process process) async {
onProcess: (ProcessData process) async {
final packageName = await vpn?.resolverProcess(process);
clashCore.setProcessMap(
clashLib?.setProcessMap(
ProcessMapItem(
id: process.id,
value: packageName ?? "",
),
);
},
onStarted: (String runTime) async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
},
onLoaded: (String groupName) {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
@@ -114,43 +137,20 @@ Future<void> vpnService() async {
},
),
);
await app?.tip(appLocalizations.startVpn);
await globalState.handleStart();
tile?.addListener(
TileListenerWithVpn(
onStop: () async {
await app?.tip(appLocalizations.stopVpn);
await globalState.handleStop();
clashCore.shutdown();
exit(0);
},
),
);
globalState.updateTraffic();
globalState.updateFunctionLists = [
() {
globalState.updateTraffic();
}
];
}
@immutable
class ServiceMessageHandler with ServiceMessageListener {
final Function(Fd fd) _onProtect;
final Function(Process process) _onProcess;
final Function(String runTime) _onStarted;
final Function(ProcessData process) _onProcess;
final Function(String providerName) _onLoaded;
const ServiceMessageHandler({
required Function(Fd fd) onProtect,
required Function(Process process) onProcess,
required Function(String runTime) onStarted,
required Function(ProcessData process) onProcess,
required Function(String providerName) onLoaded,
}) : _onProtect = onProtect,
_onProcess = onProcess,
_onStarted = onStarted,
_onLoaded = onLoaded;
@override
@@ -159,15 +159,10 @@ class ServiceMessageHandler with ServiceMessageListener {
}
@override
onProcess(Process process) {
onProcess(ProcessData process) {
_onProcess(process);
}
@override
onStarted(String runTime) {
_onStarted(runTime);
}
@override
onLoaded(String providerName) {
_onLoaded(providerName);

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:flutter/material.dart';
@@ -23,6 +24,28 @@ class _AndroidContainerState extends State<AndroidManager> {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.networkProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
routeAddress: clashConfig.routeAddress,
),
builder: (__, state, child) {
clashLib?.setState(state);
return child!;
},
child: child,
);
}
Widget _excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.appSetting.hidden,
@@ -36,6 +59,10 @@ class _AndroidContainerState extends State<AndroidManager> {
@override
Widget build(BuildContext context) {
return _excludeContainer(widget.child);
return _updateCoreState(
_excludeContainer(
widget.child,
),
);
}
}

View File

@@ -57,41 +57,20 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
);
}
Widget _updateCoreState(Widget child) {
return Selector2<Config, ClashConfig, CoreState>(
selector: (_, config, clashConfig) => CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
bypassDomain: config.networkProps.bypassDomain,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
routeAddress: clashConfig.routeAddress,
),
builder: (__, state, child) {
clashCore.setState(state);
return child!;
},
child: child,
);
}
_changeProfile() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final appController = globalState.appController;
appController.appState.delayMap = {};
await appController.applyProfile();
});
}
Widget _changeProfileContainer(Widget child) {
return Selector<Config, String?>(
selector: (_, config) => config.currentProfileId,
shouldRebuild: (prev, next) {
if (prev != next) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final appController = globalState.appController;
appController.appState.delayMap = {};
appController.applyProfile();
});
}
return prev != next;
},
builder: (__, state, child) {
_changeProfile();
return child!;
},
child: child,
@@ -101,10 +80,8 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
@override
Widget build(BuildContext context) {
return _changeProfileContainer(
_updateCoreState(
_updateContainer(
widget.child,
),
_updateContainer(
widget.child,
),
);
}
@@ -158,7 +135,7 @@ class _ClashContainerState extends State<ClashManager> with AppMessageListener {
Future<void> onLoaded(String providerName) async {
final appController = globalState.appController;
appController.appState.setProvider(
clashCore.getExternalProvider(
await clashCore.getExternalProvider(
providerName,
),
);

View File

@@ -30,13 +30,15 @@ class _TrayContainerState extends State<TrayManager> with TrayListener {
selector: (_, appState, appFlowingState, config, clashConfig) =>
TrayState(
mode: clashConfig.mode,
adminAutoLaunch: config.appSetting.adminAutoLaunch,
autoLaunch: config.appSetting.autoLaunch,
isStart: appFlowingState.isStart,
locale: config.appSetting.locale,
systemProxy: config.networkProps.systemProxy,
tunEnable: clashConfig.tun.enable,
brightness: appState.brightness,
port: clashConfig.mixedPort,
groups: appState.groups,
map: appState.selectedMap,
),
shouldRebuild: (prev, next) {
if (prev != next) {

View File

@@ -28,7 +28,6 @@ class _WindowContainerState extends State<WindowManager>
return Selector<Config, AutoLaunchState>(
selector: (_, config) => AutoLaunchState(
isAutoLaunch: config.appSetting.autoLaunch,
isAdminAutoLaunch: config.appSetting.adminAutoLaunch,
),
builder: (_, state, child) {
updateLaunchDebounce ??= debounce((AutoLaunchState state) {
@@ -88,8 +87,9 @@ class _WindowContainerState extends State<WindowManager>
}
@override
void onTaskbarCreated() {
Future<void> onTaskbarCreated() async {
globalState.appController.updateTray(true);
await globalState.appController.restartCore();
super.onTaskbarCreated();
}

View File

@@ -1,8 +1,9 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'common.dart';
import 'ffi.dart';
import 'core.dart';
import 'profile.dart';
typedef DelayMap = Map<String, int?>;

View File

@@ -482,6 +482,28 @@ class ClashConfig extends ChangeNotifier {
notifyListeners();
}
ClashConfig copyWith() {
return ClashConfig()
..mixedPort = _mixedPort
..mode = _mode
..ipv6 = _ipv6
..findProcessMode = _findProcessMode
..allowLan = _allowLan
..tcpConcurrent = _tcpConcurrent
..logLevel = _logLevel
..tun = tun
..unifiedDelay = _unifiedDelay
..geodataLoader = _geodataLoader
..externalController = _externalController
..keepAliveInterval = _keepAliveInterval
..dns = _dns
..geoXUrl = _geoXUrl
..routeMode = _routeMode
..includeRouteAddress = _includeRouteAddress
..rules = _rules
..hosts = _hosts;
}
Map<String, dynamic> toJson() {
return _$ClashConfigToJson(this);
}

View File

@@ -20,7 +20,6 @@ class AppSetting with _$AppSetting {
String? locale,
@Default(false) bool onlyProxy,
@Default(false) bool autoLaunch,
@Default(false) bool adminAutoLaunch,
@Default(false) bool silentLaunch,
@Default(false) bool autoRun,
@Default(false) bool openLogs,

View File

@@ -1,11 +1,35 @@
// ignore_for_file: invalid_annotation_target
import 'dart:convert';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'generated/ffi.freezed.dart';
part 'generated/ffi.g.dart';
part 'generated/core.freezed.dart';
part 'generated/core.g.dart';
abstract mixin class AppMessageListener {
void onLog(Log log) {}
void onDelay(Delay delay) {}
void onRequest(Connection connection) {}
void onStarted(String runTime) {}
void onLoaded(String providerName) {}
}
abstract mixin class ServiceMessageListener {
onProtect(Fd fd) {}
onProcess(ProcessData process) {}
onStarted(String runTime) {}
onLoaded(String providerName) {}
}
@freezed
class CoreState with _$CoreState {
@@ -124,14 +148,14 @@ class Now with _$Now {
}
@freezed
class Process with _$Process {
const factory Process({
class ProcessData with _$ProcessData {
const factory ProcessData({
required int id,
required Metadata metadata,
}) = _Process;
}) = _ProcessData;
factory Process.fromJson(Map<String, Object?> json) =>
_$ProcessFromJson(json);
factory ProcessData.fromJson(Map<String, Object?> json) =>
_$ProcessDataFromJson(json);
}
@freezed
@@ -212,24 +236,19 @@ class TunProps with _$TunProps {
_$TunPropsFromJson(json);
}
abstract mixin class AppMessageListener {
void onLog(Log log) {}
@freezed
class Action with _$Action {
const factory Action({
required ActionMethod method,
required dynamic data,
required String id,
}) = _Action;
void onDelay(Delay delay) {}
void onRequest(Connection connection) {}
void onStarted(String runTime) {}
void onLoaded(String providerName) {}
factory Action.fromJson(Map<String, Object?> json) => _$ActionFromJson(json);
}
abstract mixin class ServiceMessageListener {
onProtect(Fd fd) {}
onProcess(Process process) {}
onStarted(String runTime) {}
onLoaded(String providerName) {}
extension ActionExt on Action {
String get toJson {
return json.encode(this);
}
}

View File

@@ -23,7 +23,6 @@ mixin _$AppSetting {
String? get locale => throw _privateConstructorUsedError;
bool get onlyProxy => throw _privateConstructorUsedError;
bool get autoLaunch => throw _privateConstructorUsedError;
bool get adminAutoLaunch => throw _privateConstructorUsedError;
bool get silentLaunch => throw _privateConstructorUsedError;
bool get autoRun => throw _privateConstructorUsedError;
bool get openLogs => throw _privateConstructorUsedError;
@@ -56,7 +55,6 @@ abstract class $AppSettingCopyWith<$Res> {
{String? locale,
bool onlyProxy,
bool autoLaunch,
bool adminAutoLaunch,
bool silentLaunch,
bool autoRun,
bool openLogs,
@@ -88,7 +86,6 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
Object? locale = freezed,
Object? onlyProxy = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? silentLaunch = null,
Object? autoRun = null,
Object? openLogs = null,
@@ -114,10 +111,6 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting>
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
silentLaunch: null == silentLaunch
? _value.silentLaunch
: silentLaunch // ignore: cast_nullable_to_non_nullable
@@ -178,7 +171,6 @@ abstract class _$$AppSettingImplCopyWith<$Res>
{String? locale,
bool onlyProxy,
bool autoLaunch,
bool adminAutoLaunch,
bool silentLaunch,
bool autoRun,
bool openLogs,
@@ -208,7 +200,6 @@ class __$$AppSettingImplCopyWithImpl<$Res>
Object? locale = freezed,
Object? onlyProxy = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? silentLaunch = null,
Object? autoRun = null,
Object? openLogs = null,
@@ -234,10 +225,6 @@ class __$$AppSettingImplCopyWithImpl<$Res>
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
silentLaunch: null == silentLaunch
? _value.silentLaunch
: silentLaunch // ignore: cast_nullable_to_non_nullable
@@ -293,7 +280,6 @@ class _$AppSettingImpl implements _AppSetting {
{this.locale,
this.onlyProxy = false,
this.autoLaunch = false,
this.adminAutoLaunch = false,
this.silentLaunch = false,
this.autoRun = false,
this.openLogs = false,
@@ -319,9 +305,6 @@ class _$AppSettingImpl implements _AppSetting {
final bool autoLaunch;
@override
@JsonKey()
final bool adminAutoLaunch;
@override
@JsonKey()
final bool silentLaunch;
@override
@JsonKey()
@@ -356,7 +339,7 @@ class _$AppSettingImpl implements _AppSetting {
@override
String toString() {
return 'AppSetting(locale: $locale, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, adminAutoLaunch: $adminAutoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
return 'AppSetting(locale: $locale, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)';
}
@override
@@ -369,8 +352,6 @@ class _$AppSettingImpl implements _AppSetting {
other.onlyProxy == onlyProxy) &&
(identical(other.autoLaunch, autoLaunch) ||
other.autoLaunch == autoLaunch) &&
(identical(other.adminAutoLaunch, adminAutoLaunch) ||
other.adminAutoLaunch == adminAutoLaunch) &&
(identical(other.silentLaunch, silentLaunch) ||
other.silentLaunch == silentLaunch) &&
(identical(other.autoRun, autoRun) || other.autoRun == autoRun) &&
@@ -399,7 +380,6 @@ class _$AppSettingImpl implements _AppSetting {
locale,
onlyProxy,
autoLaunch,
adminAutoLaunch,
silentLaunch,
autoRun,
openLogs,
@@ -433,7 +413,6 @@ abstract class _AppSetting implements AppSetting {
{final String? locale,
final bool onlyProxy,
final bool autoLaunch,
final bool adminAutoLaunch,
final bool silentLaunch,
final bool autoRun,
final bool openLogs,
@@ -456,8 +435,6 @@ abstract class _AppSetting implements AppSetting {
@override
bool get autoLaunch;
@override
bool get adminAutoLaunch;
@override
bool get silentLaunch;
@override
bool get autoRun;

View File

@@ -56,7 +56,6 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> json) =>
locale: json['locale'] as String?,
onlyProxy: json['onlyProxy'] as bool? ?? false,
autoLaunch: json['autoLaunch'] as bool? ?? false,
adminAutoLaunch: json['adminAutoLaunch'] as bool? ?? false,
silentLaunch: json['silentLaunch'] as bool? ?? false,
autoRun: json['autoRun'] as bool? ?? false,
openLogs: json['openLogs'] as bool? ?? false,
@@ -75,7 +74,6 @@ Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
'locale': instance.locale,
'onlyProxy': instance.onlyProxy,
'autoLaunch': instance.autoLaunch,
'adminAutoLaunch': instance.adminAutoLaunch,
'silentLaunch': instance.silentLaunch,
'autoRun': instance.autoRun,
'openLogs': instance.openLogs,

View File

@@ -3,7 +3,7 @@
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of '../ffi.dart';
part of '../core.dart';
// **************************************************************************
// FreezedGenerator
@@ -2080,28 +2080,30 @@ abstract class _Now implements Now {
throw _privateConstructorUsedError;
}
Process _$ProcessFromJson(Map<String, dynamic> json) {
return _Process.fromJson(json);
ProcessData _$ProcessDataFromJson(Map<String, dynamic> json) {
return _ProcessData.fromJson(json);
}
/// @nodoc
mixin _$Process {
mixin _$ProcessData {
int get id => throw _privateConstructorUsedError;
Metadata get metadata => throw _privateConstructorUsedError;
/// Serializes this Process to a JSON map.
/// Serializes this ProcessData to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ProcessCopyWith<Process> get copyWith => throw _privateConstructorUsedError;
$ProcessDataCopyWith<ProcessData> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProcessCopyWith<$Res> {
factory $ProcessCopyWith(Process value, $Res Function(Process) then) =
_$ProcessCopyWithImpl<$Res, Process>;
abstract class $ProcessDataCopyWith<$Res> {
factory $ProcessDataCopyWith(
ProcessData value, $Res Function(ProcessData) then) =
_$ProcessDataCopyWithImpl<$Res, ProcessData>;
@useResult
$Res call({int id, Metadata metadata});
@@ -2109,16 +2111,16 @@ abstract class $ProcessCopyWith<$Res> {
}
/// @nodoc
class _$ProcessCopyWithImpl<$Res, $Val extends Process>
implements $ProcessCopyWith<$Res> {
_$ProcessCopyWithImpl(this._value, this._then);
class _$ProcessDataCopyWithImpl<$Res, $Val extends ProcessData>
implements $ProcessDataCopyWith<$Res> {
_$ProcessDataCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
@@ -2138,7 +2140,7 @@ class _$ProcessCopyWithImpl<$Res, $Val extends Process>
) as $Val);
}
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@@ -2150,10 +2152,11 @@ class _$ProcessCopyWithImpl<$Res, $Val extends Process>
}
/// @nodoc
abstract class _$$ProcessImplCopyWith<$Res> implements $ProcessCopyWith<$Res> {
factory _$$ProcessImplCopyWith(
_$ProcessImpl value, $Res Function(_$ProcessImpl) then) =
__$$ProcessImplCopyWithImpl<$Res>;
abstract class _$$ProcessDataImplCopyWith<$Res>
implements $ProcessDataCopyWith<$Res> {
factory _$$ProcessDataImplCopyWith(
_$ProcessDataImpl value, $Res Function(_$ProcessDataImpl) then) =
__$$ProcessDataImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int id, Metadata metadata});
@@ -2163,14 +2166,14 @@ abstract class _$$ProcessImplCopyWith<$Res> implements $ProcessCopyWith<$Res> {
}
/// @nodoc
class __$$ProcessImplCopyWithImpl<$Res>
extends _$ProcessCopyWithImpl<$Res, _$ProcessImpl>
implements _$$ProcessImplCopyWith<$Res> {
__$$ProcessImplCopyWithImpl(
_$ProcessImpl _value, $Res Function(_$ProcessImpl) _then)
class __$$ProcessDataImplCopyWithImpl<$Res>
extends _$ProcessDataCopyWithImpl<$Res, _$ProcessDataImpl>
implements _$$ProcessDataImplCopyWith<$Res> {
__$$ProcessDataImplCopyWithImpl(
_$ProcessDataImpl _value, $Res Function(_$ProcessDataImpl) _then)
: super(_value, _then);
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
@@ -2178,7 +2181,7 @@ class __$$ProcessImplCopyWithImpl<$Res>
Object? id = null,
Object? metadata = null,
}) {
return _then(_$ProcessImpl(
return _then(_$ProcessDataImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
@@ -2193,11 +2196,11 @@ class __$$ProcessImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$ProcessImpl implements _Process {
const _$ProcessImpl({required this.id, required this.metadata});
class _$ProcessDataImpl implements _ProcessData {
const _$ProcessDataImpl({required this.id, required this.metadata});
factory _$ProcessImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessImplFromJson(json);
factory _$ProcessDataImpl.fromJson(Map<String, dynamic> json) =>
_$$ProcessDataImplFromJson(json);
@override
final int id;
@@ -2206,14 +2209,14 @@ class _$ProcessImpl implements _Process {
@override
String toString() {
return 'Process(id: $id, metadata: $metadata)';
return 'ProcessData(id: $id, metadata: $metadata)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProcessImpl &&
other is _$ProcessDataImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.metadata, metadata) ||
other.metadata == metadata));
@@ -2223,39 +2226,40 @@ class _$ProcessImpl implements _Process {
@override
int get hashCode => Object.hash(runtimeType, id, metadata);
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
__$$ProcessImplCopyWithImpl<_$ProcessImpl>(this, _$identity);
_$$ProcessDataImplCopyWith<_$ProcessDataImpl> get copyWith =>
__$$ProcessDataImplCopyWithImpl<_$ProcessDataImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ProcessImplToJson(
return _$$ProcessDataImplToJson(
this,
);
}
}
abstract class _Process implements Process {
const factory _Process(
abstract class _ProcessData implements ProcessData {
const factory _ProcessData(
{required final int id,
required final Metadata metadata}) = _$ProcessImpl;
required final Metadata metadata}) = _$ProcessDataImpl;
factory _Process.fromJson(Map<String, dynamic> json) = _$ProcessImpl.fromJson;
factory _ProcessData.fromJson(Map<String, dynamic> json) =
_$ProcessDataImpl.fromJson;
@override
int get id;
@override
Metadata get metadata;
/// Create a copy of Process
/// Create a copy of ProcessData
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
_$$ProcessDataImplCopyWith<_$ProcessDataImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -3424,3 +3428,185 @@ abstract class _TunProps implements TunProps {
_$$TunPropsImplCopyWith<_$TunPropsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Action _$ActionFromJson(Map<String, dynamic> json) {
return _Action.fromJson(json);
}
/// @nodoc
mixin _$Action {
ActionMethod get method => throw _privateConstructorUsedError;
dynamic get data => throw _privateConstructorUsedError;
String get id => throw _privateConstructorUsedError;
/// Serializes this Action to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ActionCopyWith<Action> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ActionCopyWith<$Res> {
factory $ActionCopyWith(Action value, $Res Function(Action) then) =
_$ActionCopyWithImpl<$Res, Action>;
@useResult
$Res call({ActionMethod method, dynamic data, String id});
}
/// @nodoc
class _$ActionCopyWithImpl<$Res, $Val extends Action>
implements $ActionCopyWith<$Res> {
_$ActionCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? method = null,
Object? data = freezed,
Object? id = null,
}) {
return _then(_value.copyWith(
method: null == method
? _value.method
: method // ignore: cast_nullable_to_non_nullable
as ActionMethod,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as dynamic,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ActionImplCopyWith<$Res> implements $ActionCopyWith<$Res> {
factory _$$ActionImplCopyWith(
_$ActionImpl value, $Res Function(_$ActionImpl) then) =
__$$ActionImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({ActionMethod method, dynamic data, String id});
}
/// @nodoc
class __$$ActionImplCopyWithImpl<$Res>
extends _$ActionCopyWithImpl<$Res, _$ActionImpl>
implements _$$ActionImplCopyWith<$Res> {
__$$ActionImplCopyWithImpl(
_$ActionImpl _value, $Res Function(_$ActionImpl) _then)
: super(_value, _then);
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? method = null,
Object? data = freezed,
Object? id = null,
}) {
return _then(_$ActionImpl(
method: null == method
? _value.method
: method // ignore: cast_nullable_to_non_nullable
as ActionMethod,
data: freezed == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as dynamic,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ActionImpl implements _Action {
const _$ActionImpl(
{required this.method, required this.data, required this.id});
factory _$ActionImpl.fromJson(Map<String, dynamic> json) =>
_$$ActionImplFromJson(json);
@override
final ActionMethod method;
@override
final dynamic data;
@override
final String id;
@override
String toString() {
return 'Action(method: $method, data: $data, id: $id)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ActionImpl &&
(identical(other.method, method) || other.method == method) &&
const DeepCollectionEquality().equals(other.data, data) &&
(identical(other.id, id) || other.id == id));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, method, const DeepCollectionEquality().hash(data), id);
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ActionImplCopyWith<_$ActionImpl> get copyWith =>
__$$ActionImplCopyWithImpl<_$ActionImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ActionImplToJson(
this,
);
}
}
abstract class _Action implements Action {
const factory _Action(
{required final ActionMethod method,
required final dynamic data,
required final String id}) = _$ActionImpl;
factory _Action.fromJson(Map<String, dynamic> json) = _$ActionImpl.fromJson;
@override
ActionMethod get method;
@override
dynamic get data;
@override
String get id;
/// Create a copy of Action
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ActionImplCopyWith<_$ActionImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of '../ffi.dart';
part of '../core.dart';
// **************************************************************************
// JsonSerializableGenerator
@@ -188,13 +188,13 @@ Map<String, dynamic> _$$NowImplToJson(_$NowImpl instance) => <String, dynamic>{
'value': instance.value,
};
_$ProcessImpl _$$ProcessImplFromJson(Map<String, dynamic> json) =>
_$ProcessImpl(
_$ProcessDataImpl _$$ProcessDataImplFromJson(Map<String, dynamic> json) =>
_$ProcessDataImpl(
id: (json['id'] as num).toInt(),
metadata: Metadata.fromJson(json['metadata'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
Map<String, dynamic> _$$ProcessDataImplToJson(_$ProcessDataImpl instance) =>
<String, dynamic>{
'id': instance.id,
'metadata': instance.metadata,
@@ -289,3 +289,44 @@ Map<String, dynamic> _$$TunPropsImplToJson(_$TunPropsImpl instance) =>
'dns': instance.dns,
'dns6': instance.dns6,
};
_$ActionImpl _$$ActionImplFromJson(Map<String, dynamic> json) => _$ActionImpl(
method: $enumDecode(_$ActionMethodEnumMap, json['method']),
data: json['data'],
id: json['id'] as String,
);
Map<String, dynamic> _$$ActionImplToJson(_$ActionImpl instance) =>
<String, dynamic>{
'method': _$ActionMethodEnumMap[instance.method]!,
'data': instance.data,
'id': instance.id,
};
const _$ActionMethodEnumMap = {
ActionMethod.message: 'message',
ActionMethod.initClash: 'initClash',
ActionMethod.getIsInit: 'getIsInit',
ActionMethod.forceGc: 'forceGc',
ActionMethod.shutdown: 'shutdown',
ActionMethod.validateConfig: 'validateConfig',
ActionMethod.updateConfig: 'updateConfig',
ActionMethod.getProxies: 'getProxies',
ActionMethod.changeProxy: 'changeProxy',
ActionMethod.getTraffic: 'getTraffic',
ActionMethod.getTotalTraffic: 'getTotalTraffic',
ActionMethod.resetTraffic: 'resetTraffic',
ActionMethod.asyncTestDelay: 'asyncTestDelay',
ActionMethod.getConnections: 'getConnections',
ActionMethod.closeConnections: 'closeConnections',
ActionMethod.closeConnection: 'closeConnection',
ActionMethod.getExternalProviders: 'getExternalProviders',
ActionMethod.getExternalProvider: 'getExternalProvider',
ActionMethod.updateGeoData: 'updateGeoData',
ActionMethod.updateExternalProvider: 'updateExternalProvider',
ActionMethod.sideLoadExternalProvider: 'sideLoadExternalProvider',
ActionMethod.startLog: 'startLog',
ActionMethod.stopLog: 'stopLog',
ActionMethod.startListener: 'startListener',
ActionMethod.stopListener: 'stopListener',
};

View File

@@ -1046,13 +1046,15 @@ abstract class _ApplicationSelectorState implements ApplicationSelectorState {
/// @nodoc
mixin _$TrayState {
Mode get mode => throw _privateConstructorUsedError;
int get port => throw _privateConstructorUsedError;
bool get autoLaunch => throw _privateConstructorUsedError;
bool get adminAutoLaunch => throw _privateConstructorUsedError;
bool get systemProxy => throw _privateConstructorUsedError;
bool get tunEnable => throw _privateConstructorUsedError;
bool get isStart => throw _privateConstructorUsedError;
String? get locale => throw _privateConstructorUsedError;
Brightness? get brightness => throw _privateConstructorUsedError;
List<Group> get groups => throw _privateConstructorUsedError;
Map<String, String> get map => throw _privateConstructorUsedError;
/// Create a copy of TrayState
/// with the given fields replaced by the non-null parameter values.
@@ -1068,13 +1070,15 @@ abstract class $TrayStateCopyWith<$Res> {
@useResult
$Res call(
{Mode mode,
int port,
bool autoLaunch,
bool adminAutoLaunch,
bool systemProxy,
bool tunEnable,
bool isStart,
String? locale,
Brightness? brightness});
Brightness? brightness,
List<Group> groups,
Map<String, String> map});
}
/// @nodoc
@@ -1093,27 +1097,29 @@ class _$TrayStateCopyWithImpl<$Res, $Val extends TrayState>
@override
$Res call({
Object? mode = null,
Object? port = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? systemProxy = null,
Object? tunEnable = null,
Object? isStart = null,
Object? locale = freezed,
Object? brightness = freezed,
Object? groups = null,
Object? map = null,
}) {
return _then(_value.copyWith(
mode: null == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as Mode,
port: null == port
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as int,
autoLaunch: null == autoLaunch
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
@@ -1134,6 +1140,14 @@ class _$TrayStateCopyWithImpl<$Res, $Val extends TrayState>
? _value.brightness
: brightness // ignore: cast_nullable_to_non_nullable
as Brightness?,
groups: null == groups
? _value.groups
: groups // ignore: cast_nullable_to_non_nullable
as List<Group>,
map: null == map
? _value.map
: map // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
) as $Val);
}
}
@@ -1148,13 +1162,15 @@ abstract class _$$TrayStateImplCopyWith<$Res>
@useResult
$Res call(
{Mode mode,
int port,
bool autoLaunch,
bool adminAutoLaunch,
bool systemProxy,
bool tunEnable,
bool isStart,
String? locale,
Brightness? brightness});
Brightness? brightness,
List<Group> groups,
Map<String, String> map});
}
/// @nodoc
@@ -1171,27 +1187,29 @@ class __$$TrayStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? mode = null,
Object? port = null,
Object? autoLaunch = null,
Object? adminAutoLaunch = null,
Object? systemProxy = null,
Object? tunEnable = null,
Object? isStart = null,
Object? locale = freezed,
Object? brightness = freezed,
Object? groups = null,
Object? map = null,
}) {
return _then(_$TrayStateImpl(
mode: null == mode
? _value.mode
: mode // ignore: cast_nullable_to_non_nullable
as Mode,
port: null == port
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as int,
autoLaunch: null == autoLaunch
? _value.autoLaunch
: autoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
adminAutoLaunch: null == adminAutoLaunch
? _value.adminAutoLaunch
: adminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
systemProxy: null == systemProxy
? _value.systemProxy
: systemProxy // ignore: cast_nullable_to_non_nullable
@@ -1212,6 +1230,14 @@ class __$$TrayStateImplCopyWithImpl<$Res>
? _value.brightness
: brightness // ignore: cast_nullable_to_non_nullable
as Brightness?,
groups: null == groups
? _value._groups
: groups // ignore: cast_nullable_to_non_nullable
as List<Group>,
map: null == map
? _value._map
: map // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
));
}
}
@@ -1221,20 +1247,24 @@ class __$$TrayStateImplCopyWithImpl<$Res>
class _$TrayStateImpl implements _TrayState {
const _$TrayStateImpl(
{required this.mode,
required this.port,
required this.autoLaunch,
required this.adminAutoLaunch,
required this.systemProxy,
required this.tunEnable,
required this.isStart,
required this.locale,
required this.brightness});
required this.brightness,
required final List<Group> groups,
required final Map<String, String> map})
: _groups = groups,
_map = map;
@override
final Mode mode;
@override
final bool autoLaunch;
final int port;
@override
final bool adminAutoLaunch;
final bool autoLaunch;
@override
final bool systemProxy;
@override
@@ -1245,10 +1275,25 @@ class _$TrayStateImpl implements _TrayState {
final String? locale;
@override
final Brightness? brightness;
final List<Group> _groups;
@override
List<Group> get groups {
if (_groups is EqualUnmodifiableListView) return _groups;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_groups);
}
final Map<String, String> _map;
@override
Map<String, String> get map {
if (_map is EqualUnmodifiableMapView) return _map;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_map);
}
@override
String toString() {
return 'TrayState(mode: $mode, autoLaunch: $autoLaunch, adminAutoLaunch: $adminAutoLaunch, systemProxy: $systemProxy, tunEnable: $tunEnable, isStart: $isStart, locale: $locale, brightness: $brightness)';
return 'TrayState(mode: $mode, port: $port, autoLaunch: $autoLaunch, systemProxy: $systemProxy, tunEnable: $tunEnable, isStart: $isStart, locale: $locale, brightness: $brightness, groups: $groups, map: $map)';
}
@override
@@ -1257,10 +1302,9 @@ class _$TrayStateImpl implements _TrayState {
(other.runtimeType == runtimeType &&
other is _$TrayStateImpl &&
(identical(other.mode, mode) || other.mode == mode) &&
(identical(other.port, port) || other.port == port) &&
(identical(other.autoLaunch, autoLaunch) ||
other.autoLaunch == autoLaunch) &&
(identical(other.adminAutoLaunch, adminAutoLaunch) ||
other.adminAutoLaunch == adminAutoLaunch) &&
(identical(other.systemProxy, systemProxy) ||
other.systemProxy == systemProxy) &&
(identical(other.tunEnable, tunEnable) ||
@@ -1268,12 +1312,24 @@ class _$TrayStateImpl implements _TrayState {
(identical(other.isStart, isStart) || other.isStart == isStart) &&
(identical(other.locale, locale) || other.locale == locale) &&
(identical(other.brightness, brightness) ||
other.brightness == brightness));
other.brightness == brightness) &&
const DeepCollectionEquality().equals(other._groups, _groups) &&
const DeepCollectionEquality().equals(other._map, _map));
}
@override
int get hashCode => Object.hash(runtimeType, mode, autoLaunch,
adminAutoLaunch, systemProxy, tunEnable, isStart, locale, brightness);
int get hashCode => Object.hash(
runtimeType,
mode,
port,
autoLaunch,
systemProxy,
tunEnable,
isStart,
locale,
brightness,
const DeepCollectionEquality().hash(_groups),
const DeepCollectionEquality().hash(_map));
/// Create a copy of TrayState
/// with the given fields replaced by the non-null parameter values.
@@ -1287,20 +1343,22 @@ class _$TrayStateImpl implements _TrayState {
abstract class _TrayState implements TrayState {
const factory _TrayState(
{required final Mode mode,
required final int port,
required final bool autoLaunch,
required final bool adminAutoLaunch,
required final bool systemProxy,
required final bool tunEnable,
required final bool isStart,
required final String? locale,
required final Brightness? brightness}) = _$TrayStateImpl;
required final Brightness? brightness,
required final List<Group> groups,
required final Map<String, String> map}) = _$TrayStateImpl;
@override
Mode get mode;
@override
bool get autoLaunch;
int get port;
@override
bool get adminAutoLaunch;
bool get autoLaunch;
@override
bool get systemProxy;
@override
@@ -1311,6 +1369,10 @@ abstract class _TrayState implements TrayState {
String? get locale;
@override
Brightness? get brightness;
@override
List<Group> get groups;
@override
Map<String, String> get map;
/// Create a copy of TrayState
/// with the given fields replaced by the non-null parameter values.
@@ -3150,7 +3212,6 @@ abstract class _ProxiesActionsState implements ProxiesActionsState {
/// @nodoc
mixin _$AutoLaunchState {
bool get isAutoLaunch => throw _privateConstructorUsedError;
bool get isAdminAutoLaunch => throw _privateConstructorUsedError;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@@ -3165,7 +3226,7 @@ abstract class $AutoLaunchStateCopyWith<$Res> {
AutoLaunchState value, $Res Function(AutoLaunchState) then) =
_$AutoLaunchStateCopyWithImpl<$Res, AutoLaunchState>;
@useResult
$Res call({bool isAutoLaunch, bool isAdminAutoLaunch});
$Res call({bool isAutoLaunch});
}
/// @nodoc
@@ -3184,17 +3245,12 @@ class _$AutoLaunchStateCopyWithImpl<$Res, $Val extends AutoLaunchState>
@override
$Res call({
Object? isAutoLaunch = null,
Object? isAdminAutoLaunch = null,
}) {
return _then(_value.copyWith(
isAutoLaunch: null == isAutoLaunch
? _value.isAutoLaunch
: isAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
isAdminAutoLaunch: null == isAdminAutoLaunch
? _value.isAdminAutoLaunch
: isAdminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
@@ -3207,7 +3263,7 @@ abstract class _$$AutoLaunchStateImplCopyWith<$Res>
__$$AutoLaunchStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isAutoLaunch, bool isAdminAutoLaunch});
$Res call({bool isAutoLaunch});
}
/// @nodoc
@@ -3224,17 +3280,12 @@ class __$$AutoLaunchStateImplCopyWithImpl<$Res>
@override
$Res call({
Object? isAutoLaunch = null,
Object? isAdminAutoLaunch = null,
}) {
return _then(_$AutoLaunchStateImpl(
isAutoLaunch: null == isAutoLaunch
? _value.isAutoLaunch
: isAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
isAdminAutoLaunch: null == isAdminAutoLaunch
? _value.isAdminAutoLaunch
: isAdminAutoLaunch // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
@@ -3242,17 +3293,14 @@ class __$$AutoLaunchStateImplCopyWithImpl<$Res>
/// @nodoc
class _$AutoLaunchStateImpl implements _AutoLaunchState {
const _$AutoLaunchStateImpl(
{required this.isAutoLaunch, required this.isAdminAutoLaunch});
const _$AutoLaunchStateImpl({required this.isAutoLaunch});
@override
final bool isAutoLaunch;
@override
final bool isAdminAutoLaunch;
@override
String toString() {
return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch, isAdminAutoLaunch: $isAdminAutoLaunch)';
return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch)';
}
@override
@@ -3261,13 +3309,11 @@ class _$AutoLaunchStateImpl implements _AutoLaunchState {
(other.runtimeType == runtimeType &&
other is _$AutoLaunchStateImpl &&
(identical(other.isAutoLaunch, isAutoLaunch) ||
other.isAutoLaunch == isAutoLaunch) &&
(identical(other.isAdminAutoLaunch, isAdminAutoLaunch) ||
other.isAdminAutoLaunch == isAdminAutoLaunch));
other.isAutoLaunch == isAutoLaunch));
}
@override
int get hashCode => Object.hash(runtimeType, isAutoLaunch, isAdminAutoLaunch);
int get hashCode => Object.hash(runtimeType, isAutoLaunch);
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.
@@ -3280,14 +3326,11 @@ class _$AutoLaunchStateImpl implements _AutoLaunchState {
}
abstract class _AutoLaunchState implements AutoLaunchState {
const factory _AutoLaunchState(
{required final bool isAutoLaunch,
required final bool isAdminAutoLaunch}) = _$AutoLaunchStateImpl;
const factory _AutoLaunchState({required final bool isAutoLaunch}) =
_$AutoLaunchStateImpl;
@override
bool get isAutoLaunch;
@override
bool get isAdminAutoLaunch;
/// Create a copy of AutoLaunchState
/// with the given fields replaced by the non-null parameter values.

View File

@@ -1,7 +1,7 @@
export 'app.dart';
export 'clash_config.dart';
export 'common.dart';
export 'config.dart';
export 'core.dart';
export 'profile.dart';
export 'ffi.dart';
export 'selector.dart';
export 'common.dart';

View File

@@ -96,6 +96,21 @@ extension ProfileExtension on Profile {
return await File(profilePath!).exists();
}
Future<File> getFile() async {
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
return file;
}
Future<int> get profileLastModified async {
final file = await getFile();
return (await file.lastModified()).microsecondsSinceEpoch;
}
Future<Profile> update() async {
final response = await request.getFileResponseForUrl(url);
final disposition = response.headers.value("content-disposition");
@@ -111,12 +126,7 @@ extension ProfileExtension on Profile {
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
final file = await getFile();
await file.writeAsBytes(bytes);
return copyWith(lastUpdateDate: DateTime.now());
}
@@ -126,12 +136,7 @@ extension ProfileExtension on Profile {
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
final isExists = await file.exists();
if (!isExists) {
await file.create(recursive: true);
}
final file = await getFile();
await file.writeAsString(value);
return copyWith(lastUpdateDate: DateTime.now());
}

View File

@@ -63,13 +63,15 @@ class ApplicationSelectorState with _$ApplicationSelectorState {
class TrayState with _$TrayState {
const factory TrayState({
required Mode mode,
required int port,
required bool autoLaunch,
required bool adminAutoLaunch,
required bool systemProxy,
required bool tunEnable,
required bool isStart,
required String? locale,
required Brightness? brightness,
required List<Group> groups,
required SelectedMap map,
}) = _TrayState;
}
@@ -197,7 +199,6 @@ class ProxiesActionsState with _$ProxiesActionsState {
class AutoLaunchState with _$AutoLaunchState {
const factory AutoLaunchState({
required bool isAutoLaunch,
required bool isAdminAutoLaunch,
}) = _AutoLaunchState;
}

View File

@@ -27,7 +27,7 @@ class Vpn {
clashCore.requestGc();
case "dnsChanged":
final dns = call.arguments as String;
clashCore.updateDns(dns);
clashLib?.updateDns(dns);
default:
throw MissingPluginException();
}
@@ -40,7 +40,7 @@ class Vpn {
}
Future<bool?> startVpn() async {
final options = clashCore.getAndroidVpnOptions();
final options = clashLib?.getAndroidVpnOptions();
return await methodChannel.invokeMethod<bool>("start", {
'data': json.encode(options),
});
@@ -54,7 +54,7 @@ class Vpn {
return await methodChannel.invokeMethod<bool?>("setProtect", {'fd': fd});
}
Future<String?> resolverProcess(Process process) async {
Future<String?> resolverProcess(ProcessData process) async {
return await methodChannel.invokeMethod<String>("resolverProcess", {
"data": json.encode(process),
});
@@ -79,7 +79,7 @@ class Vpn {
receiver!.listen((message) {
_handleServiceMessage(message);
});
clashCore.startTun(fd, receiver!.sendPort.nativePort);
clashLib?.startTun(fd, receiver!.sendPort.nativePort);
}
setServiceMessageHandler(ServiceMessageListener serviceMessageListener) {
@@ -92,7 +92,7 @@ class Vpn {
case ServiceMessageType.protect:
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));
case ServiceMessageType.process:
_serviceMessageHandler?.onProcess(Process.fromJson(m.data));
_serviceMessageHandler?.onProcess(ProcessData.fromJson(m.data));
case ServiceMessageType.started:
_serviceMessageHandler?.onStarted(m.data);
case ServiceMessageType.loaded:

View File

@@ -8,9 +8,7 @@ import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/plugins/vpn.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common/common.dart';
@@ -30,6 +28,8 @@ class GlobalState {
late AppController appController;
GlobalKey<CommonScaffoldState> homeScaffoldKey = GlobalKey();
List<Function> updateFunctionLists = [];
bool lastTunEnable = false;
int? lastProfileModified;
bool get isStart => startTime != null && startTime!.isBeforeNow;
@@ -47,16 +47,61 @@ class GlobalState {
timer?.cancel();
}
Future<void> initCore({
required AppState appState,
required ClashConfig clashConfig,
required Config config,
}) async {
await globalState.init(
appState: appState,
config: config,
clashConfig: clashConfig,
);
await applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
Future<void> updateClashConfig({
required AppState appState,
required ClashConfig clashConfig,
required Config config,
bool isPatch = true,
}) async {
await config.currentProfile?.checkAndUpdate();
final useClashConfig = clashConfig.copyWith();
if (clashConfig.tun.enable != lastTunEnable &&
lastTunEnable == false &&
!Platform.isAndroid) {
final code = await system.authorizeCore();
switch (code) {
case AuthorizeCode.none:
break;
case AuthorizeCode.success:
lastTunEnable = useClashConfig.tun.enable;
await restartCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
return;
case AuthorizeCode.error:
useClashConfig.tun = useClashConfig.tun.copyWith(
enable: false,
);
}
}
if (config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
final res = await clashCore.updateConfig(
UpdateConfigParams(
profileId: config.currentProfileId ?? "",
config: clashConfig,
config: useClashConfig,
params: ConfigExtendedParams(
isPatch: isPatch,
isCompatible: true,
@@ -67,14 +112,12 @@ class GlobalState {
),
);
if (res.isNotEmpty) throw res;
}
updateCoreVersionInfo(AppState appState) {
appState.versionInfo = clashCore.getVersionInfo();
lastTunEnable = useClashConfig.tun.enable;
lastProfileModified = await config.getCurrentProfile()?.profileLastModified;
}
handleStart() async {
clashCore.start();
await clashCore.startListener();
if (globalState.isVpnService) {
await vpn?.startVpn();
startListenUpdate();
@@ -85,17 +128,32 @@ class GlobalState {
startListenUpdate();
}
restartCore({
required AppState appState,
required ClashConfig clashConfig,
required Config config,
bool isPatch = true,
}) async {
await clashService?.startCore();
await initCore(
appState: appState,
clashConfig: clashConfig,
config: config,
);
if (isStart) {
await handleStart();
}
}
updateStartTime() {
startTime = clashCore.getRunTime();
startTime = clashLib?.getRunTime();
}
Future handleStop() async {
clashCore.stop();
if (Platform.isAndroid) {
clashCore.stopTun();
}
await service?.destroy();
startTime = null;
await clashCore.stopListener();
clashLib?.stopTun();
await service?.destroy();
stopListenUpdate();
}
@@ -106,6 +164,7 @@ class GlobalState {
}) async {
clashCore.requestGc();
await updateClashConfig(
appState: appState,
clashConfig: clashConfig,
config: config,
isPatch: false,
@@ -123,30 +182,27 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) async {
appState.isInit = clashCore.isInit;
appState.isInit = await clashCore.isInit;
if (!appState.isInit) {
appState.isInit = await clashService.init(
appState.isInit = await clashCore.init(
config: config,
clashConfig: clashConfig,
);
if (Platform.isAndroid) {
clashCore.setState(
CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
bypassDomain: config.networkProps.bypassDomain,
routeAddress: clashConfig.routeAddress,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
),
);
}
clashLib?.setState(
CoreState(
enable: config.vpnProps.enable,
accessControl: config.isAccessControl ? config.accessControl : null,
ipv6: config.vpnProps.ipv6,
allowBypass: config.vpnProps.allowBypass,
systemProxy: config.vpnProps.systemProxy,
onlyProxy: config.appSetting.onlyProxy,
bypassDomain: config.networkProps.bypassDomain,
routeAddress: clashConfig.routeAddress,
currentProfileName:
config.currentProfile?.label ?? config.currentProfileId ?? "",
),
);
}
updateCoreVersionInfo(appState);
}
Future<void> updateGroups(AppState appState) async {
@@ -198,8 +254,8 @@ class GlobalState {
required Config config,
required String groupName,
required String proxyName,
}) {
clashCore.changeProxy(
}) async {
await clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
@@ -226,18 +282,21 @@ class GlobalState {
}
updateTraffic({
required Config config,
AppFlowingState? appFlowingState,
}) {
final traffic = clashCore.getTraffic();
}) async {
final onlyProxy = config.appSetting.onlyProxy;
final traffic = await clashCore.getTraffic(onlyProxy);
if (Platform.isAndroid && isVpnService == true) {
vpn?.startForeground(
title: clashCore.getCurrentProfileName(),
title: clashLib?.getCurrentProfileName() ?? "",
content: "$traffic",
);
} else {
if (appFlowingState != null) {
appFlowingState.addTraffic(traffic);
appFlowingState.totalTraffic = clashCore.getTotalTraffic();
appFlowingState.totalTraffic =
await clashCore.getTotalTraffic(onlyProxy);
}
}
}
@@ -301,132 +360,6 @@ class GlobalState {
},
);
}
Future _updateSystemTray({
required Brightness? brightness,
bool force = false,
}) async {
if (Platform.isAndroid) {
return;
}
if (Platform.isLinux || force) {
await trayManager.destroy();
}
await trayManager.setIcon(
other.getTrayIconPath(
brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness,
),
isTemplate: true,
);
if (!Platform.isLinux) {
await trayManager.setToolTip(
appName,
);
}
}
updateTray({
required AppState appState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false,
}) async {
if (!Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
onClick: (_) {
window?.show();
},
);
menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async {
globalState.appController.updateStart();
},
checked: false,
);
menuItems.add(startMenuItem);
menuItems.add(MenuItem.separator());
for (final mode in Mode.values) {
menuItems.add(
MenuItem.checkbox(
label: Intl.message(mode.name),
onClick: (_) {
globalState.appController.clashConfig.mode = mode;
},
checked: mode == clashConfig.mode,
),
);
}
menuItems.add(MenuItem.separator());
if (appFlowingState.isStart) {
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.tun,
onClick: (_) {
globalState.appController.updateTun();
},
checked: clashConfig.tun.enable,
),
);
menuItems.add(
MenuItem.checkbox(
label: appLocalizations.systemProxy,
onClick: (_) {
globalState.appController.updateSystemProxy();
},
checked: config.networkProps.systemProxy,
),
);
menuItems.add(MenuItem.separator());
}
final autoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.autoLaunch,
onClick: (_) async {
globalState.appController.updateAutoLaunch();
},
checked: config.appSetting.autoLaunch,
);
menuItems.add(autoStartMenuItem);
if (Platform.isWindows) {
final adminAutoStartMenuItem = MenuItem.checkbox(
label: appLocalizations.adminAutoLaunch,
onClick: (_) async {
globalState.appController.updateAdminAutoLaunch();
},
checked: config.appSetting.adminAutoLaunch,
);
menuItems.add(adminAutoStartMenuItem);
}
menuItems.add(MenuItem.separator());
final exitMenuItem = MenuItem(
label: appLocalizations.exit,
onClick: (_) async {
await globalState.appController.handleExit();
},
);
menuItems.add(exitMenuItem);
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
if (Platform.isLinux) {
await _updateSystemTray(
brightness: appState.brightness,
force: focus,
);
}
}
}
final globalState = GlobalState();

View File

@@ -135,9 +135,6 @@ class CommonCard extends StatelessWidget {
if (isSelected) {
return colorScheme.secondaryContainer;
}
if (states.isEmpty) {
return colorScheme.surfaceContainerLow;
}
return colorScheme.surfaceContainer;
}
}

View File

@@ -59,7 +59,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
set floatingActionButton(Widget floatingActionButton) {
set floatingActionButton(Widget? floatingActionButton) {
if (_floatingActionButton.value != floatingActionButton) {
_floatingActionButton.value = floatingActionButton;
}
@@ -122,25 +122,27 @@ class CommonScaffoldState extends State<CommonScaffold> {
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
actions.isNotEmpty ? actions : widget.actions;
return AppBar(
centerTitle: false,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarIconBrightness:
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor:
widget.bottomNavigationBar != null
? context.colorScheme.surfaceContainer
: context.colorScheme.surface,
systemNavigationBarDividerColor: Colors.transparent,
),
automaticallyImplyLeading: widget.automaticallyImplyLeading,
automaticallyImplyLeading:
widget.automaticallyImplyLeading,
leading: widget.leading,
title: Text(widget.title),
actions: [

View File

@@ -119,7 +119,7 @@ install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
# libclash.so
set(CLASH_DIR "../libclash/linux")
install(FILES "${CLASH_DIR}/libclash.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
install(PROGRAMS "${CLASH_DIR}/FlClashCore" DESTINATION "${BUILD_BUNDLE_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})

43
macos/Podfile Executable file
View File

@@ -0,0 +1,43 @@
platform :osx, '10.14'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

123
macos/Podfile.lock Executable file
View File

@@ -0,0 +1,123 @@
PODS:
- app_links (1.0.0):
- FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- dynamic_color (0.0.2):
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- HotKey (0.2.0)
- hotkey_manager_macos (0.0.1):
- FlutterMacOS
- HotKey
- mobile_scanner (6.0.2):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- screen_retriever_macos (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- tray_manager (0.0.1):
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- window_ext (0.0.1):
- FlutterMacOS
- window_manager (0.2.0):
- FlutterMacOS
DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- window_ext (from `Flutter/ephemeral/.symlinks/plugins/window_ext/macos`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
SPEC REPOS:
trunk:
- HotKey
EXTERNAL SOURCES:
app_links:
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
dynamic_color:
:path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral
hotkey_manager_macos:
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
window_ext:
:path: Flutter/ephemeral/.symlinks/plugins/window_ext/macos
window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
mobile_scanner: 07710d6b9b2c220ae899de2d7ecf5d77ffa56333
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
window_ext: cbf41f054296dcfb15a1cb53610ef3eab6311f13
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
COCOAPODS: 1.16.2

View File

@@ -28,10 +28,9 @@
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
5377B2253E1C5AB4D9D56A31 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */; };
7AC277AA2B90DE1400E026B1 /* libclash.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AC277A92B90DE1400E026B1 /* libclash.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
7AC277AB2B90DFD900E026B1 /* libclash.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 7AC277A92B90DE1400E026B1 /* libclash.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
7AC6855B2B8AF836004C123B /* (null) in Bundle Framework */ = {isa = PBXBuildFile; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CDD319C761C7664F6008596B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */; };
F50091052CF74B7700D43AEA /* FlClashCore in CopyFiles */ = {isa = PBXBuildFile; fileRef = F50091042CF74B7700D43AEA /* FlClashCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -58,12 +57,39 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
7AC277AB2B90DFD900E026B1 /* libclash.dylib in Bundle Framework */,
7AC6855B2B8AF836004C123B /* (null) in Bundle Framework */,
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
F50091032CF74B6400D43AEA /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 6;
files = (
F50091052CF74B7700D43AEA /* FlClashCore in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F5FAC0AA2CEDC4DA000CF079 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 12;
dstPath = "";
dstSubfolderSpec = 6;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
F5FAC0AE2CEDC891000CF079 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 6;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@@ -87,13 +113,13 @@
4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
779829C96DE7998FCC810C37 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7AC277A92B90DE1400E026B1 /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../libclash/macos/libclash.dylib; sourceTree = "<group>"; };
7AF070893C29500AB9129D89 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
7D929F2AFD80E155D78F3718 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
8F1D6D6423063FA738863205 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
CA9CA9C2D0B5E93A91F45924 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
F50091042CF74B7700D43AEA /* FlClashCore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = FlClashCore; path = ../libclash/macos/FlClashCore; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -109,7 +135,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7AC277AA2B90DE1400E026B1 /* libclash.dylib in Frameworks */,
CDD319C761C7664F6008596B /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -139,6 +164,7 @@
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
F50091042CF74B7700D43AEA /* FlClashCore */,
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
@@ -208,7 +234,6 @@
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
7AC277A92B90DE1400E026B1 /* libclash.dylib */,
4121E8CCDC7DC35194714CDE /* Pods_Runner.framework */,
72CBDF47BB69EDEFE644C48D /* Pods_RunnerTests.framework */,
);
@@ -226,6 +251,7 @@
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
F5FAC0AA2CEDC4DA000CF079 /* CopyFiles */,
);
buildRules = (
);
@@ -248,6 +274,8 @@
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
1522C6AC211009D2A7DFAD40 /* [CP] Embed Pods Frameworks */,
F5FAC0AE2CEDC891000CF079 /* CopyFiles */,
F50091032CF74B6400D43AEA /* CopyFiles */,
);
buildRules = (
);
@@ -582,7 +610,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/";
LIBRARY_SEARCH_PATHS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@@ -710,7 +738,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/";
LIBRARY_SEARCH_PATHS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -732,7 +760,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
LIBRARY_SEARCH_PATHS = "${SRCROOT}/../libclash/macos/";
LIBRARY_SEARCH_PATHS = "";
PRODUCT_BUNDLE_IDENTIFIER = com.follow.clash;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;

View File

@@ -1,7 +1,7 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
version: 0.8.68+202411171
version: 0.8.69+202412061
environment:
sdk: '>=3.1.0 <4.0.0'

1313
services/helper/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
[package]
name = "helper"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "helper"
path = "src/main.rs"
[dependencies]
windows-service = { version = "0.7.0", optional = true }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0.93"
warp = "0.3.7"
serde = { version = "1.0.215", features = ["derive"] }
once_cell = "1.20.2"
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"

View File

@@ -0,0 +1,20 @@
#[cfg(not(all(feature = "windows-service", target_os = "windows")))]
use tokio::runtime::Runtime;
#[cfg(not(all(feature = "windows-service", target_os = "windows")))]
use crate::service::hub::run_service;
mod service;
#[cfg(all(feature = "windows-service", target_os = "windows"))]
pub fn main() -> windows_service::Result<()> {
service::windows::main()
}
#[cfg(not(all(feature = "windows-service", target_os = "windows")))]
fn main() {
if let Ok(rt) = Runtime::new() {
rt.block_on(async {
let _ = run_service().await;
});
}
}

View File

@@ -0,0 +1,110 @@
use std::collections::VecDeque;
use std::{io, thread};
use std::io::BufRead;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use warp::{Filter, Reply};
use serde::{Deserialize, Serialize};
use once_cell::sync::Lazy;
const LISTEN_PORT: u16 = 47890;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct StartParams {
pub path: String,
pub arg: String,
}
static LOGS: Lazy<Arc<Mutex<VecDeque<String>>>> = Lazy::new(|| Arc::new(Mutex::new(VecDeque::with_capacity(100))));
static PROCESS: Lazy<Arc<Mutex<Option<std::process::Child>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
fn start(start_params: StartParams) -> impl Reply {
stop();
let mut process = PROCESS.lock().unwrap();
match Command::new(&start_params.path)
.stderr(Stdio::piped())
.arg(&start_params.arg)
.spawn()
{
Ok(child) => {
*process = Some(child);
if let Some(ref mut child) = *process {
let stderr = child.stderr.take().unwrap();
let reader = io::BufReader::new(stderr);
thread::spawn(move || {
for line in reader.lines() {
match line {
Ok(output) => {
log_message(output);
}
Err(_) => {
break;
}
}
}
});
}
"".to_string()
}
Err(e) => {
log_message(e.to_string());
e.to_string()
}
}
}
fn stop() -> impl Reply {
let mut process = PROCESS.lock().unwrap();
if let Some(mut child) = process.take() {
let _ = child.kill();
let _ = child.wait();
}
*process = None;
"".to_string()
}
fn log_message(message: String) {
let mut log_buffer = LOGS.lock().unwrap();
if log_buffer.len() == 100 {
log_buffer.pop_front();
}
log_buffer.push_back(format!("{}\n", message));
}
fn get_logs() -> impl Reply {
let log_buffer = LOGS.lock().unwrap();
let value = log_buffer.iter().cloned().collect::<Vec<String>>().join("\n");
warp::reply::with_header(value, "Content-Type", "text/plain")
}
pub async fn run_service() -> anyhow::Result<()> {
let api_ping = warp::get()
.and(warp::path("ping"))
.map(|| "2024125");
let api_start = warp::post()
.and(warp::path("start"))
.and(warp::body::json())
.map(|start_params: StartParams| {
start(start_params)
});
let api_stop = warp::post()
.and(warp::path("stop"))
.map(|| stop());
let api_logs = warp::get()
.and(warp::path("logs"))
.map(|| get_logs());
warp::serve(
api_ping
.or(api_start)
.or(api_stop)
.or(api_logs)
)
.run(([127, 0, 0, 1], LISTEN_PORT))
.await;
Ok(())
}

View File

@@ -0,0 +1,8 @@
pub mod hub;
#[cfg(all(feature = "windows-service", target_os = "windows"))]
pub mod windows;

View File

@@ -0,0 +1,67 @@
use crate::service::hub::run_service;
use std::ffi::OsString;
use std::time::Duration;
use tokio::runtime::Runtime;
use windows_service::{
define_windows_service,
service::{
ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus,
ServiceType,
},
service_control_handler::{self, ServiceControlHandlerResult},
service_dispatcher, Result,
};
const SERVICE_NAME: &str = "FlClashHelperService";
const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
pub fn main() -> Result<()> {
start_service()
}
pub fn start_service() -> Result<()> {
service_dispatcher::start(SERVICE_NAME, serveice)
}
define_windows_service!(serveice, service_main);
pub fn service_main(_arguments: Vec<OsString>) {
if let Ok(rt) = Runtime::new() {
rt.block_on(async {
let _ = run_windows_service().await;
});
}
}
async fn run_windows_service() -> anyhow::Result<()> {
let status_handle = service_control_handler::register(
SERVICE_NAME,
move |event| -> ServiceControlHandlerResult {
match event {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop => std::process::exit(0),
_ => ServiceControlHandlerResult::NotImplemented,
}
},
)?;
status_handle.set_service_status(ServiceStatus {
service_type: SERVICE_TYPE,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
})?;
run_service().await
}

View File

@@ -2,115 +2,121 @@
import 'dart:convert';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:path/path.dart';
enum PlatformType {
enum Target {
windows,
linux,
android,
macos,
}
enum Arch { amd64, arm64, arm }
class BuildLibItem {
PlatformType platform;
Arch arch;
String archName;
BuildLibItem({
required this.platform,
required this.arch,
required this.archName,
});
extension TargetExt on Target {
String get os {
if (this == Target.macos) {
return "darwin";
}
return name;
}
String get dynamicLibExtensionName {
final String extensionName;
switch (platform) {
case PlatformType.android || PlatformType.linux:
extensionName = "so";
switch (this) {
case Target.android || Target.linux:
extensionName = ".so";
break;
case PlatformType.windows:
extensionName = "dll";
case Target.windows:
extensionName = ".dll";
break;
case PlatformType.macos:
extensionName = "dylib";
case Target.macos:
extensionName = ".dylib";
break;
}
return extensionName;
}
String get os {
if (platform == PlatformType.macos) {
return "darwin";
String get executableExtensionName {
final String extensionName;
switch (this) {
case Target.windows:
extensionName = ".exe";
break;
default:
extensionName = "";
break;
}
return platform.name;
return extensionName;
}
}
enum Mode { core, lib }
enum Arch { amd64, arm64, arm }
class BuildItem {
Target target;
Arch? arch;
String? archName;
BuildItem({
required this.target,
this.arch,
this.archName,
});
@override
String toString() {
return 'BuildLibItem{platform: $platform, arch: $arch, archName: $archName}';
return 'BuildLibItem{target: $target, arch: $arch, archName: $archName}';
}
}
class Build {
static List<BuildLibItem> get buildItems => [
BuildLibItem(
platform: PlatformType.macos,
arch: Arch.amd64,
archName: '',
static List<BuildItem> get buildItems => [
BuildItem(
target: Target.macos,
),
BuildLibItem(
platform: PlatformType.macos,
arch: Arch.arm64,
archName: '',
BuildItem(
target: Target.windows,
),
BuildLibItem(
platform: PlatformType.windows,
arch: Arch.amd64,
archName: '',
BuildItem(
target: Target.linux,
),
BuildLibItem(
platform: PlatformType.windows,
arch: Arch.arm64,
archName: '',
),
BuildLibItem(
platform: PlatformType.android,
BuildItem(
target: Target.android,
arch: Arch.arm,
archName: 'armeabi-v7a',
),
BuildLibItem(
platform: PlatformType.android,
BuildItem(
target: Target.android,
arch: Arch.arm64,
archName: 'arm64-v8a',
),
BuildLibItem(
platform: PlatformType.android,
BuildItem(
target: Target.android,
arch: Arch.amd64,
archName: 'x86_64',
),
BuildLibItem(
platform: PlatformType.linux,
arch: Arch.amd64,
archName: '',
),
];
static String get appName => "FlClash";
static String get coreName => "FlClashCore";
static String get libName => "libclash";
static String get outDir => join(current, libName);
static String get _coreDir => join(current, "core");
static String get _servicesDir => join(current, "services", "helper");
static String get distPath => join(current, "dist");
static String _getCc(BuildLibItem buildItem) {
static String _getCc(BuildItem buildItem) {
final environment = Platform.environment;
if (buildItem.platform == PlatformType.android) {
if (buildItem.target == Target.android) {
final ndk = environment["ANDROID_NDK"];
assert(ndk != null);
final prebuiltDir =
@@ -158,55 +164,97 @@ class Build {
if (exitCode != 0 && name != null) throw "$name error";
}
static buildLib({
required PlatformType platform,
static buildCore({
required Mode mode,
required Target target,
Arch? arch,
}) async {
final isLib = mode == Mode.lib;
final items = buildItems.where(
(element) {
return element.platform == platform &&
return element.target == target &&
(arch == null ? true : element.arch == arch);
},
).toList();
for (final item in items) {
final outFileDir = join(
outDir,
item.platform.name,
item.target.name,
item.archName,
);
final file = File(outFileDir);
if (file.existsSync()) {
file.deleteSync(recursive: true);
}
final fileName = isLib
? "$libName${item.target.dynamicLibExtensionName}"
: "$coreName${item.target.executableExtensionName}";
final outPath = join(
outFileDir,
"$libName.${item.dynamicLibExtensionName}",
fileName,
);
final Map<String, String> env = {};
env["GOOS"] = item.os;
env["GOARCH"] = item.arch.name;
env["CGO_ENABLED"] = "1";
env["CC"] = _getCc(item);
env["CFLAGS"] = "-O3 -Werror";
final Map<String, String> env = {};
env["GOOS"] = item.target.os;
if (isLib) {
if (item.arch != null) {
env["GOARCH"] = item.arch!.name;
}
env["CGO_ENABLED"] = "1";
env["CC"] = _getCc(item);
env["CFLAGS"] = "-O3 -Werror";
} else {
env["CGO_ENABLED"] = "0";
}
final execLines = [
"go",
"build",
"-ldflags=-w -s",
"-tags=$tags",
if (isLib) "-buildmode=c-shared",
"-o",
outPath,
];
await exec(
[
"go",
"build",
"-ldflags=-w -s",
"-tags=$tags",
"-buildmode=c-shared",
"-o",
outPath,
],
name: "build libclash",
execLines,
name: "build core",
environment: env,
workingDirectory: _coreDir,
);
}
}
static buildHelper(Target target) async {
await exec(
[
"cargo",
"build",
"--release",
"--features",
"windows-service",
],
name: "build helper",
workingDirectory: _servicesDir,
);
final outPath = join(
_servicesDir,
"target",
"release",
"helper${target.executableExtensionName}",
);
final targetPath = join(outDir, target.name,
"FlClashHelperService${target.executableExtensionName}");
await File(outPath).copy(targetPath);
}
static List<String> getExecutable(String command) {
print(command);
return command.split(" ");
}
@@ -250,22 +298,30 @@ class Build {
}
class BuildCommand extends Command {
PlatformType platform;
Target target;
BuildCommand({
required this.platform,
required this.target,
}) {
if (target == Target.android) {
argParser.addOption(
"arch",
valueHelp: arches.map((e) => e.name).join(','),
help: 'The $name build desc',
);
} else {
argParser.addOption(
"arch",
help: 'The $name build archName',
);
}
argParser.addOption(
"build",
"out",
valueHelp: [
'all',
'lib',
"app",
"core",
].join(','),
help: 'The $name build type',
);
argParser.addOption(
"arch",
valueHelp: arches.map((e) => e.name).join(','),
help: 'The $name build arch',
);
}
@@ -274,17 +330,13 @@ class BuildCommand extends Command {
String get description => "build $name application";
@override
String get name => platform.name;
String get name => target.name;
List<Arch> get arches => Build.buildItems
.where((element) => element.platform == platform)
.map((e) => e.arch)
.where((element) => element.target == target && element.arch != null)
.map((e) => e.arch!)
.toList();
Future<void> _buildLib(Arch? arch) async {
await Build.buildLib(platform: platform, arch: arch);
}
_getLinuxDependencies() async {
await Build.exec(
Build.getExecutable("sudo apt update -y"),
@@ -331,51 +383,71 @@ class BuildCommand extends Command {
}
_buildDistributor({
required PlatformType platform,
required Target target,
required String targets,
String args = '',
}) async {
await Build.getDistributor();
/* final tag = Platform.environment["TAG"] ?? "+";
final isDev = tag.contains("+");
final channelArgs = isDev && platform == PlatformType.android ? "--build-flavor dev" : "";*/
await Build.exec(
name: name,
Build.getExecutable(
"flutter_distributor package --skip-clean --platform ${platform.name} --targets $targets --flutter-build-args=verbose $args",
"flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose $args",
),
);
}
Future<String?> get systemArch async {
if (Platform.isWindows) {
return Platform.environment["PROCESSOR_ARCHITECTURE"];
} else if (Platform.isLinux || Platform.isMacOS) {
final result = await Process.run('uname', ['-m']);
return result.stdout.toString().trim();
}
return null;
}
@override
Future<void> run() async {
final String build = argResults?['build'] ?? 'all';
final archName = argResults?['arch'];
final currentArches =
arches.where((element) => element.name == archName).toList();
final arch = currentArches.isEmpty ? null : currentArches.first;
if (arch == null && platform == PlatformType.windows) {
throw "Invalid arch";
final mode = target == Target.android ? Mode.lib : Mode.core;
final String out = argResults?['out'] ?? 'app';
Arch? arch;
var archName = argResults?['arch'];
if (target == Target.android) {
final currentArches =
arches.where((element) => element.name == archName).toList();
arch = currentArches.isEmpty ? null : currentArches.first;
}
await _buildLib(arch);
if (build != "all") {
await Build.buildCore(
target: target,
arch: arch,
mode: mode,
);
if (target == Target.windows) {
await Build.buildHelper(target);
}
if (out != "app") {
return;
}
switch (platform) {
case PlatformType.windows:
switch (target) {
case Target.windows:
_buildDistributor(
platform: platform,
target: target,
targets: "exe,zip",
args: "--description ${arch!.name}",
args: "--description $archName",
);
case PlatformType.linux:
case Target.linux:
await _getLinuxDependencies();
_buildDistributor(
platform: platform,
target: target,
targets: "appimage,deb,rpm",
args: "--description ${arch!.name}",
args: "--description $archName",
);
case PlatformType.android:
case Target.android:
final targetMap = {
Arch.arm: "android-arm",
Arch.arm64: "android-arm64",
@@ -387,17 +459,17 @@ class BuildCommand extends Command {
.map((e) => targetMap[e])
.toList();
_buildDistributor(
platform: platform,
target: target,
targets: "apk",
args:
"--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}",
);
case PlatformType.macos:
case Target.macos:
await _getMacosDependencies();
_buildDistributor(
platform: platform,
target: target,
targets: "dmg",
args: "--description ${arch!.name}",
args: "--description $archName",
);
}
}
@@ -405,15 +477,15 @@ class BuildCommand extends Command {
main(args) async {
final runner = CommandRunner("setup", "build Application");
runner.addCommand(BuildCommand(platform: PlatformType.android));
runner.addCommand(BuildCommand(target: Target.android));
if (Platform.isWindows) {
runner.addCommand(BuildCommand(platform: PlatformType.windows));
runner.addCommand(BuildCommand(target: Target.windows));
}
if (Platform.isLinux) {
runner.addCommand(BuildCommand(platform: PlatformType.linux));
runner.addCommand(BuildCommand(target: Target.linux));
}
if (Platform.isMacOS) {
runner.addCommand(BuildCommand(platform: PlatformType.macos));
runner.addCommand(BuildCommand(target: Target.macos));
}
runner.run(args);
}

View File

@@ -1,14 +1,35 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
void main() {
final cmdList = [];
final ignoreHosts = "\"ass\"";
cmdList.add(
["gsettings", "set", "org.gnome.system.proxy", "port", ignoreHosts],
Future<void> main() async {
// final cmdList = [];
// final ignoreHosts = "\"ass\"";
// cmdList.add(
// ["gsettings", "set", "org.gnome.system.proxy", "port", ignoreHosts],
// );
// print(cmdList.first);
final internetAddress = InternetAddress(
"/tmp/FlClashSocket.sock",
type: InternetAddressType.unix,
);
print(cmdList.first);
final socket = await Socket.connect(internetAddress, 0);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data));
},
),
)
.transform(LineSplitter())
.listen((res) {
print(res);
});
}
startService() async {

View File

@@ -90,7 +90,10 @@ set(CLASH_DIR "../libclash/windows")
# set(CLASH_DIR "../libclash/windows/x86")
# endif()
install(FILES "${CLASH_DIR}/libclash.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
install(PROGRAMS "${CLASH_DIR}/FlClashCore.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
install(PROGRAMS "${CLASH_DIR}/FlClashHelperService.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
install(FILES "EnableLoopback.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"