Compare commits

..
Author SHA1 Message Date
Levi 539ed2bba7 fix(ui): keep narrow file trees readable and scrollable 2026-11-03 22:18:32 +08:00
40 changed files with 721 additions and 1150 deletions

No files matched your search

+1 -1
View File
@@ -798,5 +798,5 @@ jobs:
linux/arm64 linux/arm64
linux/arm/v7 linux/arm/v7
tags: | tags: |
${{ !contains(env.VERSION, 'beta') && 'liwei2633/gopeed:latest' || '' }} liwei2633/gopeed:latest
liwei2633/gopeed:v${{ env.VERSION }} liwei2633/gopeed:v${{ env.VERSION }}
-3
View File
@@ -134,9 +134,6 @@ jobs:
if: runner.os == 'macOS' if: runner.os == 'macOS'
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Test Windows FFmpeg memory and stream lifecycle
if: runner.os == 'Windows'
run: go test -v -count=1 -timeout 180s ./pkg/download/engine/ffmpeg ./pkg/download/engine/inject/stream ./pkg/download/engine
- name: Install Linux WebView dependencies - name: Install Linux WebView dependencies
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
+34 -31
View File
@@ -11,11 +11,11 @@ require (
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/fclairamb/ftpserverlib v0.31.0 github.com/fclairamb/ftpserverlib v0.31.0
github.com/go-git/go-git/v5 v5.19.2 github.com/go-git/go-git/v5 v5.8.1
github.com/google/jsonschema-go v0.4.3 github.com/google/jsonschema-go v0.4.3
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/imroc/req/v3 v3.58.0 github.com/imroc/req/v3 v3.52.2
github.com/jlaffaye/ftp v0.2.4 github.com/jlaffaye/ftp v0.2.4
github.com/matoous/go-nanoid/v2 v2.0.0 github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mattn/go-ieproxy v0.0.12 github.com/mattn/go-ieproxy v0.0.12
@@ -29,13 +29,7 @@ require (
github.com/tetratelabs/wazero v1.12.0 github.com/tetratelabs/wazero v1.12.0
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
)
require (
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/onsi/gomega v1.36.3 // indirect
) )
require ( require (
@@ -48,40 +42,48 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/fgprof v0.9.5 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/icholy/digest v1.1.0 // indirect github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect github.com/pion/transport/v4 v4.0.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.51.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/utls v1.6.7 // indirect
github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect github.com/segmentio/encoding v0.5.4 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/mod v0.33.0
golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.22.0 // indirect golang.org/x/sync v0.20.0 // indirect
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.1
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/RoaringBitmap/roaring v1.9.4 github.com/RoaringBitmap/roaring v1.9.4
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/chansync v0.7.0 // indirect github.com/anacrolix/chansync v0.7.0 // indirect
github.com/anacrolix/dht/v2 v2.23.0 // indirect github.com/anacrolix/dht/v2 v2.23.0 // indirect
github.com/anacrolix/envpprof v1.5.0 // indirect github.com/anacrolix/envpprof v1.5.0 // indirect
github.com/anacrolix/generics v0.2.0 github.com/anacrolix/generics v0.2.0 // indirect
github.com/anacrolix/go-libutp v1.3.2 // indirect github.com/anacrolix/go-libutp v1.3.2 // indirect
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect github.com/anacrolix/missinggo v1.3.0 // indirect
@@ -97,7 +99,7 @@ require (
github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -105,13 +107,13 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.2 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-llsqlite/adapter v0.2.0 // indirect github.com/go-llsqlite/adapter v0.2.0 // indirect
github.com/go-llsqlite/crawshaw v0.6.0 // indirect github.com/go-llsqlite/crawshaw v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
@@ -129,7 +131,7 @@ require (
github.com/multiformats/go-varint v0.1.0 // indirect github.com/multiformats/go-varint v0.1.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pion/datachannel v1.6.0 // indirect github.com/pion/datachannel v1.6.0 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/ice/v4 v4.2.0 // indirect github.com/pion/ice/v4 v4.2.0 // indirect
github.com/pion/interceptor v0.1.42 // indirect github.com/pion/interceptor v0.1.42 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
@@ -140,15 +142,15 @@ require (
github.com/pion/sctp v1.9.1 // indirect github.com/pion/sctp v1.9.1 // indirect
github.com/pion/sdp/v3 v3.0.17 // indirect github.com/pion/sdp/v3 v3.0.17 // indirect
github.com/pion/srtp/v3 v3.0.10 // indirect github.com/pion/srtp/v3 v3.0.10 // indirect
github.com/pion/stun/v3 v3.1.5 // indirect github.com/pion/stun/v3 v3.1.1 // indirect
github.com/pion/turn/v4 v4.1.4 // indirect github.com/pion/turn/v4 v4.1.4 // indirect
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/protolambda/ctxlock v0.1.0 // indirect github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.8.1 // indirect github.com/tidwall/btree v1.8.1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
@@ -156,11 +158,12 @@ require (
go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.55.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.58.0 golang.org/x/net v0.50.0
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.41.0 golang.org/x/text v0.34.0
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.42.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
modernc.org/libc v1.67.4 // indirect modernc.org/libc v1.67.4 // indirect
+110 -65
View File
@@ -25,17 +25,29 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GopeedLab/webview_go v0.0.0-20260915010614-2567e492369a h1:Vzz+8aMVX9ZPPpkxgxa7mCorrAgdXCZL5EunOpOBvao=
github.com/GopeedLab/webview_go v0.0.0-20260915010614-2567e492369a/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
github.com/GopeedLab/webview_go v0.0.0-20260917153919-dcf00737f32a h1:nz1bC6TGwQ1kv/OVLahNZ0EEtb7v5LHfceAkW4MvS6I=
github.com/GopeedLab/webview_go v0.0.0-20260917153919-dcf00737f32a/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
github.com/GopeedLab/webview_go v0.0.0-20260917155039-1ee7ffb4169a h1:yudcfXpipKLnMaaw1losn6YeOBjvj2rAFicfjqYJexY=
github.com/GopeedLab/webview_go v0.0.0-20260917155039-1ee7ffb4169a/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
github.com/GopeedLab/webview_go v0.0.0-20260917155218-bd16da0d6f53 h1:nMSHKhbrEaWPE/x3mYl/270GPjswbUmS+E7yYjly4xk=
github.com/GopeedLab/webview_go v0.0.0-20260917155218-bd16da0d6f53/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
github.com/GopeedLab/webview_go v0.0.0-20260917161756-2a4f989dcb1d h1:4fjlhJpBWa6IcdKVPkEMMIDd6Xo9JM09dJHdKU+1V7k=
github.com/GopeedLab/webview_go v0.0.0-20260917161756-2a4f989dcb1d/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
github.com/GopeedLab/webview_go v0.0.0-20260917163929-ca26c3e6bcfa h1:VkdBMcNHYTqCMwOOCimA7b3xZKV/qFIGrhh3+vm9n3s=
github.com/GopeedLab/webview_go v0.0.0-20260917163929-ca26c3e6bcfa/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914 h1:9VJQZIZ1R6T4m+4vlNQYNXSPNUv2gf2PiTfIXiQ3MTw= github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914 h1:9VJQZIZ1R6T4m+4vlNQYNXSPNUv2gf2PiTfIXiQ3MTw=
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ= github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
@@ -45,6 +57,8 @@ github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o= github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
@@ -141,6 +155,7 @@ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2w
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -157,12 +172,11 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -185,8 +199,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -202,8 +216,8 @@ github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -212,12 +226,12 @@ github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1T
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE= github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0= github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -238,6 +252,8 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
@@ -250,8 +266,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -276,13 +292,10 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
@@ -310,6 +323,11 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -327,8 +345,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.58.0 h1:rBqJhv2gZjMymGot4vKmNSUs/mAITMpGASqElhuMvS4= github.com/imroc/req/v3 v3.52.2 h1:xJocr1aIv0a2K9knfBQ4JnZHk+kWTITdjf0mgDg229I=
github.com/imroc/req/v3 v3.58.0/go.mod h1:rC7NTGgfMSkVWR4GO46rMeJsUfaeOpa6pLsgpq9q3zI= github.com/imroc/req/v3 v3.52.2/go.mod h1:dBGsDloOSZJcFs6PnTjZXYBJK70OXbZpizHBLNqcH2k=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jlaffaye/ftp v0.2.4 h1:JqI85DdkfZj8ntaHk8W9U2SC3jNfiPUU70+wtIWmlfE= github.com/jlaffaye/ftp v0.2.4 h1:JqI85DdkfZj8ntaHk8W9U2SC3jNfiPUU70+wtIWmlfE=
@@ -345,8 +363,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -356,6 +374,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -368,6 +387,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M= github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
@@ -405,10 +426,13 @@ github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJh
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
@@ -420,8 +444,8 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0= github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk= github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw= github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4= github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ= github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
@@ -442,18 +466,18 @@ github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ= github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M= github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8= github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ= github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ= github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 h1:gnkvpxCSzeTKovDsxeFFl+W/i6WydzSHhu5lH/RpFfM= github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 h1:gnkvpxCSzeTKovDsxeFFl+W/i6WydzSHhu5lH/RpFfM=
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7/go.mod h1:/td+qaV3q20qTbRjYV9ABWjmCkvt+Pog/S+NqLMoUKk= github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7/go.mod h1:/td+qaV3q20qTbRjYV9ABWjmCkvt+Pog/S+NqLMoUKk=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -461,7 +485,10 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -481,13 +508,13 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE= github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM= github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -510,13 +537,13 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
@@ -543,6 +570,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
@@ -585,10 +614,12 @@ go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
@@ -600,9 +631,11 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -612,8 +645,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -635,8 +668,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -658,12 +691,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -680,8 +715,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -714,20 +749,26 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -735,11 +776,13 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
@@ -772,8 +815,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -818,6 +861,8 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-4
View File
@@ -46,10 +46,6 @@ type Uploader interface {
WaitUpload() error WaitUpload() error
} }
type DownloadCounter interface {
ReceivedBytes() int64
}
// FetcherMeta defines the meta information of a fetcher. // FetcherMeta defines the meta information of a fetcher.
type FetcherMeta struct { type FetcherMeta struct {
Req *base.Request `json:"req"` Req *base.Request `json:"req"`
-76
View File
@@ -290,82 +290,6 @@ func TestBTDownloadSelectedFiles(t *testing.T) {
if f.Meta().Res.Size != 48*1024 { if f.Meta().Res.Size != 48*1024 {
t.Fatal("selected resource size is incorrect") t.Fatal("selected resource size is incorrect")
} }
if downloaded := f.Progress().TotalDownloaded(); downloaded != 48*1024 || f.ReceivedBytes() <= downloaded {
t.Fatalf("selected progress = %d, received traffic = %d; want 48 KiB of progress and traffic across the shared piece", downloaded, f.ReceivedBytes())
}
}
func TestBTProgressOnlyCountsVerifiedFileBytes(t *testing.T) {
const fileSize = 56 * 1024
fixture := makeBTFixtureWithPieceSize(t, map[string][]byte{
"a.bin": btPayload(fileSize, "verified-a"),
"b.bin": btPayload(fileSize, "verified-b"),
}, 16*1024, false, true, 64*1024)
f := btDownloadFetcher(t, nil)
btResolve(t, f, fixture.Magnet)
dir := t.TempDir()
f.Meta().Opts = &base.Options{Path: dir}
btStart(t, f)
deadline := time.Now().Add(8 * time.Second)
for f.torrent.Files()[0].BytesCompleted() == 0 && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
btPause(t, f)
if f.torrent.Files()[0].BytesCompleted() == 0 || f.torrent.Piece(0).State().Complete {
t.Fatal("expected some received bytes in a piece that is still incomplete")
}
if f.ReceivedBytes() == 0 {
t.Fatal("received traffic must be visible before a piece is verified")
}
if progress := f.Progress(); len(progress) != 2 || progress[0] != 0 {
t.Errorf("progress before the first file's piece is verified = %v, want zero for the first file", progress)
}
fixture.Limiter.SetLimit(rate.Inf)
btStart(t, f)
btAwaitDone(t, f)
fixture.AssertFiles(t, dir)
progress := f.Progress()
if len(progress) != 2 || progress[0] != fileSize || progress[1] != fileSize {
t.Fatalf("verified progress at completion = %v, want %d for each file", progress, fileSize)
}
}
func TestBTUploadedBytesSamplesLiveTraffic(t *testing.T) {
payload := btPayload(48*1024, "upload-counter")
fixture := newBTFixture(t, map[string][]byte{"": payload}, 0)
const saved = 1024
f := &Fetcher{
torrent: fixture.Torrent,
data: &fetcherData{SeedBytes: saved},
seedBaseBytes: saved,
}
f.torrentReady.Store(true)
f.torrentUpload.Store(true)
cfg := torrent.TestingConfig(t)
cfg.DataDir = t.TempDir()
cfg.DisableTrackers = true
cfg.DisableUTP = true
cfg.DisableIPv6 = true
peer, err := torrent.NewClient(cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { peer.Close() })
download, _, err := peer.AddTorrentSpec(torrent.TorrentSpecFromMetaInfo(fixture.MetaInfo))
if err != nil {
t.Fatal(err)
}
download.AddPeers([]torrent.PeerInfo{{Addr: fixture.Seeder.ListenAddrs()[0]}})
download.DownloadAll()
select {
case <-download.Complete().On():
case <-time.After(10 * time.Second):
t.Fatal("loopback peer did not complete")
}
if got, want := f.UploadedBytes(), int64(saved+len(payload)); got != want {
t.Fatalf("uploaded bytes = %d, want %d including the saved bytes", got, want)
}
} }
func TestBTPauseAndResume(t *testing.T) { func TestBTPauseAndResume(t *testing.T) {
+10 -63
View File
@@ -10,7 +10,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -43,10 +42,9 @@ type Fetcher struct {
ctl *controller.Controller ctl *controller.Controller
config *config config *config
torrent *torrent.Torrent torrent *torrent.Torrent
meta *fetcher.FetcherMeta meta *fetcher.FetcherMeta
data *fetcherData data *fetcherData
seedBaseBytes int64
torrentReady atomic.Bool torrentReady atomic.Bool
torrentUpload atomic.Bool torrentUpload atomic.Bool
@@ -465,46 +463,14 @@ func (f *Fetcher) Progress() fetcher.Progress {
if !f.torrentReady.Load() { if !f.torrentReady.Load() {
return append(fetcher.Progress(nil), f.data.Progress...) return append(fetcher.Progress(nil), f.data.Progress...)
} }
pieceSize := f.torrent.Info().PieceLength
var offset int64
var verified []verifiedByteRange
for _, run := range f.torrent.PieceStateRuns() {
end := offset + int64(run.Length)*pieceSize
if pieceVerified(run.PieceState) {
verified = append(verified, verifiedByteRange{start: offset, end: end})
}
offset = end
}
files := f.torrent.Files()
for i := range f.data.Progress { for i := range f.data.Progress {
selectIndex := f.meta.Opts.SelectFiles[i] selectIndex := f.meta.Opts.SelectFiles[i]
file := files[selectIndex] file := f.torrent.Files()[selectIndex]
f.data.Progress[i] = verifiedFileBytes(verified, file.Offset(), file.Length()) f.data.Progress[i] = file.BytesCompleted()
} }
return append(fetcher.Progress(nil), f.data.Progress...) return append(fetcher.Progress(nil), f.data.Progress...)
} }
type verifiedByteRange struct {
start, end int64
}
func verifiedFileBytes(verified []verifiedByteRange, offset, length int64) int64 {
first := sort.Search(len(verified), func(i int) bool { return verified[i].end > offset })
fileEnd := offset + length
var completed int64
for _, run := range verified[first:] {
if run.start >= fileEnd {
break
}
completed += min(fileEnd, run.end) - max(offset, run.start)
}
return completed
}
func pieceVerified(state torrent.PieceState) bool {
return state.Ok && state.Complete && !state.Hashing && !state.QueuedForHash
}
func (f *Fetcher) Wait() error { func (f *Fetcher) Wait() error {
f.mu.Lock() f.mu.Lock()
var closed <-chan struct{} var closed <-chan struct{}
@@ -621,7 +587,7 @@ func (f *Fetcher) isDoneLocked() bool {
file := f.torrent.Files()[index] file := f.torrent.Files()[index]
for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ { for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ {
state := f.torrent.Piece(i).State() state := f.torrent.Piece(i).State()
if !pieceVerified(state) { if !state.Ok || !state.Complete || state.Hashing || state.QueuedForHash {
return false return false
} }
} }
@@ -719,13 +685,11 @@ func (f *Fetcher) Upload() (err error) {
} }
func (f *Fetcher) doUpload(fromUpload bool) { func (f *Fetcher) doUpload(fromUpload bool) {
f.mu.Lock()
if !f.torrentUpload.CompareAndSwap(false, true) { if !f.torrentUpload.CompareAndSwap(false, true) {
f.mu.Unlock()
return return
} }
f.seedBaseBytes = f.data.SeedBytes f.mu.Lock()
initialTime := f.data.SeedTime initialBytes, initialTime := f.data.SeedBytes, f.data.SeedTime
torrentClosed := f.torrent.Closed() torrentClosed := f.torrent.Closed()
f.mu.Unlock() f.mu.Unlock()
var doneTime int64 var doneTime int64
@@ -743,7 +707,8 @@ func (f *Fetcher) doUpload(fromUpload bool) {
f.mu.Unlock() f.mu.Unlock()
continue continue
} }
f.refreshSeedBytesLocked() stats := f.torrentStats()
f.data.SeedBytes = initialBytes + stats.BytesWrittenData.Int64()
if !fromUpload && !f.isDoneLocked() { if !fromUpload && !f.isDoneLocked() {
f.mu.Unlock() f.mu.Unlock()
continue continue
@@ -776,27 +741,9 @@ func (f *Fetcher) torrentStats() torrent.TorrentStats {
func (f *Fetcher) UploadedBytes() int64 { func (f *Fetcher) UploadedBytes() int64 {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
if f.torrentReady.Load() && f.torrentUpload.Load() {
f.refreshSeedBytesLocked()
}
return f.data.SeedBytes return f.data.SeedBytes
} }
func (f *Fetcher) refreshSeedBytesLocked() {
stats := f.torrentStats()
f.data.SeedBytes = max(f.data.SeedBytes, f.seedBaseBytes+stats.BytesWrittenData.Int64())
}
func (f *Fetcher) ReceivedBytes() int64 {
f.mu.Lock()
defer f.mu.Unlock()
if !f.torrentReady.Load() {
return 0
}
stats := f.torrentStats()
return stats.BytesReadUsefulData.Int64()
}
func (f *Fetcher) WaitUpload() (err error) { func (f *Fetcher) WaitUpload() (err error) {
<-f.uploadDoneCh <-f.uploadDoneCh
return nil return nil
+1 -6
View File
@@ -46,11 +46,6 @@ func newBTHybridFixture(t testing.TB, payload []byte) *btFixture {
} }
func makeBTFixture(t testing.TB, files map[string][]byte, bytesPerSecond int, v2, private bool) *btFixture { func makeBTFixture(t testing.TB, files map[string][]byte, bytesPerSecond int, v2, private bool) *btFixture {
t.Helper()
return makeBTFixtureWithPieceSize(t, files, bytesPerSecond, v2, private, 16*1024)
}
func makeBTFixtureWithPieceSize(t testing.TB, files map[string][]byte, bytesPerSecond int, v2, private bool, pieceSize int64) *btFixture {
t.Helper() t.Helper()
source := t.TempDir() source := t.TempDir()
name := "payload.bin" name := "payload.bin"
@@ -69,7 +64,7 @@ func makeBTFixtureWithPieceSize(t testing.TB, files map[string][]byte, bytesPerS
} }
expected[filepath.Join(name, filepath.FromSlash(path))] = append([]byte(nil), data...) expected[filepath.Join(name, filepath.FromSlash(path))] = append([]byte(nil), data...)
} }
info := metainfo.Info{PieceLength: pieceSize} info := metainfo.Info{PieceLength: 16 * 1024}
if err := info.BuildFromFilePath(root); err != nil { if err := info.BuildFromFilePath(root); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+34 -45
View File
@@ -314,7 +314,6 @@ type Fetcher struct {
prefetchDone atomic.Bool // Prefetch completed or stopped prefetchDone atomic.Bool // Prefetch completed or stopped
prefetchErr error // Error during prefetch (if any) prefetchErr error // Error during prefetch (if any)
prefetchStopCh chan struct{} // Signal to stop prefetch prefetchStopCh chan struct{} // Signal to stop prefetch
prefetchExitCh chan struct{} // Closed when the prefetch goroutine exits
resolveFallback atomic.Bool // Resolve prefetch is still available until the first Range request takes over resolveFallback atomic.Bool // Resolve prefetch is still available until the first Range request takes over
// Target file // Target file
@@ -563,7 +562,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
// For non-range resources, the response will be used directly in Start // For non-range resources, the response will be used directly in Start
if res.Range && reuseResolveBody { if res.Range && reuseResolveBody {
f.prefetchStopCh = make(chan struct{}) f.prefetchStopCh = make(chan struct{})
f.prefetchExitCh = make(chan struct{})
f.resolveFallback.Store(true) f.resolveFallback.Store(true)
go f.asyncPrefetch() go f.asyncPrefetch()
} }
@@ -581,9 +579,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
func (f *Fetcher) asyncPrefetch() { func (f *Fetcher) asyncPrefetch() {
defer func() { defer func() {
f.prefetchDone.Store(true) f.prefetchDone.Store(true)
if f.prefetchExitCh != nil {
close(f.prefetchExitCh)
}
}() }()
// Get the resolve response // Get the resolve response
@@ -659,38 +654,23 @@ func (f *Fetcher) asyncPrefetch() {
} }
} }
// stopPrefetch signals the async prefetch to stop and waits until it exits.
// Closing the resolve body unblocks a pending Read, so a stalled server cannot
// keep the prefetch goroutine writing to the temporary file after we reuse it.
func (f *Fetcher) stopPrefetch() {
if f.prefetchStopCh == nil {
return
}
// Pause and the first connection's takeover can stop concurrently, so the
// check-and-close must be under the lock to avoid closing the channel twice.
f.resolveRespLock.Lock()
select {
case <-f.prefetchStopCh:
// Already closed
default:
close(f.prefetchStopCh)
}
if f.resolveResp != nil {
f.resolveResp.Body.Close()
f.resolveResp = nil
}
f.resolveRespLock.Unlock()
if f.prefetchExitCh != nil {
<-f.prefetchExitCh
}
}
// stopPrefetchAndGetData stops the async prefetch and returns prefetched bytes // stopPrefetchAndGetData stops the async prefetch and returns prefetched bytes
// It also copies prefetched data to the target file // It also copies prefetched data to the target file
func (f *Fetcher) stopPrefetchAndCopyData() int64 { func (f *Fetcher) stopPrefetchAndCopyData() int64 {
f.stopPrefetch() // Signal prefetch to stop (safely)
if f.prefetchStopCh != nil {
select {
case <-f.prefetchStopCh:
// Already closed
default:
close(f.prefetchStopCh)
}
}
// Wait for prefetch to finish (with timeout)
for i := 0; f.prefetchStopCh != nil && i < 1000 && !f.prefetchDone.Load(); i++ {
time.Sleep(10 * time.Millisecond)
}
prefetched := f.prefetchSize.Load() prefetched := f.prefetchSize.Load()
if prefetched == 0 { if prefetched == 0 {
@@ -892,16 +872,12 @@ func (f *Fetcher) doStart() error {
// For non-range resources, the response will be used directly // For non-range resources, the response will be used directly
var prefetchedBytes int64 var prefetchedBytes int64
if f.meta.Res.Range { if f.meta.Res.Range {
// Read the size once and judge completeness by it alone: Resolve may have resolveComplete := f.knownSize() > 0 && f.prefetchDone.Load() && f.prefetchErr == nil && f.prefetchSize.Load() >= f.knownSize()
// written every byte before it sets prefetchDone, and a snapshot that
// covers the whole file would otherwise finish without copying it.
prefetched := f.prefetchSize.Load()
resolveComplete := f.knownSize() > 0 && prefetched >= f.knownSize()
if f.resolveFallback.Load() && !resolveComplete { if f.resolveFallback.Load() && !resolveComplete {
// Keep Resolve downloading until the first Range response has been // Keep Resolve downloading until the first Range response has been
// validated. The Range request starts at a stable snapshot; a valid // validated. The Range request starts at a stable snapshot; a valid
// takeover discards any overlap downloaded by Resolve in the meantime. // takeover discards any overlap downloaded by Resolve in the meantime.
prefetchedBytes = prefetched prefetchedBytes = f.prefetchSize.Load()
} else { } else {
// Resolve already completed, or no live Resolve fallback remains. // Resolve already completed, or no live Resolve fallback remains.
prefetchedBytes = f.stopPrefetchAndCopyData() prefetchedBytes = f.stopPrefetchAndCopyData()
@@ -1651,10 +1627,11 @@ func (f *Fetcher) completeFromResolveFallback(conn *connection) bool {
return false return false
} }
select { for !f.prefetchDone.Load() {
case <-f.prefetchExitCh: if conn.ctx.Err() != nil {
case <-conn.ctx.Done(): return false
return false }
time.Sleep(10 * time.Millisecond)
} }
knownSize := f.hasKnownSize() knownSize := f.hasKnownSize()
if f.prefetchErr != nil || (knownSize && f.prefetchSize.Load() != f.meta.Res.Size) { if f.prefetchErr != nil || (knownSize && f.prefetchSize.Load() != f.meta.Res.Size) {
@@ -2461,7 +2438,14 @@ func (f *Fetcher) Pause() error {
} }
// Stop prefetch if running // Stop prefetch if running
f.stopPrefetch() if f.prefetchStopCh != nil {
select {
case <-f.prefetchStopCh:
// Already closed
default:
close(f.prefetchStopCh)
}
}
// Wait for downloadLoop to exit first (it will call wg.Wait internally) // Wait for downloadLoop to exit first (it will call wg.Wait internally)
if f.downloadLoopDone != nil { if f.downloadLoopDone != nil {
@@ -2471,6 +2455,11 @@ func (f *Fetcher) Pause() error {
// Wait for all connection goroutines to stop // Wait for all connection goroutines to stop
f.wg.Wait() f.wg.Wait()
// Wait for prefetch to finish
for f.prefetchStopCh != nil && !f.prefetchDone.Load() {
time.Sleep(10 * time.Millisecond)
}
// If Pause wins before the first Range response is validated, materialize // If Pause wins before the first Range response is validated, materialize
// the Resolve prefix before discarding its temporary file. The first // the Resolve prefix before discarding its temporary file. The first
// connection was allocated after the earlier prefetch snapshot, so advance // connection was allocated after the earlier prefetch snapshot, so advance
+4 -5
View File
@@ -3261,17 +3261,16 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
} }
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) { func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
// Setup must run before Resolve, as in the downloader, so the Resolve fetcher := downloadReady(httpListener, 4, t)
// request also goes through the proxy. ctl := controller.NewController()
f := buildFetcher() ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
f.ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
return (&base.DownloaderProxyConfig{ return (&base.DownloaderProxyConfig{
Enable: true, Enable: true,
Scheme: "socks5", Scheme: "socks5",
Host: proxyListener.Addr().String(), Host: proxyListener.Addr().String(),
}).ToHandler() }).ToHandler()
} }
fetcher := doDownloadReady(f, httpListener, 4, t) fetcher.Setup(ctl)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -1,52 +0,0 @@
package http
import (
"bytes"
gohttp "net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
)
// Start can run after the Resolve prefetch wrote every byte but before it set
// prefetchDone. The prefetched body must still be copied to the target.
func TestFetcher_StartBeforePrefetchDoneCopiesFullBody(t *testing.T) {
payload := []byte("prefetched before Start")
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
gohttp.ServeContent(w, r, "f.txt", time.Time{}, bytes.NewReader(payload))
}))
defer server.Close()
f := buildFetcher()
f.Meta().Opts = &base.Options{Path: t.TempDir(), Name: "f.txt", Extra: &http.OptsExtra{Connections: 1}}
if err := f.Resolve(&base.Request{URL: server.URL + "/f.txt"}); err != nil {
t.Fatal(err)
}
for !f.prefetchDone.Load() {
time.Sleep(time.Millisecond)
}
// Reopen the window between the last write and prefetchDone.
f.prefetchDone.Store(false)
go func() {
time.Sleep(50 * time.Millisecond)
f.prefetchDone.Store(true)
}()
if err := f.Start(); err != nil {
t.Fatal(err)
}
if err := f.Wait(); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(f.meta.SingleFilepath())
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("content = %q, want %q", got, payload)
}
}
@@ -1,56 +0,0 @@
package http
import (
"bytes"
"fmt"
gohttp "net/http"
"net/http/httptest"
"testing"
"time"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
)
// A stalled Resolve body must not keep the prefetch goroutine alive after
// stopPrefetchAndCopyData returns, or it writes to a file we already removed.
func TestFetcher_StopPrefetchUnblocksStalledRead(t *testing.T) {
prefix := bytes.Repeat([]byte("p"), 64*1024)
stall := make(chan struct{})
defer close(stall)
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
w.Header().Set(base.HttpHeaderAcceptRanges, base.HttpHeaderBytes)
w.Header().Set(base.HttpHeaderContentLength, fmt.Sprintf("%d", len(prefix)*4))
w.WriteHeader(gohttp.StatusOK)
_, _ = w.Write(prefix)
w.(gohttp.Flusher).Flush()
select {
case <-stall:
case <-r.Context().Done():
}
}))
defer server.Close()
f := buildFetcher()
f.Meta().Opts = &base.Options{
Path: t.TempDir(),
Name: "stalled.data",
Extra: &http.OptsExtra{Connections: 1},
}
if err := f.Resolve(&base.Request{URL: server.URL + "/stalled.data"}); err != nil {
t.Fatal(err)
}
for f.prefetchSize.Load() < int64(len(prefix)) {
time.Sleep(time.Millisecond)
}
start := time.Now()
f.stopPrefetch()
if elapsed := time.Since(start); elapsed > 2*time.Second {
t.Fatalf("stopPrefetch took %v, want it to interrupt the stalled read", elapsed)
}
if !f.prefetchDone.Load() {
t.Fatal("prefetch goroutine still running after stopPrefetch")
}
}
-26
View File
@@ -1,26 +0,0 @@
package download
import (
"os"
"path/filepath"
"runtime/debug"
"testing"
)
func TestLogPanicPreservesPreviousCrash(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "crash.log")
const previous = "previous crash traceback\n"
if err := os.WriteFile(path, []byte(previous), 0600); err != nil {
t.Fatal(err)
}
logPanic(dir)
t.Cleanup(func() { _ = debug.SetCrashOutput(nil, debug.CrashOptions{}) })
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(data) != previous {
t.Fatalf("previous crash was overwritten: %q", data)
}
}
+8 -20
View File
@@ -345,12 +345,7 @@ func (d *Downloader) Setup() error {
} }
current := task.fetcher.Progress().TotalDownloaded() current := task.fetcher.Progress().TotalDownloaded()
delta := max(0, current-task.Progress.Downloaded) delta := current - task.Progress.Downloaded
if counter, ok := task.fetcher.(fetcher.DownloadCounter); ok && task.Status == base.DownloadStatusRunning {
received := counter.ReceivedBytes()
delta = received - task.receivedBytes
task.receivedBytes = received
}
if task.Meta != nil && task.Meta.Req != nil { if task.Meta != nil && task.Meta.Req != nil {
if producer := d.blob.Production(task.Meta.Req.URL); producer != nil { if producer := d.blob.Production(task.Meta.Req.URL); producer != nil {
downloaded, received := producer.Progress() downloaded, received := producer.Progress()
@@ -358,20 +353,19 @@ func (d *Downloader) Setup() error {
task.producer = producer task.producer = producer
task.producerReceived = 0 task.producerReceived = 0
task.speedArr = nil task.speedArr = nil
task.downloadSampledAt = time.Time{}
} }
current = downloaded current = downloaded
delta = max(0, received-task.producerReceived) delta = max(0, received-task.producerReceived)
task.producerReceived = received task.producerReceived = received
} }
} }
tick := float64(d.cfg.RefreshInterval) / 1000
downloadDataChanged := false downloadDataChanged := false
uploading := task.Uploading uploading := task.Uploading
if task.Status == base.DownloadStatusRunning { if task.Status == base.DownloadStatusRunning {
downloadDataChanged = current != task.Progress.Downloaded downloadDataChanged = current != task.Progress.Downloaded
task.Progress.Used = task.timer.Used() task.Progress.Used = task.timer.Used()
elapsed := sampleSeconds(&task.downloadSampledAt, time.Now()) task.Progress.Speed = task.updateSpeed(max(0, delta), tick)
task.Progress.Speed = task.updateSpeed(delta, elapsed)
task.Progress.Downloaded = current task.Progress.Downloaded = current
} }
@@ -380,8 +374,7 @@ func (d *Downloader) Setup() error {
uploader := task.fetcher.(fetcher.Uploader) uploader := task.fetcher.(fetcher.Uploader)
currentUploaded := uploader.UploadedBytes() currentUploaded := uploader.UploadedBytes()
uploadDataChanged = currentUploaded != task.Progress.Uploaded uploadDataChanged = currentUploaded != task.Progress.Uploaded
elapsed := sampleSeconds(&task.uploadSampledAt, time.Now()) task.Progress.UploadSpeed = task.updateUploadSpeed(currentUploaded-task.Progress.Uploaded, tick)
task.Progress.UploadSpeed = task.updateUploadSpeed(currentUploaded-task.Progress.Uploaded, elapsed)
task.Progress.Uploaded = currentUploaded task.Progress.Uploaded = currentUploaded
} }
task.statusLock.Unlock() task.statusLock.Unlock()
@@ -2460,8 +2453,6 @@ func (d *Downloader) doStart(task *Task) (err error) {
return nil return nil
} }
task.Progress.Speed = 0 task.Progress.Speed = 0
task.speedArr = nil
task.downloadSampledAt = time.Time{}
task.timer.Start() task.timer.Start()
task.statusLock.Unlock() task.statusLock.Unlock()
if err := task.fetcher.Start(); err != nil { if err := task.fetcher.Start(); err != nil {
@@ -2659,12 +2650,11 @@ func logPanic(logDir string) {
if err := util.CreateDirIfNotExist(logDir); err != nil { if err := util.CreateDirIfNotExist(logDir); err != nil {
return return
} }
f, err := os.OpenFile(filepath.Join(logDir, "crash.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) f, err := os.Create(filepath.Join(logDir, "crash.log"))
if err != nil { if err != nil {
return return
} }
defer f.Close() // SetCrashOutput duplicates the descriptor. debug.SetCrashOutput(f, debug.CrashOptions{})
_ = debug.SetCrashOutput(f, debug.CrashOptions{})
} }
func (d *Downloader) assignFetcherManager(task *Task) error { func (d *Downloader) assignFetcherManager(task *Task) error {
@@ -3071,10 +3061,8 @@ func initTask(task *Task) {
task.lock = &sync.Mutex{} task.lock = &sync.Mutex{}
task.statsLock = &sync.Mutex{} task.statsLock = &sync.Mutex{}
task.blobRefLock = &sync.Mutex{} task.blobRefLock = &sync.Mutex{}
task.speedArr = nil task.speedArr = make([]int64, 0)
task.uploadSpeedArr = nil task.uploadSpeedArr = make([]int64, 0)
task.downloadSampledAt = time.Time{}
task.uploadSampledAt = time.Time{}
} }
func (d *Downloader) releaseBlobTask(task *Task) { func (d *Downloader) releaseBlobTask(task *Task) {
-9
View File
@@ -174,12 +174,3 @@ compilation memory, host buffers and JS allocations are additional to the
linear-memory limit. Playback support depends on the selected codecs, not just linear-memory limit. Playback support depends on the selected codecs, not just
the `.mp4` suffix. Platform execution and performance must be tested on target the `.mp4` suffix. Platform execution and performance must be tested on target
devices, especially mobile/32-bit devices. devices, especially mobile/32-bit devices.
On 64-bit Windows, instance linear memory uses a reserved virtual address range with
pages committed as it grows. Module completion, errors and cancellation release
this range directly through `VirtualFree`, rather than waiting for Go's garbage
collector to reclaim large buffers. Growing an instance preserves its address
without copying the previous buffer. The 512 MiB limit is unchanged; reservation
does not commit that entire amount. Shared compilation remains resident after
the first merge, so process memory is not expected to return exactly to the
pre-FFmpeg baseline. This does not diagnose unrelated idle memory growth.
-32
View File
@@ -1,32 +0,0 @@
package ffmpeg
import (
"context"
"sync"
"github.com/tetratelabs/wazero/experimental"
)
// The runtime does not always invoke Free when a module's start function traps.
// Keep host ownership until execution returns, including failed instantiation.
func withOwnedMemory(ctx context.Context, allocator experimental.MemoryAllocator) (context.Context, func()) {
var memories []*ownedLinearMemory
ctx = experimental.WithMemoryAllocator(ctx, experimental.MemoryAllocatorFunc(
func(cap, max uint64) experimental.LinearMemory {
memory := &ownedLinearMemory{LinearMemory: allocator.Allocate(cap, max)}
memories = append(memories, memory)
return memory
}))
return ctx, func() {
for _, memory := range memories {
memory.Free()
}
}
}
type ownedLinearMemory struct {
experimental.LinearMemory
once sync.Once
}
func (m *ownedLinearMemory) Free() { m.once.Do(m.LinearMemory.Free) }
@@ -1,7 +0,0 @@
//go:build !windows || (!amd64 && !arm64)
package ffmpeg
import "context"
func withLinearMemory(ctx context.Context) (context.Context, func()) { return ctx, func() {} }
-88
View File
@@ -1,88 +0,0 @@
package ffmpeg
import (
"context"
"encoding/hex"
"testing"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/experimental"
)
// Verify the pinned runtime's allocator lifecycle on every development host:
// host-owned buffers must not be released before execution ends, or retained
// after failed instantiation, traps or cancellation.
func TestLinearMemoryModuleLifecycle(t *testing.T) {
testLinearMemoryModuleLifecycle(t, func(_, _ uint64) experimental.LinearMemory {
return &testLinearMemory{}
})
}
type testLinearMemory struct{ buffer []byte }
func (m *testLinearMemory) Reallocate(size uint64) []byte {
m.buffer = append(m.buffer, make([]byte, int(size)-len(m.buffer))...)
return m.buffer
}
func (m *testLinearMemory) Free() { m.buffer = nil }
type trackedLinearMemory struct {
experimental.LinearMemory
freeCount int
}
func (m *trackedLinearMemory) Free() {
m.freeCount++
m.LinearMemory.Free()
}
func testLinearMemoryModuleLifecycle(t *testing.T, allocate experimental.MemoryAllocatorFunc) {
t.Helper()
for _, tt := range []struct {
name string
// A module with one memory (min=1/max=2 pages), optionally a start
// function that traps or loops until its context is cancelled.
wasm string
wantErr bool
}{
{"close", "0061736d01000000050401010102", false},
{"trap", "0061736d01000000010401600000030201000504010101020801000a05010300000b", true},
{"cancel", "0061736d01000000010401600000030201000504010101020801000a0901070003400c000b0b", true},
} {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
var memory *trackedLinearMemory
ctx, release := withOwnedMemory(ctx, experimental.MemoryAllocatorFunc(
func(_, max uint64) experimental.LinearMemory {
memory = &trackedLinearMemory{LinearMemory: allocate(0, max)}
return memory
}))
defer release()
rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCloseOnContextDone(true))
defer rt.Close(context.Background())
binary, err := hex.DecodeString(tt.wasm)
if err != nil {
t.Fatal(err)
}
module, err := rt.Instantiate(ctx, binary)
if (err != nil) != tt.wantErr {
t.Fatalf("instantiate: %v", err)
}
if !tt.wantErr {
if memory == nil || memory.freeCount != 0 {
t.Fatal("module did not allocate native memory")
}
if err := module.Close(ctx); err != nil {
t.Fatal(err)
}
}
release()
release()
if memory == nil || memory.freeCount != 1 {
t.Fatal("host did not release memory exactly once")
}
})
}
}
@@ -1,70 +0,0 @@
//go:build windows && (amd64 || arm64)
package ffmpeg
import (
"context"
"fmt"
"unsafe"
"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/windows"
)
// Keep large, short-lived WASM buffers out of the Go heap. Closing a module
// releases its committed pages immediately, without waiting for GC/scavenging.
func withLinearMemory(ctx context.Context) (context.Context, func()) {
return withOwnedMemory(ctx, experimental.MemoryAllocatorFunc(
func(_, max uint64) experimental.LinearMemory {
return &windowsLinearMemory{max: max}
}))
}
// Reserve the address range once, committing zero-filled pages on demand. This
// avoids both copying the previous buffer and temporarily retaining two large
// Go allocations when memory.grow runs. The backing address never changes.
type windowsLinearMemory struct {
address uintptr
max uint64
size uint64
freed bool
}
func (m *windowsLinearMemory) Reallocate(size uint64) []byte {
if m.freed || size > m.max || m.max > uint64(^uint(0)>>1) {
return nil
}
if size == 0 {
return []byte{}
}
if m.address == 0 {
address, err := windows.VirtualAlloc(0, uintptr(m.max), windows.MEM_RESERVE, windows.PAGE_READWRITE)
if err != nil {
panic(fmt.Errorf("ffmpeg: reserve WASM memory: %w", err))
}
m.address = address
}
if size > m.size {
_, err := windows.VirtualAlloc(m.address, uintptr(size), windows.MEM_COMMIT, windows.PAGE_READWRITE)
if err != nil {
if m.size == 0 {
// Instantiation cannot proceed without the minimum memory. Wazero
// has not taken ownership yet, so release the reservation here.
m.Free()
panic(fmt.Errorf("ffmpeg: commit WASM memory: %w", err))
}
return nil // memory.grow reports failure to the guest.
}
m.size = size
}
return unsafe.Slice((*byte)(unsafe.Pointer(m.address)), int(size))
}
func (m *windowsLinearMemory) Free() {
if m.address != 0 {
_ = windows.VirtualFree(m.address, 0, windows.MEM_RELEASE)
m.address = 0
}
m.size = 0
m.freed = true
}
@@ -1,54 +0,0 @@
//go:build windows && (amd64 || arm64)
package ffmpeg
import (
"testing"
"unsafe"
"github.com/tetratelabs/wazero/experimental"
"golang.org/x/sys/windows"
)
func TestWindowsLinearMemoryGrowthAndRelease(t *testing.T) {
const page = 65536
m := &windowsLinearMemory{max: 4 * page}
defer m.Free()
first := m.Reallocate(page)
if len(first) != page {
t.Fatalf("initial allocation: %d", len(first))
}
first[0], first[page-1] = 17, 29
address := m.address
grown := m.Reallocate(3 * page)
if unsafe.SliceData(first) != unsafe.SliceData(grown) || grown[0] != 17 || grown[page-1] != 29 {
t.Fatal("growing memory moved or lost existing bytes")
}
for _, b := range grown[page:] {
if b != 0 {
t.Fatal("newly committed memory is not zero-filled")
}
}
if m.Reallocate(5*page) != nil || m.address != address || m.size != 3*page {
t.Fatal("failed growth changed the existing allocation")
}
m.Free()
m.Free()
if m.address != 0 || m.Reallocate(page) != nil {
t.Fatal("freed memory can still be used")
}
var info windows.MemoryBasicInformation
if err := windows.VirtualQuery(address, &info, unsafe.Sizeof(info)); err != nil {
t.Fatal(err)
}
const memFree = 0x10000 // MEMORY_BASIC_INFORMATION.State: MEM_FREE.
if info.State != memFree {
t.Fatalf("pages remain reserved after Free: state=%x", info.State)
}
}
func TestWindowsLinearMemoryModuleLifecycle(t *testing.T) {
testLinearMemoryModuleLifecycle(t, func(_, max uint64) experimental.LinearMemory {
return &windowsLinearMemory{max: max}
})
}
+1 -3
View File
@@ -151,9 +151,7 @@ func RunAcquired(ctx context.Context, video, audio Input, out io.Writer, format
files := &inputFS{files: map[string]Input{"video": video, "audio": audio}} files := &inputFS{files: map[string]Input{"video": video, "audio": audio}}
fsconfig := wazero.NewFSConfig().(sysfs.FSConfig).WithSysFSMount(files, "/inputs") fsconfig := wazero.NewFSConfig().(sysfs.FSConfig).WithSysFSMount(files, "/inputs")
logs := &tailLog{} logs := &tailLog{}
runCtx, freeMemory := withLinearMemory(ctx) rc, err := wasm.Run(ctx, shared.runtime, shared.module, wasm.Args{
defer freeMemory()
rc, err := wasm.Run(runCtx, shared.runtime, shared.module, wasm.Args{
Name: "ffmpeg", Args: args, Stdout: out, Stderr: logs, Name: "ffmpeg", Args: args, Stdout: out, Stderr: logs,
Config: func(c wazero.ModuleConfig) wazero.ModuleConfig { return c.WithFSConfig(fsconfig) }, Config: func(c wazero.ModuleConfig) wazero.ModuleConfig { return c.WithFSConfig(fsconfig) },
}) })
+26 -43
View File
@@ -75,23 +75,20 @@ type Task struct {
producer production.Source producer production.Source
producerReceived int64 producerReceived int64
fetcherManager fetcher.FetcherManager fetcherManager fetcher.FetcherManager
fetcher fetcher.Fetcher fetcher fetcher.Fetcher
timer *util.Timer timer *util.Timer
statusLock *sync.Mutex statusLock *sync.Mutex
lock *sync.Mutex lock *sync.Mutex
statsLock *sync.Mutex statsLock *sync.Mutex
statsLoaded bool statsLoaded bool
stats *fetcher.Stats stats *fetcher.Stats
statsSnapshot json.RawMessage statsSnapshot json.RawMessage
blobRefLock *sync.Mutex blobRefLock *sync.Mutex
blobURL string blobURL string
runGeneration uint64 runGeneration uint64
speedArr []speedSample speedArr []int64
uploadSpeedArr []speedSample uploadSpeedArr []int64
downloadSampledAt time.Time
uploadSampledAt time.Time
receivedBytes int64
} }
// TaskRuntimeStatus is the lightweight, frequently refreshed view of a task. // TaskRuntimeStatus is the lightweight, frequently refreshed view of a task.
@@ -198,21 +195,7 @@ func (t *Task) updateUploadSpeed(downloaded int64, usedTime float64) int64 {
return calcSpeed(&t.uploadSpeedArr, downloaded, usedTime) return calcSpeed(&t.uploadSpeedArr, downloaded, usedTime)
} }
type speedSample struct { func calcSpeed(speedArr *[]int64, downloaded int64, usedTime float64) int64 {
bytes int64
seconds float64
}
func sampleSeconds(lastSample *time.Time, now time.Time) float64 {
previous := *lastSample
*lastSample = now
if previous.IsZero() {
return 0
}
return now.Sub(previous).Seconds()
}
func calcSpeed(speedArr *[]speedSample, downloaded int64, usedTime float64) int64 {
if usedTime <= 0 { if usedTime <= 0 {
return 0 return 0
} }
@@ -221,18 +204,18 @@ func calcSpeed(speedArr *[]speedSample, downloaded int64, usedTime float64) int6
return 0 return 0
} }
*speedArr = append(*speedArr, speedSample{bytes: downloaded, seconds: usedTime}) *speedArr = append(*speedArr, downloaded)
var total int64 // Record last 5 seconds of download speed to calculate the average speed
var elapsed float64 if len(*speedArr) > int(5.0/usedTime) {
first := len(*speedArr) *speedArr = (*speedArr)[1:]
for first > 0 && elapsed < 5 {
first--
sample := (*speedArr)[first]
total += sample.bytes
elapsed += sample.seconds
} }
*speedArr = (*speedArr)[first:]
return int64(float64(total) / elapsed) var total int64
for _, v := range *speedArr {
total += v
}
return int64(float64(total) / float64(len(*speedArr)) / usedTime)
} }
type TaskFilter struct { type TaskFilter struct {
+2 -39
View File
@@ -1,32 +1,9 @@
package download package download
import ( import "testing"
"testing"
"time"
)
func TestTransferSpeedWithUnevenSamples(t *testing.T) {
var task Task
for _, sample := range []struct {
bytes int64
seconds float64
}{
{1000, 1},
{2000, 2},
{4000, 4},
{6000, 6},
} {
if got := task.updateSpeed(sample.bytes, sample.seconds); got != 1000 {
t.Errorf("download speed after %v seconds = %d, want 1000", sample.seconds, got)
}
if got := task.updateUploadSpeed(sample.bytes, sample.seconds); got != 1000 {
t.Errorf("upload speed after %v seconds = %d, want 1000", sample.seconds, got)
}
}
}
func TestCalcSpeedResetOnRollback(t *testing.T) { func TestCalcSpeedResetOnRollback(t *testing.T) {
speedArr := []speedSample{{1024, 1}, {2048, 1}, {4096, 1}} speedArr := []int64{1024, 2048, 4096}
if got := calcSpeed(&speedArr, -512, 1); got != 0 { if got := calcSpeed(&speedArr, -512, 1); got != 0 {
t.Fatalf("calcSpeed() = %d, want 0 after rollback", got) t.Fatalf("calcSpeed() = %d, want 0 after rollback", got)
@@ -39,17 +16,3 @@ func TestCalcSpeedResetOnRollback(t *testing.T) {
t.Fatalf("calcSpeed() = %d, want 1024 after reset", got) t.Fatalf("calcSpeed() = %d, want 1024 after reset", got)
} }
} }
func TestSampleSecondsUsesThePreviousSuccessfulSample(t *testing.T) {
start := time.Now()
var last time.Time
if got := sampleSeconds(&last, start); got != 0 {
t.Fatalf("first sample elapsed = %v, want 0", got)
}
if got := sampleSeconds(&last, start.Add(1500*time.Millisecond)); got != 1.5 {
t.Fatalf("elapsed after a delayed sample = %v, want 1.5", got)
}
if got := sampleSeconds(&last, start.Add(2*time.Second)); got != 0.5 {
t.Fatalf("elapsed after the next sample = %v, want 0.5", got)
}
}
@@ -53,7 +53,6 @@ class AppRuntimeState {
class AppRuntimeController extends AsyncNotifier<AppRuntimeState> { class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
static StartConfig? _defaultStartConfig; static StartConfig? _defaultStartConfig;
Future<void> _preferenceWrites = Future<void>.value(); Future<void> _preferenceWrites = Future<void>.value();
bool _analyticsStarted = false;
@override @override
Future<AppRuntimeState> build() async { Future<AppRuntimeState> build() async {
@@ -74,7 +73,6 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
startConfig startConfig
..apiEnable = true ..apiEnable = true
..mcpEnable = config.api.mcpEnable; ..mcpEnable = config.api.mcpEnable;
await _initAnalytics(config);
} else { } else {
startConfig startConfig
..apiEnable = config.api.enable ..apiEnable = config.api.enable
@@ -255,8 +253,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
} }
Future<void> _initAnalytics(DownloaderConfig config) async { Future<void> _initAnalytics(DownloaderConfig config) async {
if (_analyticsStarted || !AnalyticsConfig.isConfigured || !config.extra.analyticsEnabled) return; if (!AnalyticsConfig.isConfigured || !config.extra.analyticsEnabled) return;
_analyticsStarted = true;
try { try {
final generatedClientId = await Analytics.instance.init(fallbackClientId: config.extra.deviceId); final generatedClientId = await Analytics.instance.init(fallbackClientId: config.extra.deviceId);
if (generatedClientId != null) { if (generatedClientId != null) {
@@ -265,7 +262,6 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
} }
unawaited(Analytics.instance.logAppOpen()); unawaited(Analytics.instance.logAppOpen());
} catch (error, stackTrace) { } catch (error, stackTrace) {
_analyticsStarted = false;
logger.w('GA4 init failed', error, stackTrace); logger.w('GA4 init failed', error, stackTrace);
} }
} }
@@ -19,7 +19,7 @@ Future<DownloaderConfig> loadDownloaderConfig({
config = await load(); config = await load();
} catch (error, stackTrace) { } catch (error, stackTrace) {
onLoadError?.call(error, stackTrace); onLoadError?.call(error, stackTrace);
config = DownloaderConfig()..extra.analyticsEnabled = false; config = DownloaderConfig();
loaded = false; loaded = false;
} }
final changed = await initDownloaderConfig(config, defaultDownloadDir: defaultDownloadDir); final changed = await initDownloaderConfig(config, defaultDownloadDir: defaultDownloadDir);
@@ -1,9 +1,7 @@
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
import 'package:flutter/material.dart' show Colors; import 'package:flutter/material.dart' show Colors;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../../shared/theme/app_palette.dart';
import '../../shared/widgets/window/desktop_window_header.dart'; import '../../shared/widgets/window/desktop_window_header.dart';
import 'app_window_chrome.dart'; import 'app_window_chrome.dart';
@@ -18,15 +16,6 @@ class AppWindowFrame extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget framedChild = child; Widget framedChild = child;
// System insets apply to mobile devices at every responsive breakpoint.
// SafeArea also consumes the padding so page-level SafeAreas do not add it twice.
if (defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS) {
framedChild = ColoredBox(
color: AppPalette.of(context).bg,
child: SafeArea(child: framedChild),
);
}
if (AppWindowChrome.usesCustomChrome) { if (AppWindowChrome.usesCustomChrome) {
framedChild = Stack( framedChild = Stack(
children: [ children: [
@@ -0,0 +1,66 @@
import 'dart:math' as math;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../theme/app_design_tokens.dart';
/// Scrolls the header and virtualized tree together without freezing columns.
class FileTreeHorizontalViewport extends StatelessWidget {
const FileTreeHorizontalViewport({
super.key,
required this.keyPrefix,
required this.controller,
required this.contentWidth,
required this.child,
});
final String keyPrefix;
final ScrollController controller;
final double contentWidth;
final Widget child;
static double? widthOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_FileTreeHorizontalScope>()?.width;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = math.max(constraints.maxWidth, contentWidth);
final canScroll = width > constraints.maxWidth + 0.5;
return _FileTreeHorizontalScope(
width: constraints.maxWidth,
child: Scrollbar(
key: ValueKey('$keyPrefix-horizontal-scrollbar'),
controller: controller,
thumbVisibility: canScroll,
trackVisibility: canScroll,
interactive: true,
thickness: 6,
scrollbarOrientation: ScrollbarOrientation.bottom,
notificationPredicate: (notification) => notification.metrics.axis == Axis.horizontal,
child: Padding(
padding: EdgeInsets.only(bottom: canScroll ? AppDesignTokens.space12 : 0),
child: SingleChildScrollView(
key: ValueKey('$keyPrefix-horizontal-scroll'),
controller: controller,
scrollDirection: Axis.horizontal,
child: SizedBox(width: width, child: child),
),
),
),
);
},
);
}
}
class _FileTreeHorizontalScope extends InheritedWidget {
const _FileTreeHorizontalScope({required this.width, required super.child});
final double width;
@override
bool updateShouldNotify(_FileTreeHorizontalScope oldWidget) => width != oldWidget.width;
}
@@ -0,0 +1,69 @@
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'app_tooltip.dart';
import 'file_tree_horizontal_viewport.dart';
/// Keeps the full name scrollable, with an ellipsis at its visible trailing edge.
class FileTreeName extends StatelessWidget {
const FileTreeName({
super.key,
required this.name,
required this.style,
required this.controller,
required this.contentWidth,
this.treeContentOffset,
});
final String name;
final TextStyle style;
final ScrollController controller;
final double contentWidth;
/// The name's leading position on the canvas when the whole tree scrolls.
/// Null means that only the name scrolls, with the trailing columns frozen.
final double? treeContentOffset;
@override
Widget build(BuildContext context) {
final treeViewportWidth = FileTreeHorizontalViewport.widthOf(context);
final direction = Directionality.of(context);
return AppTooltip(
message: name,
child: LayoutBuilder(
builder: (context, constraints) => AnimatedBuilder(
animation: controller,
builder: (context, child) {
final offset = controller.hasClients ? controller.offset : 0.0;
final available = treeContentOffset == null
? constraints.maxWidth + offset
: (treeViewportWidth ?? constraints.maxWidth) + offset - treeContentOffset!;
final width = math.max(
0.0,
math.min(treeContentOffset == null ? contentWidth : constraints.maxWidth, available),
);
final text = SizedBox(
width: width,
child: Text(name, style: style, maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis),
);
if (treeContentOffset != null) {
return Align(alignment: AlignmentDirectional.centerStart, child: text);
}
return ClipRect(
child: OverflowBox(
alignment: AlignmentDirectional.centerStart,
minWidth: 0,
maxWidth: double.infinity,
child: Transform.translate(
offset: Offset(direction == TextDirection.rtl ? offset : -offset, 0),
child: text,
),
),
);
},
),
),
);
}
}
+100 -76
View File
@@ -4,9 +4,12 @@ import 'package:flutter/gestures.dart' show Drag;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Column, Expanded, Row; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Column, Expanded, Row;
import '../../core/utils/breakpoints.dart';
import '../theme/app_palette.dart'; import '../theme/app_palette.dart';
import '../../l10n/l10n.dart'; import '../../l10n/l10n.dart';
import '../../util/util.dart'; import '../../util/util.dart';
import 'file_tree_horizontal_viewport.dart';
import 'file_tree_name.dart';
import 'virtual_tree_view.dart'; import 'virtual_tree_view.dart';
class FileTreeItem<T> { class FileTreeItem<T> {
@@ -110,6 +113,7 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
late final Animation<double> _reveal; late final Animation<double> _reveal;
final _scrollController = ScrollController(); final _scrollController = ScrollController();
final _horizontalScrollController = ScrollController(); final _horizontalScrollController = ScrollController();
final _wholeTreeScrollController = ScrollController();
Drag? _horizontalDrag; Drag? _horizontalDrag;
final Map<String, double> _labelWidthCache = {}; final Map<String, double> _labelWidthCache = {};
TextStyle? _labelMeasurementStyle; TextStyle? _labelMeasurementStyle;
@@ -163,6 +167,7 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
_transitionController.dispose(); _transitionController.dispose();
_scrollController.dispose(); _scrollController.dispose();
_horizontalScrollController.dispose(); _horizontalScrollController.dispose();
_wholeTreeScrollController.dispose();
super.dispose(); super.dispose();
} }
@@ -196,6 +201,8 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final treeWidth = constraints.hasBoundedWidth ? constraints.maxWidth : _treeFallbackWidth; final treeWidth = constraints.hasBoundedWidth ? constraints.maxWidth : _treeFallbackWidth;
// Folder expansion must not switch between frozen and scrolling columns.
final scrollWholeTree = treeWidth < Breakpoints.mobile;
final horizontalOverflow = _horizontalOverflow( final horizontalOverflow = _horizontalOverflow(
_nodes, _nodes,
baseNameViewport: baseNameViewport:
@@ -204,57 +211,70 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
measureLabel: measureLabel, measureLabel: measureLabel,
); );
return OutlinedContainer( final content = Column(
child: Column( children: [
children: [ _FileTreeToolbar(
_FileTreeToolbar( keyPrefix: widget.keyPrefix,
keyPrefix: widget.keyPrefix, headerLeading: widget.headerLeading,
headerLeading: widget.headerLeading, allExpanded: _allFoldersExpanded,
allExpanded: _allFoldersExpanded, nameSortDirection: _sortColumn == _FileTreeSortColumn.name ? _sortDirection : _FileTreeSortDirection.none,
nameSortDirection: _sortColumn == _FileTreeSortColumn.name sizeSortDirection: _sortColumn == _FileTreeSortColumn.size ? _sortDirection : _FileTreeSortDirection.none,
? _sortDirection showSizeColumn: widget.showSizeColumn,
: _FileTreeSortDirection.none, trailingHeader: widget.trailingHeader,
sizeSortDirection: _sortColumn == _FileTreeSortColumn.size trailingColumnWidth: trailingColumnWidth,
? _sortDirection onNameSort: () => _toggleSort(_FileTreeSortColumn.name),
: _FileTreeSortDirection.none, onSizeSort: () => _toggleSort(_FileTreeSortColumn.size),
showSizeColumn: widget.showSizeColumn, onToggleExpanded: _allFoldersExpanded ? _collapseAll : _expandAll,
trailingHeader: widget.trailingHeader, ),
trailingColumnWidth: trailingColumnWidth, Expanded(
onNameSort: () => _toggleSort(_FileTreeSortColumn.name), child: _nodes.isEmpty
onSizeSort: () => _toggleSort(_FileTreeSortColumn.size), ? _FileTreeEmptyState(label: widget.emptyLabel ?? context.l10n.noFiles)
onToggleExpanded: _allFoldersExpanded ? _collapseAll : _expandAll, : VirtualTreeView<FileTreeNode<T>>(
), nodes: _nodes,
Expanded( controller: _scrollController,
child: _nodes.isEmpty branchLine: BranchLine.path,
? _FileTreeEmptyState(label: widget.emptyLabel ?? context.l10n.noFiles) rowHeight: widget.rowHeight,
: VirtualTreeView<FileTreeNode<T>>( padding: const EdgeInsets.fromLTRB(_treeContentOffset, 4, 8, 4),
nodes: _nodes, itemPadding: EdgeInsets.symmetric(vertical: treeItemInset * 0.5),
controller: _scrollController, itemBuilder: (context, entry) => _buildTreeItem(
branchLine: BranchLine.path, context,
rowHeight: widget.rowHeight, entry,
padding: const EdgeInsets.fromLTRB(_treeContentOffset, 4, 8, 4), nameContentWidth: nameContentWidth,
itemPadding: EdgeInsets.symmetric(vertical: treeItemInset * 0.5), trailingColumnWidth: trailingColumnWidth,
itemBuilder: (context, entry) => _buildTreeItem( hasTrailingColumn: hasTrailingColumn,
context, horizontalScrollEnabled: horizontalOverflow > 0.5,
entry, scrollWholeTree: scrollWholeTree,
nameContentWidth: nameContentWidth, nameContentOffset:
trailingColumnWidth: trailingColumnWidth, _treeContentOffset + (entry.depth.length - 1) * treeItemInset * 3 + fixedNameChrome,
hasTrailingColumn: hasTrailingColumn, contentTextStyle: labelStyle,
horizontalScrollEnabled: horizontalOverflow > 0.5,
contentTextStyle: labelStyle,
),
transitionBuilder: _buildTreeTransition,
), ),
transitionBuilder: _buildTreeTransition,
),
),
if (!scrollWholeTree && _nodes.isNotEmpty && horizontalOverflow > 0.5)
_FileNameHorizontalScrollbar(
keyPrefix: widget.keyPrefix,
controller: _horizontalScrollController,
overflow: horizontalOverflow,
trailingColumnWidth: trailingColumnWidth,
), ),
if (_nodes.isNotEmpty && horizontalOverflow > 0.5) ],
_FileNameHorizontalScrollbar( );
return OutlinedContainer(
child: scrollWholeTree
? FileTreeHorizontalViewport(
keyPrefix: widget.keyPrefix, keyPrefix: widget.keyPrefix,
controller: _horizontalScrollController, controller: _wholeTreeScrollController,
overflow: horizontalOverflow, contentWidth:
trailingColumnWidth: trailingColumnWidth, _treeHorizontalPadding +
), fixedNameChrome +
], _treeTrailingGap +
), trailingColumnWidth +
_widestIndentedLabelWidth(_nodes, measureLabel, treeItemInset * 3),
child: content,
)
: content,
); );
}, },
); );
@@ -267,6 +287,8 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
required double trailingColumnWidth, required double trailingColumnWidth,
required bool hasTrailingColumn, required bool hasTrailingColumn,
required bool horizontalScrollEnabled, required bool horizontalScrollEnabled,
required bool scrollWholeTree,
required double nameContentOffset,
required TextStyle contentTextStyle, required TextStyle contentTextStyle,
}) { }) {
final palette = AppPalette.of(context); final palette = AppPalette.of(context);
@@ -315,36 +337,22 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
child: GestureDetector( child: GestureDetector(
key: ValueKey('${widget.keyPrefix}-name-scroll-${data.key}'), key: ValueKey('${widget.keyPrefix}-name-scroll-${data.key}'),
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onHorizontalDragStart: horizontalScrollEnabled ? _startNameDrag : null, onHorizontalDragStart: !scrollWholeTree && horizontalScrollEnabled ? _startNameDrag : null,
onHorizontalDragUpdate: horizontalScrollEnabled onHorizontalDragUpdate: !scrollWholeTree && horizontalScrollEnabled
? (details) => _horizontalDrag?.update(details) ? (details) => _horizontalDrag?.update(details)
: null, : null,
onHorizontalDragEnd: horizontalScrollEnabled ? (details) => _horizontalDrag?.end(details) : null, onHorizontalDragEnd: !scrollWholeTree && horizontalScrollEnabled
onHorizontalDragCancel: horizontalScrollEnabled ? () => _horizontalDrag?.cancel() : null, ? (details) => _horizontalDrag?.end(details)
child: ClipRect( : null,
child: AnimatedBuilder( onHorizontalDragCancel: !scrollWholeTree && horizontalScrollEnabled
animation: _horizontalScrollController, ? () => _horizontalDrag?.cancel()
builder: (context, child) { : null,
final offset = horizontalScrollEnabled && _horizontalScrollController.hasClients child: FileTreeName(
? _horizontalScrollController.offset name: data.label,
: 0.0; style: contentTextStyle.copyWith(color: palette.textPrimary),
return Transform.translate(offset: Offset(-offset, 0), child: child); controller: scrollWholeTree ? _wholeTreeScrollController : _horizontalScrollController,
}, contentWidth: nameContentWidth,
child: OverflowBox( treeContentOffset: scrollWholeTree ? nameContentOffset : null,
alignment: Alignment.centerLeft,
minWidth: 0,
maxWidth: double.infinity,
child: SizedBox(
width: nameContentWidth,
child: Text(
data.label,
maxLines: 1,
softWrap: false,
style: contentTextStyle.copyWith(color: palette.textPrimary),
),
),
),
),
), ),
), ),
), ),
@@ -1076,6 +1084,22 @@ double _widestLabelWidth<T>(List<TreeNode<FileTreeNode<T>>> nodes, double Functi
return widest; return widest;
} }
double _widestIndentedLabelWidth<T>(
List<TreeNode<FileTreeNode<T>>> nodes,
double Function(String label) measureLabel,
double depthIndent, [
int depth = 0,
]) {
var widest = _treeMinimumNameContentWidth;
for (final node in nodes) {
final item = node as TreeItemNode<FileTreeNode<T>>;
widest = math.max(widest, depth * depthIndent + measureLabel(item.data.label));
// Include collapsed descendants so expanding folders keeps the canvas stable.
widest = math.max(widest, _widestIndentedLabelWidth(item.children, measureLabel, depthIndent, depth + 1));
}
return widest;
}
double _measureLabelWidth( double _measureLabelWidth(
String label, { String label, {
required TextStyle style, required TextStyle style,
@@ -32,22 +32,8 @@ void main() {
// The in-memory fallback still gets usable defaults. // The in-memory fallback still gets usable defaults.
expect(config.downloadDir, '/downloads'); expect(config.downloadDir, '/downloads');
expect(config.categories, hasLength(4)); expect(config.categories, hasLength(4));
expect(config.extra.analyticsEnabled, isFalse);
}); });
for (final analyticsEnabled in [false, true]) {
test('preserves the saved analytics preference ($analyticsEnabled)', () async {
final stored = configWith()..extra.analyticsEnabled = analyticsEnabled;
final config = await loadDownloaderConfig(
load: () async => stored,
save: (_) async {},
defaultDownloadDir: () async => '/downloads',
);
expect(config.extra.analyticsEnabled, analyticsEnabled);
});
}
test('skips persist when init changed nothing', () async { test('skips persist when init changed nothing', () async {
var saved = 0; var saved = 0;
await loadDownloaderConfig( await loadDownloaderConfig(
@@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride;
import 'package:flutter/gestures.dart' show kSecondaryMouseButton; import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
import 'package:flutter/material.dart' show Icons; import 'package:flutter/material.dart' show Icons;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@@ -9,9 +8,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:gopeed/api/model/task.dart' as api; import 'package:gopeed/api/model/task.dart' as api;
import 'package:gopeed/features/home/presentation/pages/home_page.dart'; import 'package:gopeed/features/home/presentation/pages/home_page.dart';
import 'package:gopeed/features/home/presentation/widgets/primary_rail.dart';
import 'package:gopeed/features/home/presentation/widgets/tasks_top_bar.dart';
import 'package:gopeed/core/window/app_window_frame.dart';
import 'package:gopeed/features/tasks/application/tasks_controller.dart'; import 'package:gopeed/features/tasks/application/tasks_controller.dart';
import 'package:gopeed/features/tasks/domain/task_record.dart'; import 'package:gopeed/features/tasks/domain/task_record.dart';
import 'package:gopeed/features/tasks/presentation/pages/task_details_page.dart'; import 'package:gopeed/features/tasks/presentation/pages/task_details_page.dart';
@@ -166,7 +162,6 @@ Future<({GroupTasks controller, GoRouter router})> showHome(WidgetTester tester,
child: shad.ShadcnApp.router( child: shad.ShadcnApp.router(
theme: AppTheme.light(), theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(), materialTheme: AppTheme.materialLight(),
builder: (context, child) => AppWindowFrame(child: child!),
routerConfig: router, routerConfig: router,
), ),
), ),
@@ -176,50 +171,6 @@ Future<({GroupTasks controller, GoRouter router})> showHome(WidgetTester tester,
} }
void main() { void main() {
for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) {
testWidgets('$platform keeps headers inside system insets across rotation', (tester) async {
debugDefaultTargetPlatformOverride = platform;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
addTearDown(tester.view.resetPadding);
addTearDown(tester.view.resetViewPadding);
await showHome(tester, 390);
for (final size in [const Size(390, 844), const Size(700, 1100), const Size(1100, 700), const Size(700, 1100)]) {
tester.view.physicalSize = size;
tester.view.padding = FakeViewPadding.zero;
tester.view.viewPadding = FakeViewPadding.zero;
await settle(tester);
final header = size.width < 720
? find
.ancestor(
of: find.byKey(const ValueKey('tasks-mobile-create-button')),
matching: find.byType(SafeArea),
)
.first
: find.byType(TasksTopBar);
final originalHeaderPosition = tester.getTopLeft(header);
const insets = FakeViewPadding(top: 24, left: 16, right: 12, bottom: 20);
tester.view.padding = insets;
tester.view.viewPadding = insets;
await settle(tester);
expect(tester.getRect(find.byType(HomePage)), Rect.fromLTRB(16, 24, size.width - 12, size.height - 20));
// Nested page SafeAreas must consume the inset only once.
expect(tester.getTopLeft(header), originalHeaderPosition + const Offset(16, 24));
expect(MediaQuery.paddingOf(tester.element(header)), EdgeInsets.zero);
if (size.width >= 720) {
expect(tester.getTopLeft(find.byType(PrimaryRail)), const Offset(16, 24));
} else {
await tester.tap(find.byKey(const ValueKey('tasks-mobile-batch-button')));
await settle(tester);
}
expect(tester.takeException(), isNull);
}
debugDefaultTargetPlatformOverride = null;
});
}
for (final width in [390.0, 700.0, 1100.0]) { for (final width in [390.0, 700.0, 1100.0]) {
testWidgets('group expands actual member tasks and keeps a multi-file BT as one card at $width', (tester) async { testWidgets('group expands actual member tasks and keeps a multi-file BT as one card at $width', (tester) async {
final app = await showHome(tester, width); final app = await showHome(tester, width);
@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/api/model/create_task_batch.dart'; import 'package:gopeed/api/model/create_task_batch.dart';
import 'package:gopeed/api/model/options.dart'; import 'package:gopeed/api/model/options.dart';
@@ -16,6 +17,7 @@ import 'package:gopeed/features/tasks/presentation/widgets/resolve_file_tree.dar
import 'package:gopeed/shared/theme/app_theme.dart'; import 'package:gopeed/shared/theme/app_theme.dart';
import 'package:gopeed/shared/widgets/app_loading_button.dart'; import 'package:gopeed/shared/widgets/app_loading_button.dart';
import 'package:gopeed/shared/widgets/app_tooltip.dart'; import 'package:gopeed/shared/widgets/app_tooltip.dart';
import 'package:gopeed/shared/widgets/file_tree_horizontal_viewport.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad; import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
PendingDownload pending(String id) => PendingDownload( PendingDownload pending(String id) => PendingDownload(
@@ -76,6 +78,50 @@ void main() {
}); });
} }
testWidgets('long pending links scroll with the tree before and after resolving on narrow screens', (tester) async {
final response = Completer<ResolveResult>();
final url = 'https://pan.quark.cn/s/${List.filled(8, 'long-resource-link').join('-')}';
final source = PendingDownload(
id: 'long',
request: Request(url: url),
options: Options(),
);
final registry = CapabilityRegistry(createAppCapabilityCodecs())
..bind(GopeedMethods.resolve, (_) => response.future);
await open(tester, [source], registry, width: 320);
expect(find.byType(FileTreeHorizontalViewport), findsOneWidget);
final paragraph = tester.renderObject<RenderParagraph>(find.text(url));
expect(paragraph.didExceedMaxLines, isTrue);
expect(paragraph.overflow, TextOverflow.ellipsis);
expect(paragraph.text.toPlainText(), url);
expect(find.byWidgetPredicate((widget) => widget is AppTooltip && widget.message == url), findsOneWidget);
final label = find.byKey(const ValueKey('resolve-tree-name-scroll-folder:long'));
final before = tester.getTopLeft(find.text(url));
final expand = find.byKey(const ValueKey('resolve-tree-node-expand-folder:long'));
final expandBefore = tester.getTopLeft(expand);
await tester.dragFrom(tester.getTopLeft(label) + const Offset(20, 8), const Offset(-100, 0));
// The pending indicator animates continuously, so do not wait for settling.
await tester.pump(const Duration(seconds: 1));
final delta = tester.getTopLeft(find.text(url)).dx - before.dx;
expect(delta, lessThan(0));
expect(tester.getTopLeft(expand).dx - expandBefore.dx, closeTo(delta, 0.01));
final horizontal = tester.widget<SingleChildScrollView>(
find.byKey(const ValueKey('resolve-tree-horizontal-scroll')),
);
horizontal.controller!.jumpTo(horizontal.controller!.position.maxScrollExtent);
await tester.pump();
expect(
paragraph.getBoxesForSelection(TextSelection(baseOffset: url.length - 4, extentOffset: url.length)),
isNotEmpty,
);
response.complete(resolved('resolved-file.txt'));
await tester.pumpAndSettle();
expect(find.text('resolved-file.txt'), findsOneWidget);
expect(find.byType(FileTreeHorizontalViewport), findsOneWidget);
expect(source.selected, {0});
expect(tester.takeException(), isNull);
});
testWidgets('shows all roots immediately and resolves sequentially while preserving choices', (tester) async { testWidgets('shows all roots immediately and resolves sequentially while preserving choices', (tester) async {
final calls = <String>[]; final calls = <String>[];
final responses = [Completer<ResolveResult>(), Completer<ResolveResult>()]; final responses = [Completer<ResolveResult>(), Completer<ResolveResult>()];
@@ -1,7 +1,10 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/core/utils/breakpoints.dart';
import 'package:gopeed/shared/theme/app_theme.dart'; import 'package:gopeed/shared/theme/app_theme.dart';
import 'package:gopeed/shared/widgets/app_tooltip.dart';
import 'package:gopeed/shared/widgets/file_tree_horizontal_viewport.dart';
import 'package:gopeed/shared/widgets/file_tree_view.dart'; import 'package:gopeed/shared/widgets/file_tree_view.dart';
import 'package:gopeed/shared/widgets/virtual_tree_view.dart'; import 'package:gopeed/shared/widgets/virtual_tree_view.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad; import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
@@ -41,7 +44,7 @@ void main() {
expect(labels(), ['z.txt', 'folder', 'a.txt']); expect(labels(), ['z.txt', 'folder', 'a.txt']);
}); });
for (final width in [390.0, 700.0, 1024.0]) { for (final width in [320.0, 390.0, 700.0, 1024.0]) {
testWidgets('long directory display labels remain readable at $width', (tester) async { testWidgets('long directory display labels remain readable at $width', (tester) async {
tester.view.physicalSize = Size(width, 800); tester.view.physicalSize = Size(width, 800);
tester.view.devicePixelRatio = 1; tester.view.devicePixelRatio = 1;
@@ -62,20 +65,195 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
for (final label in [name, nestedName]) { for (final label in [name, nestedName]) {
final paragraph = tester.renderObject<RenderParagraph>(find.text(label)); final paragraph = tester.renderObject<RenderParagraph>(find.text(label));
expect(paragraph.size.width, greaterThanOrEqualTo(paragraph.getMaxIntrinsicWidth(double.infinity))); expect(paragraph.text.toPlainText(), label);
expect(paragraph.overflow, TextOverflow.ellipsis);
expect(find.byWidgetPredicate((widget) => widget is AppTooltip && widget.message == label), findsOneWidget);
} }
expect(find.byKey(const ValueKey('file-tree-horizontal-scrollbar')), findsOneWidget); expect(find.byKey(const ValueKey('file-tree-horizontal-scrollbar')), findsOneWidget);
final root = find.byKey(const ValueKey('file-tree-name-scroll-folder:source-0')); final root = find.byKey(const ValueKey('file-tree-name-scroll-folder:source-0'));
final before = tester.getTopLeft(find.text(name)); final before = tester.getTopLeft(find.text(name));
final trailing = tester.getRect(find.byKey(const ValueKey('file-tree-trailing-folder:source-0'))); final trailing = tester.getRect(find.byKey(const ValueKey('file-tree-trailing-folder:source-0')));
await tester.drag(root, const Offset(-120, 0)); final expand = find.byKey(const ValueKey('file-tree-node-expand-folder:source-0'));
final expandBefore = tester.getTopLeft(expand);
final header = find.byKey(const ValueKey('file-tree-sort-size-asc'));
final headerBefore = tester.getTopLeft(header);
await tester.dragFrom(tester.getTopLeft(root) + const Offset(40, 10), const Offset(-120, 0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text(name)).dx, lessThan(before.dx)); final delta = tester.getTopLeft(find.text(name)).dx - before.dx;
expect(tester.getRect(find.byKey(const ValueKey('file-tree-trailing-folder:source-0'))), trailing); expect(delta, lessThan(0));
final trailingAfter = tester.getRect(find.byKey(const ValueKey('file-tree-trailing-folder:source-0')));
if (width < Breakpoints.mobile) {
expect(trailingAfter.left - trailing.left, closeTo(delta, 0.01));
expect(tester.getTopLeft(expand).dx - expandBefore.dx, closeTo(delta, 0.01));
expect(tester.getTopLeft(header).dx - headerBefore.dx, closeTo(delta, 0.01));
} else {
expect(trailingAfter, trailing);
expect(tester.getTopLeft(expand), expandBefore);
expect(tester.getTopLeft(header), headerBefore);
}
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
} }
testWidgets('deep trees scroll all controls and keep their canvas stable when folders toggle', (tester) async {
tester.view.physicalSize = const Size(1024, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
const name = '课程视频与配套练习_最终修订版.mp4';
await tester.pumpWidget(
shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: Center(
child: SizedBox(
width: 320,
height: 500,
child: FileTreeView<int>(
items: const [FileTreeItem(key: 'deep', path: 'a/b/c/d/e/f', name: name, size: 1024, data: 0)],
leadingBuilder: (_, _) => const SizedBox(width: 16, height: 16),
),
),
),
),
);
await tester.pumpAndSettle();
// The list's width, rather than the desktop window's width, chooses the mode.
expect(find.byType(FileTreeHorizontalViewport), findsOneWidget);
final horizontal = tester.widget<SingleChildScrollView>(find.byKey(const ValueKey('file-tree-horizontal-scroll')));
final controller = horizontal.controller!;
final extent = controller.position.maxScrollExtent;
expect(extent, greaterThan(0));
final paragraph = tester.renderObject<RenderParagraph>(find.text(name));
expect(paragraph.overflow, TextOverflow.ellipsis);
expect(paragraph.text.toPlainText(), name);
final root = find.byKey(const ValueKey('file-tree-node-expand-folder:a'));
final trailing = find.byKey(const ValueKey('file-tree-trailing-file:deep'));
final before = tester.getTopLeft(trailing);
final label = find.byKey(const ValueKey('file-tree-name-scroll-folder:a'));
await tester.dragFrom(tester.getTopLeft(label) + const Offset(20, 8), const Offset(-100, 0));
await tester.pumpAndSettle();
expect(tester.getTopLeft(trailing).dx, lessThan(before.dx));
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
final viewport = tester.getRect(find.byType(FileTreeHorizontalViewport));
expect(tester.getRect(trailing).right, lessThanOrEqualTo(viewport.right));
expectNameTailVisible(tester, name, viewport);
controller.jumpTo(0);
await tester.pump();
await tester.tap(root);
await tester.pumpAndSettle();
expect(find.text(name), findsNothing);
expect(find.byType(FileTreeHorizontalViewport), findsOneWidget);
expect(controller.position.maxScrollExtent, extent);
await tester.tap(root);
await tester.pumpAndSettle();
expect(find.text(name), findsOneWidget);
expect(controller.position.maxScrollExtent, extent);
expect(tester.takeException(), isNull);
});
for (final width in [320.0, 1024.0]) {
testWidgets('long names show ellipsis, reveal their tail on scroll and expose full text at $width', (tester) async {
tester.view.physicalSize = Size(width, 600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final name = '${List.filled(8, '课程视频与完整配套资料').join('_')}.mp4';
var presses = 0;
await tester.pumpWidget(
shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: FileTreeView<int>(
items: [FileTreeItem(key: 'long', path: '', name: name, size: 1024, data: 0)],
onNodePressed: (_) => presses++,
),
),
);
await tester.pumpAndSettle();
final label = find.text(name);
final paragraph = tester.renderObject<RenderParagraph>(label);
expect(paragraph.didExceedMaxLines, isTrue);
expect(paragraph.overflow, TextOverflow.ellipsis);
expect(paragraph.text.toPlainText(), name);
expect(
paragraph.getBoxesForSelection(TextSelection(baseOffset: name.length - 4, extentOffset: name.length)),
isEmpty,
);
final horizontal = tester.widget<SingleChildScrollView>(
find.byKey(const ValueKey('file-tree-horizontal-scroll')),
);
horizontal.controller!.jumpTo(horizontal.controller!.position.maxScrollExtent);
await tester.pumpAndSettle();
expectNameTailVisible(tester, name, Rect.fromLTWH(0, 0, width, 600));
horizontal.controller!.jumpTo(0);
await tester.pump();
await tester.longPressAt(tester.getTopLeft(label) + const Offset(20, 8));
await tester.pumpAndSettle();
expect(find.text(name), findsNWidgets(2));
expect(presses, 0);
expect(tester.takeException(), isNull);
});
}
testWidgets('resizing changes scrolling mode without losing folder state or tap handling', (tester) async {
tester.view.physicalSize = const Size(1024, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
late StateSetter update;
var width = 390.0;
final pressed = <String>[];
await tester.pumpWidget(
shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: StatefulBuilder(
builder: (context, setState) {
update = setState;
return Center(
child: SizedBox(
width: width,
height: 500,
child: FileTreeView<int>(
items: const [FileTreeItem(key: 'file', path: 'bundle', name: 'file.txt', size: 10, data: 0)],
directoryLabels: const {
'bundle': 'Long resource title that needs horizontal scrolling in both desktop and mobile views',
},
onNodePressed: (node) => pressed.add(node.key),
),
),
);
},
),
),
);
await tester.pumpAndSettle();
expect(find.byType(FileTreeHorizontalViewport), findsOneWidget);
await tester.tapAt(tester.getTopLeft(find.text('file.txt')) + const Offset(20, 8));
await tester.pump();
expect(pressed, ['file:file']);
final expand = find.byKey(const ValueKey('file-tree-node-expand-folder:bundle'));
await tester.tap(expand);
await tester.pumpAndSettle();
update(() => width = 900);
await tester.pumpAndSettle();
expect(find.byType(FileTreeHorizontalViewport), findsNothing);
expect(find.text('file.txt'), findsNothing);
update(() => width = 390);
await tester.pumpAndSettle();
expect(find.byType(FileTreeHorizontalViewport), findsOneWidget);
expect(find.text('file.txt'), findsNothing);
await tester.tap(expand);
await tester.pumpAndSettle();
await tester.tapAt(tester.getTopLeft(find.text('file.txt')) + const Offset(20, 8));
await tester.pump();
expect(pressed, ['file:file', 'file:file']);
expect(tester.takeException(), isNull);
});
testWidgets('empty directory root keeps its identity and expansion when files arrive', (tester) async { testWidgets('empty directory root keeps its identity and expansion when files arrive', (tester) async {
late StateSetter update; late StateSetter update;
var items = [ var items = [
@@ -159,3 +337,13 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
} }
void expectNameTailVisible(WidgetTester tester, String name, Rect viewport) {
final paragraph = tester.renderObject<RenderParagraph>(find.text(name));
final boxes = paragraph.getBoxesForSelection(TextSelection(baseOffset: name.length - 4, extentOffset: name.length));
expect(boxes, isNotEmpty);
for (final box in boxes) {
expect(paragraph.localToGlobal(Offset(box.left, 0)).dx, greaterThanOrEqualTo(viewport.left));
expect(paragraph.localToGlobal(Offset(box.right, 0)).dx, lessThanOrEqualTo(viewport.right));
}
}
-2
View File
@@ -7,7 +7,6 @@ project(runner LANGUAGES CXX)
# #
# Any new source files that you add to the application should be added here. # Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32 add_executable(${BINARY_NAME} WIN32
"crash_handler.cpp"
"flutter_window.cpp" "flutter_window.cpp"
"main.cpp" "main.cpp"
"utils.cpp" "utils.cpp"
@@ -35,7 +34,6 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# dependencies here. # dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_link_libraries(${BINARY_NAME} PRIVATE "dbghelp.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed. # Run the Flutter tool portions of the build. This must not be removed.
@@ -1,31 +0,0 @@
# Windows native crash diagnostics
The runner installs diagnostics before starting Flutter and plugins. Files are
written beside the executable under `storage/logs`, matching Windows app storage:
- `native-crash.log`: UTC timestamp, app version, process/thread, exception code,
faulting module/address/offset, and minidump success or the Windows error code.
- `native-crash.dmp`: the most recent native crash, including thread stacks,
loaded modules and unloaded module information.
- `crash.log`: Go runtime crash output, kept separately from native diagnostics.
Logs are appended and existing dumps survive normal restarts. A subsequent native
crash replaces the previous dump. Dump generation is best effort, particularly
for stack exhaustion, heap corruption or unavailable disk space. Process kills
and fail-fast termination can bypass exception handlers.
The top-level unhandled filter covers native exceptions on process threads. An
SEH boundary inside the runner's window callback records the original escaping exception
before Windows wraps a window callback failure as `0xC000041D`. A vectored handler
also observes that fatal callback status, since it can bypass the unhandled
filter. All handlers continue exception processing; they do not attempt recovery
or log ordinary handled first-chance exceptions.
On Windows, verify a release build by exercising tray clicks, Explorer restart,
system font changes and child-window open/close. In a disposable test build,
raise an unhandled access violation inside a window callback and on a worker
thread, then raise `0xC000041D`; check that the log and dump are written and
survive relaunch. A debugger may intercept these before application handlers.
Open the dump with WinDbg and matching runner/Flutter/plugin PDBs to locate the
faulting stack. Minidumps can contain application data; inspect them before
sharing.
-119
View File
@@ -1,119 +0,0 @@
#include "crash_handler.h"
#include <dbghelp.h>
#include <strsafe.h>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <string>
#include <system_error>
namespace {
HANDLE crash_log = INVALID_HANDLE_VALUE;
std::wstring dump_path;
volatile LONG recording = 0;
LPTOP_LEVEL_EXCEPTION_FILTER previous_filter = nullptr;
void WriteLog(const char* text) {
if (crash_log == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteFile(crash_log, text, static_cast<DWORD>(strlen(text)), &written,
nullptr);
FlushFileBuffers(crash_log);
}
LONG WINAPI UnhandledException(EXCEPTION_POINTERS* exception) {
RecordNativeCrash(exception);
return previous_filter ? previous_filter(exception)
: EXCEPTION_CONTINUE_SEARCH;
}
LONG CALLBACK FatalCallbackException(EXCEPTION_POINTERS* exception) {
// Windows can terminate on this callback exception without invoking the
// process's unhandled filter. Ignore ordinary first-chance exceptions (Go,
// Dart and plugins may handle those) and observe only this fatal status.
if (exception->ExceptionRecord->ExceptionCode == 0xC000041D) {
RecordNativeCrash(exception);
}
return EXCEPTION_CONTINUE_SEARCH;
}
} // namespace
LONG RecordNativeCrash(EXCEPTION_POINTERS* exception) {
if (!exception || InterlockedCompareExchange(&recording, 1, 0) != 0) {
return EXCEPTION_CONTINUE_SEARCH;
}
SYSTEMTIME time;
GetSystemTime(&time);
const auto* record = exception->ExceptionRecord;
HMODULE module = nullptr;
wchar_t module_path[MAX_PATH] = L"unknown";
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(record->ExceptionAddress),
&module);
if (module) GetModuleFileNameW(module, module_path, MAX_PATH);
char line[2048] = {};
StringCchPrintfA(
line, ARRAYSIZE(line),
"\r\n%04u-%02u-%02uT%02u:%02u:%02uZ Gopeed %s pid=%lu tid=%lu "
"exception=0x%08lX address=%p module=%ls offset=0x%llX\r\n",
time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute,
time.wSecond, FLUTTER_VERSION, GetCurrentProcessId(), GetCurrentThreadId(),
record->ExceptionCode, record->ExceptionAddress, module_path,
static_cast<unsigned long long>(
reinterpret_cast<uintptr_t>(record->ExceptionAddress) -
reinterpret_cast<uintptr_t>(module)));
WriteLog(line);
HANDLE dump = CreateFileW(dump_path.c_str(), GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (dump != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION info = {};
info.ThreadId = GetCurrentThreadId();
info.ExceptionPointers = exception;
info.ClientPointers = FALSE;
const BOOL ok = MiniDumpWriteDump(
GetCurrentProcess(), GetCurrentProcessId(), dump,
static_cast<MINIDUMP_TYPE>(MiniDumpNormal | MiniDumpWithThreadInfo |
MiniDumpWithUnloadedModules),
&info, nullptr, nullptr);
const DWORD error = ok ? ERROR_SUCCESS : GetLastError();
FlushFileBuffers(dump);
CloseHandle(dump);
StringCchPrintfA(line, ARRAYSIZE(line), "minidump success=%d error=%lu\r\n",
ok, error);
WriteLog(line);
} else {
StringCchPrintfA(line, ARRAYSIZE(line), "minidump open error=%lu\r\n",
GetLastError());
WriteLog(line);
}
// Do not swallow an exception or resume a potentially corrupted process.
return EXCEPTION_CONTINUE_SEARCH;
}
void InstallCrashHandler() {
// Match Dart's Windows storage path independently of the working directory.
wchar_t executable[32768] = {};
const DWORD length = GetModuleFileNameW(nullptr, executable, ARRAYSIZE(executable));
if (length == 0 || length >= ARRAYSIZE(executable)) return;
const auto directory = std::filesystem::path(executable).parent_path() /
L"storage" / L"logs";
std::error_code error;
std::filesystem::create_directories(directory, error);
if (error) return;
dump_path = (directory / L"native-crash.dmp").wstring();
crash_log = CreateFileW((directory / L"native-crash.log").c_str(),
FILE_APPEND_DATA, FILE_SHARE_READ, nullptr, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
previous_filter = SetUnhandledExceptionFilter(UnhandledException);
AddVectoredExceptionHandler(1, FatalCallbackException);
}
-12
View File
@@ -1,12 +0,0 @@
#ifndef RUNNER_CRASH_HANDLER_H_
#define RUNNER_CRASH_HANDLER_H_
#include <windows.h>
// Installs best-effort native diagnostics before Flutter or plugins start.
void InstallCrashHandler();
// Records the exception and continues normal Windows exception processing.
LONG RecordNativeCrash(EXCEPTION_POINTERS* exception);
#endif // RUNNER_CRASH_HANDLER_H_
+1 -3
View File
@@ -63,9 +63,7 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
switch (message) { switch (message) {
case WM_FONTCHANGE: case WM_FONTCHANGE:
if (flutter_controller_ && flutter_controller_->engine()) { flutter_controller_->engine()->ReloadSystemFonts();
flutter_controller_->engine()->ReloadSystemFonts();
}
break; break;
} }
+2 -5
View File
@@ -5,7 +5,6 @@
#include <desktop_multi_window/desktop_multi_window_plugin.h> #include <desktop_multi_window/desktop_multi_window_plugin.h>
#include "app_links/app_links_plugin_c_api.h" #include "app_links/app_links_plugin_c_api.h"
#include "crash_handler.h"
#include "flutter_window.h" #include "flutter_window.h"
#include "flutter/generated_plugin_registrant.h" #include "flutter/generated_plugin_registrant.h"
#include "utils.h" #include "utils.h"
@@ -44,7 +43,6 @@ bool IsMultiWindowLaunch(const std::vector<std::string>& arguments) {
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) { _In_ wchar_t *command_line, _In_ int show_command) {
InstallCrashHandler();
// Attach to console when present (e.g., 'flutter run') or create a // Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger. // new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
@@ -79,12 +77,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
window.SetQuitOnClose(true); window.SetQuitOnClose(true);
::MSG msg; ::MSG msg;
BOOL message_result; while (::GetMessage(&msg, nullptr, 0, 0)) {
while ((message_result = ::GetMessage(&msg, nullptr, 0, 0)) > 0) {
::TranslateMessage(&msg); ::TranslateMessage(&msg);
::DispatchMessage(&msg); ::DispatchMessage(&msg);
} }
::CoUninitialize(); ::CoUninitialize();
return message_result == -1 ? EXIT_FAILURE : static_cast<int>(msg.wParam); return EXIT_SUCCESS;
} }
+11 -19
View File
@@ -3,7 +3,6 @@
#include <dwmapi.h> #include <dwmapi.h>
#include <flutter_windows.h> #include <flutter_windows.h>
#include "crash_handler.h"
#include "resource.h" #include "resource.h"
namespace { namespace {
@@ -159,26 +158,19 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message, UINT const message,
WPARAM const wparam, WPARAM const wparam,
LPARAM const lparam) noexcept { LPARAM const lparam) noexcept {
// Capture inside the callback, before user32 wraps or suppresses an escaping if (message == WM_NCCREATE) {
// exception. Keep C++ objects requiring unwinding out of this SEH boundary. auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
__try { SetWindowLongPtr(window, GWLP_USERDATA,
if (message == WM_NCCREATE) { reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams); auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window); EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window; that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) { } else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam); return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
} __except (RecordNativeCrash(GetExceptionInformation())) {
// The filter always continues the exception search; this is unreachable.
return 0;
} }
return DefWindowProc(window, message, wparam, lparam);
} }
LRESULT LRESULT