diff --git a/.depot/workflows/android-instrumented.yml b/.depot/workflows/android-instrumented.yml index cefa1de3a..6a21c7eaf 100644 --- a/.depot/workflows/android-instrumented.yml +++ b/.depot/workflows/android-instrumented.yml @@ -34,16 +34,16 @@ jobs: runs-on: depot-ubuntu-24.04-8 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3 - name: Install SDK platform + NDK run: | @@ -71,14 +71,14 @@ jobs: - name: libcore cache id: libcore-cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: app/libs/libcore.aar key: depot-libcore-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ hashFiles('libcore_status') }} - name: Install Go if: steps.libcore-cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: ${{ env.GO_VERSION }} @@ -98,7 +98,7 @@ jobs: echo "KVM present" - name: Run migration test on emulator - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2 with: api-level: 33 arch: x86_64 diff --git a/.depot/workflows/build-apk.yml b/.depot/workflows/build-apk.yml index ca88e947e..df53a33f0 100644 --- a/.depot/workflows/build-apk.yml +++ b/.depot/workflows/build-apk.yml @@ -21,16 +21,16 @@ jobs: runs-on: depot-ubuntu-24.04-8 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3 - name: Install SDK platform + NDK run: sdkmanager --install "platforms;android-35" "build-tools;35.0.0" "ndk;${NDK_VERSION}" @@ -51,7 +51,7 @@ jobs: - name: libcore cache id: libcore-cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: app/libs/libcore.aar key: depot-libcore-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ hashFiles('libcore_status') }} @@ -72,14 +72,14 @@ jobs: - name: sidecars cache id: sidecars-cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: app/executableSo key: depot-sidecars-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ env.MIERU_VERSION }}-${{ env.MDVPN_COMMIT }}-${{ env.NAIVE_VERSION }}-${{ env.OLCRTC_COMMIT }}-${{ hashFiles('sidecars_status') }} - name: Install Go if: steps.libcore-cache.outputs.cache-hit != 'true' || steps.sidecars-cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: ${{ env.GO_VERSION }} @@ -108,7 +108,7 @@ jobs: done - name: Gradle cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.gradle key: depot-gradle-oss-${{ hashFiles('**/*.gradle.kts') }} @@ -145,7 +145,7 @@ jobs: echo "Built APK: $APK_FILE" - name: Upload APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: NekoBox-debug-arm64-v8a-apk path: ${{ env.APK_FILE }} diff --git a/.depot/workflows/guard.yml b/.depot/workflows/guard.yml index bbe6d2b52..55206fc44 100644 --- a/.depot/workflows/guard.yml +++ b/.depot/workflows/guard.yml @@ -20,7 +20,7 @@ jobs: runs-on: depot-ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Guard - AGENTS.md must stay untracked run: bash ./scripts/check-agents-untracked.sh - name: Guard - no backed-up SharedPreferences diff --git a/.depot/workflows/lint.yml b/.depot/workflows/lint.yml index 0da397585..e69eb7f42 100644 --- a/.depot/workflows/lint.yml +++ b/.depot/workflows/lint.yml @@ -30,14 +30,14 @@ jobs: runs-on: depot-ubuntu-24.04-4 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3 - name: Install SDK platform + NDK run: sdkmanager --install "platforms;android-35" "build-tools;35.0.0" "ndk;${NDK_VERSION}" - name: local.properties @@ -55,13 +55,13 @@ jobs: | awk '{print $1}' > libcore_status - name: libcore cache id: libcore-cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: app/libs/libcore.aar key: depot-libcore-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ hashFiles('libcore_status') }} - name: Install Go if: steps.libcore-cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: ${{ env.GO_VERSION }} - name: Build libcore (Go + gomobile) @@ -75,7 +75,7 @@ jobs: # If lint just created the baseline (first run), surface it so it can be committed. - name: Upload lint baseline (if generated) if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: lint-baseline path: app/lint-baseline.xml diff --git a/.depot/workflows/unit-tests.yml b/.depot/workflows/unit-tests.yml index e13dda55a..335a32dea 100644 --- a/.depot/workflows/unit-tests.yml +++ b/.depot/workflows/unit-tests.yml @@ -32,14 +32,14 @@ jobs: runs-on: depot-ubuntu-24.04-4 steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Setup Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3 - name: Install SDK platform + NDK run: sdkmanager --install "platforms;android-35" "build-tools;35.0.0" "ndk;${NDK_VERSION}" - name: local.properties @@ -57,13 +57,13 @@ jobs: | awk '{print $1}' > libcore_status - name: libcore cache id: libcore-cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: app/libs/libcore.aar key: depot-libcore-${{ env.GO_VERSION }}-${{ env.NDK_VERSION }}-${{ hashFiles('libcore_status') }} - name: Install Go if: steps.libcore-cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: ${{ env.GO_VERSION }} - name: Build libcore (Go + gomobile) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aee37e8f7..b7f4c5e4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Golang Status run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status - name: Libcore Status @@ -33,7 +33,7 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Native Build @@ -77,7 +77,7 @@ jobs: rm -rf "$tmp" exit "$fail" - name: Upload LibCore - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: libcore-aar-build path: app/libs/libcore.aar @@ -88,7 +88,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Sidecars Status run: cat buildScript/lib/mieru.sh buildScript/lib/masterdnsvpn.sh buildScript/lib/naive.sh buildScript/init/env.sh buildScript/init/env_ndk.sh | sha1sum > sidecars_status - name: Sidecars Cache @@ -100,7 +100,7 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'sidecars_status') }}-${{ env.MIERU_VERSION }}-${{ env.MDVPN_COMMIT }}-${{ env.NAIVE_VERSION }}-sidecars - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Mieru Build @@ -113,7 +113,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: ./run lib naive - name: Upload Sidecars - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: sidecars-so-build path: app/executableSo @@ -127,14 +127,14 @@ jobs: - sidecars steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Download LibCore - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: libcore-aar-build path: app/libs - name: Download Sidecars - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: sidecars-so-build path: app/executableSo @@ -174,7 +174,7 @@ jobs: APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - name: Upload Artifacts - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: NekoBoxs path: ${{ env.APK }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c92bbdea..86f1f6996 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Guard - AGENTS.md must stay untracked run: bash ./scripts/check-agents-untracked.sh - name: Guard - no backed-up SharedPreferences @@ -28,14 +28,14 @@ jobs: - libcore steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Download LibCore - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: libcore-aar-ci path: app/libs @@ -55,14 +55,14 @@ jobs: - libcore steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Download LibCore - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: libcore-aar-ci path: app/libs @@ -84,9 +84,9 @@ jobs: - guard steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' @@ -103,14 +103,14 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }}-ci - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Native Build if: steps.cache.outputs.cache-hit != 'true' run: ./run lib core - name: Upload LibCore - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: libcore-aar-ci path: app/libs/libcore.aar @@ -123,7 +123,7 @@ jobs: - guard steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Sidecars Status run: | git ls-files -s -- \ @@ -145,7 +145,7 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'sidecars_status') }}-${{ env.MIERU_VERSION }}-${{ env.MDVPN_COMMIT }}-${{ env.NAIVE_VERSION }}-${{ env.OLCRTC_COMMIT }}-sidecars-ci - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Mieru Build @@ -161,7 +161,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: ./run lib olcrtc - name: Upload Sidecars - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: sidecars-so-ci path: app/executableSo @@ -175,19 +175,19 @@ jobs: - sidecars steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5 with: distribution: 'temurin' java-version: '17' - name: Download LibCore - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: libcore-aar-ci path: app/libs - name: Download Sidecars - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: sidecars-so-ci path: app/executableSo @@ -231,7 +231,7 @@ jobs: APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - name: Upload Artifacts - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: NekoBox-debug-apks path: ${{ env.APK }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index f72261ceb..b244826b3 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -15,7 +15,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Golang Status run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status - name: Libcore Status @@ -29,14 +29,14 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Native Build if: steps.cache.outputs.cache-hit != 'true' run: ./run lib core - name: Upload LibCore - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: libcore-aar-preview path: app/libs/libcore.aar @@ -47,7 +47,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Sidecars Status run: cat buildScript/lib/mieru.sh buildScript/lib/masterdnsvpn.sh buildScript/lib/naive.sh buildScript/init/env.sh buildScript/init/env_ndk.sh | sha1sum > sidecars_status - name: Sidecars Cache @@ -59,7 +59,7 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'sidecars_status') }}-${{ env.MIERU_VERSION }}-${{ env.MDVPN_COMMIT }}-${{ env.NAIVE_VERSION }}-sidecars - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Mieru Build @@ -72,7 +72,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: ./run lib naive - name: Upload Sidecars - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: sidecars-so-preview path: app/executableSo @@ -86,14 +86,14 @@ jobs: - sidecars steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Download LibCore - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: libcore-aar-preview path: app/libs - name: Download Sidecars - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: sidecars-so-preview path: app/executableSo @@ -128,7 +128,7 @@ jobs: fi APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - - uses: namespace-actions/upload-artifact@v1 + - uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: APKs path: ${{ env.APK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0243b23e..110e79d68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Golang Status run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status - name: Libcore Status @@ -35,14 +35,14 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }} - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Native Build if: steps.cache.outputs.cache-hit != 'true' run: ./run lib core - name: Upload LibCore - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: libcore-aar-release path: app/libs/libcore.aar @@ -53,7 +53,7 @@ jobs: runs-on: namespace-profile-nekoyay steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Sidecars Status run: cat buildScript/lib/mieru.sh buildScript/lib/masterdnsvpn.sh buildScript/lib/naive.sh buildScript/init/env.sh buildScript/init/env_ndk.sh | sha1sum > sidecars_status - name: Sidecars Cache @@ -65,7 +65,7 @@ jobs: key: ${{ hashFiles('.github/workflows/*', 'sidecars_status') }}-${{ env.MIERU_VERSION }}-${{ env.MDVPN_COMMIT }}-${{ env.NAIVE_VERSION }}-sidecars - name: Install Golang if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-go@v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version: '1.26.4' - name: Mieru Build @@ -78,7 +78,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: ./run lib naive - name: Upload Sidecars - uses: namespace-actions/upload-artifact@v1 + uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: sidecars-so-release path: app/executableSo @@ -92,14 +92,14 @@ jobs: - sidecars steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Download LibCore - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: libcore-aar-release path: app/libs - name: Download Sidecars - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: sidecars-so-release path: app/executableSo @@ -138,7 +138,7 @@ jobs: fi APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - - uses: namespace-actions/upload-artifact@v1 + - uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1 with: name: APKs path: ${{ env.APK }} @@ -151,9 +151,9 @@ jobs: needs: build steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Donwload Artifacts - uses: namespace-actions/download-artifact@v1 + uses: namespace-actions/download-artifact@5c070f7d7ebdc47682b04aa736c76e46ff5f6e1e # v1 with: name: APKs path: artifacts diff --git a/.gitignore b/.gitignore index f4ad61bcd..6b43c3206 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ app/src/main/java/io/nekohasekai/sagernet/ktx/AgentDebugLog.kt # Local on-device DB/config backups (profiles + credentials — never commit) device-backups/ +.worklog/ diff --git a/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl b/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl index cb41c2bd7..abe3e0d3a 100644 --- a/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl +++ b/app/src/main/aidl/io/nekohasekai/sagernet/aidl/ISagerNetServiceCallback.aidl @@ -8,5 +8,6 @@ oneway interface ISagerNetServiceCallback { void missingPlugin(String profileName, String pluginName); void cbSpeedUpdate(in SpeedDisplayData stats); void cbTrafficUpdate(in TrafficData stats); + void cbTrafficUpdateList(in List stats); void cbSelectorUpdate(long id); } diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index a3a9e9294..673b9778a 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -48,6 +48,7 @@ object Key { const val MIXED_PORT = "mixedPort" const val MIXED_SECRET = "mixedSecret" // storage key for the generated inbound secret + const val CLASH_API_SECRET = "clashApiSecret" // per-install secret for the local Clash API const val MIXED_USERNAME = "neko" // username presented to the authed mixed inbound const val ALLOW_ACCESS = "allowAccess" const val REQUIRE_PROXY_IN_VPN = "requireProxyInVPN" // keep local mixed inbound open in VPN mode diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt index 443b255c4..9d1c34405 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import libcore.Libcore import moe.matsuri.nb4a.Protocols +import moe.matsuri.nb4a.proxy.config.ConfigBean import moe.matsuri.nb4a.utils.Util import java.net.UnknownHostException import java.util.concurrent.ConcurrentHashMap @@ -140,7 +141,7 @@ class BaseService { Runtime.getRuntime().exit(0) return } - if (!callbackIdMap.contains(cb)) { + if (!callbackIdMap.containsKey(cb)) { callbacks.register(cb) } callbackIdMap[cb] = id @@ -277,14 +278,20 @@ class BaseService { } fun canReloadSelector(): Boolean { - if ((data.proxy?.config?.selectorGroupId ?: -1L) < 0) return false + val running = data.proxy?.lastSelectorGroupId ?: -1L + if (running < 0L) return false val ent = SagerDatabase.proxyDao.getById(DataStore.selectedProxy) ?: return false - val tmpBox = ProxyInstance(ent) - tmpBox.buildConfigTmp() - if (tmpBox.lastSelectorGroupId == data.proxy?.lastSelectorGroupId) { - return true + // Mirrors ConfigBuilder.buildConfig()'s selectorGroupId derivation + // (TYPE_CONFIG/type==0 early exit; else group.isSelector -> group.id). + // Keep in sync with ConfigBuilder.kt:106-119,179,1194. + if (ent.type == ProxyEntity.TYPE_CONFIG && + (ent.requireBean() as? ConfigBean)?.type == 0 + ) { + return false } - return false + val group = SagerDatabase.groupDao.getById(ent.groupId) ?: return false + val newSelectorGroupId = if (group.isSelector) group.id else -1L + return newSelectorGroupId == running } suspend fun startProcesses() { @@ -308,7 +315,7 @@ class BaseService { wakeLock = null } runOnDefaultDispatcher { - DefaultNetworkListener.stop(this) + DefaultNetworkListener.stop(this@Interface) } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt index 47e92afbf..53a1f57ea 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt @@ -42,6 +42,9 @@ class SagerConnection( fun cbSpeedUpdate(stats: SpeedDisplayData) {} fun cbTrafficUpdate(data: TrafficData) {} + fun cbTrafficUpdateList(data: List) { + data.forEach { cbTrafficUpdate(it) } + } fun cbSelectorUpdate(id: Long) {} fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) @@ -86,6 +89,13 @@ class SagerConnection( } } + override fun cbTrafficUpdateList(stats: MutableList) { + val callback = callback ?: return + runOnMainDispatcher { + callback.cbTrafficUpdateList(stats) + } + } + override fun cbSelectorUpdate(id: Long) { val callback = callback ?: return runOnMainDispatcher { diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt index 03226630a..0f4e9d290 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt @@ -5,8 +5,8 @@ import io.nekohasekai.sagernet.bg.BaseService import io.nekohasekai.sagernet.bg.ServiceNotification import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.ktx.Logs -import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.utils.Commandline +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.runBlocking import moe.matsuri.nb4a.utils.JavaUtil @@ -60,10 +60,12 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = override fun launch() { box.setAsMain() super.launch() // start box - runOnDefaultDispatcher { - looper = service?.let { TrafficLooper(it.data, this) } - looper?.start() - } + // Assign the looper synchronously so close() always observes it (no + // launch/close race). GlobalScope matches the previous scope semantics: + // runOnDefaultDispatcher was GlobalScope.launch(Dispatchers.Default), and + // TrafficLooper.start() launches its own loop on this scope. + looper = service?.let { TrafficLooper(it.data, GlobalScope) } + looper?.start() } override fun close() { diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt index b59803558..a9f4f3fb0 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt @@ -6,39 +6,61 @@ import io.nekohasekai.sagernet.database.ProxyEntity import io.nekohasekai.sagernet.fmt.buildConfig import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher -import io.nekohasekai.sagernet.ktx.tryResume -import io.nekohasekai.sagernet.ktx.tryResumeWithException import io.nekohasekai.sagernet.utils.Commandline +import kotlinx.coroutines.suspendCancellableCoroutine import libcore.Libcore import moe.matsuri.nb4a.net.LocalResolverImpl -import kotlin.coroutines.suspendCoroutine +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class TestInstance(profile: ProxyEntity, val link: String, private val timeout: Int) : BoxInstance(profile) { - suspend fun doTest(): Int { - return suspendCoroutine { c -> - processes = GuardedProcessPool { - Logs.w(it) - c.tryResumeWithException(it) + // close() can be reached from two paths that may overlap on cancellation: the + // suspendCancellableCoroutine's invokeOnCancellation and the `use { }` block's + // exit. BoxInstance.close() is not safe to run twice (native box.close()), so + // guard it to run exactly once. + private val closed = AtomicBoolean(false) + + override fun close() { + if (closed.getAndSet(true)) return + super.close() + } + + suspend fun doTest(): Int = suspendCancellableCoroutine { c -> + processes = GuardedProcessPool { + Logs.w(it) + if (c.isActive) c.resumeWithException(it) + } + // Close the box/sidecars if the caller cancels while the test is in flight + // (e.g. the user pressed Stop). This prevents leaking up to + // connectionTestConcurrent full sing-box instances + plugin sidecars that + // otherwise run to completion in the background. + c.invokeOnCancellation { + try { + close() + } catch (e: Exception) { + Logs.w(e) } - runOnDefaultDispatcher { - use { - try { - init() - launch() - if (processes.processCount > 0) { - // Wait until the external plugin sidecar(s) have actually bound - // their loopback SOCKS port before testing, instead of a fixed - // 500ms guess that often raced the sidecar (flaky "connection - // refused"). strict = true turns a never-bound listener into a - // clear error rather than a misleading connection failure. - awaitExternalProcessesReady(strict = true) - } - c.tryResume(Libcore.urlTest(box, link, timeout)) - } catch (e: Exception) { - c.tryResumeWithException(e) + } + runOnDefaultDispatcher { + use { + try { + init() + launch() + if (processes.processCount > 0) { + // Wait until the external plugin sidecar(s) have actually bound + // their loopback SOCKS port before testing, instead of a fixed + // 500ms guess that often raced the sidecar (flaky "connection + // refused"). strict = true turns a never-bound listener into a + // clear error rather than a misleading connection failure. + awaitExternalProcessesReady(strict = true) } + val result = Libcore.urlTest(box, link, timeout) + if (c.isActive) c.resume(result) + } catch (e: Exception) { + if (c.isActive) c.resumeWithException(e) } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt index 6bbbd598d..244d6688e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TrafficLooper.kt @@ -63,9 +63,7 @@ class TrafficLooper( } } data.binder.broadcast { b -> - for (t in traffic) { - b.cbTrafficUpdate(t.value) - } + b.cbTrafficUpdateList(ArrayList(traffic.values)) } Logs.d("finally traffic post done") } @@ -206,11 +204,11 @@ class TrafficLooper( if (data.binder.callbackIdMap[b] == SagerConnection.CONNECTION_ID_MAIN_ACTIVITY_FOREGROUND) { b.cbSpeedUpdate(speed) if (profileTrafficStatistics) { + val batch = ArrayList(idMap.size) idMap.forEach { (id, item) -> - b.cbTrafficUpdate( - TrafficData(id = id, rx = item.rx, tx = item.tx), // display - ) + batch.add(TrafficData(id = id, rx = item.rx, tx = item.tx)) // display } + b.cbTrafficUpdateList(batch) } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index 1ffb18753..67d9825ff 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -104,7 +104,8 @@ object DataStore : OnPreferenceDataStoreChangeListener { val current = currentGroup() if (current.type == GroupType.BASIC) return current.id val groups = SagerDatabase.groupDao.allGroups() - return groups.find { it.type == GroupType.BASIC }!!.id + groups.find { it.type == GroupType.BASIC }?.let { return it.id } + return SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true)) } var appTLSVersion by configurationStore.string(Key.APP_TLS_VERSION) @@ -183,6 +184,16 @@ object DataStore : OnPreferenceDataStoreChangeListener { return s } + val clashApiSecret: String + @Synchronized get() { + var s = configurationStore.getString(Key.CLASH_API_SECRET) + if (s.isNullOrEmpty()) { + s = java.util.UUID.randomUUID().toString().replace("-", "") + configurationStore.putString(Key.CLASH_API_SECRET, s) + } + return s + } + var mixedPort: Int get() = getLocalPort(Key.MIXED_PORT, 2080) set(value) = saveLocalPort(Key.MIXED_PORT, value) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt b/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt index e7c2c398f..dbf19e946 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/GroupManager.kt @@ -54,7 +54,10 @@ object GroupManager { } suspend fun clearGroup(groupId: Long) { - DataStore.selectedProxy = 0L + val selected = DataStore.selectedProxy + if (selected != 0L && SagerDatabase.proxyDao.getById(selected)?.groupId == groupId) { + DataStore.selectedProxy = 0L + } SagerDatabase.proxyDao.deleteAll(groupId) iterator { groupUpdated(groupId) } } @@ -98,6 +101,10 @@ object GroupManager { } suspend fun deleteGroup(groupId: Long) { + val selected = DataStore.selectedProxy + if (selected != 0L && SagerDatabase.proxyDao.getById(selected)?.groupId == groupId) { + DataStore.selectedProxy = 0L + } SagerDatabase.groupDao.deleteById(groupId) SagerDatabase.proxyDao.deleteByGroup(groupId) iterator { groupRemoved(groupId) } @@ -105,6 +112,11 @@ object GroupManager { } suspend fun deleteGroup(group: List) { + val ids = group.map { it.id }.toSet() + val selected = DataStore.selectedProxy + if (selected != 0L && SagerDatabase.proxyDao.getById(selected)?.groupId in ids) { + DataStore.selectedProxy = 0L + } SagerDatabase.groupDao.deleteGroup(group) SagerDatabase.proxyDao.deleteByGroup(group.map { it.id }.toLongArray()) for (proxyGroup in group) iterator { groupRemoved(proxyGroup.id) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt index a03c062a2..dd76f04bb 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt @@ -16,6 +16,9 @@ object ProfileManager { interface Listener { suspend fun onAdd(profile: ProxyEntity) suspend fun onUpdated(data: TrafficData) + suspend fun onUpdated(data: List) { + data.forEach { onUpdated(it) } + } suspend fun onUpdated(profile: ProxyEntity, noTraffic: Boolean) suspend fun onRemoved(groupId: Long, profileId: Long) } @@ -96,6 +99,16 @@ object ProfileManager { } } + /** + * Batch-persist profiles WITHOUT firing per-profile onUpdated listener rounds. + * For callers that follow up with GroupManager.postReload(groupId), which + * re-renders the whole group anyway (e.g. connection-test finalization). + */ + suspend fun updateProfileQuietly(profiles: List) { + if (profiles.isEmpty()) return + SagerDatabase.proxyDao.updateProxy(profiles) + } + suspend fun updateTraffic(profileId: Long, rx: Long, tx: Long) { SagerDatabase.proxyDao.updateTraffic(profileId, rx, tx) } @@ -156,6 +169,10 @@ object ProfileManager { iterator { onUpdated(data) } } + suspend fun postUpdate(data: List) { + iterator { onUpdated(data) } + } + suspend fun createRule(rule: RuleEntity, post: Boolean = true): RuleEntity { rule.userOrder = SagerDatabase.rulesDao.nextOrder() ?: 1 rule.id = SagerDatabase.rulesDao.createRule(rule) diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index ac94bff82..ebdfabd70 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -240,6 +240,8 @@ fun buildConfig(proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean clash_api = ClashAPIOptions().apply { external_controller = "127.0.0.1:9090" external_ui = "../files/yacd" + // Exported/shared configs must not carry the per-install token. + if (!forExport) secret = DataStore.clashApiSecret } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/GroupInterfaceAdapter.kt b/app/src/main/java/io/nekohasekai/sagernet/group/GroupInterfaceAdapter.kt index c1704d1ae..6f624a6ff 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/GroupInterfaceAdapter.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/GroupInterfaceAdapter.kt @@ -4,9 +4,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.GroupManager import io.nekohasekai.sagernet.database.ProxyGroup +import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.onMainDispatcher import io.nekohasekai.sagernet.ktx.runOnMainDispatcher import io.nekohasekai.sagernet.ui.ThemedActivity +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -14,14 +16,23 @@ import kotlin.coroutines.suspendCoroutine class GroupInterfaceAdapter(val context: ThemedActivity) : GroupManager.Interface { override suspend fun confirm(message: String): Boolean { - return suspendCoroutine { + return suspendCoroutine { cont -> runOnMainDispatcher { - MaterialAlertDialogBuilder(context).setTitle(R.string.confirm) - .setMessage(message) - .setPositiveButton(R.string.yes) { _, _ -> it.resume(true) } - .setNegativeButton(R.string.no) { _, _ -> it.resume(false) } - .setOnCancelListener { _ -> it.resume(false) } - .show() + if (context.isFinishing || context.isDestroyed) { + cont.resume(false) + return@runOnMainDispatcher + } + try { + MaterialAlertDialogBuilder(context).setTitle(R.string.confirm) + .setMessage(message) + .setPositiveButton(R.string.yes) { _, _ -> cont.resume(true) } + .setNegativeButton(R.string.no) { _, _ -> cont.resume(false) } + .setOnCancelListener { _ -> cont.resume(false) } + .show() + } catch (e: Exception) { + Logs.w(e) + cont.resume(false) + } } } } @@ -37,16 +48,23 @@ class GroupInterfaceAdapter(val context: ThemedActivity) : GroupManager.Interfac ) { if (changed == 0 && duplicate.isEmpty()) { if (byUser) { - context.snackbar( - context.getString( - R.string.group_no_difference, - group.displayName(), - ), - ).show() + onMainDispatcher { + if (context.isFinishing || context.isDestroyed) return@onMainDispatcher + try { + context.snackbar( + context.getString( + R.string.group_no_difference, + group.displayName(), + ), + ).show() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logs.w(e) + } + } } } else { - context.snackbar(context.getString(R.string.group_updated, group.name, changed)).show() - var status = "" if (added.isNotEmpty()) { status += context.getString( @@ -76,32 +94,58 @@ class GroupInterfaceAdapter(val context: ThemedActivity) : GroupManager.Interfac } onMainDispatcher { - delay(1000L) + if (context.isFinishing || context.isDestroyed) return@onMainDispatcher + try { + context.snackbar( + context.getString(R.string.group_updated, group.name, changed), + ).show() + delay(1000L) - MaterialAlertDialogBuilder(context).setTitle( - context.getString( - R.string.group_diff, - group.displayName(), - ), - ).setMessage(status.trim()).setPositiveButton(android.R.string.ok, null).show() + MaterialAlertDialogBuilder(context).setTitle( + context.getString( + R.string.group_diff, + group.displayName(), + ), + ).setMessage(status.trim()).setPositiveButton(android.R.string.ok, null).show() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logs.w(e) + } } } } override suspend fun onUpdateFailure(group: ProxyGroup, message: String) { onMainDispatcher { - context.snackbar(message).show() + if (context.isFinishing || context.isDestroyed) return@onMainDispatcher + try { + context.snackbar(message).show() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Logs.w(e) + } } } override suspend fun alert(message: String) { - return suspendCoroutine { + return suspendCoroutine { cont -> runOnMainDispatcher { - MaterialAlertDialogBuilder(context).setTitle(R.string.ooc_warning) - .setMessage(message) - .setPositiveButton(android.R.string.ok) { _, _ -> it.resume(Unit) } - .setOnCancelListener { _ -> it.resume(Unit) } - .show() + if (context.isFinishing || context.isDestroyed) { + cont.resume(Unit) + return@runOnMainDispatcher + } + try { + MaterialAlertDialogBuilder(context).setTitle(R.string.ooc_warning) + .setMessage(message) + .setPositiveButton(android.R.string.ok) { _, _ -> cont.resume(Unit) } + .setOnCancelListener { _ -> cont.resume(Unit) } + .show() + } catch (e: Exception) { + Logs.w(e) + cont.resume(Unit) + } } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt index 432c7888b..1d00a9642 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt @@ -134,7 +134,10 @@ abstract class GroupUpdater { suspend fun executeUpdate(proxyGroup: ProxyGroup, byUser: Boolean): Boolean { return coroutineScope { - if (!updating.add(proxyGroup.id)) cancel() + if (!updating.add(proxyGroup.id)) { + // already updating this group in another run; skip quietly + return@coroutineScope false + } GroupManager.postReload(proxyGroup.id) val subscription = proxyGroup.subscription!! @@ -152,7 +155,6 @@ abstract class GroupUpdater { ) ) { finishUpdate(proxyGroup) - cancel() return@coroutineScope true } } @@ -160,6 +162,9 @@ abstract class GroupUpdater { try { RawUpdater.doUpdate(proxyGroup, subscription, userInterface, byUser) true + } catch (e: CancellationException) { + finishUpdate(proxyGroup) + throw e } catch (e: Throwable) { Logs.w(e) userInterface?.onUpdateFailure(proxyGroup, e.readableMessage) diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt index 2bc2861ff..0a75daa5f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt @@ -163,17 +163,18 @@ object RawUpdater : GroupUpdater() { Logs.d("Unique profiles: ${nameMap.size}") val toDelete = ArrayList() - val toReplace = exists.mapNotNull { entity -> + val toReplace = HashMap() + for (entity in exists) { val name = entity.displayName() - if (nameMap.contains(name)) { - name to entity + if (nameMap.contains(name) && !toReplace.containsKey(name)) { + // first existing row claiming this name -> replace target + toReplace[name] = entity } else { - let { - toDelete.add(entity) - null - } + // name not in the new set, OR a duplicate of an already-claimed + // name -> delete so the post-transaction count matches proxies.size + toDelete.add(entity) } - }.toMap() + } Logs.d("toDelete profiles: ${toDelete.size}") Logs.d("toReplace profiles: ${toReplace.size}") diff --git a/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt b/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt index 64848f875..ccafad964 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt @@ -40,18 +40,21 @@ object PluginManager { } private fun initNative(pluginId: String): InitResult? { - val info = Plugins.getPlugin(pluginId) ?: return null - - // internal so - if (info.applicationInfo == null) { - try { - initNativeInternal(pluginId)?.let { return InitResult(it, info) } - } catch (t: Throwable) { - Logs.w("initNativeInternal failed", t) + // A bundled sidecar always wins over an externally-installed provider: + // external providers are matched by authority prefix with no signature + // check, so they must not be able to shadow binaries we ship. + try { + initNativeInternal(pluginId)?.let { path -> + return InitResult( + path, + ProviderInfo().apply { authority = Plugins.AUTHORITIES_PREFIX_NEKO_EXE }, + ) } - return null + } catch (t: Throwable) { + Logs.w("initNativeInternal failed", t) } + val info = Plugins.getPluginExternal(pluginId) ?: return null try { initNativeFaster(info)?.let { return InitResult(it, info) } } catch (t: Throwable) { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index 0b5162e9d..064876c0b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -971,12 +971,10 @@ class ConfigurationFragment @JvmOverloads constructor( runOnDefaultDispatcher { mainJob.cancel() testJobs.forEach { it.cancel() } - test.results.forEach { - try { - ProfileManager.updateProfile(it) - } catch (e: Exception) { - Logs.w(e) - } + try { + ProfileManager.updateProfileQuietly(test.results.toList()) + } catch (e: Exception) { + Logs.w(e) } GroupManager.postReload(DataStore.currentGroupId()) DataStore.runningTest = false @@ -1055,12 +1053,10 @@ class ConfigurationFragment @JvmOverloads constructor( runOnDefaultDispatcher { mainJob.cancel() testJobs.forEach { it.cancel() } - test.results.forEach { - try { - ProfileManager.updateProfile(it) - } catch (e: Exception) { - Logs.w(e) - } + try { + ProfileManager.updateProfileQuietly(test.results.toList()) + } catch (e: Exception) { + Logs.w(e) } GroupManager.postReload(DataStore.currentGroupId()) DataStore.runningTest = false @@ -1720,6 +1716,27 @@ class ConfigurationFragment @JvmOverloads constructor( } } + override suspend fun onUpdated(data: List) { + try { + val positions = HashMap(configurationIdList.size) + configurationIdList.forEachIndexed { index, id -> positions[id] = index } + val updates = ArrayList>() + for (item in data) { + val index = positions[item.id] ?: continue + val holder = layoutManager.findViewByPosition(index) + ?.let { configurationListView.getChildViewHolder(it) } as ConfigurationHolder? + if (holder != null) updates.add(holder to item) + } + if (updates.isNotEmpty()) { + onMainDispatcher { + for ((holder, item) in updates) holder.bind(holder.entity, item) + } + } + } catch (e: Exception) { + Logs.w(e) + } + } + override suspend fun onRemoved(groupId: Long, profileId: Long) { if (groupId != proxyGroup.id) return val index = configurationIdList.indexOf(profileId) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt index 0e951bb4a..0875ff5b7 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt @@ -164,11 +164,9 @@ class GroupFragment : suspend fun reload() { val groups = SagerDatabase.groupDao.allGroups().toMutableList() - if (groups.size > 1 && SagerDatabase.proxyDao.countByGroup( - groups.find { - it.ungrouped - }!!.id, - ) == 0L + val ungrouped = groups.find { it.ungrouped } + if (groups.size > 1 && ungrouped != null && + SagerDatabase.proxyDao.countByGroup(ungrouped.id) == 0L ) { groups.removeAll { it.ungrouped } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt index 8b6aa2e6d..1608752ed 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt @@ -430,6 +430,12 @@ class MainActivity : } } + override fun cbTrafficUpdateList(data: List) { + runOnDefaultDispatcher { + ProfileManager.postUpdate(data) + } + } + override fun cbSelectorUpdate(id: Long) { val old = DataStore.selectedProxy DataStore.selectedProxy = id diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt index 040b2c253..cb0931252 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/WebviewFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.webkit.* import android.widget.EditText import androidx.appcompat.widget.Toolbar +import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.nekohasekai.sagernet.BuildConfig import io.nekohasekai.sagernet.R @@ -63,7 +64,29 @@ class WebviewFragment : ToolbarFragment(R.layout.layout_webview), Toolbar.OnMenu super.onPageFinished(view, url) } } - mWebView.loadUrl(DataStore.yacdURL) + mWebView.loadUrl(dashboardUrl()) + } + + private fun dashboardUrl(): String { + val base = DataStore.yacdURL + val uri = try { + base.toUri() + } catch (e: Exception) { + return base + } + // Only inject the token into the local controller's own UI; never append it + // to a user-configured remote dashboard URL. Match host + port exactly (a + // prefix check would also match e.g. 127.0.0.1:90909). + if (!(uri.scheme == "http" && uri.host == "127.0.0.1" && uri.port == 9090)) return base + if (uri.getQueryParameter("secret") != null) return base + // Build the query via Uri so the params land in the query component, not + // inside a #fragment (appending a raw "?..." after a fragment hides them). + return uri.buildUpon() + .appendQueryParameter("hostname", "127.0.0.1") + .appendQueryParameter("port", "9090") + .appendQueryParameter("secret", DataStore.clashApiSecret) + .build() + .toString() } @SuppressLint("CheckResult") @@ -78,7 +101,7 @@ class WebviewFragment : ToolbarFragment(R.layout.layout_webview), Toolbar.OnMenu .setView(view) .setPositiveButton(android.R.string.ok) { _, _ -> DataStore.yacdURL = view.text.toString() - mWebView.loadUrl(DataStore.yacdURL) + mWebView.loadUrl(dashboardUrl()) } .setNegativeButton(android.R.string.cancel, null) .show() diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt index d0645c18d..5c56e3a58 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/Commandline.kt @@ -46,7 +46,7 @@ object Commandline { private val SENSITIVE_OUTPUT_PATTERNS = listOf( Regex( "(?i)(\\\"" + - "(?:clientId|key|keyHex|password|roomId|serverPassword|serverUsername|" + + "(?:clientId|key|keyHex|password|roomId|secret|serverPassword|serverUsername|" + "socksPass|socksUser|username)" + "\\\"\\s*:\\s*\\\")[^\\\"]*(\\\")", ) to "\$1\$2", diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt index f63b2f868..d98596860 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt @@ -19,19 +19,29 @@ object PackageCache { lateinit var installedPluginPackages: Map lateinit var installedApps: Map lateinit var packageMap: Map - val uidMap = HashMap>() + + @Volatile + var uidMap: Map> = emptyMap() val loaded = Mutex(true) var registerd = AtomicBoolean(false) // called from init (suspend) fun register() { if (registerd.getAndSet(true)) return - reload() - app.listenForPackageChanges(false) { + try { reload() - labelMap.clear() + app.listenForPackageChanges(false) { + reload() + labelMap.clear() + } + } catch (e: Throwable) { + // Never leave `loaded` permanently locked or block a later retry: on a + // failed first load, allow re-registration and still unlock waiters. + registerd.set(false) + throw e + } finally { + if (loaded.isLocked) loaded.unlock() } - loaded.unlock() } @SuppressLint("InlinedApi") @@ -57,29 +67,25 @@ object PackageCache { val installed = app.packageManager.getInstalledApplications(PackageManager.GET_META_DATA) installedApps = installed.associateBy { it.packageName } packageMap = installed.associate { it.packageName to it.uid } - uidMap.clear() + val newUidMap = HashMap>() for (info in installed) { - val uid = info.uid - uidMap.getOrPut(uid) { HashSet() }.add(info.packageName) + newUidMap.getOrPut(info.uid) { HashSet() }.add(info.packageName) } + uidMap = newUidMap } operator fun get(uid: Int) = uidMap[uid] operator fun get(packageName: String) = packageMap[packageName] fun awaitLoadSync() { - if (::packageMap.isInitialized) { - return - } + if (::packageMap.isInitialized) return + // Ensure registration has started exactly once; the winner unlocks `loaded` + // after the first reload(). Losers fall through and await the mutex. if (!registerd.get()) { register() - return - } - runBlocking { - loaded.withLock { - // just await - } } + if (::packageMap.isInitialized) return + runBlocking { loaded.withLock { /* await first reload */ } } } private val labelMap = mutableMapOf() diff --git a/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt b/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt index ae7ef21a9..df147c5ed 100644 --- a/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt +++ b/app/src/main/java/moe/matsuri/nb4a/NativeInterface.kt @@ -90,15 +90,14 @@ class NativeInterface : BoxPlatformInterface, NB4AInterface { Libcore.resetAllConnections(true) DataStore.baseService?.apply { runOnDefaultDispatcher { - val id = data.proxy!!.config.profileTagMap + val proxy = data.proxy ?: return@runOnDefaultDispatcher + val id = proxy.config.profileTagMap .filterValues { it == tag }.keys.firstOrNull() ?: -1 val ent = SagerDatabase.proxyDao.getById(id) ?: return@runOnDefaultDispatcher // traffic & title - data.proxy?.apply { - looper?.selectMain(id) - displayProfileName = ServiceNotification.genTitle(ent) - data.notification?.postNotificationTitle(displayProfileName) - } + proxy.looper?.selectMain(id) + proxy.displayProfileName = ServiceNotification.genTitle(ent) + data.notification?.postNotificationTitle(proxy.displayProfileName) // post binder data.binder.broadcast { b -> b.cbSelectorUpdate(id) diff --git a/buildScript/lib/mieru.sh b/buildScript/lib/mieru.sh index f0fcc0bbf..4bf3f675b 100755 --- a/buildScript/lib/mieru.sh +++ b/buildScript/lib/mieru.sh @@ -19,6 +19,9 @@ fi # Mieru release tag to build from source. MIERU_VERSION="${MIERU_VERSION:-v3.34.0}" +# Immutable commit that MIERU_VERSION points to (pinned for reproducible builds; +# update together with MIERU_VERSION on any bump). +MIERU_COMMIT="${MIERU_COMMIT:-1532c85cc8ca08dff469326f35a3f027697c6950}" DEPS="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" # macOS NDK host dirs are darwin-x86_64 / darwin-arm64; fall back if linux is absent. @@ -40,7 +43,7 @@ OUT="$(pwd)/app/executableSo" # checkout is missing or its git operations fail (e.g. corrupted by a prior build). need_clone=1 if [ -d "$WORK/.git" ]; then - if git -C "$WORK" fetch --depth 1 origin "refs/tags/$MIERU_VERSION" \ + if git -C "$WORK" fetch --depth 1 origin "$MIERU_COMMIT" \ && git -C "$WORK" checkout -q FETCH_HEAD; then need_clone=0 else @@ -49,7 +52,10 @@ if [ -d "$WORK/.git" ]; then fi if [ "$need_clone" -eq 1 ]; then rm -rf "$WORK" - git clone --depth 1 --branch "$MIERU_VERSION" https://github.com/enfein/mieru.git "$WORK" + git init -q "$WORK" + git -C "$WORK" remote add origin https://github.com/enfein/mieru.git + git -C "$WORK" fetch --depth 1 origin "$MIERU_COMMIT" + git -C "$WORK" checkout -q FETCH_HEAD fi pushd "$WORK" >/dev/null diff --git a/libcore/init.sh b/libcore/init.sh index 50bf4cfbd..75213e513 100755 --- a/libcore/init.sh +++ b/libcore/init.sh @@ -7,11 +7,21 @@ if [ -z "$GOPATH" ]; then GOPATH=$(go env GOPATH) fi +# gomobile toolchain pin. Resolved from MatsuriDayo/gomobile master2 and pinned to +# an immutable commit for reproducible JNI-bridge generation. Bump deliberately. +GOMOBILE_COMMIT="${GOMOBILE_COMMIT:-17d6af34f6bd6d7e1e428e0c652c8b54a46bda4f}" + # Install gomobile if [ ! -f "$GOPATH/bin/gomobile-matsuri" ]; then - git clone https://github.com/MatsuriDayo/gomobile.git + # Fresh checkout dir every time so a partial/stale clone from an interrupted + # prior run can't be reused; fail fast if any git step fails rather than + # building from wrong/absent sources. + rm -rf gomobile + git init -q gomobile + git -C gomobile remote add origin https://github.com/MatsuriDayo/gomobile.git + git -C gomobile fetch --depth 1 origin "$GOMOBILE_COMMIT" || exit 1 + git -C gomobile checkout -q FETCH_HEAD || exit 1 pushd gomobile - git checkout origin/master2 pushd cmd pushd gomobile go install -v