fix(http): reset discarded prefetch progress on resume (#1462)

This commit is contained in:
Levi authored and GitHub committed 2026-08-31 23:05:01 +08:00
1 parent 962ff4dc98
commit c5c8bb2bf5
6 files changed
+93 -9

No files matched your search

+22 -3
View File
@@ -91,7 +91,7 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Test webview
if: runner.os == 'Windows'
run: go test -tags webview -v -timeout 300s ./internal/webview/goprovider/
run: go test -tags webview -v -count=1 -timeout 300s ./internal/webview/goprovider/
env:
CGO_ENABLED: 1
- name: Test webview
@@ -107,7 +107,7 @@ jobs:
cd ui/flutter
flutter pub get
flutter run -d macos >"$LOG_PATH" 2>&1 &
flutter run --machine -d macos >"$LOG_PATH" 2>&1 &
FLUTTER_PID=$!
cd ../..
@@ -116,7 +116,26 @@ jobs:
}
trap cleanup EXIT
if ! go test -tags webview -v -timeout 600s ./internal/webview/rpcprovider/ -args -webview-rpc-network unix -webview-rpc-address "$SOCKET_PATH"; then
FLUTTER_STARTED=false
for _ in $(seq 1 600); do
if grep -Eq '"event"[[:space:]]*:[[:space:]]*"app\.started"' "$LOG_PATH"; then
FLUTTER_STARTED=true
break
fi
if ! kill -0 "$FLUTTER_PID" >/dev/null 2>&1; then
echo "Flutter exited before reporting app.started"
cat "$LOG_PATH"
exit 1
fi
sleep 1
done
if [[ "$FLUTTER_STARTED" != true ]]; then
echo "Timed out waiting for Flutter to report app.started"
cat "$LOG_PATH"
exit 1
fi
if ! go test -tags webview -v -count=1 -timeout 600s ./internal/webview/rpcprovider/ -args -webview-rpc-network unix -webview-rpc-address "$SOCKET_PATH"; then
cat "$LOG_PATH"
exit 1
fi
+4
View File
@@ -584,6 +584,10 @@ func (f *Fetcher) cleanupPrefetchFile() {
os.Remove(f.prefetchFilePath)
f.prefetchFilePath = ""
}
// The byte count is only reusable while the backing prefetch file exists.
// Pause may discard that file before the first Start, so retaining the
// count would make the next Start skip bytes that were never copied.
f.prefetchSize.Store(0)
}
func (f *Fetcher) Start() error {
+55
View File
@@ -1,7 +1,9 @@
package http
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
@@ -377,6 +379,59 @@ func TestFetcher_DownloadContinue(t *testing.T) {
downloadContinue(listener, 16, t)
}
func TestFetcher_PauseBeforeStartDiscardsPrefetchProgress(t *testing.T) {
payload := make([]byte, 32*1024*1024)
for i := range payload {
payload[i] = byte(i % 251)
}
prefetchServed := make(chan struct{}, 1)
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
if r.Header.Get(base.HttpHeaderRange) == "" {
w.Header().Set(base.HttpHeaderAcceptRanges, base.HttpHeaderBytes)
w.Header().Set(base.HttpHeaderContentLength, fmt.Sprintf("%d", len(payload)))
w.WriteHeader(gohttp.StatusOK)
if _, err := w.Write(payload); err == nil {
prefetchServed <- struct{}{}
}
return
}
gohttp.ServeContent(w, r, test.BuildName, time.Unix(0, 0), bytes.NewReader(payload))
}))
defer server.Close()
fetcher := buildFetcher()
if err := fetcher.Resolve(&base.Request{URL: server.URL + "/" + test.BuildName}, &base.Options{
Path: t.TempDir(),
Name: test.DownloadName,
Extra: &http.OptsExtra{
Connections: 2,
},
}); err != nil {
t.Fatal(err)
}
select {
case <-prefetchServed:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for resolve prefetch response")
}
if err := fetcher.Pause(); err != nil {
t.Fatal(err)
}
if err := fetcher.Start(); err != nil {
t.Fatal(err)
}
if err := fetcher.Wait(); err != nil {
t.Fatal(err)
}
want := fmt.Sprintf("%x", md5.Sum(payload))
got := test.FileMd5(fetcher.Meta().SingleFilepath())
if want != got {
t.Fatalf("download after pause before start got MD5 %s, want %s", got, want)
}
}
func TestFetcher_DownloadContinue_NoRangeRestart(t *testing.T) {
listener := test.StartTestNoRangeSlowServer(time.Millisecond)
defer listener.Close()
+5 -1
View File
@@ -81,6 +81,10 @@ type nativeCookieJSON struct {
var providerSeq atomic.Uint64
// Keep native page creation aligned with the provider availability budget used
// by the integration contract. Cold WebView2 startup can exceed ten seconds.
const startupTimeout = time.Minute
func newPageWrapper(opts enginewebview.OpenOptions) *pageWrapper {
seq := providerSeq.Add(1)
return &pageWrapper{
@@ -142,7 +146,7 @@ func (p *pageWrapper) start() error {
case err := <-p.ready:
p.tracef("open %dms", time.Since(startedAt).Milliseconds())
return err
case <-time.After(10 * time.Second):
case <-time.After(startupTimeout):
p.tracef("open timeout %dms", time.Since(startedAt).Milliseconds())
return fmt.Errorf("webview startup timeout")
}
+5 -1
View File
@@ -14,6 +14,10 @@ import (
const TestUserAgent = "GopeedWebViewIntegration/1.0"
// Providers may need to initialize an external host or a native browser runtime
// before the first page can be created.
const defaultAvailabilityTimeout = time.Minute
type ContractOptions struct {
AvailabilityTimeout time.Duration
CookieDomainMode CookieDomainMode
@@ -32,7 +36,7 @@ func RunProviderContract(t *testing.T, provider enginewebview.Provider, opts Con
timeout := opts.AvailabilityTimeout
if timeout <= 0 {
timeout = 10 * time.Second
timeout = defaultAvailabilityTimeout
}
waitForProviderAvailable(t, provider, timeout)
@@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/GopeedLab/gopeed/internal/webview/integrationtest"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
@@ -26,9 +25,8 @@ func TestProviderContract(t *testing.T) {
Token: *webViewRPCToken,
})
integrationtest.RunProviderContract(t, provider, integrationtest.ContractOptions{
AvailabilityTimeout: 5 * time.Minute,
CookieDomainMode: integrationtest.CookieDomainModeRequired,
CookieTestURL: "https://example.com/",
CookieDomainMode: integrationtest.CookieDomainModeRequired,
CookieTestURL: "https://example.com/",
})
}