Compare commits

..
Author SHA1 Message Date
Levi 4c2321d686 Merge branch 'main' into ci/test 2026-03-25 13:58:42 +08:00
Levi 78e23217f9 FIX 2026-03-18 21:56:43 +08:00
Levi 61f62ec39a fix 2026-03-18 21:44:51 +08:00
Levi 5d76d062ad fix 2026-03-18 21:27:30 +08:00
Levi fc489b6b97 ci test 2026-03-18 21:16:03 +08:00
112 changed files with 2481 additions and 15821 deletions

No files matched your search

+11 -13
View File
@@ -13,8 +13,8 @@ on:
default: "false" default: "false"
env: env:
GO_VERSION: "1.25" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.41.6" FLUTTER_VERSION: "3.41.2"
GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }} GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }}
GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }} GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }}
@@ -81,7 +81,7 @@ jobs:
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV "CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
$b >> $env:GITHUB_PATH $b >> $env:GITHUB_PATH
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -198,7 +198,7 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Build - name: Build
@@ -224,7 +224,7 @@ jobs:
needs: [get-release, build-macos-amd64-lib] needs: [get-release, build-macos-amd64-lib]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
@@ -315,7 +315,7 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -324,7 +324,7 @@ jobs:
channel: master channel: master
- run: | - run: |
sudo apt update -y sudo apt update -y
sudo apt install -y ninja-build libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-1 libayatana-appindicator3-dev rpm patchelf libfuse2 locate libkeybinder-3.0-dev sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev rpm patchelf libfuse2 locate libkeybinder-3.0-dev
arch=$(uname -m) arch=$(uname -m)
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage" wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
chmod +x appimagetool chmod +x appimagetool
@@ -411,7 +411,7 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Setup LXD - name: Setup LXD
@@ -480,7 +480,6 @@ jobs:
- cmake - cmake
- curl - curl
- libgtk-3-dev - libgtk-3-dev
- libwebkit2gtk-4.1-dev
- libkeybinder-3.0-dev - libkeybinder-3.0-dev
- ninja-build - ninja-build
- unzip - unzip
@@ -496,7 +495,6 @@ jobs:
source: . source: .
plugin: nil plugin: nil
stage-packages: stage-packages:
- libwebkit2gtk-4.1-0
- libgtk-3-dev - libgtk-3-dev
- libkeybinder-3.0-dev - libkeybinder-3.0-dev
- libappindicator3-dev - libappindicator3-dev
@@ -547,7 +545,7 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
@@ -589,7 +587,7 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -622,7 +620,7 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
+2 -2
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.25" GO_VERSION: "1.24"
permissions: permissions:
contents: write contents: write
@@ -31,7 +31,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v5
+15 -71
View File
@@ -28,8 +28,8 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
GO_VERSION: "1.25" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.41.6" FLUTTER_VERSION: "3.41.2"
jobs: jobs:
# lint: # lint:
@@ -50,18 +50,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Install webview dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev xvfb
- name: Run Unit tests. - name: Run Unit tests.
run: | run: |
# Get packages for testing and coverage (including cmd/web for flags_test.go) # Get packages for testing and coverage (including cmd/web for flags_test.go)
PACKAGES=$(go list ./... | awk '!/\/bind\// && (!/\/cmd\// || /\/cmd\/web$/)') PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
env CGO_ENABLED=1 xvfb-run -a go test -tags webview -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
# Filter out main.go from coverage report to avoid lowering coverage percentage # Filter out main.go from coverage report to avoid lowering coverage percentage
grep -v "main.go" coverage.txt > coverage_filtered.txt || true grep -v "main.go" coverage.txt > coverage_filtered.txt || true
mv coverage_filtered.txt coverage.txt mv coverage_filtered.txt coverage.txt
@@ -69,58 +65,6 @@ jobs:
with: with:
files: ./coverage.txt files: ./coverage.txt
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
test-webview-integration:
name: Test webview integration (${{ matrix.name }})
strategy:
fail-fast: false
matrix:
include:
- name: Windows
os: windows-latest
- name: macOS
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
if: runner.os == 'macOS'
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Test webview
if: runner.os == 'Windows'
run: go test -tags webview -v -timeout 300s ./internal/webview/goprovider/
env:
CGO_ENABLED: 1
- name: Test webview
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
SOCKET_PATH="$HOME/Library/Application Support/com.gopeed.gopeed/gopeed_webview.sock"
LOG_PATH="$RUNNER_TEMP/gopeed-webview-rpc.log"
rm -f "$SOCKET_PATH"
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter pub get
flutter run -d macos >"$LOG_PATH" 2>&1 &
FLUTTER_PID=$!
cd ../..
cleanup() {
kill "$FLUTTER_PID" >/dev/null 2>&1 || true
}
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
cat "$LOG_PATH"
exit 1
fi
build-desktop: build-desktop:
strategy: strategy:
matrix: matrix:
@@ -136,7 +80,7 @@ jobs:
flutter_channel: "main" flutter_channel: "main"
name: Build desktop check (${{ matrix.os }}) name: Build desktop check (${{ matrix.os }})
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Enable long paths for flutter main branch checks - name: Enable long paths for flutter main branch checks
@@ -165,7 +109,7 @@ jobs:
"CGO_ENABLED=1" >> $env:GITHUB_ENV "CGO_ENABLED=1" >> $env:GITHUB_ENV
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV "CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
$b >> $env:GITHUB_PATH $b >> $env:GITHUB_PATH
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -185,7 +129,7 @@ jobs:
- if: runner.os == 'Linux' - if: runner.os == 'Linux'
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libkeybinder-3.0-dev sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev libkeybinder-3.0-dev
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux flutter build linux
@@ -194,11 +138,11 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ macos-14, ubuntu-latest ] os: [macos-14, ubuntu-latest]
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -227,10 +171,10 @@ jobs:
build-web: build-web:
name: Build web check name: Build web check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
@@ -248,10 +192,10 @@ jobs:
build-docker: build-docker:
name: Build docker check name: Build docker check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.25.4-alpine3.22 AS go FROM golang:1.24.11-alpine3.23 AS go
WORKDIR /app WORKDIR /app
COPY ./go.mod ./go.sum ./ COPY ./go.mod ./go.sum ./
RUN go mod download RUN go mod download
+1 -1
View File
@@ -201,7 +201,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### 🌍 Environment ### 🌍 Environment
1. Golang 1.25+ 1. Golang 1.24+
2. Flutter 3.38+ 2. Flutter 3.38+
### 📋 Clone ### 📋 Clone
+1 -1
View File
@@ -182,7 +182,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
### 🌍 環境 ### 🌍 環境
1. Go 言語 1.25+ 1. Go 言語 1.24+
2. Flutter 3.38+ 2. Flutter 3.38+
### 📋 クローン ### 📋 クローン
+1 -1
View File
@@ -188,7 +188,7 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
### 🌍 Environment ### 🌍 Environment
1. Golang 1.25+ 1. Golang 1.24+
2. Flutter 3.38+ 2. Flutter 3.38+
### 📋 Clone ### 📋 Clone
+1 -1
View File
@@ -192,7 +192,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
### 🌍 环境要求 ### 🌍 环境要求
1. Golang 1.25+ 1. Golang 1.24+
2. Flutter 3.38+ 2. Flutter 3.38+
### 📋 克隆项目 ### 📋 克隆项目
+1 -1
View File
@@ -189,7 +189,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
### 🌍 開發環境 ### 🌍 開發環境
1. Golang 1.25+ 1. Golang 1.24+
2. Flutter 3.38+ 2. Flutter 3.38+
### 📋 克隆項目 ### 📋 克隆項目
-1
View File
@@ -16,7 +16,6 @@ func Start(cfg *C.char) (int, *C.char) {
return 0, C.CString(err.Error()) return 0, C.CString(err.Error())
} }
config.ProductionMode = true config.ProductionMode = true
applyWebViewProvider(&config)
realPort, err := rest.Start(&config) realPort, err := rest.Start(&config)
if err != nil { if err != nil {
return 0, C.CString(err.Error()) return 0, C.CString(err.Error())
-23
View File
@@ -1,23 +0,0 @@
//go:build darwin
package main
import (
"github.com/GopeedLab/gopeed/internal/webview/rpcprovider"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func applyWebViewProvider(config *model.StartConfig) {
if config == nil {
return
}
config.WebViewProvider = newDesktopWebViewProvider(config.WebViewRPCConfig)
}
func newDesktopWebViewProvider(rpcCfg *enginewebview.RPCConfig) enginewebview.Provider {
if rpcCfg != nil && rpcCfg.Enabled() {
return rpcprovider.New(*rpcCfg)
}
return enginewebview.NewUnavailableProvider()
}
-15
View File
@@ -1,15 +0,0 @@
//go:build !darwin
package main
import (
"github.com/GopeedLab/gopeed/internal/webview/goprovider"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func applyWebViewProvider(config *model.StartConfig) {
if config == nil {
return
}
config.WebViewProvider = goprovider.New()
}
-9
View File
@@ -4,7 +4,6 @@ package libgopeed
import "C" import "C"
import ( import (
"encoding/json" "encoding/json"
"github.com/GopeedLab/gopeed/internal/webview/rpcprovider"
"github.com/GopeedLab/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
) )
@@ -15,17 +14,9 @@ func Start(cfg string) (int, error) {
return 0, err return 0, err
} }
config.ProductionMode = true config.ProductionMode = true
applyWebViewProvider(&config)
return rest.Start(&config) return rest.Start(&config)
} }
func Stop() { func Stop() {
rest.Stop() rest.Stop()
} }
func applyWebViewProvider(config *model.StartConfig) {
if config == nil || config.WebViewRPCConfig == nil || !config.WebViewRPCConfig.Enabled() {
return
}
config.WebViewProvider = rpcprovider.New(*config.WebViewRPCConfig)
}
-723
View File
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -1,9 +1,10 @@
module github.com/GopeedLab/gopeed module github.com/GopeedLab/gopeed
go 1.25.4 go 1.24.9
toolchain go1.24.11
require ( require (
github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5
github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0 github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/bodgit/sevenzip v1.6.1 github.com/bodgit/sevenzip v1.6.1
@@ -16,7 +17,6 @@ require (
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
github.com/mholt/archives v0.1.5 github.com/mholt/archives v0.1.5
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
@@ -45,6 +45,7 @@ require (
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/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 // 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/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
+2 -2
View File
@@ -23,8 +23,6 @@ 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-20260423085439-7a2f88b6e9b5 h1:86LVTaNDM/j/uTAD2HedkXNO9LIQlt642VAdbU/0D/M=
github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
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=
@@ -388,6 +386,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5 h1:tEOucyKJzFsCz5Gr41jFHj8i2g1zemTyS4uyErJgFHc=
github.com/monkeyWie/goed2k v0.0.0-20260317094144-6e18d43056e5/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg=
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 h1:88kRsNwkDKPA4NzRUWdOoIL7SyMNDwtmAxzShKBvKTI= github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 h1:88kRsNwkDKPA4NzRUWdOoIL7SyMNDwtmAxzShKBvKTI=
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg= github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447/go.mod h1:Ry2y1QlzerUgA1hVmExBdXXzE4Sjk1M7w0nSh6dhDOg=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -376,7 +376,7 @@ func TestFetcher_DownloadError(t *testing.T) {
} }
func TestFetcher_DownloadLimit(t *testing.T) { func TestFetcher_DownloadLimit(t *testing.T) {
listener := test.StartTestConLimitServer(4) listener := test.StartTestLimitServer(4, 0)
defer listener.Close() defer listener.Close()
downloadNormal(listener, 1, t) downloadNormal(listener, 1, t)
@@ -614,7 +614,7 @@ func TestFetcher_SlowStartExpansion(t *testing.T) {
os.Remove(test.DownloadFile) os.Remove(test.DownloadFile)
// Use 100ns delay per byte for faster test (~10MB/s theoretical) // Use 100ns delay per byte for faster test (~10MB/s theoretical)
listener := test.StartTestLowSpeedServer(100 * time.Nanosecond) listener := test.StartTestSlowStartServer(100 * time.Nanosecond)
// Ensure cleanup happens before next subtest // Ensure cleanup happens before next subtest
cleanup := func() { cleanup := func() {
@@ -758,7 +758,7 @@ func TestFetcher_AsyncPrefetch(t *testing.T) {
t.Run("PrefetchPartial", func(t *testing.T) { t.Run("PrefetchPartial", func(t *testing.T) {
// Use slow server with 100 nanosecond delay per byte // Use slow server with 100 nanosecond delay per byte
// This means ~10MB/s speed, so 100ms should download ~1MB // This means ~10MB/s speed, so 100ms should download ~1MB
listener := test.StartTestLowSpeedServer(100 * time.Nanosecond) listener := test.StartTestSlowStartServer(100 * time.Nanosecond)
defer listener.Close() defer listener.Close()
fetcher := buildFetcher() fetcher := buildFetcher()
+16 -9
View File
@@ -433,8 +433,9 @@ func StartTestExpiringRedirectServer(requestsBeforeExpire int32, delayPerByte ti
}) })
} }
// StartTestLowSpeedServer creates a server with configurable delay per request // StartTestSlowStartServer creates a server with configurable delay per request
func StartTestLowSpeedServer(delayPerByte time.Duration) net.Listener { // This allows testing slow-start connection expansion to reach max connections
func StartTestSlowStartServer(delayPerByte time.Duration) net.Listener {
return startTestServer(func(sl *shutdownListener) http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) { mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
@@ -483,9 +484,10 @@ func slowCopyNWithDelay(sl *shutdownListener, dst io.Writer, src io.Reader, n in
} }
} }
// StartTestConLimitServer connections limit server // StartTestLimitServer connections limit server
func StartTestConLimitServer(maxConnections int32) net.Listener { func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
var connections atomic.Int32 var connections atomic.Int32
var slowOnce atomic.Bool
return startTestServer(func(sl *shutdownListener) http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -498,11 +500,19 @@ func StartTestConLimitServer(maxConnections int32) net.Listener {
writer.WriteHeader(403) writer.WriteHeader(403)
return return
} }
// First request intentionally delays the first write to trigger a single read timeout,
// subsequent requests respond at normal speed.
useInitialDelay := delay > 0 && !slowOnce.Swap(true)
rangeFileHandle( rangeFileHandle(
writer, writer,
request, request,
nil, nil,
func(file *os.File, n int64) { func(file *os.File, n int64) {
if useInitialDelay {
slowCopyAfterDelay(sl, writer, file, n, delay)
return
}
slowCopyN(sl, writer, file, n, 0) slowCopyN(sl, writer, file, n, 0)
}, },
) )
@@ -701,14 +711,11 @@ func rangeFileHandle(writer http.ResponseWriter, request *http.Request, modifyEn
writer.WriteHeader(400) writer.WriteHeader(400)
return return
} }
end := int64(BuildSize - 1) end, err := strconv.ParseInt(s[1], 10, 64)
if s[1] != "" {
end, err = strconv.ParseInt(s[1], 10, 64)
if err != nil { if err != nil {
writer.WriteHeader(400) writer.WriteHeader(400)
return return
} }
}
if start < 0 || end < 0 || start > end { if start < 0 || end < 0 || start > end {
writer.WriteHeader(400) writer.WriteHeader(400)
return return
@@ -873,7 +880,7 @@ func StartSocks5Server(usr, pwd string) net.Listener {
panic(err) panic(err)
} }
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0") // 你可以根据需要更改监听地址
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -1,36 +0,0 @@
//go:build darwin
package goprovider
var mainThreadTasks chan func()
func RunMainThreadLoop(run func() int) int {
if mainThreadTasks != nil {
return run()
}
mainThreadTasks = make(chan func())
resultCh := make(chan int, 1)
go func() {
resultCh <- run()
}()
for {
select {
case task := <-mainThreadTasks:
task()
case code := <-resultCh:
mainThreadTasks = nil
return code
}
}
}
func postMainThreadTask(task func()) bool {
if mainThreadTasks == nil {
return false
}
mainThreadTasks <- task
return true
}
@@ -1,11 +0,0 @@
//go:build !darwin
package goprovider
func RunMainThreadLoop(run func() int) int {
return run()
}
func postMainThreadTask(func()) bool {
return false
}
File diff suppressed because it is too large. Load diff
@@ -1,15 +0,0 @@
//go:build cgo && webview && !darwin
package goprovider
import (
"testing"
"github.com/GopeedLab/gopeed/internal/webview/integrationtest"
)
func TestProviderContract(t *testing.T) {
integrationtest.RunProviderContract(t, New(), integrationtest.ContractOptions{
CookieDomainMode: integrationtest.CookieDomainModeRequired,
})
}
@@ -1,9 +0,0 @@
//go:build !cgo
package goprovider
import enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
func New() enginewebview.Provider {
return enginewebview.NewUnavailableProvider()
}
@@ -1,38 +0,0 @@
//go:build cgo && darwin
package goprovider
/*
#cgo darwin CFLAGS: -x objective-c
#cgo darwin LDFLAGS: -framework Cocoa
#include <Cocoa/Cocoa.h>
static void gopeedApplyHeadlessWindowOptions(void *windowPtr, int headless) {
if (windowPtr == NULL) {
return;
}
NSWindow *window = (__bridge NSWindow *)windowPtr;
if (headless) {
[window orderOut:nil];
}
}
*/
import "C"
import (
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
webview "github.com/GopeedLab/webview_go"
)
func applyWindowOptions(w webview.WebView, opts enginewebview.OpenOptions) {
C.gopeedApplyHeadlessWindowOptions(w.Window(), C.int(boolToInt(opts.Headless)))
}
func boolToInt(value bool) int {
if value {
return 1
}
return 0
}
@@ -1,10 +0,0 @@
//go:build cgo && !darwin
package goprovider
import (
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
webview "github.com/GopeedLab/webview_go"
)
func applyWindowOptions(_ webview.WebView, _ enginewebview.OpenOptions) {}
@@ -1,378 +0,0 @@
package integrationtest
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
const TestUserAgent = "GopeedWebViewIntegration/1.0"
type ContractOptions struct {
AvailabilityTimeout time.Duration
CookieDomainMode CookieDomainMode
CookieTestURL string
}
type CookieDomainMode int
const (
CookieDomainModeRequired CookieDomainMode = iota
CookieDomainModeOmit
)
func RunProviderContract(t *testing.T, provider enginewebview.Provider, opts ContractOptions) {
t.Helper()
timeout := opts.AvailabilityTimeout
if timeout <= 0 {
timeout = 10 * time.Second
}
waitForProviderAvailable(t, provider, timeout)
page, server, testURL := openTestPage(t, provider, enginewebview.OpenOptions{
Headless: true,
Title: "Gopeed WebView Integration",
Width: 640,
Height: 480,
UserAgent: TestUserAgent,
})
if err := page.Goto(testURL, map[string]any{"timeoutMs": 10000}); err != nil {
t.Fatalf("goto failed: %v", err)
}
ua, err := page.Execute(`() => navigator.userAgent`)
if err != nil {
t.Fatalf("navigator.userAgent execute failed: %v", err)
}
if ua != TestUserAgent {
t.Fatalf("unexpected user agent: got %v want %q", ua, TestUserAgent)
}
if err := page.Focus("#name"); err != nil {
t.Fatalf("focus failed: %v", err)
}
activeElement, err := page.Execute(`() => document.activeElement && document.activeElement.id`)
if err != nil {
t.Fatalf("activeElement execute failed: %v", err)
}
if activeElement != "name" {
t.Fatalf("expected active element to be #name, got %v", activeElement)
}
if err := page.Type("#name", "Gopeed", map[string]any{"delay": 5}); err != nil {
t.Fatalf("type failed: %v", err)
}
typeState, err := page.Execute(`() => ({
value: document.querySelector('#name').value,
mirror: document.querySelector('#name-value').textContent,
inputCount: window.__inputEvents.length
})`)
if err != nil {
t.Fatalf("type state execute failed: %v", err)
}
stateMap, ok := typeState.(map[string]any)
if !ok {
t.Fatalf("unexpected type state payload: %T", typeState)
}
if stateMap["value"] != "Gopeed" || stateMap["mirror"] != "Gopeed" {
t.Fatalf("unexpected type state: %#v", stateMap)
}
if stateMap["inputCount"] != float64(len("Gopeed")) {
t.Fatalf("unexpected input event count: %#v", stateMap["inputCount"])
}
if err := page.Click("#clicker"); err != nil {
t.Fatalf("click failed: %v", err)
}
clickCount, err := page.Execute(`() => document.querySelector('#click-count').textContent`)
if err != nil {
t.Fatalf("click count execute failed: %v", err)
}
if clickCount != "1" {
t.Fatalf("expected click count to be 1, got %v", clickCount)
}
found, err := page.WaitForSelector("#delayed", map[string]any{
"timeoutMs": 5000,
"pollIntervalMs": 25,
"visible": true,
})
if err != nil {
t.Fatalf("waitForSelector failed: %v", err)
}
if !found {
t.Fatal("expected delayed selector to appear")
}
value, err := page.WaitForFunction(`() => window.__readyValue`, map[string]any{
"timeoutMs": 5000,
"pollIntervalMs": 25,
})
if err != nil {
t.Fatalf("waitForFunction failed: %v", err)
}
readyValue, ok := value.(map[string]any)
if !ok {
t.Fatalf("unexpected waitForFunction value: %T", value)
}
if readyValue["status"] != "ready" {
t.Fatalf("unexpected waitForFunction payload: %#v", readyValue)
}
found, err = page.WaitForSelector("#missing", map[string]any{
"timeoutMs": 200,
"pollIntervalMs": 25,
})
if err != nil {
t.Fatalf("waitForSelector timeout case failed: %v", err)
}
if found {
t.Fatal("expected missing selector wait to time out")
}
cookieTestURL := opts.CookieTestURL
if cookieTestURL == "" {
cookieTestURL = testURL
}
runCookieLifecycleAssertions(t, page, server, cookieTestURL, opts.CookieDomainMode)
}
func waitForProviderAvailable(t *testing.T, provider enginewebview.Provider, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if provider != nil && provider.IsAvailable() {
return
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("provider did not become available within %s", timeout)
}
func openTestPage(t *testing.T, provider enginewebview.Provider, opts enginewebview.OpenOptions) (*enginewebview.PageHandle, *httptest.Server, string) {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
switch r.URL.Path {
case "/next":
fmt.Fprint(w, `<!DOCTYPE html>
<html>
<head><title>Next Page</title></head>
<body>
<h1 id="next-page">Navigation complete</h1>
</body>
</html>`)
default:
fmt.Fprint(w, `<!DOCTYPE html>
<html>
<head><title>Test Page</title></head>
<body>
<input id="name" type="text" />
<div id="name-value"></div>
<button id="clicker" type="button">Click me</button>
<div id="click-count">0</div>
<button id="nav-btn" type="button">Navigate</button>
<script>
window.__inputEvents = [];
window.__readyValue = null;
const nameInput = document.getElementById('name');
const nameValue = document.getElementById('name-value');
nameInput.addEventListener('input', () => {
nameValue.textContent = nameInput.value;
window.__inputEvents.push(nameInput.value);
});
document.getElementById('clicker').addEventListener('click', () => {
const counter = document.getElementById('click-count');
counter.textContent = String(Number(counter.textContent || '0') + 1);
});
document.getElementById('nav-btn').addEventListener('click', () => {
setTimeout(() => {
window.location.href = '/next';
}, 50);
});
setTimeout(() => {
const marker = document.createElement('div');
marker.id = 'delayed';
marker.textContent = 'Loaded later';
document.body.appendChild(marker);
}, 150);
setTimeout(() => {
window.__readyValue = { status: 'ready', source: 'timer' };
}, 200);
</script>
</body>
</html>`)
}
}))
t.Cleanup(server.Close)
runtime := enginewebview.NewRuntime(provider, true)
page, err := runtime.Open(map[string]any{
"headless": opts.Headless,
"debug": opts.Debug,
"title": opts.Title,
"width": opts.Width,
"height": opts.Height,
"userAgent": opts.UserAgent,
})
if err != nil {
t.Fatalf("failed to open webview: %v", err)
}
t.Cleanup(func() {
_ = runtime.Close()
})
return page, server, server.URL
}
func runCookieLifecycleAssertions(t *testing.T, page *enginewebview.PageHandle, _ *httptest.Server, testURL string, domainMode CookieDomainMode) {
t.Helper()
parsedURL, err := url.Parse(testURL)
if err != nil {
t.Fatalf("parse cookie test url failed: %v", err)
}
cookieDomain := parsedURL.Hostname()
if err := page.ClearCookies(); err != nil {
t.Fatalf("clearCookies before cookie assertions failed: %v", err)
}
visibleCookie := map[string]any{
"name": "visible",
"value": "1",
"path": "/",
}
secretCookie := map[string]any{
"name": "secret",
"value": "2",
"path": "/",
"httpOnly": true,
"expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339Nano),
}
deleteCookie := map[string]any{
"name": "visible",
"path": "/",
}
if domainMode == CookieDomainModeRequired {
visibleCookie["domain"] = cookieDomain
secretCookie["domain"] = cookieDomain
deleteCookie["domain"] = cookieDomain
}
if err := page.SetCookie(visibleCookie); err != nil {
t.Fatalf("set visible cookie failed: %v", err)
}
if err := page.SetCookie(secretCookie); err != nil {
t.Fatalf("set secret cookie failed: %v", err)
}
if err := page.Goto(testURL, map[string]any{"timeoutMs": 10000}); err != nil {
t.Fatalf("goto after setCookie failed: %v", err)
}
documentCookie, err := page.Execute(`() => document.cookie`)
if err != nil {
t.Fatalf("document.cookie execute failed: %v", err)
}
documentCookieText, ok := documentCookie.(string)
if !ok {
t.Fatalf("unexpected document.cookie payload: %T", documentCookie)
}
if !strings.Contains(documentCookieText, "visible=1") {
t.Fatalf("expected visible cookie in document.cookie, got %q", documentCookieText)
}
if strings.Contains(documentCookieText, "secret=2") {
t.Fatalf("expected httpOnly cookie to be hidden from document.cookie, got %q", documentCookieText)
}
cookies, err := page.GetCookies()
if err != nil {
t.Fatalf("getCookies failed: %v", err)
}
if len(cookies) < 2 {
t.Fatalf("expected at least 2 cookies, got %#v", cookies)
}
if !hasCookie(cookies, "visible", "1") {
t.Fatalf("expected visible cookie in native cookie store, got %#v", cookies)
}
if !hasCookie(cookies, "secret", "2") {
t.Fatalf("expected secret cookie in native cookie store, got %#v", cookies)
}
if err := page.DeleteCookie(deleteCookie); err != nil {
t.Fatalf("deleteCookie failed: %v", err)
}
if err := page.Goto(testURL, map[string]any{"timeoutMs": 10000}); err != nil {
t.Fatalf("goto after deleteCookie failed: %v", err)
}
documentCookie, err = page.Execute(`() => document.cookie`)
if err != nil {
t.Fatalf("document.cookie execute after delete failed: %v", err)
}
documentCookieText, ok = documentCookie.(string)
if !ok {
t.Fatalf("unexpected document.cookie payload after delete: %T", documentCookie)
}
if strings.Contains(documentCookieText, "visible=1") {
t.Fatalf("expected visible cookie to be deleted, got %q", documentCookieText)
}
if strings.Contains(documentCookieText, "secret=2") {
t.Fatalf("expected httpOnly cookie to remain hidden after delete, got %q", documentCookieText)
}
cookies, err = page.GetCookies()
if err != nil {
t.Fatalf("getCookies after delete failed: %v", err)
}
if hasCookie(cookies, "visible", "1") {
t.Fatalf("expected visible cookie to be removed from native store, got %#v", cookies)
}
if !hasCookie(cookies, "secret", "2") {
t.Fatalf("expected secret cookie to remain in native store after delete, got %#v", cookies)
}
if err := page.ClearCookies(); err != nil {
t.Fatalf("clearCookies failed: %v", err)
}
if err := page.Goto(testURL, map[string]any{"timeoutMs": 10000}); err != nil {
t.Fatalf("goto after clearCookies failed: %v", err)
}
documentCookie, err = page.Execute(`() => document.cookie`)
if err != nil {
t.Fatalf("document.cookie execute after clear failed: %v", err)
}
documentCookieText, ok = documentCookie.(string)
if !ok {
t.Fatalf("unexpected document.cookie payload after clear: %T", documentCookie)
}
if documentCookieText != "" {
t.Fatalf("expected document.cookie to be empty after clear, got %q", documentCookieText)
}
cookies, err = page.GetCookies()
if err != nil {
t.Fatalf("getCookies after clear failed: %v", err)
}
if len(cookies) != 0 {
t.Fatalf("expected cookie store to be empty after clear, got %#v", cookies)
}
}
func hasCookie(cookies []enginewebview.Cookie, name string, value string) bool {
for _, cookie := range cookies {
if cookie.Name == name && cookie.Value == value {
return true
}
}
return false
}
-488
View File
@@ -1,488 +0,0 @@
# rpcprovider
`rpcprovider` is the RPC-backed implementation of Gopeed's `webview.Provider` interface.
It is intended for environments where WebView capability is owned by another process, most notably mobile hosts. This package does not implement a WebView itself. Instead, it translates the `pkg/download/engine/webview` page API into single-request `POST + JSON` calls sent to a local host-side RPC service.
## When to use it
Use `rpcprovider` for entrypoints such as `bind/mobile`, where the Go process cannot directly own a native WebView.
Do not use it for desktop or `cmd/web` local WebView integration. Those entrypoints should use `internal/webview/goprovider` instead.
## Configuration
The provider is configured through `pkg/download/engine/webview.RPCConfig`:
```go
type RPCConfig struct {
Network string `json:"network"`
Address string `json:"address"`
Token string `json:"token,omitempty"`
}
```
Notes:
- `Network` currently supports `tcp` and `unix`.
- `Address` examples:
- `127.0.0.1:38765`
- `/path/to/webview.sock`
- `Token` is optional. When set, the client sends `Authorization: Bearer <token>`.
Example:
```go
provider := rpcprovider.New(webview.RPCConfig{
Network: "tcp",
Address: "127.0.0.1:38765",
Token: "secret-token",
})
```
## Transport
- Endpoint: `POST /webview`
- Content type: `application/json`
- One request, one response
- No `id` field yet
- The envelope is intentionally close to JSON-RPC so it can evolve later without renaming the methods or reshaping the payloads
Request example:
```json
{
"method": "page.execute",
"params": {
"pageId": "page-1",
"expression": "document.title",
"args": []
}
}
```
Successful response:
```json
{
"result": {
"title": "Example"
},
"error": null
}
```
Failure response:
```json
{
"result": null,
"error": {
"code": "PAGE_NOT_FOUND",
"message": "page not found"
}
}
```
## Supported methods
- `webview.isAvailable`
- `page.open`
- `page.addInitScript`
- `page.goto`
- `page.execute`
- `page.getCookies`
- `page.setCookie`
- `page.deleteCookie`
- `page.clearCookies`
- `page.close`
The canonical wire models for these methods are defined in:
- [pkg/download/engine/webview/rpc.go](../../../pkg/download/engine/webview/rpc.go)
- [pkg/download/engine/webview/runtime.go](../../../pkg/download/engine/webview/runtime.go)
The README is only a guide. The Go source above is the authoritative protocol reference.
## Method reference
Each method below uses the common envelope:
```json
{
"method": "<method-name>",
"params": { ... }
}
```
and returns:
```json
{
"result": ...,
"error": null
}
```
or:
```json
{
"result": null,
"error": {
"code": "<ERROR_CODE>",
"message": "<message>"
}
}
```
### `webview.isAvailable`
Request:
```json
{
"method": "webview.isAvailable",
"params": {}
}
```
Success:
```json
{
"result": {
"available": true
},
"error": null
}
```
### `page.open`
Request:
```json
{
"method": "page.open",
"params": {
"headless": true,
"debug": false,
"title": "Gopeed WebView",
"width": 1280,
"height": 720,
"userAgent": "Mozilla/5.0 ..."
}
}
```
Success:
```json
{
"result": {
"pageId": "page-1"
},
"error": null
}
```
### `page.addInitScript`
Request:
```json
{
"method": "page.addInitScript",
"params": {
"pageId": "page-1",
"script": "window.__READY__ = true;"
}
}
```
Success:
```json
{
"result": {},
"error": null
}
```
### `page.goto`
Request:
```json
{
"method": "page.goto",
"params": {
"pageId": "page-1",
"url": "https://example.com",
"timeoutMs": 15000,
"waitUntil": "domcontentloaded"
}
}
```
Success:
```json
{
"result": {},
"error": null
}
```
`waitUntil` currently supports `load` and `domcontentloaded`. The default is `load`.
### `page.execute`
Request:
```json
{
"method": "page.execute",
"params": {
"pageId": "page-1",
"expression": "document.title",
"args": []
}
}
```
Success:
```json
{
"result": "Example Domain",
"error": null
}
```
### `page.getCookies`
Request:
```json
{
"method": "page.getCookies",
"params": {
"pageId": "page-1"
}
}
```
Success:
```json
{
"result": [
{
"name": "session",
"value": "abc",
"domain": ".example.com",
"path": "/",
"expires": "2026-03-26T12:00:00Z",
"secure": true,
"httpOnly": true
}
],
"error": null
}
```
### `page.setCookie`
Request:
```json
{
"method": "page.setCookie",
"params": {
"pageId": "page-1",
"cookie": {
"name": "session",
"value": "abc",
"domain": ".example.com",
"path": "/",
"expires": "2026-03-26T12:00:00Z",
"secure": true,
"httpOnly": true
}
}
}
```
Success:
```json
{
"result": {},
"error": null
}
```
### `page.deleteCookie`
Request:
```json
{
"method": "page.deleteCookie",
"params": {
"pageId": "page-1",
"cookie": {
"name": "session",
"domain": ".example.com",
"path": "/"
}
}
}
```
Success:
```json
{
"result": {},
"error": null
}
```
### `page.clearCookies`
Request:
```json
{
"method": "page.clearCookies",
"params": {
"pageId": "page-1"
}
}
```
Success:
```json
{
"result": {},
"error": null
}
```
### `page.close`
Request:
```json
{
"method": "page.close",
"params": {
"pageId": "page-1"
}
}
```
Success:
```json
{
"result": {},
"error": null
}
```
## Method semantics
- `page.open`
- Creates a page session
- Returns `pageId`
- `page.addInitScript`
- Registers a page initialization script
- `page.goto`
- Goes to the target URL
- The server should return only after navigation completes or fails
- `page.execute`
- Executes an expression or serialized function body
- The returned value must be JSON-serializable
- `page.getCookies`
- Returns cookies from the underlying native WebView cookie store
- `page.setCookie`
- Inserts or updates a cookie
- `page.deleteCookie`
- Deletes a cookie using `name/domain/path`
- `page.clearCookies`
- Clears the current cookie store
- `page.close`
- Closes the page and releases related resources
## Cookie model
Cookies use `runtime/webview.Cookie`.
Example payload:
```json
{
"name": "session",
"value": "abc",
"domain": ".example.com",
"path": "/",
"expires": "2026-03-26T12:00:00Z",
"secure": true,
"httpOnly": true
}
```
Notes:
- `expires` uses RFC3339 / RFC3339Nano string form.
- If the host platform supports a full native cookie store, it should return fields such as `httpOnly` and `secure`.
- `setCookie -> goto` is a valid pattern for preloading authenticated session state.
## Error codes
The current error code set is defined in `pkg/download/engine/webview/rpc.go`:
- `INVALID_REQUEST`
- `UNKNOWN_METHOD`
- `UNAVAILABLE`
- `BROWSER_NOT_FOUND`
- `PAGE_NOT_FOUND`
- `NAVIGATION_FAILED`
- `EVALUATION_FAILED`
- `TIMEOUT`
- `INTERNAL_ERROR`
`BROWSER_NOT_FOUND` is a historical leftover from an earlier shape of the protocol. The current protocol no longer has a separate browser model, so this can be cleaned up later if the protocol is tightened further.
## Current protocol boundary
This protocol is intentionally limited:
- Single-direction request/response only
- Not REST-style resource routing
- No bidirectional events
- No streaming event subscriptions
- No host-initiated push channel
If the protocol needs to evolve toward full JSON-RPC later, the intended migration path is to keep the existing `method/params/result/error` structure and add:
- `id`
- notifications
- a bidirectional transport model
## Host implementation guidance
A host implementation should usually:
- Listen on a local `tcp` address or `unix socket`
- Expose `POST /webview`
- Validate the bearer token if configured
- Maintain `pageId -> native webview/session`
- Own page lifecycle, cleanup, and timeout handling
With that split, Gopeed stays a pure RPC client and does not need to know how the host's native WebView is implemented.
-195
View File
@@ -1,195 +0,0 @@
package rpcprovider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
type Provider struct {
client *Client
}
func New(cfg enginewebview.RPCConfig) enginewebview.Provider {
return &Provider{
client: NewClient(cfg),
}
}
func (p *Provider) IsAvailable() bool {
if p == nil || p.client == nil || !p.client.Enabled() {
return false
}
var result enginewebview.IsAvailableResult
if err := p.client.Call(enginewebview.MethodIsAvailable, enginewebview.IsAvailableParams{}, &result); err != nil {
return false
}
return result.Available
}
func (p *Provider) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
if p == nil || p.client == nil || !p.IsAvailable() {
return nil, enginewebview.ErrUnavailable
}
var result enginewebview.PageOpenResult
if err := p.client.Call(enginewebview.MethodPageOpen, enginewebview.NewPageOpenParams(opts), &result); err != nil {
return nil, err
}
return &page{
client: p.client,
id: result.PageID,
}, nil
}
type page struct {
client *Client
id string
}
func (p *page) AddInitScript(script string) error {
return p.client.Call(enginewebview.MethodPageAddInitScript, enginewebview.PageAddInitScriptParams{
PageID: p.id,
Script: script,
}, nil)
}
func (p *page) Goto(url string, opts enginewebview.GotoOptions) error {
return p.client.Call(enginewebview.MethodPageGoto, enginewebview.NewPageGotoParams(p.id, url, opts), nil)
}
func (p *page) Execute(expression string, args ...any) (any, error) {
var result any
if err := p.client.Call(enginewebview.MethodPageExecute, enginewebview.PageExecuteParams{
PageID: p.id,
Expression: expression,
Args: args,
}, &result); err != nil {
return nil, err
}
return result, nil
}
func (p *page) GetCookies() ([]enginewebview.Cookie, error) {
var result []enginewebview.Cookie
if err := p.client.Call(enginewebview.MethodPageGetCookies, enginewebview.PageGetCookiesParams{
PageID: p.id,
}, &result); err != nil {
return nil, err
}
if result == nil {
return []enginewebview.Cookie{}, nil
}
return result, nil
}
func (p *page) SetCookie(cookie enginewebview.Cookie) error {
return p.client.Call(enginewebview.MethodPageSetCookie, enginewebview.PageSetCookieParams{
PageID: p.id,
Cookie: cookie,
}, nil)
}
func (p *page) DeleteCookie(cookie enginewebview.Cookie) error {
return p.client.Call(enginewebview.MethodPageDeleteCookie, enginewebview.PageDeleteCookieParams{
PageID: p.id,
Cookie: cookie,
}, nil)
}
func (p *page) ClearCookies() error {
return p.client.Call(enginewebview.MethodPageClearCookies, enginewebview.PageClearCookiesParams{
PageID: p.id,
}, nil)
}
func (p *page) Close() error {
return p.client.Call(enginewebview.MethodPageClose, enginewebview.PageCloseParams{
PageID: p.id,
}, nil)
}
type Client struct {
endpoint string
token string
http *http.Client
}
func NewClient(cfg enginewebview.RPCConfig) *Client {
client := &Client{token: cfg.Token}
if !cfg.Enabled() {
return client
}
switch strings.ToLower(cfg.Network) {
case "unix":
client.endpoint = "http://unix" + enginewebview.RPCEndpointPath
client.http = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", cfg.Address)
},
},
}
default:
client.endpoint = "http://" + cfg.Address + enginewebview.RPCEndpointPath
client.http = &http.Client{}
}
return client
}
func (c *Client) Enabled() bool {
return c != nil && c.endpoint != "" && c.http != nil
}
func (c *Client) Call(method string, params any, result any) error {
if !c.Enabled() {
return enginewebview.ErrUnavailable
}
payload, err := json.Marshal(enginewebview.RPCRequest{
Method: method,
Params: params,
})
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, c.endpoint, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("webview rpc status %d", resp.StatusCode)
}
var body enginewebview.RPCResponse
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return err
}
if body.Error != nil {
return body.Error
}
if result == nil {
return nil
}
if len(body.Result) == 0 || string(body.Result) == "null" {
return nil
}
return json.Unmarshal(body.Result, result)
}
@@ -1,41 +0,0 @@
//go:build webview && darwin
package rpcprovider
import (
"flag"
"os"
"path/filepath"
"testing"
"time"
"github.com/GopeedLab/gopeed/internal/webview/integrationtest"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
var (
webViewRPCNetwork = flag.String("webview-rpc-network", "unix", "WebView RPC network for integration tests")
webViewRPCAddress = flag.String("webview-rpc-address", defaultWebViewRPCAddress(), "WebView RPC address for integration tests")
webViewRPCToken = flag.String("webview-rpc-token", "", "WebView RPC token for integration tests")
)
func TestProviderContract(t *testing.T) {
provider := New(enginewebview.RPCConfig{
Network: *webViewRPCNetwork,
Address: *webViewRPCAddress,
Token: *webViewRPCToken,
})
integrationtest.RunProviderContract(t, provider, integrationtest.ContractOptions{
AvailabilityTimeout: 5 * time.Minute,
CookieDomainMode: integrationtest.CookieDomainModeRequired,
CookieTestURL: "https://example.com/",
})
}
func defaultWebViewRPCAddress() string {
homeDir, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(homeDir, "Library", "Application Support", "com.gopeed.gopeed", "gopeed_webview.sock")
}
@@ -1,50 +0,0 @@
package rpcprovider
import (
"encoding/json"
"net"
"net/http"
"os"
"path/filepath"
"testing"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
func TestProviderIsAvailableOverUnixSocket(t *testing.T) {
socketPath := filepath.Join("/tmp", "gopeed-rpcwebview-"+t.Name()+".sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
defer os.Remove(socketPath)
server := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req enginewebview.RPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatal(err)
}
if req.Method != enginewebview.MethodIsAvailable {
t.Fatalf("unexpected unix method: %s", req.Method)
}
if err := json.NewEncoder(w).Encode(map[string]any{
"result": enginewebview.IsAvailableResult{Available: true},
"error": nil,
}); err != nil {
t.Fatal(err)
}
}),
}
defer server.Close()
go server.Serve(listener)
provider := New(enginewebview.RPCConfig{
Network: "unix",
Address: socketPath,
})
if !provider.IsAvailable() {
t.Fatal("expected unix provider to be available")
}
}
@@ -1,186 +0,0 @@
package rpcprovider
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
func TestProviderLifecycleOverHTTP(t *testing.T) {
var calls []enginewebview.RPCRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Fatalf("unexpected method: %s", r.Method)
}
if r.URL.Path != enginewebview.RPCEndpointPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer secret-token" {
t.Fatalf("unexpected auth header: %q", got)
}
var req enginewebview.RPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatal(err)
}
calls = append(calls, req)
var body any
switch req.Method {
case enginewebview.MethodIsAvailable:
body = map[string]any{
"result": enginewebview.IsAvailableResult{Available: true},
"error": nil,
}
case enginewebview.MethodPageOpen:
body = map[string]any{
"result": enginewebview.PageOpenResult{PageID: "page-open-1"},
"error": nil,
}
case enginewebview.MethodPageAddInitScript, enginewebview.MethodPageGoto:
body = map[string]any{
"result": enginewebview.EmptyResult{},
"error": nil,
}
case enginewebview.MethodPageExecute:
body = map[string]any{
"result": map[string]any{"title": "Example"},
"error": nil,
}
case enginewebview.MethodPageClose:
body = map[string]any{
"result": enginewebview.EmptyResult{},
"error": nil,
}
default:
t.Fatalf("unexpected method: %s", req.Method)
}
if err := json.NewEncoder(w).Encode(body); err != nil {
t.Fatal(err)
}
}))
defer server.Close()
cfg := enginewebview.RPCConfig{
Network: "tcp",
Address: server.Listener.Addr().String(),
Token: "secret-token",
}
provider := New(cfg)
if !provider.IsAvailable() {
t.Fatal("expected provider to be available")
}
opener, ok := provider.(enginewebview.Opener)
if !ok {
t.Fatal("expected provider to implement runtime webview opener")
}
opened, err := opener.Open(enginewebview.OpenOptions{
Headless: true,
Width: 1280,
Height: 720,
Title: "Gopeed",
UserAgent: "UA",
})
if err != nil {
t.Fatal(err)
}
if err := opened.AddInitScript("window.__TEST__ = true"); err != nil {
t.Fatal(err)
}
if err := opened.Goto("https://example.com", enginewebview.GotoOptions{
TimeoutMS: 3210,
WaitUntil: "domcontentloaded",
}); err != nil {
t.Fatal(err)
}
value, err := opened.Execute("document.title")
if err != nil {
t.Fatal(err)
}
got, ok := value.(map[string]any)
if !ok || got["title"] != "Example" {
t.Fatalf("unexpected evaluate result: %#v", value)
}
if err := opened.Close(); err != nil {
t.Fatal(err)
}
expectedMethods := []string{
enginewebview.MethodIsAvailable,
enginewebview.MethodIsAvailable,
enginewebview.MethodPageOpen,
enginewebview.MethodPageAddInitScript,
enginewebview.MethodPageGoto,
enginewebview.MethodPageExecute,
enginewebview.MethodPageClose,
}
if len(calls) != len(expectedMethods) {
t.Fatalf("unexpected call count: got %d want %d", len(calls), len(expectedMethods))
}
for i, method := range expectedMethods {
if calls[i].Method != method {
t.Fatalf("unexpected method at %d: got %s want %s", i, calls[i].Method, method)
}
}
gotoParamsJSON, err := json.Marshal(calls[4].Params)
if err != nil {
t.Fatal(err)
}
var gotoParams enginewebview.PageGotoParams
if err := json.Unmarshal(gotoParamsJSON, &gotoParams); err != nil {
t.Fatal(err)
}
if gotoParams.WaitUntil != "domcontentloaded" {
t.Fatalf("unexpected waitUntil: %q", gotoParams.WaitUntil)
}
}
func TestProviderLaunchUnavailableWithoutEndpoint(t *testing.T) {
provider := New(enginewebview.RPCConfig{})
if provider.IsAvailable() {
t.Fatal("expected empty provider to be unavailable")
}
opener, ok := provider.(enginewebview.Opener)
if !ok {
t.Fatal("expected provider to implement runtime webview opener")
}
if _, err := opener.Open(enginewebview.OpenOptions{}); err == nil || err.Error() != enginewebview.ErrUnavailable.Error() {
t.Fatalf("unexpected launch error: %v", err)
}
}
func TestPageSetCookieParamsOmitsZeroExpires(t *testing.T) {
params := enginewebview.PageSetCookieParams{
PageID: "page-1",
Cookie: enginewebview.Cookie{
Name: "visible",
Value: "1",
Domain: "example.com",
Path: "/",
},
}
payload, err := json.Marshal(params)
if err != nil {
t.Fatal(err)
}
if string(payload) == "" {
t.Fatal("expected non-empty json payload")
}
if got := string(payload); strings.Contains(got, `"expires"`) {
t.Fatalf("unexpected zero expires in payload: %s", got)
}
params.Cookie.Expires = time.Now().UTC().Truncate(time.Second)
payload, err = json.Marshal(params)
if err != nil {
t.Fatal(err)
}
if got := string(payload); !strings.Contains(got, `"expires"`) {
t.Fatalf("expected expires in payload: %s", got)
}
}
-19
View File
@@ -14,7 +14,6 @@ import (
// Request download request // Request download request
type Request struct { type Request struct {
RawURL string `json:"rawUrl"`
URL string `json:"url"` URL string `json:"url"`
Extra any `json:"extra"` Extra any `json:"extra"`
// Labels is used to mark the download task // Labels is used to mark the download task
@@ -25,24 +24,6 @@ type Request struct {
SkipVerifyCert bool `json:"skipVerifyCert"` SkipVerifyCert bool `json:"skipVerifyCert"`
} }
// SetLabels replaces all labels on the request.
func (r *Request) SetLabels(labels map[string]string) {
r.Labels = labels
}
// PutLabel sets a label on the request.
func (r *Request) PutLabel(key, value string) {
if r.Labels == nil {
r.Labels = make(map[string]string)
}
r.Labels[key] = value
}
// DelLabel deletes a label from the request.
func (r *Request) DelLabel(key string) {
delete(r.Labels, key)
}
func (r *Request) Validate() error { func (r *Request) Validate() error {
if r.URL == "" { if r.URL == "" {
return fmt.Errorf("invalid request url") return fmt.Errorf("invalid request url")
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+78 -126
View File
@@ -3,19 +3,16 @@ package engine
import ( import (
_ "embed" _ "embed"
"errors" "errors"
"fmt"
"sync"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error" gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/stream"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop" "github.com/dop251/goja_nodejs/eventloop"
gojaurl "github.com/dop251/goja_nodejs/url" gojaurl "github.com/dop251/goja_nodejs/url"
"time"
) )
//go:embed polyfill/out/index.js //go:embed polyfill/out/index.js
@@ -27,118 +24,93 @@ type Engine struct {
Runtime *goja.Runtime Runtime *goja.Runtime
} }
type JSFunction func(goja.FunctionCall) goja.Value
// RunString executes the script and returns the go type value // RunString executes the script and returns the go type value
// if script result is promise, it will be resolved // if script result is promise, it will be resolved
func (e *Engine) RunString(script string) (value any, err error) { func (e *Engine) RunString(script string) (value any, err error) {
return e.runOnLoop(func(runtime *goja.Runtime) (goja.Value, error) { defer func() {
return runtime.RunString(script) if r := recover(); r != nil {
err = r.(error)
}
}()
var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) {
result, err = runtime.RunString(script)
if err == nil {
go e.await(result)
}
}) })
if err != nil {
return
}
return resolveResult(result)
} }
// CallFunction calls the function and returns the go type value // CallFunction calls the function and returns the go type value
// if function result is promise, it will be resolved // if function result is promise, it will be resolved
func (e *Engine) CallFunction(fn any, args ...any) (value any, err error) { func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
return e.runOnLoop(func(runtime *goja.Runtime) (goja.Value, error) { defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) {
if args == nil {
result, err = fn(nil)
} else {
var jsArgs []goja.Value var jsArgs []goja.Value
for _, arg := range args { for _, arg := range args {
jsArgs = append(jsArgs, runtime.ToValue(arg)) jsArgs = append(jsArgs, runtime.ToValue(arg))
} }
switch f := fn.(type) { result, err = fn(nil, jsArgs...)
case goja.Callable:
if args == nil {
return f(nil)
} }
return f(nil, jsArgs...) if err == nil {
case JSFunction: go e.await(result)
return callExportedFunction(func(call goja.FunctionCall) goja.Value {
return f(call)
}, jsArgs...)
default:
return nil, fmt.Errorf("unsupported function type: %T", fn)
} }
}) })
if err != nil {
return
}
return resolveResult(result)
} }
func (e *Engine) runOnLoop(fn func(runtime *goja.Runtime) (goja.Value, error)) (any, error) { // loop.Run will hang if the script result has a non-stop code, such as setInterval.
type result struct { // This method will stop the event loop when the promise result is resolved.
value any func (e *Engine) await(value any) {
err error if value == nil {
}
ch := make(chan result, 1)
ok := e.loop.RunOnLoop(func(runtime *goja.Runtime) {
var once sync.Once
sendResult := func(res result) {
once.Do(func() {
ch <- res
})
}
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
sendResult(result{err: v})
case goja.Value:
sendResult(result{err: exportJSError(v)})
default:
sendResult(result{err: fmt.Errorf("panic: %v", r)})
}
}
}()
value, err := fn(runtime)
if err != nil {
sendResult(result{err: err})
return return
} }
if p, ok := value.Export().(*goja.Promise); ok {
switch p.State() { if v, ok := value.(goja.Value); ok {
case goja.PromiseStateFulfilled: // if result is promise, wait for it to be resolved
sendResult(result{value: exportJSValue(p.Result())}) if p, ok := v.Export().(*goja.Promise); ok {
return if p.State() != goja.PromiseStatePending {
case goja.PromiseStateRejected:
sendResult(result{err: exportJSError(p.Result())})
return return
} }
promiseObj := value.ToObject(runtime)
thenVal := promiseObj.Get("then") // check promise state every 100 milliseconds, until it is resolved
thenFn, ok := goja.AssertFunction(thenVal) for {
if !ok { time.Sleep(time.Millisecond * 100)
sendResult(result{err: errors.New("promise.then is not callable")}) if p.State() == goja.PromiseStatePending {
return continue
} }
onFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value { break
sendResult(result{value: exportJSValue(call.Argument(0))})
return goja.Undefined()
})
onRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
sendResult(result{err: exportJSError(call.Argument(0))})
return goja.Undefined()
})
if _, err := thenFn(promiseObj, onFulfilled, onRejected); err != nil {
sendResult(result{err: err})
} }
return
// stop the event loop
e.loop.StopNoWait()
} }
sendResult(result{value: exportJSValue(value)})
})
if !ok {
return nil, errors.New("engine loop terminated")
} }
res := <-ch
if res.err != nil {
return nil, res.err
}
return res.value, nil
} }
func (e *Engine) Close() { func (e *Engine) Close() {
e.loop.Terminate() e.loop.StopNoWait()
} }
type Config struct { type Config struct {
ProxyConfig *base.DownloaderProxyConfig ProxyConfig *base.DownloaderProxyConfig
StreamConfig *stream.Config
} }
func NewEngine(cfg *Config) *Engine { func NewEngine(cfg *Config) *Engine {
@@ -149,10 +121,7 @@ func NewEngine(cfg *Config) *Engine {
engine := &Engine{ engine := &Engine{
loop: loop, loop: loop,
} }
loop.Start() loop.Run(func(runtime *goja.Runtime) {
done := make(chan struct{})
loop.RunOnLoop(func(runtime *goja.Runtime) {
defer close(done)
engine.Runtime = runtime engine.Runtime = runtime
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime) vm.Enable(runtime)
@@ -184,12 +153,8 @@ func NewEngine(cfg *Config) *Engine {
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil { if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
return return
} }
if err := stream.Enable(runtime, loop, cfg.StreamConfig); err != nil {
return
}
return return
}) })
<-done
return engine return engine
} }
@@ -198,45 +163,32 @@ func Run(script string) (value any, err error) {
return engine.RunString(script) return engine.RunString(script)
} }
func exportJSValue(value goja.Value) any { // if the value is Promise, it will be resolved and return the result.
if value == nil { func resolveResult(value goja.Value) (any, error) {
return nil export := value.Export()
} switch export.(type) {
return value.Export() case *goja.Promise:
} p := export.(*goja.Promise)
switch p.State() {
func exportJSError(value goja.Value) error { case goja.PromiseStatePending:
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) { return nil, nil
return errors.New("promise rejected") case goja.PromiseStateFulfilled:
} return p.Result().Export(), nil
if err, ok := value.Export().(error); ok { case goja.PromiseStateRejected:
return err if err, ok := p.Result().Export().(error); ok {
} return nil, err
stack := value.String() } else {
if ro, ok := value.(*goja.Object); ok { stack := p.Result().String()
result := p.Result()
if ro, ok := result.(*goja.Object); ok {
stackVal := ro.Get("stack") stackVal := ro.Get("stack")
if stackVal != nil && stackVal.String() != "" { if stackVal != nil && stackVal.String() != "" {
stack = stackVal.String() stack = stackVal.String()
} }
} }
return errors.New(stack) return nil, errors.New(stack)
}
func callExportedFunction(fn func(goja.FunctionCall) goja.Value, args ...goja.Value) (ret goja.Value, err error) {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case error:
err = v
case goja.Value:
err = exportJSError(v)
default:
err = fmt.Errorf("panic: %v", r)
} }
} }
}() }
return fn(goja.FunctionCall{ return export, nil
This: nil,
Arguments: args,
}), nil
} }
-148
View File
@@ -26,11 +26,7 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "MessageError") doTestPolyfill(t, "MessageError")
doTestPolyfill(t, "XMLHttpRequest") doTestPolyfill(t, "XMLHttpRequest")
doTestPolyfill(t, "Blob") doTestPolyfill(t, "Blob")
doTestPolyfill(t, "Event")
doTestPolyfill(t, "EventTarget")
doTestPolyfill(t, "FormData") doTestPolyfill(t, "FormData")
doTestPolyfill(t, "AbortController")
doTestPolyfill(t, "AbortSignal")
doTestPolyfill(t, "TextDecoder") doTestPolyfill(t, "TextDecoder")
doTestPolyfill(t, "TextEncoder") doTestPolyfill(t, "TextEncoder")
doTestPolyfill(t, "fetch") doTestPolyfill(t, "fetch")
@@ -47,68 +43,6 @@ func TestError(t *testing.T) {
} }
} }
func TestCallFunction_DetachedAsyncWorkDoesNotBlockReturn(t *testing.T) {
engine := NewEngine(nil)
defer engine.Close()
if _, err := engine.RunString(`
globalThis.__done = false;
async function testDetached() {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
globalThis.__done = true;
})();
return "ok";
}
`); err != nil {
t.Fatal(err)
}
fn, ok := goja.AssertFunction(engine.Runtime.Get("testDetached"))
if !ok {
t.Fatal("testDetached is not callable")
}
startedAt := time.Now()
value, err := engine.CallFunction(fn)
if err != nil {
t.Fatal(err)
}
elapsed := time.Since(startedAt)
if elapsed > 200*time.Millisecond {
t.Fatalf("detached async work blocked return for %s", elapsed)
}
if value != "ok" {
t.Fatalf("unexpected return value: %#v", value)
}
}
func TestCallFunction_AwaitedPromiseBlocksUntilSettled(t *testing.T) {
engine := NewEngine(nil)
defer engine.Close()
if _, err := engine.RunString(`
async function testAwaited() {
await new Promise((resolve) => setTimeout(resolve, 120));
return "ok";
}
`); err != nil {
t.Fatal(err)
}
fn, ok := goja.AssertFunction(engine.Runtime.Get("testAwaited"))
if !ok {
t.Fatal("testAwaited is not callable")
}
startedAt := time.Now()
value, err := engine.CallFunction(fn)
if err != nil {
t.Fatal(err)
}
elapsed := time.Since(startedAt)
if elapsed < 100*time.Millisecond {
t.Fatalf("awaited promise returned too early: %s", elapsed)
}
if value != "ok" {
t.Fatalf("unexpected return value: %#v", value)
}
}
func TestFetch(t *testing.T) { func TestFetch(t *testing.T) {
server := startServer() server := startServer()
defer server.Close() defer server.Close()
@@ -130,50 +64,6 @@ async function testText(){
return await resp.text(); return await resp.text();
} }
async function testResponseBodyGetReader() {
const resp = await fetch(host+'/text', {
method: 'POST',
body: 'stream-body'
});
if (!resp.body || typeof resp.body.getReader !== 'function') {
throw new Error('response.body.getReader is not available');
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
result += decoder.decode(value, { stream: true });
}
result += decoder.decode();
return result;
}
async function testStreamingFetchResponseBody() {
const startedAt = Date.now();
const resp = await fetch(host+'/stream');
const resolvedAt = Date.now() - startedAt;
const reader = resp.body.getReader();
const decoder = new TextDecoder();
const first = await reader.read();
const firstAt = Date.now() - startedAt;
const second = await reader.read();
const secondAt = Date.now() - startedAt;
let result = '';
result += decoder.decode(first.value, { stream: true });
result += decoder.decode(second.value, { stream: true });
result += decoder.decode();
return {
resolvedAt,
firstAt,
secondAt,
text: result
};
}
async function testOctetStream(file){ async function testOctetStream(file){
const resp = await fetch(host+'/octetStream',{ const resp = await fetch(host+'/octetStream',{
method: 'POST', method: 'POST',
@@ -361,32 +251,6 @@ async function testFingerprintSafari(){
t.Fatalf("testText failed, want %s, got %s", "test", result) t.Fatalf("testText failed, want %s, got %s", "test", result)
} }
result, err = callTestFun(engine, "testResponseBodyGetReader")
if err != nil {
t.Fatal(err)
}
if result != "stream-body" {
t.Fatalf("testResponseBodyGetReader failed, want %s, got %s", "stream-body", result)
}
result, err = callTestFun(engine, "testStreamingFetchResponseBody")
if err != nil {
t.Fatal(err)
}
streaming := result.(map[string]any)
if streaming["text"] != "chunk-1chunk-2" {
t.Fatalf("testStreamingFetchResponseBody failed, want %s, got %v", "chunk-1chunk-2", streaming["text"])
}
if int(streaming["resolvedAt"].(int64)) >= 120 {
t.Fatalf("fetch resolved too late, got %dms", streaming["resolvedAt"].(int64))
}
if int(streaming["firstAt"].(int64)) >= 120 {
t.Fatalf("first chunk arrived too late, got %dms", streaming["firstAt"].(int64))
}
if int(streaming["secondAt"].(int64)) < 120 {
t.Fatalf("second chunk arrived too early, got %dms", streaming["secondAt"].(int64))
}
func() { func() {
jsFile, _, md5 := buildFile(t, engine.Runtime) jsFile, _, md5 := buildFile(t, engine.Runtime)
result, err = callTestFun(engine, "testOctetStream", jsFile) result, err = callTestFun(engine, "testOctetStream", jsFile)
@@ -600,18 +464,6 @@ func startServer() net.Listener {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(buf) w.Write(buf)
}) })
mux.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if flusher, ok := w.(http.Flusher); ok {
_, _ = w.Write([]byte("chunk-1"))
flusher.Flush()
time.Sleep(150 * time.Millisecond)
_, _ = w.Write([]byte("chunk-2"))
flusher.Flush()
return
}
_, _ = w.Write([]byte("chunk-1chunk-2"))
})
mux.HandleFunc("/octetStream", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/octetStream", func(w http.ResponseWriter, r *http.Request) {
md5 := calcMd5(r.Body) md5 := calcMd5(r.Body)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
File diff suppressed because it is too large. Load diff
@@ -1,278 +0,0 @@
package stream_test
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/GopeedLab/gopeed/pkg/download/engine"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/stream"
)
type createdObjectURL struct {
opts *stream.ObjectURLOptions
open stream.ObjectURLOpener
}
func TestResponseBlobObjectURLReadsResponseStream(t *testing.T) {
const payload = "response blob payload"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
_, _ = io.WriteString(w, payload[:8])
w.(http.Flusher).Flush() // force an unknown-length/chunked response
_, _ = io.WriteString(w, payload[8:])
}))
defer server.Close()
created := make(chan createdObjectURL, 1)
engine, cleanup := newStreamTestEngine(t, created)
defer func() {
cleanup()
engine.Close()
}()
value, err := engine.RunString(fmt.Sprintf(`
(async () => {
const response = await fetch(%q);
const blob = await response.blob();
return __gopeed_blob_create_object_url(blob);
})()
`, server.URL))
if err != nil {
t.Fatal(err)
}
if value != "blob:test-1" {
t.Fatalf("unexpected object URL: %#v", value)
}
object := <-created
if object.opts.Size != int64(len(payload)) || object.opts.ContentType != "text/plain" {
t.Fatalf("unexpected object URL options: %#v", object.opts)
}
if !object.opts.Range {
t.Fatal("materialized response Blob should advertise range support")
}
for i := 0; i < 2; i++ {
reader, err := object.open(context.Background(), stream.ObjectURLOpenRequest{Offset: 0, End: -1})
if err != nil {
t.Fatal(err)
}
data, readErr := io.ReadAll(reader)
_ = reader.Close()
if readErr != nil {
t.Fatal(readErr)
}
if got := string(data); got != payload {
t.Fatalf("unexpected response Blob data on open %d: %q", i+1, got)
}
}
rangeReader, err := object.open(context.Background(), stream.ObjectURLOpenRequest{Offset: 9, End: 12})
if err != nil {
t.Fatal(err)
}
rangeData, readErr := io.ReadAll(rangeReader)
_ = rangeReader.Close()
if readErr != nil {
t.Fatal(readErr)
}
if got, want := string(rangeData), payload[9:13]; got != want {
t.Fatalf("unexpected materialized Blob range: got %q want %q", got, want)
}
}
func TestResponseBlobRejectsTruncatedResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", "64")
_, _ = io.WriteString(w, "partial")
}))
defer server.Close()
created := make(chan createdObjectURL, 1)
engine, cleanup := newStreamTestEngine(t, created)
defer func() {
cleanup()
engine.Close()
}()
value, err := engine.RunString(fmt.Sprintf(`
(async () => {
const response = await fetch(%q);
try {
await response.blob();
return "resolved";
} catch (_) {
return "rejected";
}
})()
`, server.URL))
if err != nil {
t.Fatal(err)
}
if value != "rejected" {
t.Fatalf("truncated response unexpectedly materialized a Blob: %#v", value)
}
}
func TestEmptyBlobObjectURL(t *testing.T) {
created := make(chan createdObjectURL, 1)
engine, cleanup := newStreamTestEngine(t, created)
defer func() {
cleanup()
engine.Close()
}()
value, err := engine.RunString(`
__gopeed_blob_create_object_url(new Blob([], { type: "application/empty" }))
`)
if err != nil {
t.Fatal(err)
}
if value != "blob:test-1" {
t.Fatalf("unexpected object URL: %#v", value)
}
object := <-created
if object.opts.Size != 0 || object.opts.Range {
t.Fatalf("unexpected empty Blob options: %#v", object.opts)
}
reader, err := object.open(context.Background(), stream.ObjectURLOpenRequest{Offset: 0, End: -1})
if err != nil {
t.Fatal(err)
}
defer reader.Close()
data, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
if len(data) != 0 {
t.Fatalf("expected empty Blob, got %q", data)
}
}
func TestBlobObjectURLReaderCloseCancelsPendingJSReader(t *testing.T) {
created := make(chan createdObjectURL, 1)
engine, cleanup := newStreamTestEngine(t, created)
defer func() {
cleanup()
engine.Close()
}()
value, err := engine.RunString(`
globalThis.__blobPipeCancelState = {
readCalls: 0,
cancelCalls: 0,
releaseCalls: 0,
cancelReason: "",
resolveRead: null,
};
const pendingSource = {
getReader() {
return {
read() {
__blobPipeCancelState.readCalls++;
return new Promise((resolve) => {
__blobPipeCancelState.resolveRead = resolve;
});
},
cancel(reason) {
__blobPipeCancelState.cancelCalls++;
__blobPipeCancelState.cancelReason = String(reason);
if (__blobPipeCancelState.resolveRead) {
__blobPipeCancelState.resolveRead({ done: true, value: undefined });
}
return Promise.resolve();
},
releaseLock() {
__blobPipeCancelState.releaseCalls++;
},
};
},
};
__gopeed_blob_create_object_url(async () => pendingSource);
`)
if err != nil {
t.Fatal(err)
}
if value != "blob:test-1" {
t.Fatalf("unexpected object URL: %#v", value)
}
object := <-created
reader, err := object.open(context.Background(), stream.ObjectURLOpenRequest{Offset: 0, End: -1})
if err != nil {
t.Fatal(err)
}
readDone := make(chan error, 1)
go func() {
_, readErr := reader.Read(make([]byte, 1))
readDone <- readErr
}()
waitForJSValue(t, engine, `String(__blobPipeCancelState.readCalls)`, "1")
if err := reader.Close(); err != nil {
t.Fatal(err)
}
if err := reader.Close(); err != nil {
t.Fatal(err)
}
select {
case readErr := <-readDone:
if !errors.Is(readErr, context.Canceled) {
t.Fatalf("unexpected pending read result: %v", readErr)
}
case <-time.After(2 * time.Second):
t.Fatal("pending Go read was not released by Close")
}
waitForJSValue(t, engine, `JSON.stringify({
cancelCalls: __blobPipeCancelState.cancelCalls,
releaseCalls: __blobPipeCancelState.releaseCalls,
cancelReason: __blobPipeCancelState.cancelReason,
})`, `{"cancelCalls":1,"releaseCalls":1,"cancelReason":"blob request closed"}`)
}
func waitForJSValue(t *testing.T, engine *engine.Engine, expression, want string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
var got any
for time.Now().Before(deadline) {
var err error
got, err = engine.RunString(expression)
if err != nil {
t.Fatal(err)
}
if got == want {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("timed out waiting for JavaScript value %q, got %#v", want, got)
}
func newStreamTestEngine(t *testing.T, created chan<- createdObjectURL) (*engine.Engine, func()) {
t.Helper()
var next atomic.Int64
var cleanup func()
e := engine.NewEngine(&engine.Config{
StreamConfig: &stream.Config{
CreateObjectURL: func(opts *stream.ObjectURLOptions, open stream.ObjectURLOpener) (string, error) {
created <- createdObjectURL{opts: opts, open: open}
return fmt.Sprintf("blob:test-%d", next.Add(1)), nil
},
RevokeObjectURL: func(string) error { return nil },
RegisterCleanup: func(fn func()) {
cleanup = fn
},
},
})
if cleanup == nil {
t.Fatal("stream cleanup was not registered")
}
return e, cleanup
}
@@ -1,191 +0,0 @@
package stream
import (
"context"
"io"
"strings"
"sync/atomic"
"testing"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/dop251/goja"
)
func TestExportFetchRequestSnapshotsArrayBuffer(t *testing.T) {
runtime := goja.New()
buffer := runtime.NewArrayBuffer([]byte("before"))
request := runtime.NewObject()
setFetchRequestDefaults(t, request)
if err := request.Set("body", buffer); err != nil {
t.Fatal(err)
}
meta, err := exportFetchRequest(runtime, request)
if err != nil {
t.Fatal(err)
}
buffer.Bytes()[0] = 'X'
body, ok := meta.Body.([]byte)
if !ok {
t.Fatalf("expected byte snapshot, got %T", meta.Body)
}
if got := string(body); got != "before" {
t.Fatalf("ArrayBuffer snapshot changed after export: %q", got)
}
}
func TestExportFetchRequestSnapshotsFormData(t *testing.T) {
runtime := goja.New()
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
if err := formdata.Enable(runtime); err != nil {
t.Fatal(err)
}
value, err := runtime.RunString(`
globalThis.form = new FormData();
form.append("field", "before");
form;
`)
if err != nil {
t.Fatal(err)
}
request := runtime.NewObject()
setFetchRequestDefaults(t, request)
if err := request.Set("body", value); err != nil {
t.Fatal(err)
}
meta, err := exportFetchRequest(runtime, request)
if err != nil {
t.Fatal(err)
}
if _, err := runtime.RunString(`form.set("field", "after")`); err != nil {
t.Fatal(err)
}
body, ok := meta.Body.(*fetchFormDataSnapshot)
if !ok {
t.Fatalf("expected FormData snapshot, got %T", meta.Body)
}
if len(body.entries) != 1 {
t.Fatalf("expected one FormData entry, got %d", len(body.entries))
}
if got := body.entries[0]; got.name != "field" || got.value != "before" {
t.Fatalf("unexpected FormData snapshot: %#v", got)
}
}
func setFetchRequestDefaults(t *testing.T, request *goja.Object) {
t.Helper()
for key, value := range map[string]any{
"url": "https://example.test",
"method": "POST",
"redirect": "follow",
"credentials": "same-origin",
} {
if err := request.Set(key, value); err != nil {
t.Fatal(err)
}
}
}
func TestSnapshotFetchBodyCopiesFileMetadata(t *testing.T) {
source := &file.File{
Reader: strings.NewReader("payload"),
Name: "before.txt",
Size: 7,
}
value, err := snapshotFetchBody(source)
if err != nil {
t.Fatal(err)
}
snapshot, ok := value.(*file.File)
if !ok {
t.Fatalf("expected file snapshot, got %T", value)
}
source.Name = "after.txt"
source.Size = 99
if snapshot.Name != "before.txt" || snapshot.Size != 7 {
t.Fatalf("file metadata was not snapshotted: %#v", snapshot)
}
}
func TestFetchRegistryCloseAll(t *testing.T) {
registry := newFetchRegistry()
streamCtx, cancel := context.WithCancel(context.Background())
body := &trackingReadCloser{}
registry.streams["active"] = &fetchStream{
body: body,
cancel: cancel,
ctx: streamCtx,
ch: make(chan fetchChunk),
}
registry.CloseAll()
registry.CloseAll()
select {
case <-streamCtx.Done():
default:
t.Fatal("active fetch context was not canceled")
}
if got := body.closeCalls.Load(); got != 1 {
t.Fatalf("expected body to close once, got %d", got)
}
registry.mu.Lock()
closed := registry.closed
remaining := len(registry.streams)
registry.mu.Unlock()
if !closed || remaining != 0 {
t.Fatalf("registry not fully closed: closed=%v remaining=%d", closed, remaining)
}
if _, err := registry.Open("", nil, &fetchRequest{URL: "https://example.test"}); err == nil {
t.Fatal("expected a closed registry to reject new fetches")
}
}
func TestFetchRegistryReadPreservesChunkOrder(t *testing.T) {
registry := newFetchRegistry()
ctx, cancel := context.WithCancel(context.Background())
stream := &fetchStream{
body: io.NopCloser(strings.NewReader("")),
cancel: cancel,
ctx: ctx,
ch: make(chan fetchChunk, 2),
}
stream.ch <- fetchChunk{data: []byte("abc")}
stream.ch <- fetchChunk{data: []byte("def")}
registry.streams["ordered"] = stream
defer registry.CloseAll()
var got strings.Builder
for range 4 {
chunk, done, err := registry.Read("ordered", 2)
if err != nil {
t.Fatal(err)
}
if done {
t.Fatal("stream ended before buffered chunks were read")
}
got.Write(chunk)
}
if got.String() != "abcdef" {
t.Fatalf("fetch chunks were reordered: %q", got.String())
}
}
type trackingReadCloser struct {
closeCalls atomic.Int32
}
func (r *trackingReadCloser) Read([]byte) (int, error) {
return 0, io.EOF
}
func (r *trackingReadCloser) Close() error {
r.closeCalls.Add(1)
return nil
}
File diff suppressed because it is too large. Load diff
+1 -8
View File
@@ -1,8 +1,6 @@
package vm package vm
import ( import (
"fmt"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop" "github.com/dop251/goja_nodejs/eventloop"
) )
@@ -27,12 +25,7 @@ func (vm *Vm) Get(name string) (value any) {
func (vm *Vm) RunString(script string) (value any, err error) { func (vm *Vm) RunString(script string) (value any, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
switch v := r.(type) { err = r.(error)
case error:
err = v
default:
err = fmt.Errorf("panic: %v", r)
}
} }
}() }()
+1 -1
View File
@@ -1 +1 @@
(()=>{var e={847(e,t,r){var n,o;n=function(){"use strict";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function t(e,t,r){return t=l(t),function(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(e,c()?Reflect.construct(t,r||[],l(e).constructor):t.apply(e,r))}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,f(n.key),n)}}function i(e,t,r){return t&&o(e.prototype,t),r&&o(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function a(t,r){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=function(t,r){if(t){if("string"==typeof t)return e(t,r);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?e(t,r):void 0}}(t))||r&&t&&"number"==typeof t.length){n&&(t=n);var o=0,i=function(){};return{s:i,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,l=!1;return{s:function(){n=n.call(t)},n:function(){var e=n.next();return s=e.done,e},e:function(e){l=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}}function s(){return s="undefined"!=typeof Reflect&&Reflect.get?Reflect.get.bind():function(e,t,r){var n=function(e,t){for(;!{}.hasOwnProperty.call(e,t)&&null!==(e=l(e)););return e}(e,t);if(n){var o=Object.getOwnPropertyDescriptor(n,t);return o.get?o.get.call(arguments.length<3?e:r):o.value}},s.apply(null,arguments)}function l(e){return l=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},l(e)}function c(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(c=function(){return!!e})()}function u(e,t){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},u(e,t)}function f(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}!function(e){e.AbortSignal,e.AbortController}("undefined"!=typeof self?self:r.g);var p=function(){return i(function e(){n(this,e),Object.defineProperty(this,"listeners",{value:{},writable:!0,configurable:!0})},[{key:"addEventListener",value:function(e,t,r){e in this.listeners||(this.listeners[e]=[]),this.listeners[e].push({callback:t,options:r})}},{key:"removeEventListener",value:function(e,t){if(e in this.listeners)for(var r=this.listeners[e],n=0,o=r.length;n<o;n++)if(r[n].callback===t)return void r.splice(n,1)}},{key:"dispatchEvent",value:function(e){var t=this;if(e.type in this.listeners){for(var r=this.listeners[e.type].slice(),n=function(){var n=r[o];try{n.callback.call(t,e)}catch(e){Promise.resolve().then(function(){throw e})}n.options&&n.options.once&&t.removeEventListener(e.type,n.callback)},o=0,i=r.length;o<i;o++)n();return!e.defaultPrevented}}}])}(),d=function(e){function r(){var e;return n(this,r),(e=t(this,r)).listeners||p.call(e),Object.defineProperty(e,"aborted",{value:!1,writable:!0,configurable:!0}),Object.defineProperty(e,"onabort",{value:null,writable:!0,configurable:!0}),Object.defineProperty(e,"reason",{value:void 0,writable:!0,configurable:!0}),e}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&u(e,t)}(r,e),i(r,[{key:"toString",value:function(){return"[object AbortSignal]"}},{key:"dispatchEvent",value:function(e){var t,n,o,i,a;"abort"===e.type&&(this.aborted=!0,"function"==typeof this.onabort&&this.onabort.call(this,e)),(t=r,n="dispatchEvent",o=this,a=s(l(1&(i=3)?t.prototype:t),n,o),2&i&&"function"==typeof a?function(e){return a.apply(o,e)}:a)([e])}},{key:"throwIfAborted",value:function(){var e=this.aborted,t=this.reason;if(e)throw void 0===t?"Aborted":t}}],[{key:"timeout",value:function(e){var t=new h;return setTimeout(function(){return t.abort(new DOMException("This signal is timeout in ".concat(e,"ms"),"TimeoutError"))},e),t.signal}},{keLine truncated (()=>{var e={725:function(e,t,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(e){"use strict";var t=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=t&&t.prototype.append&&t.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function c(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function h(e,r){r=r||{};var o=new t;return c(e).forEach((function(e){o.append(e)})),r.type?o.getBlob(r.type):o.getBlob()}function d(e,t){return new o(c(e),t||{})}i.Blob&&(h.prototype=Blob.prototype,d.prototype=Blob.prototype);var p="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var t=0,r=e.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);t<r;){var u=e.charCodeAt(t++);if(u>=55296&&u<=56319){if(t<r){var f=e.charCodeAt(t);56320==(64512&f)&&(++t,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+t/e.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o<t;){var n,i,a,s,u=e[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=t)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",d=0;d<c;)h+=String.fromCharCode.apply(String,r.slice(d,d+=4096));return h};function b(){var t=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;t&&r&&(XMLHttpRequest.prototype.send=function(e){e instanceof Blob?(this.setRequestHeader("Content-Type",e.type),r.call(this,e)):r.call(this,e)});try{new File([],""),e.File=i.File,e.FileReader=i.FileReader}catch(t){try{e.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(t){e.File=function(e,t,r){var o=new Blob(e,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=t.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),e.Blob=f?i.Blob:d):l?(b(),e.Blob=h):function(){function t(e){for(var t=new Array(e.byteLength),r=new Uint8Array(e),o=t.length;o--;)t[o]=r[o];return t}function o(e){for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<e.length;o+=3){var n=e[o],i=o+1<e.length,a=i?e[o+1]:0,s=o+2<e.length,u=s?e[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(t[f],t[l],t[c],t[h])}return r.join("")}var s=Object.create||function(e){function t(){}return t.prototype=e,new t};function u(e){return Object.prototype.toString.call(e).slice(8,-1)}function f(e,t){return"object"==typeof e&&Object.prototype.isPrototypeOf.call(e.prototype,t)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(e){return t=l,r=u(e),-1!==t.indexOf(r)||f(i.ArrayBuffer,e);var t,r}function h(e,r){r=null==r?{}:r;for(var o=0,n=(e=e?e.slice():[]).length;o<n;o++){var a=e[o];a instanceof h?e[o]=a._buffer:"string"==typeof a?e[o]=p(a):"DataView"===u(s=a)||f(i.DataView,s)?e[o]=t(a.buffer):c(a)?e[o]=t(a):e[o]=p(String(a))}var s;this._buffer=i.Uint8Array?function(e){for(var t=0,r=e.length;r--;)t+=e[r].length;for(var o=new Uint8Array(t),n=0,i=0;i<e.length;i++){var a=e[i];o.set(a,n),n+=a.byteLength||a.length}return o}(e):[].concat.apply([],e),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
File diff suppressed because it is too large. Load diff
@@ -18,9 +18,7 @@
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },
"dependencies": { "dependencies": {
"abortcontroller-polyfill": "^1.7.8",
"blob-polyfill": "^7.0.20220408", "blob-polyfill": "^7.0.20220408",
"event-target-shim": "^6.0.2",
"fastestsmallesttextencoderdecoder": "^1.0.22", "fastestsmallesttextencoderdecoder": "^1.0.22",
"whatwg-fetch": "^3.6.20" "whatwg-fetch": "^3.6.20"
} }
@@ -1,2 +0,0 @@
import "../event/index.js";
import "abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js";
@@ -1,9 +0,0 @@
import { Event, EventTarget } from "event-target-shim";
if (typeof globalThis.Event !== "function") {
globalThis.Event = Event;
}
if (typeof globalThis.EventTarget !== "function") {
globalThis.EventTarget = EventTarget;
}
@@ -1,5 +1,3 @@
import "./event/index.js"
import "./abort/index.js"
import "./blob/index.js" import "./blob/index.js"
import "./crypto/index.js" import "./crypto/index.js"
// polyfill TextEncoder // polyfill TextEncoder
-20
View File
@@ -1,20 +0,0 @@
package webview
type Provider interface {
IsAvailable() bool
Open(opts OpenOptions) (Page, error)
}
type unavailableProvider struct{}
func NewUnavailableProvider() Provider {
return unavailableProvider{}
}
func (unavailableProvider) IsAvailable() bool {
return false
}
func (unavailableProvider) Open(OpenOptions) (Page, error) {
return nil, ErrUnavailable
}
Loaded 50 of 112 files, more files were not shown because too many files have changed in this diff. Show more