From 599a4d297171ba141a8e85f43bd02f13762417e1 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Tue, 3 Dec 2024 21:47:12 +0800 Subject: [PATCH] Fix fallback issues Optimize change proxy Remake desktop Optimize lots of details --- .github/workflows/build.yaml | 83 +- .github/workflows/change.yaml | 66 - core/Clash.Meta | 2 +- core/action.go | 66 + core/common.go | 99 +- core/hub.go | 433 ++++++ core/lib.go | 441 +----- core/lib_android.go | 258 ++++ core/log.go | 38 - core/main.go | 8 +- core/message.go | 34 +- core/process.go | 81 - core/server.go | 160 ++ core/state.go | 47 - core/state/state.go | 9 +- core/tun.go | 157 -- core/tun/tun.go | 7 +- lib/application.dart | 8 +- lib/clash/clash.dart | 3 +- lib/clash/core.dart | 411 ++---- lib/clash/generated/clash_ffi.dart | 277 ++-- lib/clash/interface.dart | 59 + lib/clash/lib.dart | 367 +++++ lib/clash/message.dart | 54 +- lib/clash/service.dart | 448 +++++- lib/common/common.dart | 56 +- lib/common/constant.dart | 9 +- lib/common/future.dart | 42 + lib/common/http.dart | 4 + lib/common/launch.dart | 48 +- lib/common/lock.dart | 30 + lib/common/other.dart | 12 +- lib/common/path.dart | 49 +- lib/common/request.dart | 105 +- lib/common/system.dart | 79 +- lib/common/window.dart | 6 +- lib/common/windows.dart | 77 +- lib/controller.dart | 96 +- lib/enum/enum.dart | 36 + lib/fragments/application_setting.dart | 28 - lib/fragments/connections.dart | 26 +- lib/fragments/dashboard/dashboard.dart | 10 +- .../dashboard/network_detection.dart | 36 +- lib/fragments/proxies/card.dart | 9 +- lib/fragments/proxies/providers.dart | 8 +- lib/fragments/proxies/tab.dart | 5 +- lib/l10n/arb/intl_en.arb | 3 +- lib/l10n/arb/intl_zh_CN.arb | 3 +- lib/l10n/intl/messages_en.dart | 2 + lib/l10n/intl/messages_zh_CN.dart | 2 + lib/l10n/l10n.dart | 10 + lib/main.dart | 91 +- lib/manager/android_manager.dart | 29 +- lib/manager/clash_manager.dart | 49 +- lib/manager/tray_manager.dart | 1 - lib/manager/window_manager.dart | 4 +- lib/models/app.dart | 3 +- lib/models/clash_config.dart | 22 + lib/models/config.dart | 1 - lib/models/{ffi.dart => core.dart} | 67 +- lib/models/generated/config.freezed.dart | 25 +- lib/models/generated/config.g.dart | 2 - .../{ffi.freezed.dart => core.freezed.dart} | 270 +++- .../generated/{ffi.g.dart => core.g.dart} | 49 +- lib/models/generated/selector.freezed.dart | 62 +- lib/models/models.dart | 4 +- lib/models/profile.dart | 29 +- lib/models/selector.dart | 2 - lib/plugins/vpn.dart | 10 +- lib/state.dart | 147 +- lib/widgets/card.dart | 3 - linux/CMakeLists.txt | 2 +- macos/Podfile | 43 + macos/Podfile.lock | 123 ++ macos/Runner.xcodeproj/project.pbxproj | 46 +- services/helper/Cargo.lock | 1313 +++++++++++++++++ services/helper/Cargo.toml | 22 + services/helper/src/main.rs | 11 + services/helper/src/service/mod.rs | 33 + services/helper/src/service/service.rs | 131 ++ services/helper/src/service/windows.rs | 73 + setup.dart | 314 ++-- test/command_test.dart | 33 +- windows/CMakeLists.txt | 5 +- 84 files changed, 5363 insertions(+), 2063 deletions(-) delete mode 100644 .github/workflows/change.yaml create mode 100644 core/action.go create mode 100644 core/lib_android.go delete mode 100644 core/log.go delete mode 100644 core/process.go create mode 100644 core/server.go delete mode 100644 core/state.go delete mode 100644 core/tun.go create mode 100644 lib/clash/interface.dart create mode 100644 lib/clash/lib.dart create mode 100644 lib/common/future.dart create mode 100644 lib/common/lock.dart rename lib/models/{ffi.dart => core.dart} (90%) rename lib/models/generated/{ffi.freezed.dart => core.freezed.dart} (93%) rename lib/models/generated/{ffi.g.dart => core.g.dart} (84%) create mode 100755 macos/Podfile create mode 100755 macos/Podfile.lock create mode 100644 services/helper/Cargo.lock create mode 100644 services/helper/Cargo.toml create mode 100644 services/helper/src/main.rs create mode 100644 services/helper/src/service/mod.rs create mode 100644 services/helper/src/service/service.rs create mode 100644 services/helper/src/service/windows.rs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a3868d45..e5713ed1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,7 +6,70 @@ on: - 'v*' jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout + if: ${{ !contains(github.ref, '+') }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate + if: ${{ !contains(github.ref, '+') }} + run: | + tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) + preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1) + currentTag="" + for ((i = 0; i <= ${#tags[@]}; i++)); do + if (( i < ${#tags[@]} )); then + tag=${tags[$i]} + else + tag="" + fi + if [ -n "$currentTag" ]; then + if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then + break + fi + fi + if [ -n "$currentTag" ]; then + echo "## $currentTag" >> NEW_CHANGELOG.md + echo "" >> NEW_CHANGELOG.md + if [ -n "$tag" ]; then + git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md + else + git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md + fi + echo "" >> NEW_CHANGELOG.md + fi + currentTag=$tag + done + cat CHANGELOG.md >> NEW_CHANGELOG.md + cat NEW_CHANGELOG.md > CHANGELOG.md + + - name: Commit + if: ${{ !contains(github.ref, '+') }} + run: | + git add CHANGELOG.md + if ! git diff --cached --quiet; then + echo "Commit pushing" + git config --local user.email "chen08209@gmail.com" + git config --local user.name "chen08209" + git commit -m "Update changelog" + git push + if [ $? -eq 0 ]; then + echo "Push succeeded" + else + echo "Push failed" + exit 1 + fi + fi + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build: + needs: [ changelog ] runs-on: ${{ matrix.os }} strategy: matrix: @@ -27,25 +90,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 +147,6 @@ jobs: path: ./dist overwrite: true - upload: permissions: write-all needs: [ build ] diff --git a/.github/workflows/change.yaml b/.github/workflows/change.yaml deleted file mode 100644 index ea99dc0f..00000000 --- a/.github/workflows/change.yaml +++ /dev/null @@ -1,66 +0,0 @@ -name: change - -on: - push: - branches: - - 'main' - -jobs: - changelog: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Generate - run: | - tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) - preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1) - currentTag="" - for ((i = 0; i <= ${#tags[@]}; i++)); do - if (( i < ${#tags[@]} )); then - tag=${tags[$i]} - else - tag="" - fi - if [ -n "$currentTag" ]; then - if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then - break - fi - fi - if [ -n "$currentTag" ]; then - echo "## $currentTag" >> NEW_CHANGELOG.md - echo "" >> NEW_CHANGELOG.md - if [ -n "$tag" ]; then - git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md - else - git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md - fi - echo "" >> NEW_CHANGELOG.md - fi - currentTag=$tag - done - cat CHANGELOG.md >> NEW_CHANGELOG.md - cat NEW_CHANGELOG.md > CHANGELOG.md - - - name: Commit - run: | - git add CHANGELOG.md - if ! git diff --cached --quiet; then - echo "Commit pushing" - git config --local user.email "chen08209@gmail.com" - git config --local user.name "chen08209" - git commit -m "Update changelog" - git push - if [ $? -eq 0 ]; then - echo "Push succeeded" - else - echo "Push failed" - exit 1 - fi - fi - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/core/Clash.Meta b/core/Clash.Meta index 148f1a24..f7c61f88 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 148f1a2445cf90a98e922006293b1df0d5886c9a +Subproject commit f7c61f885ca32b9883125917e2187be0e175afc8 diff --git a/core/action.go b/core/action.go new file mode 100644 index 00000000..b04d20e5 --- /dev/null +++ b/core/action.go @@ -0,0 +1,66 @@ +package main + +import ( + "encoding/json" +) + +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"` +} + +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 +} diff --git a/core/common.go b/core/common.go index 0ffca86d..b6d95602 100644 --- a/core/common.go +++ b/core/common.go @@ -1,35 +1,30 @@ package main -import "C" import ( "context" - "core/state" "errors" "fmt" + "github.com/metacubex/mihomo/common/batch" "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" "github.com/metacubex/mihomo/adapter/provider" - "github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" cp "github.com/metacubex/mihomo/constant/provider" "github.com/metacubex/mihomo/hub" - "github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/log" rp "github.com/metacubex/mihomo/rules/provider" @@ -81,31 +76,12 @@ 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) - } -} +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)) +) func readFile(path string) ([]byte, error) { if _, err := os.Stat(path); os.IsNotExist(err) { @@ -119,19 +95,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 +225,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 +286,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 +319,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 +376,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 } diff --git a/core/hub.go b/core/hub.go index 06ab7d0f..c26f1028 100644 --- a/core/hub.go +++ b/core/hub.go @@ -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, + }) + } +} diff --git a/core/lib.go b/core/lib.go index f7a6291c..155b2dac 100644 --- a/core/lib.go +++ b/core/lib.go @@ -5,91 +5,54 @@ 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 initNativeApiBridge +func initNativeApiBridge(api unsafe.Pointer) { + bridge.InitDartApi(api) +} -//export start -func start() { - runLock.Lock() - defer runLock.Unlock() - isRunning = true +//export initMessage +func initMessage(port C.longlong) { + i := int64(port) + Port = i } -//export stop -func stop() { - runLock.Lock() - go func() { - defer runLock.Unlock() - isRunning = false - stopListeners() - }() +//export freeCString +func freeCString(s *C.char) { + C.free(unsafe.Pointer(s)) } //export initClash func initClash(homeDirStr *C.char) bool { - if !isInit { - constant.SetHomeDir(C.GoString(homeDirStr)) - isInit = true - } - return isInit + return handleInitClash(C.GoString(homeDirStr)) } -//export getIsInit -func getIsInit() bool { - return isInit +//export startListener +func startListener() { + handleStartListener() +} + +//export stopListener +func stopListener() { + handleStopListener() } -//export restartClash -func restartClash() bool { - execPath, _ := os.Executable() - go restartExecutable(execPath) - return true +//export getIsInit +func getIsInit() bool { + return handleGetIsInit() } //export shutdownClash func shutdownClash() bool { - stopListeners() - executor.Shutdown() - runtime.GC() - isInit = false - return true + return handleShutdown() } //export forceGc func forceGc() { - go func() { - log.Infoln("[APP] request force GC") - runtime.GC() - }() + handleForceGc() } //export validateConfig @@ -97,379 +60,121 @@ 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, "") + bridge.SendToPort(i, handleValidateConfig(bytes)) }() } -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) + bytes := []byte(C.GoString(s)) go func() { - _ = removeFile(getProfilePath(id)) - _ = removeFile(getProfileProvidersPath(id)) + bridge.SendToPort(i, handleUpdateConfig(bytes)) }() } //export getProxies func getProxies() *C.char { - data, err := json.Marshal(tunnel.ProxiesWithProviders()) - if err != nil { - return C.CString("") - } - return C.CString(string(data)) + return C.CString(handleGetProxies()) } //export changeProxy -func changeProxy(s *C.char) { +func changeProxy(s *C.char, port C.longlong) { + i := int64(port) 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) - } + handleChangeProxy(paramsString, func(value string) { + bridge.SendToPort(i, value) + }) } //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)) +func getTraffic(port C.int) *C.char { + onlyProxy := int(port) == 1 + return C.CString(handleGetTraffic(onlyProxy)) } //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)) +func getTotalTraffic(port C.int) *C.char { + onlyProxy := int(port) == 1 + return C.CString(handleGetTotalTraffic(onlyProxy)) } //export resetTraffic func resetTraffic() { - statistic.DefaultManager.ResetStatistic() + handleResetTraffic() } //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 + handleAsyncTestDelay(paramsString, func(value string) { + bridge.SendToPort(i, value) }) } -//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)) + return C.CString(handleGetConnections()) } //export closeConnections func closeConnections() { - statistic.DefaultManager.Range(func(c statistic.Tracker) bool { - err := c.Close() - if err != nil { - return false - } - return true - }) + handleCloseConnections() } //export closeConnection func closeConnection(id *C.char) { connectionId := C.GoString(id) - c := statistic.DefaultManager.Get(connectionId) - if c == nil { - return - } - _ = c.Close() + handleCloseConnection(connectionId) } //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)) + return C.CString(handleGetExternalProviders()) } //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)) +func getExternalProvider(externalProviderNameChar *C.char) *C.char { + externalProviderName := C.GoString(externalProviderNameChar) + return C.CString(handleGetExternalProvider(externalProviderName)) } //export updateGeoData -func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) { +func updateGeoData(geoTypeChar *C.char, geoNameChar *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, "") - }() + geoType := C.GoString(geoTypeChar) + geoName := C.GoString(geoNameChar) + handleUpdateGeoData(geoType, geoName, func(value string) { + bridge.SendToPort(i, value) + }) } //export updateExternalProvider -func updateExternalProvider(providerName *C.char, port C.longlong) { +func updateExternalProvider(providerNameChar *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, "") - }() + providerName := C.GoString(providerNameChar) + handleUpdateExternalProvider(providerName, func(value string) { + bridge.SendToPort(i, value) + }) } //export sideLoadExternalProvider -func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) { +func sideLoadExternalProvider(providerNameChar *C.char, dataChar *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) -} - -//export initMessage -func initMessage(port C.longlong) { - i := int64(port) - Port = i + providerName := C.GoString(providerNameChar) + data := []byte(C.GoString(dataChar)) + handleSideLoadExternalProvider(providerName, data, func(value string) { + bridge.SendToPort(i, value) + }) } -//export freeCString -func freeCString(s *C.char) { - C.free(unsafe.Pointer(s)) +//export startLog +func startLog() { + handleStartLog() } -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 stopLog +func stopLog() { + handleStopLog() } diff --git a/core/lib_android.go b/core/lib_android.go new file mode 100644 index 00000000..e526fc08 --- /dev/null +++ b/core/lib_android.go @@ -0,0 +1,258 @@ +//go:build android + +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/listener/sing_tun" + "github.com/metacubex/mihomo/log" + "strconv" + "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 + } +} diff --git a/core/log.go b/core/log.go deleted file mode 100644 index d9520b4f..00000000 --- a/core/log.go +++ /dev/null @@ -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 - } -} diff --git a/core/main.go b/core/main.go index c21ec05f..ad099644 100644 --- a/core/main.go +++ b/core/main.go @@ -3,8 +3,14 @@ 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]) } diff --git a/core/message.go b/core/message.go index caa25195..1326143f 100644 --- a/core/message.go +++ b/core/message.go @@ -6,21 +6,8 @@ import ( "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"` @@ -31,6 +18,21 @@ type Process struct { Metadata *constant.Metadata `json:"metadata"` } +var ( + Port int64 = -1 + ServicePort int64 = -1 +) + +const ( + LogMessage MessageType = "log" + ProtectMessage MessageType = "protect" + DelayMessage MessageType = "delay" + ProcessMessage MessageType = "process" + RequestMessage MessageType = "request" + StartedMessage MessageType = "started" + LoadedMessage MessageType = "loaded" +) + type Message struct { Type MessageType `json:"type"` Data interface{} `json:"data"` @@ -46,6 +48,12 @@ func SendMessage(message Message) { if err != nil { return } + if Port == -1 && ServicePort == -1 { + Action{ + Method: messageMethod, + }.callback(s) + return + } if handler, ok := messageHandlers[message.Type]; ok { handler(s) } else { diff --git a/core/process.go b/core/process.go deleted file mode 100644 index 5044c88f..00000000 --- a/core/process.go +++ /dev/null @@ -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) - } - }() -} diff --git a/core/server.go b/core/server.go new file mode 100644 index 00000000..51a9468a --- /dev/null +++ b/core/server.go @@ -0,0 +1,160 @@ +package main + +import "C" +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "strconv" +) + +var conn net.Conn + +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 + } + + 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), ¶ms) + 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), ¶ms) + 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 + } + +} diff --git a/core/state.go b/core/state.go deleted file mode 100644 index dcb22d6d..00000000 --- a/core/state.go +++ /dev/null @@ -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 - } -} diff --git a/core/state/state.go b/core/state/state.go index 0cdf43c6..7ba24fcc 100644 --- a/core/state/state.go +++ b/core/state/state.go @@ -1,13 +1,11 @@ -package state +//go:build android -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 } diff --git a/core/tun.go b/core/tun.go deleted file mode 100644 index bae958c3..00000000 --- a/core/tun.go +++ /dev/null @@ -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 -} diff --git a/core/tun/tun.go b/core/tun/tun.go index 780dd5bb..adf56d53 100644 --- a/core/tun/tun.go +++ b/core/tun/tun.go @@ -5,6 +5,7 @@ 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, diff --git a/lib/application.dart b/lib/application.dart index ceea1d69..60fff14b 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -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 { @override Future dispose() async { linkManager.destroy(); + _cancelTimer(); + await clashService?.destroy(); await globalState.appController.savePreferences(); + await globalState.appController.handleExit(); super.dispose(); - _cancelTimer(); } } diff --git a/lib/clash/clash.dart b/lib/clash/clash.dart index b7a61a37..295b4231 100644 --- a/lib/clash/clash.dart +++ b/lib/clash/clash.dart @@ -1,3 +1,4 @@ export 'core.dart'; +export 'lib.dart'; +export 'message.dart'; export 'service.dart'; -export 'message.dart'; \ No newline at end of file diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 6357cb39..25c44fb8 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -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(); - final isInit = clashFFI.initClash(homeDirChar) == 1; - malloc.free(homeDirChar); - return isInit; + Future _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 bytes = data.buffer.asUint8List(); + await geoFile.writeAsBytes(bytes, flush: true); + } + } catch (e) { + exit(0); + } } - shutdown() { - clashFFI.shutdownClash(); - lib.close(); + Future init({ + required ClashConfig clashConfig, + required Config config, + }) async { + await _initGeo(); + final homeDirPath = await appPath.getHomeDirPath(); + return await clashInterface.init(homeDirPath); } - bool get isInit => clashFFI.getIsInit() == 1; - - Future validateConfig(String data) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final dataChar = data.toNativeUtf8().cast(); - clashFFI.validateConfig( - dataChar, - receiver.sendPort.nativePort, - ); - malloc.free(dataChar); - return completer.future; + shutdown() async { + await clashInterface.shutdown(); } - Future updateConfig(UpdateConfigParams updateConfigParams) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final params = json.encode(updateConfigParams); - final paramsChar = params.toNativeUtf8().cast(); - clashFFI.updateConfig( - paramsChar, - receiver.sendPort.nativePort, - ); - malloc.free(paramsChar); - return completer.future; + FutureOr get isInit => clashInterface.isInit; + + FutureOr validateConfig(String data) { + return clashInterface.validateConfig(data); } - initMessage() { - clashFFI.initMessage( - receiver.sendPort.nativePort, - ); + Future updateConfig(UpdateConfigParams updateConfigParams) async { + return await clashInterface.updateConfig(updateConfigParams); } - Future> getProxiesGroups() { - final proxiesRaw = clashFFI.getProxies(); - final proxiesRawString = proxiesRaw.cast().toDartString(); - clashFFI.freeCString(proxiesRaw); + Future> getProxiesGroups() async { + final proxiesRawString = await clashInterface.getProxies(); return Isolate.run>(() { if (proxiesRawString.isEmpty) return []; final proxies = (json.decode(proxiesRawString) ?? {}) as Map; @@ -134,256 +113,112 @@ class ClashCore { }); } - Future> getExternalProviders() { - final externalProvidersRaw = clashFFI.getExternalProviders(); + FutureOr changeProxy(ChangeProxyParams changeProxyParams) async { + return await clashInterface.changeProxy(changeProxyParams); + } + + Future> 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> getExternalProviders() async { final externalProvidersRawString = - externalProvidersRaw.cast().toDartString(); - clashFFI.freeCString(externalProvidersRaw); - return Isolate.run>(() { - final externalProviders = - (json.decode(externalProvidersRawString) as List) - .map( - (item) => ExternalProvider.fromJson(item), - ) - .toList(); - return externalProviders; - }); + await clashInterface.getExternalProviders(); + return Isolate.run>( + () { + final externalProviders = + (json.decode(externalProvidersRawString) as List) + .map( + (item) => ExternalProvider.fromJson(item), + ) + .toList(); + return externalProviders; + }, + ); } - ExternalProvider? getExternalProvider(String externalProviderName) { - final externalProviderNameChar = - externalProviderName.toNativeUtf8().cast(); - final externalProviderRaw = - clashFFI.getExternalProvider(externalProviderNameChar); - malloc.free(externalProviderNameChar); - final externalProviderRawString = - externalProviderRaw.cast().toDartString(); - clashFFI.freeCString(externalProviderRaw); - if (externalProviderRawString.isEmpty) return null; - return ExternalProvider.fromJson(json.decode(externalProviderRawString)); + Future 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 updateGeoData({ required String geoType, required String geoName, }) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final geoTypeChar = geoType.toNativeUtf8().cast(); - final geoNameChar = geoName.toNativeUtf8().cast(); - clashFFI.updateGeoData( - geoTypeChar, - geoNameChar, - receiver.sendPort.nativePort, - ); - malloc.free(geoTypeChar); - malloc.free(geoNameChar); - return completer.future; + return clashInterface.updateGeoData(geoType: geoType, geoName: geoName); } Future sideLoadExternalProvider({ required String providerName, required String data, }) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final providerNameChar = providerName.toNativeUtf8().cast(); - final dataChar = data.toNativeUtf8().cast(); - clashFFI.sideLoadExternalProvider( - providerNameChar, - dataChar, - receiver.sendPort.nativePort, - ); - malloc.free(providerNameChar); - malloc.free(dataChar); - return completer.future; + return clashInterface.sideLoadExternalProvider( + providerName: providerName, data: data); } Future updateExternalProvider({ required String providerName, - }) { - final completer = Completer(); - final receiver = ReceivePort(); - receiver.listen((message) { - if (!completer.isCompleted) { - completer.complete(message); - receiver.close(); - } - }); - final providerNameChar = providerName.toNativeUtf8().cast(); - 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(); - clashFFI.changeProxy(paramsChar); - malloc.free(paramsChar); + startListener() async { + await clashInterface.startListener(); } - start() { - clashFFI.start(); + stopListener() async { + await clashInterface.stopListener(); } - stop() { - clashFFI.stop(); + Future getDelay(String proxyName) async { + final data = await clashInterface.asyncTestDelay(proxyName); + return Delay.fromJson(json.decode(data)); } - Future getDelay(String proxyName) { - final delayParams = { - "proxy-name": proxyName, - "timeout": httpTimeoutDuration.inMilliseconds, - }; - final completer = Completer(); - 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(); - clashFFI.asyncTestDelay( - delayParamsChar, - receiver.sendPort.nativePort, - ); - malloc.free(delayParamsChar); - return completer.future; + Future getTraffic(bool value) async { + final trafficString = await clashInterface.getTraffic(value); + return Traffic.fromMap(json.decode(trafficString)); } - clearEffect(String profileId) { - final profileIdChar = profileId.toNativeUtf8().cast(); - clashFFI.clearEffect(profileIdChar); - malloc.free(profileIdChar); + Future 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().toDartString()); - clashFFI.freeCString(versionInfoRaw); - return VersionInfo.fromJson(versionInfo); + resetTraffic() { + clashInterface.resetTraffic(); } - setState(CoreState state) { - final stateChar = json.encode(state).toNativeUtf8().cast(); - clashFFI.setState(stateChar); - malloc.free(stateChar); - } - - String getCurrentProfileName() { - final currentProfileRaw = clashFFI.getCurrentProfileName(); - final currentProfile = currentProfileRaw.cast().toDartString(); - clashFFI.freeCString(currentProfileRaw); - return currentProfile; - } - - AndroidVpnOptions getAndroidVpnOptions() { - final vpnOptionsRaw = clashFFI.getAndroidVpnOptions(); - final vpnOptions = json.decode(vpnOptionsRaw.cast().toDartString()); - clashFFI.freeCString(vpnOptionsRaw); - return AndroidVpnOptions.fromJson(vpnOptions); - } - - Traffic getTraffic() { - final trafficRaw = clashFFI.getTraffic(); - final trafficMap = json.decode(trafficRaw.cast().toDartString()); - clashFFI.freeCString(trafficRaw); - return Traffic.fromMap(trafficMap); - } - - Traffic getTotalTraffic() { - final trafficRaw = clashFFI.getTotalTraffic(); - final trafficMap = json.decode(trafficRaw.cast().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(); - 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(); - clashFFI.setProcessMap(processMapItemChar); - malloc.free(processMapItemChar); - } - - void setFdMap(int fd) { - clashFFI.setFdMap(fd); - } - - DateTime? getRunTime() { - final runTimeRaw = clashFFI.getRunTime(); - final runTimeString = runTimeRaw.cast().toDartString(); - clashFFI.freeCString(runTimeRaw); - if (runTimeString.isEmpty) return null; - return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); - } - - List getConnections() { - final connectionsDataRaw = clashFFI.getConnections(); - final connectionsData = - json.decode(connectionsDataRaw.cast().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(); - clashFFI.closeConnection(idChar); - malloc.free(idChar); - } - - closeConnections() { - clashFFI.closeConnections(); + clashInterface.forceGc(); } } diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index cb345d22..4d37d9a7 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -2362,21 +2362,46 @@ class ClashFFI { late final _updateDns = _updateDnsPtr.asFunction)>(); - void start() { - return _start(); + void initNativeApiBridge( + ffi.Pointer api, + ) { + return _initNativeApiBridge( + api, + ); } - late final _startPtr = - _lookup>('start'); - late final _start = _startPtr.asFunction(); + late final _initNativeApiBridgePtr = + _lookup)>>( + 'initNativeApiBridge'); + late final _initNativeApiBridge = _initNativeApiBridgePtr + .asFunction)>(); - void stop() { - return _stop(); + void initMessage( + int port, + ) { + return _initMessage( + port, + ); + } + + late final _initMessagePtr = + _lookup>( + 'initMessage'); + late final _initMessage = _initMessagePtr.asFunction(); + + void freeCString( + ffi.Pointer s, + ) { + return _freeCString( + s, + ); } - late final _stopPtr = - _lookup>('stop'); - late final _stop = _stopPtr.asFunction(); + late final _freeCStringPtr = + _lookup)>>( + 'freeCString'); + late final _freeCString = + _freeCStringPtr.asFunction)>(); int initClash( ffi.Pointer homeDirStr, @@ -2392,6 +2417,22 @@ class ClashFFI { late final _initClash = _initClashPtr.asFunction)>(); + void startListener() { + return _startListener(); + } + + late final _startListenerPtr = + _lookup>('startListener'); + late final _startListener = _startListenerPtr.asFunction(); + + void stopListener() { + return _stopListener(); + } + + late final _stopListenerPtr = + _lookup>('stopListener'); + late final _stopListener = _stopListenerPtr.asFunction(); + int getIsInit() { return _getIsInit(); } @@ -2400,14 +2441,6 @@ class ClashFFI { _lookup>('getIsInit'); late final _getIsInit = _getIsInitPtr.asFunction(); - int restartClash() { - return _restartClash(); - } - - late final _restartClashPtr = - _lookup>('restartClash'); - late final _restartClash = _restartClashPtr.asFunction(); - int shutdownClash() { return _shutdownClash(); } @@ -2458,20 +2491,6 @@ class ClashFFI { late final _updateConfig = _updateConfigPtr.asFunction, int)>(); - void clearEffect( - ffi.Pointer s, - ) { - return _clearEffect( - s, - ); - } - - late final _clearEffectPtr = - _lookup)>>( - 'clearEffect'); - late final _clearEffect = - _clearEffectPtr.asFunction)>(); - ffi.Pointer getProxies() { return _getProxies(); } @@ -2484,37 +2503,48 @@ class ClashFFI { void changeProxy( ffi.Pointer s, + int port, ) { return _changeProxy( s, + port, ); } - late final _changeProxyPtr = - _lookup)>>( - 'changeProxy'); + late final _changeProxyPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.LongLong)>>('changeProxy'); late final _changeProxy = - _changeProxyPtr.asFunction)>(); + _changeProxyPtr.asFunction, int)>(); - ffi.Pointer getTraffic() { - return _getTraffic(); + ffi.Pointer getTraffic( + int port, + ) { + return _getTraffic( + port, + ); } late final _getTrafficPtr = - _lookup Function()>>( + _lookup Function(ffi.Int)>>( 'getTraffic'); late final _getTraffic = - _getTrafficPtr.asFunction Function()>(); + _getTrafficPtr.asFunction Function(int)>(); - ffi.Pointer getTotalTraffic() { - return _getTotalTraffic(); + ffi.Pointer getTotalTraffic( + int port, + ) { + return _getTotalTraffic( + port, + ); } late final _getTotalTrafficPtr = - _lookup Function()>>( + _lookup Function(ffi.Int)>>( 'getTotalTraffic'); late final _getTotalTraffic = - _getTotalTrafficPtr.asFunction Function()>(); + _getTotalTrafficPtr.asFunction Function(int)>(); void resetTraffic() { return _resetTraffic(); @@ -2541,16 +2571,6 @@ class ClashFFI { late final _asyncTestDelay = _asyncTestDelayPtr .asFunction, int)>(); - ffi.Pointer getVersionInfo() { - return _getVersionInfo(); - } - - late final _getVersionInfoPtr = - _lookup Function()>>( - 'getVersionInfo'); - late final _getVersionInfo = - _getVersionInfoPtr.asFunction Function()>(); - ffi.Pointer getConnections() { return _getConnections(); } @@ -2595,10 +2615,10 @@ class ClashFFI { _getExternalProvidersPtr.asFunction Function()>(); ffi.Pointer getExternalProvider( - ffi.Pointer name, + ffi.Pointer externalProviderNameChar, ) { return _getExternalProvider( - name, + externalProviderNameChar, ); } @@ -2610,13 +2630,13 @@ class ClashFFI { .asFunction Function(ffi.Pointer)>(); void updateGeoData( - ffi.Pointer geoType, - ffi.Pointer geoName, + ffi.Pointer geoTypeChar, + ffi.Pointer geoNameChar, int port, ) { return _updateGeoData( - geoType, - geoName, + geoTypeChar, + geoNameChar, port, ); } @@ -2629,11 +2649,11 @@ class ClashFFI { void Function(ffi.Pointer, ffi.Pointer, int)>(); void updateExternalProvider( - ffi.Pointer providerName, + ffi.Pointer providerNameChar, int port, ) { return _updateExternalProvider( - providerName, + providerNameChar, port, ); } @@ -2646,13 +2666,13 @@ class ClashFFI { .asFunction, int)>(); void sideLoadExternalProvider( - ffi.Pointer providerName, - ffi.Pointer data, + ffi.Pointer providerNameChar, + ffi.Pointer dataChar, int port, ) { return _sideLoadExternalProvider( - providerName, - data, + providerNameChar, + dataChar, port, ); } @@ -2665,62 +2685,66 @@ class ClashFFI { _sideLoadExternalProviderPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, int)>(); - void initNativeApiBridge( - ffi.Pointer api, - ) { - return _initNativeApiBridge( - api, - ); + void startLog() { + return _startLog(); } - late final _initNativeApiBridgePtr = - _lookup)>>( - 'initNativeApiBridge'); - late final _initNativeApiBridge = _initNativeApiBridgePtr - .asFunction)>(); + late final _startLogPtr = + _lookup>('startLog'); + late final _startLog = _startLogPtr.asFunction(); - void initMessage( + void stopLog() { + return _stopLog(); + } + + late final _stopLogPtr = + _lookup>('stopLog'); + late final _stopLog = _stopLogPtr.asFunction(); + + void startTUN( + int fd, int port, ) { - return _initMessage( + return _startTUN( + fd, port, ); } - late final _initMessagePtr = - _lookup>( - 'initMessage'); - late final _initMessage = _initMessagePtr.asFunction(); + late final _startTUNPtr = + _lookup>( + 'startTUN'); + late final _startTUN = _startTUNPtr.asFunction(); - void freeCString( - ffi.Pointer s, - ) { - return _freeCString( - s, - ); + ffi.Pointer getRunTime() { + return _getRunTime(); } - late final _freeCStringPtr = - _lookup)>>( - 'freeCString'); - late final _freeCString = - _freeCStringPtr.asFunction)>(); + late final _getRunTimePtr = + _lookup Function()>>( + 'getRunTime'); + late final _getRunTime = + _getRunTimePtr.asFunction Function()>(); - void startLog() { - return _startLog(); + void stopTun() { + return _stopTun(); } - late final _startLogPtr = - _lookup>('startLog'); - late final _startLog = _startLogPtr.asFunction(); + late final _stopTunPtr = + _lookup>('stopTun'); + late final _stopTun = _stopTunPtr.asFunction(); - void stopLog() { - return _stopLog(); + void setFdMap( + int fd, + ) { + return _setFdMap( + fd, + ); } - late final _stopLogPtr = - _lookup>('stopLog'); - late final _stopLog = _stopLogPtr.asFunction(); + late final _setFdMapPtr = + _lookup>('setFdMap'); + late final _setFdMap = _setFdMapPtr.asFunction(); void setProcessMap( ffi.Pointer s, @@ -2769,51 +2793,6 @@ class ClashFFI { 'setState'); late final _setState = _setStatePtr.asFunction)>(); - - void startTUN( - int fd, - int port, - ) { - return _startTUN( - fd, - port, - ); - } - - late final _startTUNPtr = - _lookup>( - 'startTUN'); - late final _startTUN = _startTUNPtr.asFunction(); - - ffi.Pointer getRunTime() { - return _getRunTime(); - } - - late final _getRunTimePtr = - _lookup Function()>>( - 'getRunTime'); - late final _getRunTime = - _getRunTimePtr.asFunction Function()>(); - - void stopTun() { - return _stopTun(); - } - - late final _stopTunPtr = - _lookup>('stopTun'); - late final _stopTun = _stopTunPtr.asFunction(); - - void setFdMap( - int fd, - ) { - return _setFdMap( - fd, - ); - } - - late final _setFdMapPtr = - _lookup>('setFdMap'); - late final _setFdMap = _setFdMapPtr.asFunction(); } final class __mbstate_t extends ffi.Union { diff --git a/lib/clash/interface.dart b/lib/clash/interface.dart new file mode 100644 index 00000000..96fb7829 --- /dev/null +++ b/lib/clash/interface.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:fl_clash/models/models.dart'; + +mixin ClashInterface { + FutureOr init(String homeDir); + + FutureOr shutdown(); + + FutureOr get isInit; + + forceGc(); + + FutureOr validateConfig(String data); + + Future asyncTestDelay(String proxyName); + + FutureOr updateConfig(UpdateConfigParams updateConfigParams); + + FutureOr getProxies(); + + FutureOr changeProxy(ChangeProxyParams changeProxyParams); + + Future startListener(); + + Future stopListener(); + + FutureOr getExternalProviders(); + + FutureOr? getExternalProvider(String externalProviderName); + + Future updateGeoData({ + required String geoType, + required String geoName, + }); + + Future sideLoadExternalProvider({ + required String providerName, + required String data, + }); + + Future updateExternalProvider(String providerName); + + FutureOr getTraffic(bool value); + + FutureOr getTotalTraffic(bool value); + + resetTraffic(); + + startLog(); + + stopLog(); + + FutureOr getConnections(); + + FutureOr closeConnection(String id); + + FutureOr closeConnections(); +} diff --git a/lib/clash/lib.dart b/lib/clash/lib.dart new file mode 100644 index 00000000..923c26d1 --- /dev/null +++ b/lib/clash/lib.dart @@ -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(); + 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 validateConfig(String data) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final dataChar = data.toNativeUtf8().cast(); + clashFFI.validateConfig( + dataChar, + receiver.sendPort.nativePort, + ); + malloc.free(dataChar); + return completer.future; + } + + @override + Future updateConfig(UpdateConfigParams updateConfigParams) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final params = json.encode(updateConfigParams); + final paramsChar = params.toNativeUtf8().cast(); + clashFFI.updateConfig( + paramsChar, + receiver.sendPort.nativePort, + ); + malloc.free(paramsChar); + return completer.future; + } + + @override + String getProxies() { + final proxiesRaw = clashFFI.getProxies(); + final proxiesRawString = proxiesRaw.cast().toDartString(); + clashFFI.freeCString(proxiesRaw); + return proxiesRawString; + } + + @override + String getExternalProviders() { + final externalProvidersRaw = clashFFI.getExternalProviders(); + final externalProvidersRawString = + externalProvidersRaw.cast().toDartString(); + clashFFI.freeCString(externalProvidersRaw); + return externalProvidersRawString; + } + + @override + String getExternalProvider(String externalProviderName) { + final externalProviderNameChar = + externalProviderName.toNativeUtf8().cast(); + final externalProviderRaw = + clashFFI.getExternalProvider(externalProviderNameChar); + malloc.free(externalProviderNameChar); + final externalProviderRawString = + externalProviderRaw.cast().toDartString(); + clashFFI.freeCString(externalProviderRaw); + return externalProviderRawString; + } + + @override + Future updateGeoData({ + required String geoType, + required String geoName, + }) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final geoTypeChar = geoType.toNativeUtf8().cast(); + final geoNameChar = geoName.toNativeUtf8().cast(); + clashFFI.updateGeoData( + geoTypeChar, + geoNameChar, + receiver.sendPort.nativePort, + ); + malloc.free(geoTypeChar); + malloc.free(geoNameChar); + return completer.future; + } + + @override + Future sideLoadExternalProvider({ + required String providerName, + required String data, + }) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final providerNameChar = providerName.toNativeUtf8().cast(); + final dataChar = data.toNativeUtf8().cast(); + clashFFI.sideLoadExternalProvider( + providerNameChar, + dataChar, + receiver.sendPort.nativePort, + ); + malloc.free(providerNameChar); + malloc.free(dataChar); + return completer.future; + } + + @override + Future updateExternalProvider(String providerName) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final providerNameChar = providerName.toNativeUtf8().cast(); + clashFFI.updateExternalProvider( + providerNameChar, + receiver.sendPort.nativePort, + ); + malloc.free(providerNameChar); + return completer.future; + } + + @override + Future changeProxy(ChangeProxyParams changeProxyParams) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final params = json.encode(changeProxyParams); + final paramsChar = params.toNativeUtf8().cast(); + clashFFI.changeProxy( + paramsChar, + receiver.sendPort.nativePort, + ); + malloc.free(paramsChar); + return completer.future; + } + + @override + String getConnections() { + final connectionsDataRaw = clashFFI.getConnections(); + final connectionsString = connectionsDataRaw.cast().toDartString(); + clashFFI.freeCString(connectionsDataRaw); + return connectionsString; + } + + @override + closeConnection(String id) { + final idChar = id.toNativeUtf8().cast(); + 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 asyncTestDelay(String proxyName) { + final delayParams = { + "proxy-name": proxyName, + "timeout": httpTimeoutDuration.inMilliseconds, + }; + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final delayParamsChar = + json.encode(delayParams).toNativeUtf8().cast(); + 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().toDartString(); + clashFFI.freeCString(trafficRaw); + return trafficString; + } + + @override + String getTotalTraffic(bool value) { + final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0); + clashFFI.freeCString(trafficRaw); + return trafficRaw.cast().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(); + clashFFI.updateDns(dnsChar); + malloc.free(dnsChar); + } + + setProcessMap(ProcessMapItem processMapItem) { + final processMapItemChar = + json.encode(processMapItem).toNativeUtf8().cast(); + clashFFI.setProcessMap(processMapItemChar); + malloc.free(processMapItemChar); + } + + setState(CoreState state) { + final stateChar = json.encode(state).toNativeUtf8().cast(); + clashFFI.setState(stateChar); + malloc.free(stateChar); + } + + String getCurrentProfileName() { + final currentProfileRaw = clashFFI.getCurrentProfileName(); + final currentProfile = currentProfileRaw.cast().toDartString(); + clashFFI.freeCString(currentProfileRaw); + return currentProfile; + } + + AndroidVpnOptions getAndroidVpnOptions() { + final vpnOptionsRaw = clashFFI.getAndroidVpnOptions(); + final vpnOptions = json.decode(vpnOptionsRaw.cast().toDartString()); + clashFFI.freeCString(vpnOptionsRaw); + return AndroidVpnOptions.fromJson(vpnOptions); + } + + setFdMap(int fd) { + clashFFI.setFdMap(fd); + } + + DateTime? getRunTime() { + final runTimeRaw = clashFFI.getRunTime(); + final runTimeString = runTimeRaw.cast().toDartString(); + clashFFI.freeCString(runTimeRaw); + if (runTimeString.isEmpty) return null; + return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); + } +} + +final clashLib = Platform.isAndroid ? ClashLib() : null; diff --git a/lib/clash/message.dart b/lib/clash/message.dart index bcfba409..2f50a4ea 100644 --- a/lib/clash/message.dart +++ b/lib/clash/message.dart @@ -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._(); diff --git a/lib/clash/service.dart b/lib/clash/service.dart index d035c8a1..c3d6f449 100644 --- a/lib/clash/service.dart +++ b/lib/clash/service.dart @@ -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 'core.dart'; - -class ClashService { - Future 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 bytes = data.buffer.asUint8List(); - await geoFile.writeAsBytes(bytes, flush: true); +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/core.dart'; + +class ClashService with ClashInterface { + static ClashService? _instance; + + Completer serverCompleter = Completer(); + + Completer socketCompleter = Completer(); + + Map 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.fromHandlers( + handleData: (Uint8List data, EventSink sink) { + sink.add(utf8.decode(data, allowMalformed: true)); + }, + ), + ) + .transform(LineSplitter()) + .listen( + (data) { + _handleAction( + Action.fromJson( + json.decode(data.trim()), + ), + ); + }, + ); + } + } + + 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(); } - } catch (e) { - debugPrint("$e"); - exit(0); } } - Future init({ - required ClashConfig clashConfig, - required Config config, + _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 _invoke({ + required ActionMethod method, + dynamic data, + Duration? timeout, + FutureOr 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(); + socket.writeln( + json.encode( + Action( + id: id, + method: method, + data: data, + ), + ), + ); + return (callbackCompleterMap[id] as Completer).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 init(String homeDir) { + return _invoke( + method: ActionMethod.initClash, + data: homeDir, + ); + } + + @override + shutdown() async { + await _invoke( + method: ActionMethod.shutdown, + ); + if (Platform.isWindows) { + await request.stopCoreByHelper(); + } + await _destroySocket(); + process?.kill(); + process = null; + } + + @override + Future get isInit { + return _invoke( + method: ActionMethod.getIsInit, + ); + } + + @override + forceGc() { + _prueInvoke(method: ActionMethod.forceGc); + } + + @override + FutureOr validateConfig(String data) { + return _invoke( + method: ActionMethod.validateConfig, + data: data, + ); + } + + @override + Future updateConfig(UpdateConfigParams updateConfigParams) async { + return await _invoke( + method: ActionMethod.updateConfig, + data: json.encode(updateConfigParams), + timeout: const Duration(seconds: 10), + ); + } + + @override + Future getProxies() { + return _invoke( + method: ActionMethod.getProxies, + ); + } + + @override + FutureOr changeProxy(ChangeProxyParams changeProxyParams) { + return _invoke( + method: ActionMethod.changeProxy, + data: json.encode(changeProxyParams), + ); + } + + @override + FutureOr getExternalProviders() { + return _invoke( + method: ActionMethod.getExternalProviders, + ); + } + + @override + FutureOr getExternalProvider(String externalProviderName) { + return _invoke( + method: ActionMethod.getExternalProvider, + data: externalProviderName, + ); + } + + @override + Future updateGeoData({ + required String geoType, + required String geoName, + }) { + return _invoke( + method: ActionMethod.updateGeoData, + data: json.encode( + { + "geoType": geoType, + "geoName": geoName, + }, + ), + ); + } + + @override + Future sideLoadExternalProvider({ + required String providerName, + required String data, + }) { + return _invoke( + method: ActionMethod.sideLoadExternalProvider, + data: json.encode({ + "providerName": providerName, + "data": data, + }), + ); + } + + @override + Future updateExternalProvider(String providerName) { + return _invoke( + method: ActionMethod.updateExternalProvider, + data: providerName, + ); + } + + @override + FutureOr getConnections() { + return _invoke( + method: ActionMethod.getConnections, + ); + } + + @override + Future closeConnections() { + return _invoke( + method: ActionMethod.closeConnections, + ); + } + + @override + Future closeConnection(String id) { + return _invoke( + method: ActionMethod.closeConnection, + data: id, + ); + } + + @override + FutureOr getTotalTraffic(bool value) { + return _invoke( + method: ActionMethod.getTotalTraffic, + data: value, + ); + } + + @override + FutureOr getTraffic(bool value) { + return _invoke( + 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 startListener() { + return _invoke( + method: ActionMethod.startListener, + ); + } + + @override + stopListener() { + return _invoke( + method: ActionMethod.stopListener, + ); + } + + @override + Future asyncTestDelay(String proxyName) { + final delayParams = { + "proxy-name": proxyName, + "timeout": httpTimeoutDuration.inMilliseconds, + }; + return _invoke( + 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; diff --git a/lib/common/common.dart b/lib/common/common.dart index e47463dc..f9c2497a 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -1,33 +1,35 @@ -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'; \ No newline at end of file +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 'window.dart'; +export 'windows.dart'; diff --git a/lib/common/constant.dart b/lib/common/constant.dart index cb9fa1d6..b5d3b8b9 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -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 = "2024124"; 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": diff --git a/lib/common/future.dart b/lib/common/future.dart new file mode 100644 index 00000000..6083e1a9 --- /dev/null +++ b/lib/common/future.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:ui'; + +extension CompleterExt on Completer { + safeFuture({ + Duration? timeout, + VoidCallback? onLast, + FutureOr 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 on Future { + Future withTimeout({ + required Duration timeout, + required String functionName, + FutureOr Function()? onTimeout, + }) { + return this.timeout( + timeout, + onTimeout: () async { + if (onTimeout != null) { + return onTimeout(); + } else { + throw TimeoutException('$functionName timeout'); + } + }, + ); + } +} diff --git a/lib/common/http.dart b/lib/common/http.dart index c7b7476b..4702777e 100644 --- a/lib/common/http.dart +++ b/lib/common/http.dart @@ -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; diff --git a/lib/common/launch.dart b/lib/common/launch.dart index 5a752e60..04298573 100644 --- a/lib/common/launch.dart +++ b/lib/common/launch.dart @@ -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 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 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 windowsEnable() async { - await disable(); - return await windows?.registerTask(appName) ?? false; - } - Future 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(); diff --git a/lib/common/lock.dart b/lib/common/lock.dart new file mode 100644 index 00000000..42467e78 --- /dev/null +++ b/lib/common/lock.dart @@ -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 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(); diff --git a/lib/common/other.dart b/lib/common/other.dart index 8b5b4355..8f62e997 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -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"; diff --git a/lib/common/path.dart b/lib/common/path.dart index 7b60ad12..7d6a1488 100644 --- a/lib/common/path.dart +++ b/lib/common/path.dart @@ -13,34 +13,17 @@ class AppPath { Completer tempDir = Completer(); late String appDirPath; - // Future _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 getDownloadDirPath() async { final directory = await downloadDir.future; return directory.path; @@ -58,6 +58,11 @@ class AppPath { return directory.path; } + Future getLockFilePath() async { + final directory = await dataDir.future; + return join(directory.path, "FlClash.lock"); + } + Future getProfilesPath() async { final directory = await dataDir.future; return join(directory.path, profilesDirectoryName); @@ -69,6 +74,12 @@ class AppPath { return join(directory, "$id.yaml"); } + Future getProvidersPath(String? id) async { + if (id == null) return null; + final directory = await getProfilesPath(); + return join(directory, "providers", id); + } + Future get tempPath async { final directory = await tempDir.future; return directory.path; diff --git a/lib/common/request.dart b/lib/common/request.dart index abbbbb39..37d895f3 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; @@ -76,28 +78,99 @@ class Request { }; Future checkIp({CancelToken? cancelToken}) async { - for (final source in _ipInfoSources.entries) { - try { + try { + for (final source in _ipInfoSources.entries) { final response = await _dio - .get>( - source.key, - cancelToken: cancelToken, - ) - .timeout( - httpTimeoutDuration, - ); - if (response.statusCode == 200 && response.data != null) { - return source.value(response.data!); + .get>(source.key, cancelToken: cancelToken) + .timeout(httpTimeoutDuration); + if (response.statusCode != 200 || response.data == null) { + continue; } - } catch (e) { - if (cancelToken?.isCancelled == true) { - throw "cancelled"; - } - continue; + return source.value(response.data!); + } + } catch (e) { + debugPrint("checkIp error ===> $e"); + if (cancelToken?.isCancelled == true) { + throw "cancelled"; } } return null; } + + Future 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 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 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(); diff --git a/lib/common/system.dart b/lib/common/system.dart index c6c68b10..be680998 100644 --- a/lib/common/system.dart +++ b/lib/common/system.dart @@ -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 get version async { final deviceInfo = await DeviceInfoPlugin().deviceInfo; return switch (Platform.operatingSystem) { @@ -35,6 +31,73 @@ class System { }; } + Future 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 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( + 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(); diff --git a/lib/common/window.dart b/lib/common/window.dart index 30808b0c..4c222e60 100755 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -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"); diff --git a/lib/common/windows.dart b/lib/common/windows.dart index 548f2b1d..af95d753 100644 --- a/lib/common/windows.dart +++ b/lib/common/windows.dart @@ -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 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 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 registerTask(String appName) async { final taskXml = ''' diff --git a/lib/controller.dart b/lib/controller.dart index d1bff04d..c9032b55 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -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(); @@ -43,6 +44,13 @@ class AppController { applyProfileDebounce = debounce(() 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,9 +76,18 @@ 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(); @@ -73,10 +98,6 @@ class AppController { } } - updateCoreVersionInfo() { - globalState.updateCoreVersionInfo(appState); - } - updateRunTime() { final startTime = globalState.startTime; if (startTime != null) { @@ -90,6 +111,7 @@ class AppController { updateTraffic() { globalState.updateTraffic( + config: config, appFlowingState: appFlowingState, ); } @@ -102,7 +124,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 +152,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 +236,8 @@ class AppController { changeProxy({ required String groupName, required String proxyName, - }) { - globalState.changeProxy( + }) async { + await globalState.changeProxy( config: config, groupName: groupName, proxyName: proxyName, @@ -234,22 +257,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 +321,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 +343,6 @@ class AppController { } else { await updateStatus(config.appSetting.autoRun); } - autoUpdateProfiles(); - autoCheckUpdate(); } setDelay(Delay delay) { @@ -525,6 +550,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 +585,6 @@ class AppController { ); } - updateAdminAutoLaunch() { - config.appSetting = config.appSetting.copyWith( - adminAutoLaunch: !config.appSetting.adminAutoLaunch, - ); - } - updateVisible() async { final visible = await window?.isVisible(); if (visible != null && !visible) { diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 6ef7dfeb..970f9b37 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -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, +} diff --git a/lib/fragments/application_setting.dart b/lib/fragments/application_setting.dart index 14de55d5..94ee09b1 100644 --- a/lib/fragments/application_setting.dart +++ b/lib/fragments/application_setting.dart @@ -60,32 +60,6 @@ class UsageSwitch extends StatelessWidget { } } -class AdminAutoLaunchItem extends StatelessWidget { - const AdminAutoLaunchItem({super.key}); - - @override - Widget build(BuildContext context) { - return Selector( - 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( selector: (_, config) => config.appSetting.silentLaunch, diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart index f997e3cc..81c1b576 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -27,18 +27,19 @@ class _ConnectionsFragmentState extends State { @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 { 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 { ); } - _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(), ); } diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index ffb1c8e4..4b5402be 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -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 { // 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(), diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index 0472c2b7..5b8731de 100644 --- a/lib/fragments/dashboard/network_detection.dart +++ b/lib/fragments/dashboard/network_detection.dart @@ -9,6 +9,13 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +final networkDetectionState = ValueNotifier( + 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 { - final networkDetectionState = ValueNotifier( - const NetworkDetectionState( - isTesting: true, - ipInfo: null, - ), - ); bool? _preIsStart; Function? _checkIpDebounce; Timer? _setTimeoutTimer; @@ -55,17 +56,20 @@ class _NetworkDetectionState extends State { ); 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 { } @override - void dispose() { + dispose() { super.dispose(); - networkDetectionState.dispose(); } String countryCodeToEmoji(String countryCode) { @@ -109,7 +112,7 @@ class _NetworkDetectionState extends State { @override Widget build(BuildContext context) { - _checkIpDebounce ??= debounce(_checkIp); + _checkIpDebounce ??= debounce(_checkIp, milliseconds: 600); return _checkIpContainer( ValueListenableBuilder( valueListenable: networkDetectionState, @@ -156,7 +159,8 @@ class _NetworkDetectionState extends State { .textTheme .titleLarge ?.copyWith( - fontFamily: FontFamily.twEmoji.value, + fontFamily: + FontFamily.twEmoji.value, ), ), ) diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index 51f39119..f239bcf1 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -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( diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart index a2bf945b..bc3d2710 100644 --- a/lib/fragments/proxies/providers.dart +++ b/lib/fragments/proxies/providers.dart @@ -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 { 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; }); diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index 38a22e3a..c45d9218 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -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'; @@ -381,7 +382,9 @@ class _DelayTestButtonState extends State _healthcheck() async { _controller.forward(); await widget.onClick(); - _controller.reverse(); + if (mounted) { + _controller.reverse(); + } } @override diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 905e24b3..86c7abe7 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -330,5 +330,6 @@ "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" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index bb2aedf6..9904cd3b 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -330,5 +330,6 @@ "routeMode_bypassPrivate": "绕过私有路由地址", "routeMode_config": "使用配置", "routeAddress": "路由地址", - "routeAddressDesc": "配置监听路由地址" + "routeAddressDesc": "配置监听路由地址", + "pleaseInputAdminPassword": "请输入管理员密码" } diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 66d3bca8..671c3d68 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -326,6 +326,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( diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 030fe35e..6a7669c3 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -259,6 +259,8 @@ class MessageLookup extends MessageLookupByLibrary { "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"), "pleaseBindWebDAV": MessageLookupByLibrary.simpleMessage("请绑定WebDAV"), + "pleaseInputAdminPassword": + MessageLookupByLibrary.simpleMessage("请输入管理员密码"), "pleaseUploadFile": MessageLookupByLibrary.simpleMessage("请上传文件"), "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage("请上传有效的二维码"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 333a9a2c..7363abe5 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -3369,6 +3369,16 @@ class AppLocalizations { args: [], ); } + + /// `Please enter the admin password` + String get pleaseInputAdminPassword { + return Intl.message( + 'Please enter the admin password', + name: 'pleaseInputAdminPassword', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index c9c93ea6..9eccc48c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main() async { WidgetsFlutterBinding.ensureInitialized(); - clashCore.initMessage(); + clashLib?.initMessage(); globalState.packageInfo = await PackageInfo.fromPlatform(); final version = await system.version; final config = await preferences.getConfig() ?? Config(); @@ -42,11 +43,6 @@ Future main() async { config: config, clashConfig: clashConfig, ); - await globalState.init( - appState: appState, - config: config, - clashConfig: clashConfig, - ); HttpOverrides.global = FlClashHttpOverrides(); runAppWithPreferences( const Application(), @@ -69,39 +65,66 @@ Future 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 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); diff --git a/lib/manager/android_manager.dart b/lib/manager/android_manager.dart index 132dbad1..4c9c05bf 100644 --- a/lib/manager/android_manager.dart +++ b/lib/manager/android_manager.dart @@ -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 { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); } + Widget _updateCoreState(Widget child) { + return Selector2( + 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( selector: (_, config) => config.appSetting.hidden, @@ -36,6 +59,10 @@ class _AndroidContainerState extends State { @override Widget build(BuildContext context) { - return _excludeContainer(widget.child); + return _updateCoreState( + _excludeContainer( + widget.child, + ), + ); } } diff --git a/lib/manager/clash_manager.dart b/lib/manager/clash_manager.dart index 23739cec..2ba2753c 100644 --- a/lib/manager/clash_manager.dart +++ b/lib/manager/clash_manager.dart @@ -57,41 +57,20 @@ class _ClashContainerState extends State with AppMessageListener { ); } - Widget _updateCoreState(Widget child) { - return Selector2( - 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( 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 with AppMessageListener { @override Widget build(BuildContext context) { return _changeProfileContainer( - _updateCoreState( - _updateContainer( - widget.child, - ), + _updateContainer( + widget.child, ), ); } @@ -158,7 +135,7 @@ class _ClashContainerState extends State with AppMessageListener { Future onLoaded(String providerName) async { final appController = globalState.appController; appController.appState.setProvider( - clashCore.getExternalProvider( + await clashCore.getExternalProvider( providerName, ), ); diff --git a/lib/manager/tray_manager.dart b/lib/manager/tray_manager.dart index 5a984ebf..57b7edc5 100755 --- a/lib/manager/tray_manager.dart +++ b/lib/manager/tray_manager.dart @@ -30,7 +30,6 @@ class _TrayContainerState extends State 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, diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index 42397160..487b9318 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -28,7 +28,6 @@ class _WindowContainerState extends State return Selector( 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 } @override - void onTaskbarCreated() { + Future onTaskbarCreated() async { globalState.appController.updateTray(true); + await globalState.appController.restartCore(); super.onTaskbarCreated(); } diff --git a/lib/models/app.dart b/lib/models/app.dart index 5332f236..ee418ecc 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -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; diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index f670a2fd..2ae52e11 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -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 toJson() { return _$ClashConfigToJson(this); } diff --git a/lib/models/config.dart b/lib/models/config.dart index fa6c3434..bc8bc775 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -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, diff --git a/lib/models/ffi.dart b/lib/models/core.dart similarity index 90% rename from lib/models/ffi.dart rename to lib/models/core.dart index 5f29352c..9fe716df 100644 --- a/lib/models/ffi.dart +++ b/lib/models/core.dart @@ -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 json) => - _$ProcessFromJson(json); + factory ProcessData.fromJson(Map json) => + _$ProcessDataFromJson(json); } @freezed @@ -212,24 +236,19 @@ class TunProps with _$TunProps { _$TunPropsFromJson(json); } -abstract mixin class AppMessageListener { - void onLog(Log log) {} - - void onDelay(Delay delay) {} - - void onRequest(Connection connection) {} - - void onStarted(String runTime) {} +@freezed +class Action with _$Action { + const factory Action({ + required ActionMethod method, + required dynamic data, + required String id, + }) = _Action; - void onLoaded(String providerName) {} + factory Action.fromJson(Map 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); + } } diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index da63a731..74020469 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -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; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index c5f09808..e45c516f 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -56,7 +56,6 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map 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 _$$AppSettingImplToJson(_$AppSettingImpl instance) => 'locale': instance.locale, 'onlyProxy': instance.onlyProxy, 'autoLaunch': instance.autoLaunch, - 'adminAutoLaunch': instance.adminAutoLaunch, 'silentLaunch': instance.silentLaunch, 'autoRun': instance.autoRun, 'openLogs': instance.openLogs, diff --git a/lib/models/generated/ffi.freezed.dart b/lib/models/generated/core.freezed.dart similarity index 93% rename from lib/models/generated/ffi.freezed.dart rename to lib/models/generated/core.freezed.dart index d6269e38..e4b33817 100644 --- a/lib/models/generated/ffi.freezed.dart +++ b/lib/models/generated/core.freezed.dart @@ -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 json) { - return _Process.fromJson(json); +ProcessData _$ProcessDataFromJson(Map 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 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 get copyWith => throw _privateConstructorUsedError; + $ProcessDataCopyWith 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 json) => - _$$ProcessImplFromJson(json); + factory _$ProcessDataImpl.fromJson(Map 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 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 json) = _$ProcessImpl.fromJson; + factory _ProcessData.fromJson(Map 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 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 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 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 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 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 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; +} diff --git a/lib/models/generated/ffi.g.dart b/lib/models/generated/core.g.dart similarity index 84% rename from lib/models/generated/ffi.g.dart rename to lib/models/generated/core.g.dart index 8793a5e3..0ba39ae9 100644 --- a/lib/models/generated/ffi.g.dart +++ b/lib/models/generated/core.g.dart @@ -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 _$$NowImplToJson(_$NowImpl instance) => { 'value': instance.value, }; -_$ProcessImpl _$$ProcessImplFromJson(Map json) => - _$ProcessImpl( +_$ProcessDataImpl _$$ProcessDataImplFromJson(Map json) => + _$ProcessDataImpl( id: (json['id'] as num).toInt(), metadata: Metadata.fromJson(json['metadata'] as Map), ); -Map _$$ProcessImplToJson(_$ProcessImpl instance) => +Map _$$ProcessDataImplToJson(_$ProcessDataImpl instance) => { 'id': instance.id, 'metadata': instance.metadata, @@ -289,3 +289,44 @@ Map _$$TunPropsImplToJson(_$TunPropsImpl instance) => 'dns': instance.dns, 'dns6': instance.dns6, }; + +_$ActionImpl _$$ActionImplFromJson(Map json) => _$ActionImpl( + method: $enumDecode(_$ActionMethodEnumMap, json['method']), + data: json['data'], + id: json['id'] as String, + ); + +Map _$$ActionImplToJson(_$ActionImpl instance) => + { + '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', +}; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 5fcfb783..52d14322 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -1047,7 +1047,6 @@ abstract class _ApplicationSelectorState implements ApplicationSelectorState { mixin _$TrayState { Mode get mode => 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; @@ -1069,7 +1068,6 @@ abstract class $TrayStateCopyWith<$Res> { $Res call( {Mode mode, bool autoLaunch, - bool adminAutoLaunch, bool systemProxy, bool tunEnable, bool isStart, @@ -1094,7 +1092,6 @@ class _$TrayStateCopyWithImpl<$Res, $Val extends TrayState> $Res call({ Object? mode = null, Object? autoLaunch = null, - Object? adminAutoLaunch = null, Object? systemProxy = null, Object? tunEnable = null, Object? isStart = null, @@ -1110,10 +1107,6 @@ class _$TrayStateCopyWithImpl<$Res, $Val extends TrayState> ? _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 @@ -1149,7 +1142,6 @@ abstract class _$$TrayStateImplCopyWith<$Res> $Res call( {Mode mode, bool autoLaunch, - bool adminAutoLaunch, bool systemProxy, bool tunEnable, bool isStart, @@ -1172,7 +1164,6 @@ class __$$TrayStateImplCopyWithImpl<$Res> $Res call({ Object? mode = null, Object? autoLaunch = null, - Object? adminAutoLaunch = null, Object? systemProxy = null, Object? tunEnable = null, Object? isStart = null, @@ -1188,10 +1179,6 @@ class __$$TrayStateImplCopyWithImpl<$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, systemProxy: null == systemProxy ? _value.systemProxy : systemProxy // ignore: cast_nullable_to_non_nullable @@ -1222,7 +1209,6 @@ class _$TrayStateImpl implements _TrayState { const _$TrayStateImpl( {required this.mode, required this.autoLaunch, - required this.adminAutoLaunch, required this.systemProxy, required this.tunEnable, required this.isStart, @@ -1234,8 +1220,6 @@ class _$TrayStateImpl implements _TrayState { @override final bool autoLaunch; @override - final bool adminAutoLaunch; - @override final bool systemProxy; @override final bool tunEnable; @@ -1248,7 +1232,7 @@ class _$TrayStateImpl implements _TrayState { @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, autoLaunch: $autoLaunch, systemProxy: $systemProxy, tunEnable: $tunEnable, isStart: $isStart, locale: $locale, brightness: $brightness)'; } @override @@ -1259,8 +1243,6 @@ class _$TrayStateImpl implements _TrayState { (identical(other.mode, mode) || other.mode == mode) && (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) || @@ -1272,8 +1254,8 @@ class _$TrayStateImpl implements _TrayState { } @override - int get hashCode => Object.hash(runtimeType, mode, autoLaunch, - adminAutoLaunch, systemProxy, tunEnable, isStart, locale, brightness); + int get hashCode => Object.hash(runtimeType, mode, autoLaunch, systemProxy, + tunEnable, isStart, locale, brightness); /// Create a copy of TrayState /// with the given fields replaced by the non-null parameter values. @@ -1288,7 +1270,6 @@ abstract class _TrayState implements TrayState { const factory _TrayState( {required final Mode mode, required final bool autoLaunch, - required final bool adminAutoLaunch, required final bool systemProxy, required final bool tunEnable, required final bool isStart, @@ -1300,8 +1281,6 @@ abstract class _TrayState implements TrayState { @override bool get autoLaunch; @override - bool get adminAutoLaunch; - @override bool get systemProxy; @override bool get tunEnable; @@ -3150,7 +3129,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 +3143,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 +3162,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 +3180,7 @@ abstract class _$$AutoLaunchStateImplCopyWith<$Res> __$$AutoLaunchStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isAutoLaunch, bool isAdminAutoLaunch}); + $Res call({bool isAutoLaunch}); } /// @nodoc @@ -3224,17 +3197,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 +3210,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 +3226,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 +3243,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. diff --git a/lib/models/models.dart b/lib/models/models.dart index d75d5a0b..b55c23ed 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -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'; \ No newline at end of file diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 7951122b..112ad233 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -96,6 +96,21 @@ extension ProfileExtension on Profile { return await File(profilePath!).exists(); } + Future 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 get profileLastModified async { + final file = await getFile(); + return (await file.lastModified()).microsecondsSinceEpoch; + } + Future 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()); } diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 3926db4f..7f0e657e 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -64,7 +64,6 @@ class TrayState with _$TrayState { const factory TrayState({ required Mode mode, required bool autoLaunch, - required bool adminAutoLaunch, required bool systemProxy, required bool tunEnable, required bool isStart, @@ -197,7 +196,6 @@ class ProxiesActionsState with _$ProxiesActionsState { class AutoLaunchState with _$AutoLaunchState { const factory AutoLaunchState({ required bool isAutoLaunch, - required bool isAdminAutoLaunch, }) = _AutoLaunchState; } diff --git a/lib/plugins/vpn.dart b/lib/plugins/vpn.dart index 6d267c12..8cba609b 100644 --- a/lib/plugins/vpn.dart +++ b/lib/plugins/vpn.dart @@ -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 startVpn() async { - final options = clashCore.getAndroidVpnOptions(); + final options = clashLib?.getAndroidVpnOptions(); return await methodChannel.invokeMethod("start", { 'data': json.encode(options), }); @@ -54,7 +54,7 @@ class Vpn { return await methodChannel.invokeMethod("setProtect", {'fd': fd}); } - Future resolverProcess(Process process) async { + Future resolverProcess(ProcessData process) async { return await methodChannel.invokeMethod("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: diff --git a/lib/state.dart b/lib/state.dart index 54f1c578..4cd2a639 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -30,6 +30,8 @@ class GlobalState { late AppController appController; GlobalKey homeScaffoldKey = GlobalKey(); List updateFunctionLists = []; + bool lastTunEnable = false; + int? lastProfileModified; bool get isStart => startTime != null && startTime!.isBeforeNow; @@ -47,16 +49,61 @@ class GlobalState { timer?.cancel(); } + Future 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 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 +114,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,15 +130,30 @@ 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 clashCore.stopListener(); + clashLib?.stopTun(); await service?.destroy(); startTime = null; stopListenUpdate(); @@ -106,6 +166,7 @@ class GlobalState { }) async { clashCore.requestGc(); await updateClashConfig( + appState: appState, clashConfig: clashConfig, config: config, isPatch: false, @@ -123,30 +184,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 updateGroups(AppState appState) async { @@ -198,8 +256,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 +284,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); } } } @@ -333,6 +394,9 @@ class GlobalState { required ClashConfig clashConfig, bool focus = false, }) async { + if (Platform.isAndroid) { + return; + } if (!Platform.isLinux) { await _updateSystemTray( brightness: appState.brightness, @@ -399,17 +463,6 @@ class GlobalState { 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, diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 589954a9..0110bd07 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -135,9 +135,6 @@ class CommonCard extends StatelessWidget { if (isSelected) { return colorScheme.secondaryContainer; } - if (states.isEmpty) { - return colorScheme.surfaceContainerLow; - } return colorScheme.surfaceContainer; } } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 06b9d045..538d9284 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -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}) diff --git a/macos/Podfile b/macos/Podfile new file mode 100755 index 00000000..c795730d --- /dev/null +++ b/macos/Podfile @@ -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 diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100755 index 00000000..a525f4db --- /dev/null +++ b/macos/Podfile.lock @@ -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 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 190228d6..295c4cae 100755 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; - 7AC277A92B90DE1400E026B1 /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../libclash/macos/libclash.dylib; sourceTree = ""; }; 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 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 = ""; }; + F50091042CF74B7700D43AEA /* FlClashCore */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = FlClashCore; path = ../libclash/macos/FlClashCore; sourceTree = ""; }; /* 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; diff --git a/services/helper/Cargo.lock b/services/helper/Cargo.lock new file mode 100644 index 00000000..4d8f7cc4 --- /dev/null +++ b/services/helper/Cargo.lock @@ -0,0 +1,1313 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "helper" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "serde", + "tokio", + "warp", + "windows-service", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "libc" +version = "0.2.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http 0.2.12", + "hyper", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "windows-service" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +dependencies = [ + "bitflags", + "widestring", + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/services/helper/Cargo.toml b/services/helper/Cargo.toml new file mode 100644 index 00000000..2ad0a4e5 --- /dev/null +++ b/services/helper/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "helper" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "helper" +path = "src/main.rs" + +[dependencies] +windows-service = "0.7.0" +tokio = { version = "1", features = ["full"] } +anyhow = "1.0.93" +warp = "0.3.7" +bytes = "1.9.0" +serde = { version = "1.0.215", features = ["derive"] } + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" \ No newline at end of file diff --git a/services/helper/src/main.rs b/services/helper/src/main.rs new file mode 100644 index 00000000..a11664a7 --- /dev/null +++ b/services/helper/src/main.rs @@ -0,0 +1,11 @@ +mod service; + +#[cfg(windows)] +fn main() -> windows_service::Result<()> { + service::main() +} + +#[cfg(not(windows))] +fn main() { + service::main(); +} diff --git a/services/helper/src/service/mod.rs b/services/helper/src/service/mod.rs new file mode 100644 index 00000000..e0b2448a --- /dev/null +++ b/services/helper/src/service/mod.rs @@ -0,0 +1,33 @@ +mod windows; +mod service; + +#[cfg(not(windows))] +use crate::service::service::run_service; + +#[cfg(not(windows))] +use tokio::runtime::Runtime; + +#[cfg(not(windows))] +pub fn main() { + if let Ok(rt) = Runtime::new() { + rt.block_on(async { + let _ = run_service().await; + }); + } +} + +#[cfg(windows)] +use windows_service::Result; +#[cfg(windows)] +use crate::service::windows::start_service; + +#[cfg(windows)] +pub fn main() -> Result<()> { + start_service() +} + + + + + + diff --git a/services/helper/src/service/service.rs b/services/helper/src/service/service.rs new file mode 100644 index 00000000..33588890 --- /dev/null +++ b/services/helper/src/service/service.rs @@ -0,0 +1,131 @@ +use std::collections::VecDeque; +use std::io; +use std::io::BufRead; +use std::process::{Command, Stdio}; +use std::sync::{Arc, Mutex}; +use warp::Filter; +use serde::{Deserialize, Serialize}; + +const LISTEN_PORT: u16 = 47890; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct StartParams { + pub path: String, + pub arg: String, +} + +pub struct ProcessManager { + process: Arc>>, + logs: Arc>>, +} + +impl ProcessManager { + pub fn new() -> Self { + ProcessManager { + process: Arc::new(Mutex::new(None)), + logs: Arc::new(Mutex::new(VecDeque::with_capacity(200))), + } + } + + fn log_message(&self, message: String) { + let mut log_buffer = self.logs.lock().unwrap(); + if log_buffer.len() == 100 { + log_buffer.pop_front(); + } + log_buffer.push_back(format!("{}\n", message)); + } + + pub fn start(&self, start_params: StartParams) { + self.stop(); + let mut process = self.process.lock().unwrap(); + + *process = Some( + Command::new(&start_params.path) + .stderr(Stdio::piped()) + .arg(&start_params.arg) + .spawn() + .expect("Failed to spawn process") + ); + + if let Some(ref mut child) = *process { + let stderr = child.stderr.take().unwrap(); + let reader = io::BufReader::new(stderr); + for line in reader.lines() { + match line { + Ok(output) => { + self.log_message(output) + } + Err(_) => { + break; + } + } + } + } + } + + pub fn stop(&self) { + let mut process = self.process.lock().unwrap(); + if let Some(mut child) = process.take() { + let _ = child.kill(); + let _ = child.wait(); + } + *process = None; + } + + pub fn get_logs(&self) -> Vec { + let log_buffer = self.logs.lock().unwrap(); + log_buffer.iter().cloned().collect() + } +} + +pub async fn run_service() -> anyhow::Result<()> { + let process_manager = Arc::new(ProcessManager::new()); + + let api_ping = warp::get() + .and(warp::path("ping")) + .map(|| "2024124"); + + let api_start = warp::post() + .and(warp::path("start")) + .and(warp::body::json()) + .map({ + let process_manager = Arc::clone(&process_manager); + move |start_params: StartParams| { + let process_manager = Arc::clone(&process_manager); + process_manager.start(start_params); + "" + } + }); + + let api_stop = warp::post() + .and(warp::path("stop")) + .map({ + let process_manager = Arc::clone(&process_manager); + move || { + let process_manager = Arc::clone(&process_manager); + process_manager.stop(); + "" + } + }); + + let api_logs = warp::get() + .and(warp::path("logs")) + .map({ + let process_manager = Arc::clone(&process_manager); + move || { + let process_manager = Arc::clone(&process_manager); + warp::reply::json(&process_manager.get_logs()) + } + }); + + warp::serve( + api_ping + .or(api_start) + .or(api_stop) + .or(api_logs) + ) + .run(([127, 0, 0, 1], LISTEN_PORT)) + .await; + + Ok(()) +} \ No newline at end of file diff --git a/services/helper/src/service/windows.rs b/services/helper/src/service/windows.rs new file mode 100644 index 00000000..9126d460 --- /dev/null +++ b/services/helper/src/service/windows.rs @@ -0,0 +1,73 @@ +#[cfg(windows)] +use std::ffi::OsString; + +#[cfg(windows)] +use std::time::Duration; + +#[cfg(windows)] +use tokio::runtime::Runtime; + +#[cfg(windows)] +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, Result, +}; + +#[cfg(windows)] +use crate::service::service::run_service; + +#[cfg(windows)] +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +#[cfg(windows)] +const SERVICE_NAME: &str = "FlClashHelperService"; + +#[cfg(windows)] +define_windows_service!(serveice, service_main); + +#[cfg(windows)] +pub fn service_main(_arguments: Vec) { + if let Ok(rt) = Runtime::new() { + rt.block_on(async { + let _ = run_windows_service().await; + }); + } +} + +#[cfg(windows)] +async fn run_windows_service() -> anyhow::Result<()> { + #[cfg(windows)] + 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, + } + }, + )?; + + #[cfg(windows)] + 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 +} + +#[cfg(windows)] +pub fn start_service() -> Result<()> { + service_dispatcher::start(SERVICE_NAME, serveice) +} \ No newline at end of file diff --git a/setup.dart b/setup.dart index 4f04e60d..d9221ada 100755 --- a/setup.dart +++ b/setup.dart @@ -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 get buildItems => [ - BuildLibItem( - platform: PlatformType.macos, - arch: Arch.amd64, - archName: '', + static List get buildItems => [ + BuildItem( + target: Target.macos, ), - BuildLibItem( - platform: PlatformType.macos, - arch: Arch.arm64, - archName: '', - ), - BuildLibItem( - platform: PlatformType.windows, - arch: Arch.amd64, - archName: '', + BuildItem( + target: Target.windows, ), - BuildLibItem( - platform: PlatformType.windows, - arch: Arch.arm64, - archName: '', + BuildItem( + target: Target.linux, ), - 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,93 @@ 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 env = {}; - env["GOOS"] = item.os; - env["GOARCH"] = item.arch.name; - env["CGO_ENABLED"] = "1"; - env["CC"] = _getCc(item); - env["CFLAGS"] = "-O3 -Werror"; + 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"; + } + 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", + ], + 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 getExecutable(String command) { + print(command); return command.split(" "); } @@ -250,22 +294,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 +326,13 @@ class BuildCommand extends Command { String get description => "build $name application"; @override - String get name => platform.name; + String get name => target.name; List 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 _buildLib(Arch? arch) async { - await Build.buildLib(platform: platform, arch: arch); - } - _getLinuxDependencies() async { await Build.exec( Build.getExecutable("sudo apt update -y"), @@ -331,51 +379,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 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 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 +455,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 +473,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); } diff --git a/test/command_test.dart b/test/command_test.dart index d4af2dd8..47738391 100644 --- a/test/command_test.dart +++ b/test/command_test.dart @@ -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 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.fromHandlers( + handleData: (Uint8List data, EventSink sink) { + sink.add(utf8.decode(data)); + }, + ), + ) + .transform(LineSplitter()) + .listen((res) { + print(res); + }); } startService() async { diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index cb3721cd..6f79055a 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -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}"