diff --git a/v3/examples/binding/go.mod b/v3/examples/binding/go.mod index 7fe99e7db..47ffa7a4a 100644 --- a/v3/examples/binding/go.mod +++ b/v3/examples/binding/go.mod @@ -1,6 +1,8 @@ module binding -go 1.20 +go 1.21 + +toolchain go1.21.0 require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 @@ -17,7 +19,6 @@ require ( github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/go-webview2 v1.0.2 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.5.1 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.9.0 // indirect @@ -25,6 +26,4 @@ require ( replace github.com/wailsapp/wails/v3 => ../.. -replace github.com/wailsapp/wails/v2 => ../../../v2 - replace github.com/ebitengine/purego v0.4.0-alpha.4 => github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01 diff --git a/v3/examples/frameless/go.mod b/v3/examples/frameless/go.mod index fc7e373b9..7265c79f2 100644 --- a/v3/examples/frameless/go.mod +++ b/v3/examples/frameless/go.mod @@ -1,6 +1,8 @@ module frameless -go 1.20 +go 1.21 + +toolchain go1.21.0 require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 @@ -17,12 +19,9 @@ require ( github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/go-webview2 v1.0.2 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.5.1 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.9.0 // indirect ) replace github.com/wailsapp/wails/v3 => ../.. - -replace github.com/wailsapp/wails/v2 => ../../../v2 diff --git a/v3/examples/plugins/go.mod b/v3/examples/plugins/go.mod index 6b15a58cc..1e82aa24f 100644 --- a/v3/examples/plugins/go.mod +++ b/v3/examples/plugins/go.mod @@ -1,6 +1,8 @@ module plugin_demo -go 1.20 +go 1.21 + +toolchain go1.21.0 require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 @@ -23,7 +25,6 @@ require ( github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/go-webview2 v1.0.2 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.5.1 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.7.0 // indirect @@ -42,5 +43,3 @@ require ( ) replace github.com/wailsapp/wails/v3 => ../.. - -replace github.com/wailsapp/wails/v2 => ../../../v2 diff --git a/v3/go.mod b/v3/go.mod index fd395bbe5..b919e71a5 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -28,7 +28,8 @@ require ( github.com/samber/lo v1.37.0 github.com/tc-hib/winres v0.1.6 github.com/wailsapp/go-webview2 v1.0.2 - github.com/wailsapp/wails/v2 v2.5.1 + github.com/wailsapp/mimetype v1.4.1 + golang.org/x/net v0.7.0 golang.org/x/sys v0.9.0 modernc.org/sqlite v1.21.0 ) @@ -56,7 +57,6 @@ require ( github.com/joho/godotenv v1.5.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect - github.com/leaanthony/slicer v1.5.0 // indirect github.com/lithammer/fuzzysearch v1.1.5 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-zglob v0.0.4 // indirect @@ -70,14 +70,12 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/sajari/fuzzy v1.0.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect - github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/image v0.5.0 // indirect golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/term v0.9.0 // indirect @@ -99,6 +97,4 @@ require ( mvdan.cc/sh/v3 v3.7.0 // indirect ) -replace github.com/wailsapp/wails/v2 => ../v2 - replace github.com/ebitengine/purego v0.4.0-alpha.4 => github.com/tmclane/purego v0.0.0-20230601213035-1f25e70d7b01 diff --git a/v3/go.sum b/v3/go.sum index 72dedf374..7fcc0daa4 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -232,7 +232,6 @@ github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oO github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= -github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= diff --git a/v3/internal/debug/debug.go b/v3/internal/debug/debug.go index be3480a23..d0f0c9dc2 100644 --- a/v3/internal/debug/debug.go +++ b/v3/internal/debug/debug.go @@ -32,8 +32,6 @@ func isLocalBuild() bool { // RelativePath returns a qualified path created by joining the // directory of the calling file and the given relative path. -// -// Example: RelativePath("..") in *this* file would give you '/path/to/wails2/v2/internal` func RelativePath(relativepath string, optionalpaths ...string) string { _, thisFile, _, _ := runtime.Caller(1) localDir := filepath.Dir(thisFile) diff --git a/v3/internal/plugins/template/go.mod.tmpl b/v3/internal/plugins/template/go.mod.tmpl index 77249136f..84ff2e07b 100644 --- a/v3/internal/plugins/template/go.mod.tmpl +++ b/v3/internal/plugins/template/go.mod.tmpl @@ -8,6 +8,5 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/leaanthony/slicer v1.5.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect ) diff --git a/v3/internal/plugins/template/go.sum b/v3/internal/plugins/template/go.sum index 29c7b303e..2e2337936 100644 --- a/v3/internal/plugins/template/go.sum +++ b/v3/internal/plugins/template/go.sum @@ -5,8 +5,6 @@ github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5Az github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 h1:Wn+nhnS+VytzE0PegUzSh4T3hXJCtggKGD/4U5H9+wQ= -github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8= github.com/wailsapp/wails/v3 v3.0.0-alpha.0 h1:T5gqG98Xr8LBf69oxlPkhpsFD59w2SnqUZk6XHj8Zoc= github.com/wailsapp/wails/v3 v3.0.0-alpha.0/go.mod h1:OAfO5bP0TSUvCIHZYc6Dqfow/9RqxzHvYtmhWPpo1c0= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/v3/internal/templates/_base/default/Taskfile.tmpl.yml b/v3/internal/templates/_base/default/Taskfile.tmpl.yml index f21158a39..6fe474737 100644 --- a/v3/internal/templates/_base/default/Taskfile.tmpl.yml +++ b/v3/internal/templates/_base/default/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,7 +52,7 @@ tasks: - windows cmds: - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp.exe +- task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build build: diff --git a/v3/internal/templates/_base/default/go.mod.tmpl b/v3/internal/templates/_base/default/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/_base/default/go.mod.tmpl +++ b/v3/internal/templates/_base/default/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/lit-ts/Taskfile.tmpl.yml b/v3/internal/templates/lit-ts/Taskfile.tmpl.yml index f21158a39..6fe474737 100644 --- a/v3/internal/templates/lit-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/lit-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,7 +52,7 @@ tasks: - windows cmds: - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp.exe +- task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build build: diff --git a/v3/internal/templates/lit-ts/go.mod.tmpl b/v3/internal/templates/lit-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/lit-ts/go.mod.tmpl +++ b/v3/internal/templates/lit-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/lit/Taskfile.tmpl.yml b/v3/internal/templates/lit/Taskfile.tmpl.yml index f21158a39..af4686530 100644 --- a/v3/internal/templates/lit/Taskfile.tmpl.yml +++ b/v3/internal/templates/lit/Taskfile.tmpl.yml @@ -39,8 +39,9 @@ tasks: - darwin cmds: - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/lit/go.mod.tmpl b/v3/internal/templates/lit/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/lit/go.mod.tmpl +++ b/v3/internal/templates/lit/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/preact-ts/Taskfile.tmpl.yml b/v3/internal/templates/preact-ts/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/preact-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/preact-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/preact-ts/go.mod.tmpl b/v3/internal/templates/preact-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/preact-ts/go.mod.tmpl +++ b/v3/internal/templates/preact-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/preact/Taskfile.tmpl.yml b/v3/internal/templates/preact/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/preact/Taskfile.tmpl.yml +++ b/v3/internal/templates/preact/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/preact/go.mod.tmpl b/v3/internal/templates/preact/go.mod.tmpl index 3883674e9..5a7721646 100644 --- a/v3/internal/templates/preact/go.mod.tmpl +++ b/v3/internal/templates/preact/go.mod.tmpl @@ -8,10 +8,8 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/leaanthony/slicer v1.5.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/react-swc-ts/Taskfile.tmpl.yml b/v3/internal/templates/react-swc-ts/Taskfile.tmpl.yml index f21158a39..6fe474737 100644 --- a/v3/internal/templates/react-swc-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/react-swc-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,7 +52,7 @@ tasks: - windows cmds: - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp.exe +- task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build build: diff --git a/v3/internal/templates/react-swc-ts/go.mod.tmpl b/v3/internal/templates/react-swc-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/react-swc-ts/go.mod.tmpl +++ b/v3/internal/templates/react-swc-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/react-swc/Taskfile.tmpl.yml b/v3/internal/templates/react-swc/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/react-swc/Taskfile.tmpl.yml +++ b/v3/internal/templates/react-swc/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/react-swc/go.mod.tmpl b/v3/internal/templates/react-swc/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/react-swc/go.mod.tmpl +++ b/v3/internal/templates/react-swc/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/react-ts/Taskfile.tmpl.yml b/v3/internal/templates/react-ts/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/react-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/react-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/react-ts/go.mod.tmpl b/v3/internal/templates/react-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/react-ts/go.mod.tmpl +++ b/v3/internal/templates/react-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/react/Taskfile.tmpl.yml b/v3/internal/templates/react/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/react/Taskfile.tmpl.yml +++ b/v3/internal/templates/react/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/react/go.mod.tmpl b/v3/internal/templates/react/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/react/go.mod.tmpl +++ b/v3/internal/templates/react/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/svelte-ts/Taskfile.tmpl.yml b/v3/internal/templates/svelte-ts/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/svelte-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/svelte-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/svelte-ts/go.mod.tmpl b/v3/internal/templates/svelte-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/svelte-ts/go.mod.tmpl +++ b/v3/internal/templates/svelte-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/svelte/Taskfile.tmpl.yml b/v3/internal/templates/svelte/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/svelte/Taskfile.tmpl.yml +++ b/v3/internal/templates/svelte/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/svelte/go.mod.tmpl b/v3/internal/templates/svelte/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/svelte/go.mod.tmpl +++ b/v3/internal/templates/svelte/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/vanilla-ts/Taskfile.tmpl.yml b/v3/internal/templates/vanilla-ts/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/vanilla-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/vanilla-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/vanilla-ts/go.mod.tmpl b/v3/internal/templates/vanilla-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/vanilla-ts/go.mod.tmpl +++ b/v3/internal/templates/vanilla-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/vanilla/Taskfile.tmpl.yml b/v3/internal/templates/vanilla/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/vanilla/Taskfile.tmpl.yml +++ b/v3/internal/templates/vanilla/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/vanilla/go.mod.tmpl b/v3/internal/templates/vanilla/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/vanilla/go.mod.tmpl +++ b/v3/internal/templates/vanilla/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/vue-ts/Taskfile.tmpl.yml b/v3/internal/templates/vue-ts/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/vue-ts/Taskfile.tmpl.yml +++ b/v3/internal/templates/vue-ts/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/vue-ts/go.mod.tmpl b/v3/internal/templates/vue-ts/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/vue-ts/go.mod.tmpl +++ b/v3/internal/templates/vue-ts/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/internal/templates/vue/Taskfile.tmpl.yml b/v3/internal/templates/vue/Taskfile.tmpl.yml index f21158a39..68631d952 100644 --- a/v3/internal/templates/vue/Taskfile.tmpl.yml +++ b/v3/internal/templates/vue/Taskfile.tmpl.yml @@ -38,9 +38,10 @@ tasks: platforms: - darwin cmds: - - task: pre-build - - go build -gcflags=all="-N -l" -o bin/testapp - - task: post-build + - task: pre-build + - task: build-frontend + - go build -gcflags=all="-N -l" -o bin/testapp + - task: post-build env: CGO_CFLAGS: "-mmacosx-version-min=10.13" CGO_LDFLAGS: "-mmacosx-version-min=10.13" @@ -51,6 +52,7 @@ tasks: - windows cmds: - task: pre-build + - task: build-frontend - go build -gcflags=all="-N -l" -o bin/testapp.exe - task: post-build diff --git a/v3/internal/templates/vue/go.mod.tmpl b/v3/internal/templates/vue/go.mod.tmpl index 4fb0017bc..3dad9b6fa 100644 --- a/v3/internal/templates/vue/go.mod.tmpl +++ b/v3/internal/templates/vue/go.mod.tmpl @@ -11,11 +11,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/net v0.7.0 // indirect ) {{if gt (len .LocalModulePath) 0}} replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3 -replace github.com/wailsapp/wails/v2 => {{.LocalModulePath}}v2 {{end}} diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 1d1caee7a..e64fdcceb 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -16,9 +16,8 @@ import ( "github.com/samber/lo" - "github.com/wailsapp/wails/v2/pkg/assetserver" - "github.com/wailsapp/wails/v2/pkg/assetserver/webview" - assetserveroptions "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v3/pkg/assetserver" + "github.com/wailsapp/wails/v3/pkg/assetserver/webview" wailsruntime "github.com/wailsapp/wails/v3/internal/runtime" "github.com/wailsapp/wails/v3/pkg/events" @@ -72,10 +71,10 @@ func New(appOptions Options) *App { result.Events = NewWailsEventProcessor(result.dispatchEventToWindows) - opts := assetserveroptions.Options{ + opts := &assetserver.Options{ Assets: appOptions.Assets.FS, Handler: appOptions.Assets.Handler, - Middleware: assetserveroptions.Middleware(appOptions.Assets.Middleware), + Middleware: assetserver.Middleware(appOptions.Assets.Middleware), } // TODO ServingFrom disk? diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index af0b1934a..1842f3c0a 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -128,7 +128,7 @@ import "C" import ( "unsafe" - "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + "github.com/wailsapp/wails/v3/pkg/assetserver/webview" "github.com/wailsapp/wails/v3/pkg/events" ) diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 132d85727..ca1727848 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -7,7 +7,7 @@ import ( "strings" "unsafe" - "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + "github.com/wailsapp/wails/v3/pkg/assetserver/webview" "github.com/wailsapp/wails/v3/pkg/events" ) diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index ef44dd98b..ea859ffb6 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -9,7 +9,7 @@ import ( "unsafe" "github.com/ebitengine/purego" - "github.com/wailsapp/wails/v2/pkg/assetserver/webview" + "github.com/wailsapp/wails/v3/pkg/assetserver/webview" "github.com/wailsapp/wails/v3/pkg/events" ) diff --git a/v3/pkg/application/plugins.go b/v3/pkg/application/plugins.go index 90ade7358..69247272b 100644 --- a/v3/pkg/application/plugins.go +++ b/v3/pkg/application/plugins.go @@ -1,6 +1,6 @@ package application -import "github.com/wailsapp/wails/v2/pkg/assetserver" +import "github.com/wailsapp/wails/v3/pkg/assetserver" type Plugin interface { Name() string diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index fe6ddc808..182e758a8 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -8,7 +8,6 @@ import ( "sync" "unsafe" - "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v3/pkg/events" ) @@ -29,7 +28,7 @@ type linuxWebviewWindow struct { parent *WebviewWindow menubar pointer vbox pointer - menu *menu.Menu + menu *Menu accels pointer lastWidth int lastHeight int diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 1af70097e..22343c38c 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -7,8 +7,8 @@ import ( "fmt" "github.com/bep/debounce" "github.com/wailsapp/go-webview2/webviewloader" - "github.com/wailsapp/wails/v2/pkg/assetserver" "github.com/wailsapp/wails/v3/internal/capabilities" + "github.com/wailsapp/wails/v3/pkg/assetserver" "io" "net/http" "net/http/httptest" diff --git a/v3/pkg/assetserver/assethandler.go b/v3/pkg/assetserver/assethandler.go new file mode 100644 index 000000000..1bca1bd50 --- /dev/null +++ b/v3/pkg/assetserver/assethandler.go @@ -0,0 +1,202 @@ +package assetserver + +import ( + "bytes" + "embed" + "errors" + "fmt" + "io" + iofs "io/fs" + "net/http" + "os" + "path" + "strings" +) + +type Logger interface { + Debug(message string, args ...interface{}) + Error(message string, args ...interface{}) +} + +//go:embed defaultindex.html +var defaultHTML []byte + +const ( + indexHTML = "index.html" +) + +type assetHandler struct { + fs iofs.FS + handler http.Handler + + logger Logger + + retryMissingFiles bool +} + +func NewAssetHandler(options *Options, log Logger) (http.Handler, error) { + + vfs := options.Assets + if vfs != nil { + if _, err := vfs.Open("."); err != nil { + return nil, err + } + + subDir, err := FindPathToFile(vfs, indexHTML) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + msg := "no `index.html` could be found in your Assets fs.FS" + if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs { + rootFolder, _ := FindEmbedRootPath(embedFs) + msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder) + } + + return nil, fmt.Errorf(msg) + } + + return nil, err + } + + vfs, err = iofs.Sub(vfs, path.Clean(subDir)) + if err != nil { + return nil, err + } + } + + var result http.Handler = &assetHandler{ + fs: vfs, + handler: options.Handler, + logger: log, + } + + if middleware := options.Middleware; middleware != nil { + result = middleware(result) + } + + return result, nil +} + +func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + url := req.URL.Path + handler := d.handler + if strings.EqualFold(req.Method, http.MethodGet) { + filename := path.Clean(strings.TrimPrefix(url, "/")) + + d.logDebug("Handling request '%s' (file='%s')", url, filename) + if err := d.serveFSFile(rw, req, filename); err != nil { + if os.IsNotExist(err) { + if handler != nil { + d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, url) + handler.ServeHTTP(rw, req) + err = nil + } else { + rw.WriteHeader(http.StatusNotFound) + err = nil + } + } + + if err != nil { + d.logError("Unable to handle request '%s': %s", url, err) + http.Error(rw, err.Error(), http.StatusInternalServerError) + } + } + } else if handler != nil { + d.logDebug("No GET request, serving '%s' by AssetHandler", url) + handler.ServeHTTP(rw, req) + } else { + rw.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// serveFile will try to load the file from the fs.FS and write it to the response +func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error { + if d.fs == nil { + return os.ErrNotExist + } + + file, err := d.fs.Open(filename) + if err != nil { + return err + } + defer file.Close() + + statInfo, err := file.Stat() + if err != nil { + return err + } + + url := req.URL.Path + isDirectoryPath := url == "" || url[len(url)-1] == '/' + if statInfo.IsDir() { + if !isDirectoryPath { + // If the URL doesn't end in a slash normally a http.redirect should be done, but that currently doesn't work on + // WebKit WebViews (macOS/Linux). + // So we handle this as a specific error + return fmt.Errorf("a directory has been requested without a trailing slash, please add a trailing slash to your request") + } + + filename = path.Join(filename, indexHTML) + + file, err = d.fs.Open(filename) + if err != nil { + return err + } + defer file.Close() + + statInfo, err = file.Stat() + if err != nil { + return err + } + } else if isDirectoryPath { + return fmt.Errorf("a file has been requested with a trailing slash, please remove the trailing slash from your request") + } + + var buf [512]byte + var n int + if _, haveType := rw.Header()[HeaderContentType]; !haveType { + // Detect MimeType by sniffing the first 512 bytes + n, err = file.Read(buf[:]) + if err != nil && err != io.EOF { + return err + } + + // Do the custom MimeType sniffing even though http.ServeContent would do it in case + // of an io.ReadSeeker. We would like to have a consistent behaviour in both cases. + if contentType := GetMimetype(filename, buf[:n]); contentType != "" { + rw.Header().Set(HeaderContentType, contentType) + } + } + + if fileSeeker, _ := file.(io.ReadSeeker); fileSeeker != nil { + if _, err := fileSeeker.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("seeker can't seek") + } + + http.ServeContent(rw, req, statInfo.Name(), statInfo.ModTime(), fileSeeker) + return nil + } + + rw.Header().Set(HeaderContentLength, fmt.Sprintf("%d", statInfo.Size())) + + // Write the first 512 bytes used for MimeType sniffing + _, err = io.Copy(rw, bytes.NewReader(buf[:n])) + if err != nil { + return err + } + + // Copy the remaining content of the file + _, err = io.Copy(rw, file) + return err +} + +func (d *assetHandler) logDebug(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Debug("[AssetHandler] "+message, args...) + } +} + +func (d *assetHandler) logError(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Error("[AssetHandler] "+message, args...) + } +} diff --git a/v3/pkg/assetserver/assethandler_external.go b/v3/pkg/assetserver/assethandler_external.go new file mode 100644 index 000000000..56d286bcb --- /dev/null +++ b/v3/pkg/assetserver/assethandler_external.go @@ -0,0 +1,78 @@ +//go:build dev +// +build dev + +package assetserver + +import ( + "errors" + "fmt" + "net/http" + "net/http/httputil" + "net/url" +) + +func NewExternalAssetsHandler(logger Logger, options Options, url *url.URL) http.Handler { + baseHandler := options.Handler + + errSkipProxy := fmt.Errorf("skip proxying") + + proxy := httputil.NewSingleHostReverseProxy(url) + baseDirector := proxy.Director + proxy.Director = func(r *http.Request) { + baseDirector(r) + if logger != nil { + logger.Debug("[ExternalAssetHandler] Loading '%s'", r.URL) + } + } + + proxy.ModifyResponse = func(res *http.Response) error { + if baseHandler == nil { + return nil + } + + if res.StatusCode == http.StatusSwitchingProtocols { + return nil + } + + if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed { + return errSkipProxy + } + + return nil + } + + proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) { + if baseHandler != nil && errors.Is(err, errSkipProxy) { + if logger != nil { + logger.Debug("[ExternalAssetHandler] Loading '%s' failed, using original AssetHandler", r.URL) + } + baseHandler.ServeHTTP(rw, r) + } else { + if logger != nil { + logger.Error("[ExternalAssetHandler] Proxy error: %v", err) + } + rw.WriteHeader(http.StatusBadGateway) + } + } + + var result http.Handler = http.HandlerFunc( + func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet { + proxy.ServeHTTP(rw, req) + return + } + + if baseHandler != nil { + baseHandler.ServeHTTP(rw, req) + return + } + + rw.WriteHeader(http.StatusMethodNotAllowed) + }) + + if middleware := options.Middleware; middleware != nil { + result = middleware(result) + } + + return result +} diff --git a/v3/pkg/assetserver/assetserver.go b/v3/pkg/assetserver/assetserver.go new file mode 100644 index 000000000..de9faa57a --- /dev/null +++ b/v3/pkg/assetserver/assetserver.go @@ -0,0 +1,246 @@ +package assetserver + +import ( + "bytes" + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "strings" + + "golang.org/x/net/html" +) + +const ( + runtimeJSPath = "/wails/runtime.js" + ipcJSPath = "/wails/ipc.js" + runtimePath = "/wails/runtime" + capabilitiesPath = "/wails/capabilities" + flagsPath = "/wails/flags" +) + +type RuntimeAssets interface { + DesktopIPC() []byte + WebsocketIPC() []byte + RuntimeDesktopJS() []byte +} + +type RuntimeHandler interface { + HandleRuntimeCall(w http.ResponseWriter, r *http.Request) +} + +type AssetServer struct { + handler http.Handler + runtimeJS []byte + ipcJS func(*http.Request) []byte + + logger Logger + runtime RuntimeAssets + + servingFromDisk bool + appendSpinnerToBody bool + + // Use http based runtime + runtimeHandler RuntimeHandler + + // plugin scripts + pluginScripts map[string]string + + // GetCapabilities returns the capabilities of the runtime + GetCapabilities func() []byte + + // GetFlags returns the application flags + GetFlags func() []byte + + assetServerWebView +} + +func NewAssetServerMainPage(bindingsJSON string, options *Options, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + return NewAssetServer(bindingsJSON, options, servingFromDisk, logger, runtime) +} + +func NewAssetServer(bindingsJSON string, options *Options, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + handler, err := NewAssetHandler(options, logger) + if err != nil { + return nil, err + } + + return NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime) +} + +func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + var buffer bytes.Buffer + if bindingsJSON != "" { + buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n") + } + buffer.Write(runtime.RuntimeDesktopJS()) + + result := &AssetServer{ + handler: handler, + runtimeJS: buffer.Bytes(), + + // Check if we have been given a directory to serve assets from. + // If so, this means we are in dev mode and are serving assets off disk. + // We indicate this through the `servingFromDisk` flag to ensure requests + // aren't cached in dev mode. + servingFromDisk: servingFromDisk, + logger: logger, + runtime: runtime, + } + + return result, nil +} + +func (d *AssetServer) UseRuntimeHandler(handler RuntimeHandler) { + d.runtimeHandler = handler +} + +func (d *AssetServer) AddPluginScript(pluginName string, script string) { + if d.pluginScripts == nil { + d.pluginScripts = make(map[string]string) + } + pluginName = strings.ReplaceAll(pluginName, "/", "_") + pluginName = html.EscapeString(pluginName) + pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000)) + d.pluginScripts[pluginScriptName] = script +} + +func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if isWebSocket(req) { + // WebSockets are not supported by the AssetServer + rw.WriteHeader(http.StatusNotImplemented) + return + } + + header := rw.Header() + if d.servingFromDisk { + header.Add(HeaderCacheControl, "no-cache") + } + + path := req.URL.Path + switch path { + case "", "/", "/index.html": + recorder := httptest.NewRecorder() + d.handler.ServeHTTP(recorder, req) + for k, v := range recorder.HeaderMap { + header[k] = v + } + + switch recorder.Code { + case http.StatusOK: + content, err := d.processIndexHTML(recorder.Body.Bytes()) + if err != nil { + d.serveError(rw, err, "Unable to processIndexHTML") + return + } + d.writeBlob(rw, indexHTML, content) + + case http.StatusNotFound: + d.writeBlob(rw, indexHTML, defaultHTML) + + default: + rw.WriteHeader(recorder.Code) + + } + + case runtimeJSPath: + d.writeBlob(rw, path, d.runtimeJS) + + case capabilitiesPath: + var data = []byte("{}") + if d.GetCapabilities != nil { + data = d.GetCapabilities() + } + d.writeBlob(rw, path, data) + + case flagsPath: + var data = []byte("{}") + if d.GetFlags != nil { + data = d.GetFlags() + } + d.writeBlob(rw, path, data) + + case runtimePath: + if d.runtimeHandler != nil { + d.runtimeHandler.HandleRuntimeCall(rw, req) + } else { + d.handler.ServeHTTP(rw, req) + } + + case ipcJSPath: + content := d.runtime.DesktopIPC() + if d.ipcJS != nil { + content = d.ipcJS(req) + } + d.writeBlob(rw, path, content) + + default: + // Check if this is a plugin script + if script, ok := d.pluginScripts[path]; ok { + d.writeBlob(rw, path, []byte(script)) + return + } + d.handler.ServeHTTP(rw, req) + } +} + +func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) { + htmlNode, err := getHTMLNode(indexHTML) + if err != nil { + return nil, err + } + + if d.appendSpinnerToBody { + err = appendSpinnerToBody(htmlNode) + if err != nil { + return nil, err + } + } + + if err := insertScriptInHead(htmlNode, runtimeJSPath); err != nil { + return nil, err + } + + if err := insertScriptInHead(htmlNode, ipcJSPath); err != nil { + return nil, err + } + + // Inject plugins + for scriptName := range d.pluginScripts { + if err := insertScriptInHead(htmlNode, scriptName); err != nil { + return nil, err + } + } + + var buffer bytes.Buffer + err = html.Render(&buffer, htmlNode) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) { + err := serveFile(rw, filename, blob) + if err != nil { + d.serveError(rw, err, "Unable to write content %s", filename) + } +} + +func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) { + args = append(args, err) + d.logError(msg+": %s", args...) + rw.WriteHeader(http.StatusInternalServerError) +} + +func (d *AssetServer) logDebug(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Debug("[AssetServer] "+message, args...) + } +} + +func (d *AssetServer) logError(message string, args ...interface{}) { + if d.logger != nil { + d.logger.Error("[AssetServer] "+message, args...) + } +} diff --git a/v3/pkg/assetserver/assetserver_dev.go b/v3/pkg/assetserver/assetserver_dev.go new file mode 100644 index 000000000..f6a2a0d2f --- /dev/null +++ b/v3/pkg/assetserver/assetserver_dev.go @@ -0,0 +1,31 @@ +//go:build dev +// +build dev + +package assetserver + +import ( + "net/http" + "strings" +) + +/* +The assetserver for the dev mode. +Depending on the UserAgent it injects a websocket based IPC script into `index.html` or the default desktop IPC. The +default desktop IPC is injected when the webview accesses the devserver. +*/ +func NewDevAssetServer(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { + result, err := NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime) + if err != nil { + return nil, err + } + + result.appendSpinnerToBody = true + result.ipcJS = func(req *http.Request) []byte { + if strings.Contains(req.UserAgent(), WailsUserAgentValue) { + return runtime.DesktopIPC() + } + return runtime.WebsocketIPC() + } + + return result, nil +} diff --git a/v3/pkg/assetserver/assetserver_legacy.go b/v3/pkg/assetserver/assetserver_legacy.go new file mode 100644 index 000000000..44f50a5f9 --- /dev/null +++ b/v3/pkg/assetserver/assetserver_legacy.go @@ -0,0 +1,78 @@ +package assetserver + +import ( + "io" + "net/http" + + "github.com/wailsapp/wails/v3/pkg/assetserver/webview" +) + +// ProcessHTTPRequest processes the HTTP Request by faking a golang HTTP Server. +// The request will be finished with a StatusNotImplemented code if no handler has written to the response. +func (d *AssetServer) ProcessHTTPRequestLegacy(rw http.ResponseWriter, reqGetter func() (*http.Request, error)) { + d.processWebViewRequest(&legacyRequest{reqGetter: reqGetter, rw: rw}) +} + +type legacyRequest struct { + req *http.Request + rw http.ResponseWriter + + reqGetter func() (*http.Request, error) +} + +func (r *legacyRequest) URL() (string, error) { + req, err := r.request() + if err != nil { + return "", err + } + return req.URL.String(), nil +} + +func (r *legacyRequest) Method() (string, error) { + req, err := r.request() + if err != nil { + return "", err + } + return req.Method, nil +} + +func (r *legacyRequest) Header() (http.Header, error) { + req, err := r.request() + if err != nil { + return nil, err + } + return req.Header, nil +} + +func (r *legacyRequest) Body() (io.ReadCloser, error) { + req, err := r.request() + if err != nil { + return nil, err + } + return req.Body, nil +} + +func (r legacyRequest) Response() webview.ResponseWriter { + return &legacyRequestNoOpCloserResponseWriter{r.rw} +} + +func (r legacyRequest) Close() error { return nil } + +func (r *legacyRequest) request() (*http.Request, error) { + if r.req != nil { + return r.req, nil + } + + req, err := r.reqGetter() + if err != nil { + return nil, err + } + r.req = req + return req, nil +} + +type legacyRequestNoOpCloserResponseWriter struct { + http.ResponseWriter +} + +func (*legacyRequestNoOpCloserResponseWriter) Finish() {} diff --git a/v3/pkg/assetserver/assetserver_webview.go b/v3/pkg/assetserver/assetserver_webview.go new file mode 100644 index 000000000..698db5d4f --- /dev/null +++ b/v3/pkg/assetserver/assetserver_webview.go @@ -0,0 +1,161 @@ +package assetserver + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + "github.com/wailsapp/wails/v3/pkg/assetserver/webview" +) + +type assetServerWebView struct { + // ExpectedWebViewHost is checked against the Request Host of every WebViewRequest, other hosts won't be processed. + ExpectedWebViewHost string + + dispatchInit sync.Once + dispatchReqC chan<- webview.Request + dispatchWorkers int +} + +// ServeWebViewRequest processes the HTTP Request asynchronously by faking a golang HTTP Server. +// The request will be finished with a StatusNotImplemented code if no handler has written to the response. +// The AssetServer takes ownership of the request and the caller mustn't close it or access it in any other way. +func (d *AssetServer) ServeWebViewRequest(req webview.Request) { + d.dispatchInit.Do(func() { + workers := d.dispatchWorkers + if workers == 0 { + workers = 10 + } + + workerC := make(chan webview.Request, workers*2) + for i := 0; i < workers; i++ { + go func() { + for req := range workerC { + uri, _ := req.URL() + d.processWebViewRequest(req) + if err := req.Close(); err != nil { + d.logError("Unable to call close for request for uri '%s'", uri) + } + } + }() + } + + dispatchC := make(chan webview.Request) + go queueingDispatcher(50, dispatchC, workerC) + + d.dispatchReqC = dispatchC + }) + + d.dispatchReqC <- req +} + +// processHTTPRequest processes the HTTP Request by faking a golang HTTP Server. +// The request will be finished with a StatusNotImplemented code if no handler has written to the response. +func (d *AssetServer) processWebViewRequest(r webview.Request) { + wrw := r.Response() + defer wrw.Finish() + + var rw http.ResponseWriter = &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer + defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status + + uri, err := r.URL() + if err != nil { + d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + method, err := r.Method() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err)) + return + } + + header, err := r.Header() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err)) + return + } + + body, err := r.Body() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err)) + return + } + + if body == nil { + body = http.NoBody + } + defer body.Close() + + req, err := http.NewRequest(method, uri, body) + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err)) + return + } + req.Header = header + + if req.RemoteAddr == "" { + // 192.0.2.0/24 is "TEST-NET" in RFC 5737 + req.RemoteAddr = "192.0.2.1:1234" + } + + if req.RequestURI == "" && req.URL != nil { + req.RequestURI = req.URL.String() + } + + if req.ContentLength == 0 { + req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64) + } else { + req.Header.Set(HeaderContentLength, fmt.Sprintf("%d", req.ContentLength)) + } + + if host := req.Header.Get(HeaderHost); host != "" { + req.Host = host + } + + if expectedHost := d.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host)) + return + } + + d.ServeHTTP(rw, req) +} + +func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) { + logInfo := uri + if uri, err := url.ParseRequestURI(uri); err == nil { + logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1) + } + + d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err) + http.Error(rw, err.Error(), http.StatusInternalServerError) +} + +func queueingDispatcher[T any](minQueueSize uint, inC <-chan T, outC chan<- T) { + q := newRingqueue[T](minQueueSize) + for { + in, ok := <-inC + if !ok { + return + } + + q.Add(in) + for q.Len() != 0 { + out, _ := q.Peek() + select { + case outC <- out: + q.Remove() + case in, ok := <-inC: + if !ok { + return + } + + q.Add(in) + } + } + } +} diff --git a/v3/pkg/assetserver/common.go b/v3/pkg/assetserver/common.go new file mode 100644 index 000000000..b787cc6bb --- /dev/null +++ b/v3/pkg/assetserver/common.go @@ -0,0 +1,115 @@ +package assetserver + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "golang.org/x/net/html" +) + +const ( + HeaderHost = "Host" + HeaderContentType = "Content-Type" + HeaderContentLength = "Content-Length" + HeaderUserAgent = "User-Agent" + HeaderCacheControl = "Cache-Control" + HeaderUpgrade = "Upgrade" + + WailsUserAgentValue = "wails.io" +) + +func serveFile(rw http.ResponseWriter, filename string, blob []byte) error { + header := rw.Header() + header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob))) + if mimeType := header.Get(HeaderContentType); mimeType == "" { + mimeType = GetMimetype(filename, blob) + header.Set(HeaderContentType, mimeType) + } + + rw.WriteHeader(http.StatusOK) + _, err := io.Copy(rw, bytes.NewReader(blob)) + return err +} + +func createScriptNode(scriptName string) *html.Node { + return &html.Node{ + Type: html.ElementNode, + Data: "script", + Attr: []html.Attribute{ + { + Key: "src", + Val: scriptName, + }, + }, + } +} + +func createDivNode(id string) *html.Node { + return &html.Node{ + Type: html.ElementNode, + Data: "div", + Attr: []html.Attribute{ + { + Namespace: "", + Key: "id", + Val: id, + }, + }, + } +} + +func insertScriptInHead(htmlNode *html.Node, scriptName string) error { + headNode := findFirstTag(htmlNode, "head") + if headNode == nil { + return errors.New("cannot find head in HTML") + } + scriptNode := createScriptNode(scriptName) + if headNode.FirstChild != nil { + headNode.InsertBefore(scriptNode, headNode.FirstChild) + } else { + headNode.AppendChild(scriptNode) + } + return nil +} + +func appendSpinnerToBody(htmlNode *html.Node) error { + bodyNode := findFirstTag(htmlNode, "body") + if bodyNode == nil { + return errors.New("cannot find body in HTML") + } + scriptNode := createDivNode("wails-spinner") + bodyNode.AppendChild(scriptNode) + return nil +} + +func getHTMLNode(htmldata []byte) (*html.Node, error) { + return html.Parse(bytes.NewReader(htmldata)) +} + +func findFirstTag(htmlnode *html.Node, tagName string) *html.Node { + var extractor func(*html.Node) *html.Node + var result *html.Node + extractor = func(node *html.Node) *html.Node { + if node.Type == html.ElementNode && node.Data == tagName { + return node + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + result := extractor(child) + if result != nil { + return result + } + } + return nil + } + result = extractor(htmlnode) + return result +} + +func isWebSocket(req *http.Request) bool { + upgrade := req.Header.Get(HeaderUpgrade) + return strings.EqualFold(upgrade, "websocket") +} diff --git a/v3/pkg/assetserver/content_type_sniffer.go b/v3/pkg/assetserver/content_type_sniffer.go new file mode 100644 index 000000000..475428ae5 --- /dev/null +++ b/v3/pkg/assetserver/content_type_sniffer.go @@ -0,0 +1,42 @@ +package assetserver + +import ( + "net/http" +) + +type contentTypeSniffer struct { + rw http.ResponseWriter + + wroteHeader bool +} + +func (rw *contentTypeSniffer) Header() http.Header { + return rw.rw.Header() +} + +func (rw *contentTypeSniffer) Write(buf []byte) (int, error) { + rw.writeHeader(buf) + return rw.rw.Write(buf) +} + +func (rw *contentTypeSniffer) WriteHeader(code int) { + if rw.wroteHeader { + return + } + + rw.rw.WriteHeader(code) + rw.wroteHeader = true +} + +func (rw *contentTypeSniffer) writeHeader(b []byte) { + if rw.wroteHeader { + return + } + + m := rw.rw.Header() + if _, hasType := m[HeaderContentType]; !hasType { + m.Set(HeaderContentType, http.DetectContentType(b)) + } + + rw.WriteHeader(http.StatusOK) +} diff --git a/v3/pkg/assetserver/defaultindex.html b/v3/pkg/assetserver/defaultindex.html new file mode 100644 index 000000000..1ea97c405 --- /dev/null +++ b/v3/pkg/assetserver/defaultindex.html @@ -0,0 +1,39 @@ + + + + + index.html not found + + + + +
index.html not found
+

Please try reloading the page

+ + \ No newline at end of file diff --git a/v3/pkg/assetserver/fs.go b/v3/pkg/assetserver/fs.go new file mode 100644 index 000000000..7ecc9cec8 --- /dev/null +++ b/v3/pkg/assetserver/fs.go @@ -0,0 +1,75 @@ +package assetserver + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files. +func FindEmbedRootPath(fsys embed.FS) (string, error) { + stopErr := fmt.Errorf("files or multiple dirs found") + + fPath := "" + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + fPath = path + if entries, dErr := fs.ReadDir(fsys, path); dErr != nil { + return dErr + } else if len(entries) <= 1 { + return nil + } + } + + return stopErr + }) + + if err != nil && err != stopErr { + return "", err + } + + return fPath, nil +} + +func FindPathToFile(fsys fs.FS, file string) (string, error) { + stat, _ := fs.Stat(fsys, file) + if stat != nil { + return ".", nil + } + var indexFiles []string + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(path, file) { + indexFiles = append(indexFiles, path) + } + return nil + }) + if err != nil { + return "", err + } + + if len(indexFiles) > 1 { + selected := indexFiles[0] + for _, f := range indexFiles { + if len(f) < len(selected) { + selected = f + } + } + path, _ := filepath.Split(selected) + return path, nil + } + if len(indexFiles) > 0 { + path, _ := filepath.Split(indexFiles[0]) + return path, nil + } + return "", fmt.Errorf("%s: %w", file, os.ErrNotExist) +} diff --git a/v3/pkg/assetserver/middleware.go b/v3/pkg/assetserver/middleware.go new file mode 100644 index 000000000..b3826ab7d --- /dev/null +++ b/v3/pkg/assetserver/middleware.go @@ -0,0 +1,20 @@ +package assetserver + +import ( + "net/http" +) + +// Middleware defines a HTTP middleware that can be applied to the AssetServer. +// The handler passed as next is the next handler in the chain. One can decide to call the next handler +// or implement a specialized handling. +type Middleware func(next http.Handler) http.Handler + +// ChainMiddleware allows chaining multiple middlewares to one middleware. +func ChainMiddleware(middleware ...Middleware) Middleware { + return func(h http.Handler) http.Handler { + for i := len(middleware) - 1; i >= 0; i-- { + h = middleware[i](h) + } + return h + } +} diff --git a/v3/pkg/assetserver/mimecache.go b/v3/pkg/assetserver/mimecache.go new file mode 100644 index 000000000..9d97e8f5a --- /dev/null +++ b/v3/pkg/assetserver/mimecache.go @@ -0,0 +1,67 @@ +package assetserver + +import ( + "net/http" + "path/filepath" + "sync" + + "github.com/wailsapp/mimetype" +) + +var ( + mimeCache = map[string]string{} + mimeMutex sync.Mutex + + // The list of builtin mime-types by extension as defined by + // the golang standard lib package "mime" + // The standard lib also takes into account mime type definitions from + // etc files like '/etc/apache2/mime.types' but we want to have the + // same behavivour on all platforms and not depend on some external file. + mimeTypesByExt = map[string]string{ + ".avif": "image/avif", + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json", + ".mjs": "text/javascript; charset=utf-8", + ".pdf": "application/pdf", + ".png": "image/png", + ".svg": "image/svg+xml", + ".wasm": "application/wasm", + ".webp": "image/webp", + ".xml": "text/xml; charset=utf-8", + } +) + +func GetMimetype(filename string, data []byte) string { + mimeMutex.Lock() + defer mimeMutex.Unlock() + + result := mimeTypesByExt[filepath.Ext(filename)] + if result != "" { + return result + } + + result = mimeCache[filename] + if result != "" { + return result + } + + detect := mimetype.Detect(data) + if detect == nil { + result = http.DetectContentType(data) + } else { + result = detect.String() + } + + if result == "" { + result = "application/octet-stream" + } + + mimeCache[filename] = result + return result +} diff --git a/v3/pkg/assetserver/mimecache_test.go b/v3/pkg/assetserver/mimecache_test.go new file mode 100644 index 000000000..48e5943fa --- /dev/null +++ b/v3/pkg/assetserver/mimecache_test.go @@ -0,0 +1,46 @@ +package assetserver + +import ( + "testing" +) + +func TestGetMimetype(t *testing.T) { + type args struct { + filename string + data []byte + } + bomUTF8 := []byte{0xef, 0xbb, 0xbf} + var emptyMsg []byte + css := []byte("body{margin:0;padding:0;background-color:#d579b2}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;background-color:#ededed}#nav{padding:30px}#nav a{font-weight:700;color:#2c\n3e50}#nav a.router-link-exact-active{color:#42b983}.hello[data-v-4e26ad49]{margin:10px 0}") + html := []byte("title") + bomHtml := append(bomUTF8, html...) + svg := []byte("") + svgWithComment := append([]byte(""), svg...) + svgWithCommentAndControlChars := append([]byte(" \r\n "), svgWithComment...) + svgWithBomCommentAndControlChars := append(bomUTF8, append([]byte(" \r\n "), svgWithComment...)...) + + tests := []struct { + name string + args args + want string + }{ + {"nil data", args{"nil.svg", nil}, "image/svg+xml"}, + {"empty data", args{"empty.html", emptyMsg}, "text/html; charset=utf-8"}, + {"css", args{"test.css", css}, "text/css; charset=utf-8"}, + {"js", args{"test.js", []byte("let foo = 'bar'; console.log(foo);")}, "text/javascript; charset=utf-8"}, + {"mjs", args{"test.mjs", []byte("let foo = 'bar'; console.log(foo);")}, "text/javascript; charset=utf-8"}, + {"html-utf8", args{"test_utf8.html", html}, "text/html; charset=utf-8"}, + {"html-bom-utf8", args{"test_bom_utf8.html", bomHtml}, "text/html; charset=utf-8"}, + {"svg", args{"test.svg", svg}, "image/svg+xml"}, + {"svg-w-comment", args{"test_comment.svg", svgWithComment}, "image/svg+xml"}, + {"svg-w-control-comment", args{"test_control_comment.svg", svgWithCommentAndControlChars}, "image/svg+xml"}, + {"svg-w-bom-control-comment", args{"test_bom_control_comment.svg", svgWithBomCommentAndControlChars}, "image/svg+xml"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetMimetype(tt.args.filename, tt.args.data); got != tt.want { + t.Errorf("GetMimetype() = '%v', want '%v'", got, tt.want) + } + }) + } +} diff --git a/v3/pkg/assetserver/options.go b/v3/pkg/assetserver/options.go new file mode 100644 index 000000000..674451a0c --- /dev/null +++ b/v3/pkg/assetserver/options.go @@ -0,0 +1,45 @@ +package assetserver + +import ( + "fmt" + "io/fs" + "net/http" +) + +// Options defines the configuration of the AssetServer. +type Options struct { + // Assets defines the static assets to be used. A GET request is first tried to be served from this Assets. If the Assets returns + // `os.ErrNotExist` for that file, the request handling will fallback to the Handler and tries to serve the GET + // request from it. + // + // If set to nil, all GET requests will be forwarded to Handler. + Assets fs.FS + + // Handler will be called for every GET request that can't be served from Assets, due to `os.ErrNotExist`. Furthermore all + // non GET requests will always be served from this Handler. + // + // If not defined, the result is the following in cases where the Handler would have been called: + // GET request: `http.StatusNotFound` + // Other request: `http.StatusMethodNotAllowed` + Handler http.Handler + + // Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default + // request handler dynamically, e.g. implement specialized Routing etc. + // The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default + // handler used by the AssetServer as an argument. + // + // If not defined, the default AssetServer request chain is executed. + // + // Multiple Middlewares can be chained together with: + // ChainMiddleware(middleware ...Middleware) Middleware + Middleware Middleware +} + +// Validate the options +func (o Options) Validate() error { + if o.Assets == nil && o.Handler == nil && o.Middleware == nil { + return fmt.Errorf("AssetServer options invalid: either Assets, Handler or Middleware must be set") + } + + return nil +} diff --git a/v3/pkg/assetserver/ringqueue.go b/v3/pkg/assetserver/ringqueue.go new file mode 100644 index 000000000..b94e7cd5c --- /dev/null +++ b/v3/pkg/assetserver/ringqueue.go @@ -0,0 +1,101 @@ +// Code from https://github.com/erikdubbelboer/ringqueue +/* +The MIT License (MIT) + +Copyright (c) 2015 Erik Dubbelboer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package assetserver + +type ringqueue[T any] struct { + nodes []T + head int + tail int + cnt int + + minSize int +} + +func newRingqueue[T any](minSize uint) *ringqueue[T] { + if minSize < 2 { + minSize = 2 + } + return &ringqueue[T]{ + nodes: make([]T, minSize), + minSize: int(minSize), + } +} + +func (q *ringqueue[T]) resize(n int) { + nodes := make([]T, n) + if q.head < q.tail { + copy(nodes, q.nodes[q.head:q.tail]) + } else { + copy(nodes, q.nodes[q.head:]) + copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail]) + } + + q.tail = q.cnt % n + q.head = 0 + q.nodes = nodes +} + +func (q *ringqueue[T]) Add(i T) { + if q.cnt == len(q.nodes) { + // Also tested a grow rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy + // In Go this resulted in a higher memory usage. + q.resize(q.cnt * 2) + } + q.nodes[q.tail] = i + q.tail = (q.tail + 1) % len(q.nodes) + q.cnt++ +} + +func (q *ringqueue[T]) Peek() (T, bool) { + if q.cnt == 0 { + var none T + return none, false + } + return q.nodes[q.head], true +} + +func (q *ringqueue[T]) Remove() (T, bool) { + if q.cnt == 0 { + var none T + return none, false + } + i := q.nodes[q.head] + q.head = (q.head + 1) % len(q.nodes) + q.cnt-- + + if n := len(q.nodes) / 2; n > q.minSize && q.cnt <= n { + q.resize(n) + } + + return i, true +} + +func (q *ringqueue[T]) Cap() int { + return cap(q.nodes) +} + +func (q *ringqueue[T]) Len() int { + return q.cnt +} diff --git a/v3/pkg/assetserver/testdata/index.html b/v3/pkg/assetserver/testdata/index.html new file mode 100644 index 000000000..76da518f4 --- /dev/null +++ b/v3/pkg/assetserver/testdata/index.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/v3/pkg/assetserver/testdata/main.css b/v3/pkg/assetserver/testdata/main.css new file mode 100644 index 000000000..57b00e6c6 --- /dev/null +++ b/v3/pkg/assetserver/testdata/main.css @@ -0,0 +1,39 @@ + +html { + text-align: center; + color: white; + background-color: rgba(1, 1, 1, 0.1); +} + +body { + color: white; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + margin: 0; +} + +#result { + margin-top: 1rem; +} + +button { + -webkit-appearance: default-button; + padding: 6px; +} + +#name { + border-radius: 3px; + outline: none; + height: 20px; + -webkit-font-smoothing: antialiased; +} + +#logo { + width: 40%; + height: 40%; + padding-top: 20%; + margin: auto; + display: block; + background-position: center; + background-repeat: no-repeat; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTUxIDQzNiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0xMDQuMDEgMzQ0LjM4OGgxOC40MDFsLTEuNzY4IDM5LjE2MSAxMi4xNDctMzkuMTYxaDE0Ljg2N2wtLjE4MSAzOS4xNjEgMTAuNTYxLTM5LjE2MWgxOC40MDFsLTIzLjI5NyA2Ni4xNzVoLTE2Ljk5N2wuMTgxLTQxLjM4My0xMi45MTcgNDEuMzgzaC0xNi45NTFsLTIuNDQ3LTY2LjE3NXptMTIwLjk3NSA0My4wNTloNy4zODhsLjIyNy0yNC41MjEtNy42MTUgMjQuNTIxem0tMjUuNzQ0IDIzLjExNmwyNC44ODMtNjYuMTc1aDIxLjgwMWw0LjY2NyA2Ni4xNzVoLTE4LjY3NGwuMDkyLTkuNzQ2aC0xMC45MjRsLTIuOTAxIDkuNzQ2aC0xOC45NDR6bTg4LjE4MyAwbDEwLjQ3LTY2LjE3NmgxOC40OTNsLTEwLjUxNiA2Ni4xNzUtMTguNDQ2LjAwMXptNjUuNzkzIDBsMTAuNTE2LTY2LjE3NWgxOC41MzZsLTcuODg2IDQ5Ljc2NmgxMy41NTJsLTIuNTgyIDE2LjQwOWgtMzIuMTM2em03NC43MjItMjAuMzUyYzIuMDU0IDEuNzIzIDQuMjE1IDMuMDUzIDYuNDgyIDMuOTlzNC40NCAxLjQwNCA2LjUyNiAxLjQwNGMxLjg0MyAwIDMuMzA4LS41MDYgNC4zOTYtMS41MThzMS42MzItMi4zOTUgMS42MzItNC4xNDhjMC0xLjUwOS0uNDU0LTMuMDEzLTEuMzU5LTQuNTA5cy0yLjY2LTMuNDgxLTUuMjU4LTUuOTU5Yy0zLjE0NC0zLjA1Mi01LjMwMy01Ljc0MS02LjQ4Mi04LjA2OXMtMS43NjYtNC44OTQtMS43NjYtNy43MDRjMC02LjMxNSAyLjAwMS0xMS4zMzIgNi4wMDUtMTUuMDQ4czkuNDM0LTUuNTc1IDE2LjI5NC01LjU3NWMyLjc4IDAgNS40MjIuMzEgNy45MzEuOTNzNS4wNiAxLjU3OSA3LjY2MSAyLjg3OGwtMi42MyAxNi4xMzZjLTEuOTk1LTEuMzktMy45MzUtMi40NDctNS44MjMtMy4xNzNzLTMuNjk0LTEuMDg5LTUuNDE3LTEuMDg5Yy0xLjU0MSAwLTIuNzU4LjQtMy42NDkgMS4ycy0xLjMzOCAxLjg5OC0xLjMzOCAzLjI4OGMwIDEuODc1IDEuNzA4IDQuNTAzIDUuMTIzIDcuODg2bC45OTcuOTk2YzMuNDQ1IDMuMzg2IDUuNzExIDYuMjg2IDYuNzk4IDguNzA1czEuNjMxIDUuMjA5IDEuNjMxIDguMzg0YzAgNy4wNzEtMi4xODMgMTIuNjQ2LTYuNTUgMTYuNzI0cy0xMC4zNDEgNi4xMi0xNy45MjUgNi4xMmMtMy4yMzQgMC02LjI5Mi0uMzg1LTkuMTc4LTEuMTU1cy01LjMxLTEuODM4LTcuMjc0LTMuMTk3bDMuMTczLTE3LjQ5NXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNLjg4My0uMDgxTC4xMjEuMDgxLjI1Ni0uMDYzLjg4My0uMDgxeiIgZmlsbD0idXJsKCNBKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTE2Ni41OTkgNC41NzEzMiA0LjU3MTMyIDE2Ni41OTkgMTQ3LjQwMyAxNjcuNjQ4KSIvPjxwYXRoIGQ9Ik0uODc4LS4yODVMLS4wNzMuNzEtMS4xODYuNTQyLjAxNS4yMDctLjg0Ni4wNzcuMzU1LS4yNThsLS44Ni0uMTNMLjY0OS0uNzFsLjIyOS40MjV6IiBmaWxsPSJ1cmwoI0IpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTA2LjQ0MyAtMTYuMDY2OSAtMTYuMDY2OSAxMDYuNDQzIDQyOC4xOSAxODguMDMzKSIvPjxwYXRoIGQ9Ik0uNDQtLjA0aDAgMEwuMjY1LS4wNTYuMTc3LjQzNy0uMzExLS4yNTUuMjYyLS40MzdoLjMwNkwuNDQtLjA0eiIgZmlsbD0idXJsKCNDKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTExNC40ODQgLTE2Mi40MDggLTE2Mi40MDggMTE0LjQ4NCAzMzMuMjkxIDI4NS44MDQpIi8+PHBhdGggZD0iTS41IDBoMCAwIDB6IiBmaWxsPSJ1cmwoI0QpIiB0cmFuc2Zvcm09Im1hdHJpeCg2MS42OTE5IDU4LjgwOTEgNTguODA5MSAtNjEuNjkxOSAyNTguNjMxIDE4MC40MTMpIi8+PHBhdGggZD0iTS42MjItLjExNWguMTM5bC4wNDUuMTAyLjAyLjE5NS0uMjA0LS4yOTd6IiBmaWxsPSJ1cmwoI0UpIiB0cmFuc2Zvcm09Im1hdHJpeCgyMzguMTI2IDI5OC44OTMgMjk4Ljg5MyAtMjM4LjEyNiAxMTMuNTE2IC0xNTAuNTM2KSIvPjxwYXRoIGQ9Ik0uNDY3LjAwNUwuNDkuMDYyLjI3MS0uMDYyLjQ2Ny4wMDV6IiBmaWxsPSJ1cmwoI0YpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMzY5LjUyOSAtOTcuNDExOCAtOTcuNDExOCAzNjkuNTI5IDU4Mi4zOCA5NC4wMjcpIi8+PGcgZmlsbD0idXJsKCNCKSI+PHBhdGggZD0iTS4yLjAwMWwuMDE5LS4wMTkuMzk1LjAzLS4wOTUuMDc3TC4yODIuMDY4LjIuMTM1LjQ2My4xOTQuMzc0LjI2Ni4xMzguMTg2aDAgMEwuMDQ3LjAzMy0uMTMxLS4yNjYuMi4wMDF6IiB0cmFuc2Zvcm09Im1hdHJpeCgtNDk2LjE1NiAtNTMuOTc1MSAtNTMuOTc1MSA0OTYuMTU2IDM2Ny44ODggMTI1LjA4NSkiLz48cGF0aCBkPSJNLjczNSAwaDAgMCAweiIgdHJhbnNmb3JtPSJtYXRyaXgoMTg1LjA3NiAxNzYuNDI3IDE3Ni40MjcgLTE4NS4wNzYgMTUzLjQ0NiA4MC4xNDg4KSIvPjwvZz48L2c+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJBIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0zLjQ2OTQ1ZS0xOCwtMy40Njk0NWUtMTgsLTEsMCwtMy4wNTc2MWUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkIiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkMiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsLTEuMTEwMjJlLTE2LC0xLjExMDIyZS0xNiwtMSwwLC0yLjYxODYxZS0wNikiIHhsaW5rOmhyZWY9IiNHIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlMzMyMzIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2YjAwMGQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iRCIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjAiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSwtNS41NTExMmUtMTcsLTUuNTUxMTJlLTE3LC0xLDAsLTEuNTc1NjJlLTA2KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJFIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC44MDE4OTksLTAuNTk3NDYsLTAuNTk3NDYsMC44MDE4OTksMS4zNDk1LDAuNDQ3NDU3KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJGIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0yLjc3NTU2ZS0xNywtMi43NzU1NmUtMTcsLTEsMCwtMS45MjgyNmUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIi8+PC9kZWZzPjwvc3ZnPg=="); +} diff --git a/v3/pkg/assetserver/testdata/main.js b/v3/pkg/assetserver/testdata/main.js new file mode 100644 index 000000000..274b4667c --- /dev/null +++ b/v3/pkg/assetserver/testdata/main.js @@ -0,0 +1,20 @@ +import {ready} from '@wails/runtime'; + +ready(() => { + // Get input + focus + let nameElement = document.getElementById("name"); + nameElement.focus(); + + // Setup the greet function + window.greet = function () { + + // Get name + let name = nameElement.value; + + // Call App.Greet(name) + window.backend.main.App.Greet(name).then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }); + }; +}); \ No newline at end of file diff --git a/v3/pkg/assetserver/testdata/subdir/index.html b/v3/pkg/assetserver/testdata/subdir/index.html new file mode 100644 index 000000000..76da518f4 --- /dev/null +++ b/v3/pkg/assetserver/testdata/subdir/index.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/v3/pkg/assetserver/testdata/subdir/main.css b/v3/pkg/assetserver/testdata/subdir/main.css new file mode 100644 index 000000000..57b00e6c6 --- /dev/null +++ b/v3/pkg/assetserver/testdata/subdir/main.css @@ -0,0 +1,39 @@ + +html { + text-align: center; + color: white; + background-color: rgba(1, 1, 1, 0.1); +} + +body { + color: white; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + margin: 0; +} + +#result { + margin-top: 1rem; +} + +button { + -webkit-appearance: default-button; + padding: 6px; +} + +#name { + border-radius: 3px; + outline: none; + height: 20px; + -webkit-font-smoothing: antialiased; +} + +#logo { + width: 40%; + height: 40%; + padding-top: 20%; + margin: auto; + display: block; + background-position: center; + background-repeat: no-repeat; + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTUxIDQzNiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iPjxwYXRoIGQ9Ik0xMDQuMDEgMzQ0LjM4OGgxOC40MDFsLTEuNzY4IDM5LjE2MSAxMi4xNDctMzkuMTYxaDE0Ljg2N2wtLjE4MSAzOS4xNjEgMTAuNTYxLTM5LjE2MWgxOC40MDFsLTIzLjI5NyA2Ni4xNzVoLTE2Ljk5N2wuMTgxLTQxLjM4My0xMi45MTcgNDEuMzgzaC0xNi45NTFsLTIuNDQ3LTY2LjE3NXptMTIwLjk3NSA0My4wNTloNy4zODhsLjIyNy0yNC41MjEtNy42MTUgMjQuNTIxem0tMjUuNzQ0IDIzLjExNmwyNC44ODMtNjYuMTc1aDIxLjgwMWw0LjY2NyA2Ni4xNzVoLTE4LjY3NGwuMDkyLTkuNzQ2aC0xMC45MjRsLTIuOTAxIDkuNzQ2aC0xOC45NDR6bTg4LjE4MyAwbDEwLjQ3LTY2LjE3NmgxOC40OTNsLTEwLjUxNiA2Ni4xNzUtMTguNDQ2LjAwMXptNjUuNzkzIDBsMTAuNTE2LTY2LjE3NWgxOC41MzZsLTcuODg2IDQ5Ljc2NmgxMy41NTJsLTIuNTgyIDE2LjQwOWgtMzIuMTM2em03NC43MjItMjAuMzUyYzIuMDU0IDEuNzIzIDQuMjE1IDMuMDUzIDYuNDgyIDMuOTlzNC40NCAxLjQwNCA2LjUyNiAxLjQwNGMxLjg0MyAwIDMuMzA4LS41MDYgNC4zOTYtMS41MThzMS42MzItMi4zOTUgMS42MzItNC4xNDhjMC0xLjUwOS0uNDU0LTMuMDEzLTEuMzU5LTQuNTA5cy0yLjY2LTMuNDgxLTUuMjU4LTUuOTU5Yy0zLjE0NC0zLjA1Mi01LjMwMy01Ljc0MS02LjQ4Mi04LjA2OXMtMS43NjYtNC44OTQtMS43NjYtNy43MDRjMC02LjMxNSAyLjAwMS0xMS4zMzIgNi4wMDUtMTUuMDQ4czkuNDM0LTUuNTc1IDE2LjI5NC01LjU3NWMyLjc4IDAgNS40MjIuMzEgNy45MzEuOTNzNS4wNiAxLjU3OSA3LjY2MSAyLjg3OGwtMi42MyAxNi4xMzZjLTEuOTk1LTEuMzktMy45MzUtMi40NDctNS44MjMtMy4xNzNzLTMuNjk0LTEuMDg5LTUuNDE3LTEuMDg5Yy0xLjU0MSAwLTIuNzU4LjQtMy42NDkgMS4ycy0xLjMzOCAxLjg5OC0xLjMzOCAzLjI4OGMwIDEuODc1IDEuNzA4IDQuNTAzIDUuMTIzIDcuODg2bC45OTcuOTk2YzMuNDQ1IDMuMzg2IDUuNzExIDYuMjg2IDYuNzk4IDguNzA1czEuNjMxIDUuMjA5IDEuNjMxIDguMzg0YzAgNy4wNzEtMi4xODMgMTIuNjQ2LTYuNTUgMTYuNzI0cy0xMC4zNDEgNi4xMi0xNy45MjUgNi4xMmMtMy4yMzQgMC02LjI5Mi0uMzg1LTkuMTc4LTEuMTU1cy01LjMxLTEuODM4LTcuMjc0LTMuMTk3bDMuMTczLTE3LjQ5NXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNLjg4My0uMDgxTC4xMjEuMDgxLjI1Ni0uMDYzLjg4My0uMDgxeiIgZmlsbD0idXJsKCNBKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTE2Ni41OTkgNC41NzEzMiA0LjU3MTMyIDE2Ni41OTkgMTQ3LjQwMyAxNjcuNjQ4KSIvPjxwYXRoIGQ9Ik0uODc4LS4yODVMLS4wNzMuNzEtMS4xODYuNTQyLjAxNS4yMDctLjg0Ni4wNzcuMzU1LS4yNThsLS44Ni0uMTNMLjY0OS0uNzFsLjIyOS40MjV6IiBmaWxsPSJ1cmwoI0IpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMTA2LjQ0MyAtMTYuMDY2OSAtMTYuMDY2OSAxMDYuNDQzIDQyOC4xOSAxODguMDMzKSIvPjxwYXRoIGQ9Ik0uNDQtLjA0aDAgMEwuMjY1LS4wNTYuMTc3LjQzNy0uMzExLS4yNTUuMjYyLS40MzdoLjMwNkwuNDQtLjA0eiIgZmlsbD0idXJsKCNDKSIgdHJhbnNmb3JtPSJtYXRyaXgoLTExNC40ODQgLTE2Mi40MDggLTE2Mi40MDggMTE0LjQ4NCAzMzMuMjkxIDI4NS44MDQpIi8+PHBhdGggZD0iTS41IDBoMCAwIDB6IiBmaWxsPSJ1cmwoI0QpIiB0cmFuc2Zvcm09Im1hdHJpeCg2MS42OTE5IDU4LjgwOTEgNTguODA5MSAtNjEuNjkxOSAyNTguNjMxIDE4MC40MTMpIi8+PHBhdGggZD0iTS42MjItLjExNWguMTM5bC4wNDUuMTAyLjAyLjE5NS0uMjA0LS4yOTd6IiBmaWxsPSJ1cmwoI0UpIiB0cmFuc2Zvcm09Im1hdHJpeCgyMzguMTI2IDI5OC44OTMgMjk4Ljg5MyAtMjM4LjEyNiAxMTMuNTE2IC0xNTAuNTM2KSIvPjxwYXRoIGQ9Ik0uNDY3LjAwNUwuNDkuMDYyLjI3MS0uMDYyLjQ2Ny4wMDV6IiBmaWxsPSJ1cmwoI0YpIiB0cmFuc2Zvcm09Im1hdHJpeCgtMzY5LjUyOSAtOTcuNDExOCAtOTcuNDExOCAzNjkuNTI5IDU4Mi4zOCA5NC4wMjcpIi8+PGcgZmlsbD0idXJsKCNCKSI+PHBhdGggZD0iTS4yLjAwMWwuMDE5LS4wMTkuMzk1LjAzLS4wOTUuMDc3TC4yODIuMDY4LjIuMTM1LjQ2My4xOTQuMzc0LjI2Ni4xMzguMTg2aDAgMEwuMDQ3LjAzMy0uMTMxLS4yNjYuMi4wMDF6IiB0cmFuc2Zvcm09Im1hdHJpeCgtNDk2LjE1NiAtNTMuOTc1MSAtNTMuOTc1MSA0OTYuMTU2IDM2Ny44ODggMTI1LjA4NSkiLz48cGF0aCBkPSJNLjczNSAwaDAgMCAweiIgdHJhbnNmb3JtPSJtYXRyaXgoMTg1LjA3NiAxNzYuNDI3IDE3Ni40MjcgLTE4NS4wNzYgMTUzLjQ0NiA4MC4xNDg4KSIvPjwvZz48L2c+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJBIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0zLjQ2OTQ1ZS0xOCwtMy40Njk0NWUtMTgsLTEsMCwtMy4wNTc2MWUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkIiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkMiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIwIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEsLTEuMTEwMjJlLTE2LC0xLjExMDIyZS0xNiwtMSwwLC0yLjYxODYxZS0wNikiIHhsaW5rOmhyZWY9IiNHIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlMzMyMzIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2YjAwMGQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iRCIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjAiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSwtNS41NTExMmUtMTcsLTUuNTUxMTJlLTE3LC0xLDAsLTEuNTc1NjJlLTA2KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJFIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtMC44MDE4OTksLTAuNTk3NDYsLTAuNTk3NDYsMC44MDE4OTksMS4zNDk1LDAuNDQ3NDU3KSIgeGxpbms6aHJlZj0iI0ciPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2UzMzIzMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzZiMDAwZCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJGIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxLC0yLjc3NTU2ZS0xNywtMi43NzU1NmUtMTcsLTEsMCwtMS45MjgyNmUtMDYpIiB4bGluazpocmVmPSIjRyI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZTMzMjMyIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNmIwMDBkIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9IkciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIi8+PC9kZWZzPjwvc3ZnPg=="); +} diff --git a/v3/pkg/assetserver/testdata/subdir/main.js b/v3/pkg/assetserver/testdata/subdir/main.js new file mode 100644 index 000000000..274b4667c --- /dev/null +++ b/v3/pkg/assetserver/testdata/subdir/main.js @@ -0,0 +1,20 @@ +import {ready} from '@wails/runtime'; + +ready(() => { + // Get input + focus + let nameElement = document.getElementById("name"); + nameElement.focus(); + + // Setup the greet function + window.greet = function () { + + // Get name + let name = nameElement.value; + + // Call App.Greet(name) + window.backend.main.App.Greet(name).then((result) => { + // Update result with data back from App.Greet() + document.getElementById("result").innerText = result; + }); + }; +}); \ No newline at end of file diff --git a/v3/pkg/assetserver/testdata/testdata.go b/v3/pkg/assetserver/testdata/testdata.go new file mode 100644 index 000000000..5387070ec --- /dev/null +++ b/v3/pkg/assetserver/testdata/testdata.go @@ -0,0 +1,6 @@ +package testdata + +import "embed" + +//go:embed index.html main.css main.js +var TopLevelFS embed.FS diff --git a/v3/pkg/assetserver/webview/request.go b/v3/pkg/assetserver/webview/request.go new file mode 100644 index 000000000..18ff29890 --- /dev/null +++ b/v3/pkg/assetserver/webview/request.go @@ -0,0 +1,17 @@ +package webview + +import ( + "io" + "net/http" +) + +type Request interface { + URL() (string, error) + Method() (string, error) + Header() (http.Header, error) + Body() (io.ReadCloser, error) + + Response() ResponseWriter + + Close() error +} diff --git a/v3/pkg/assetserver/webview/request_darwin.go b/v3/pkg/assetserver/webview/request_darwin.go new file mode 100644 index 000000000..f0e85780b --- /dev/null +++ b/v3/pkg/assetserver/webview/request_darwin.go @@ -0,0 +1,248 @@ +//go:build darwin + +package webview + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework WebKit + +#import +#import +#include + +static void URLSchemeTaskRetain(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + [urlSchemeTask retain]; +} + +static void URLSchemeTaskRelease(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + [urlSchemeTask release]; +} + +static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + return [urlSchemeTask.request.URL.absoluteString UTF8String]; + } +} + +static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + return [urlSchemeTask.request.HTTPMethod UTF8String]; + } +} + +static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + NSData *headerData = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil]; + if (!headerData) { + return nil; + } + + NSString* headerString = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease]; + const char * headerJSON = [headerString UTF8String]; + + return strdup(headerJSON); + } +} + +static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBody) { + return false; + } + + *body = urlSchemeTask.request.HTTPBody.bytes; + *bodyLen = urlSchemeTask.request.HTTPBody.length; + return true; + } +} + +static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBodyStream) { + return false; + } + + [urlSchemeTask.request.HTTPBodyStream open]; + return true; + } +} + +static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBodyStream) { + return; + } + + [urlSchemeTask.request.HTTPBodyStream close]; + } +} + +static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) { + id urlSchemeTask = (id) wkUrlSchemeTask; + + @autoreleasepool { + NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream; + if (!stream) { + return -2; + } + + NSStreamStatus status = stream.streamStatus; + if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) { + return 0; + } else if (status != NSStreamStatusOpen) { + return -3; + } + + return [stream read:buf maxLength:bufLen]; + } +} +*/ +import "C" + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "unsafe" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `id` +func NewRequest(wkURLSchemeTask unsafe.Pointer) Request { + C.URLSchemeTaskRetain(wkURLSchemeTask) + return newRequestFinalizer(&request{task: wkURLSchemeTask}) +} + +var _ Request = &request{} + +type request struct { + task unsafe.Pointer + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) URL() (string, error) { + return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil +} + +func (r *request) Method() (string, error) { + return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + header := http.Header{} + if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil { + if headers := C.GoString(cHeaders); headers != "" { + var h map[string]string + if err := json.Unmarshal([]byte(headers), &h); err != nil { + return nil, fmt.Errorf("unable to unmarshal request headers: %s", err) + } + + for k, v := range h { + header.Add(k, v) + } + } + C.free(unsafe.Pointer(cHeaders)) + } + r.header = header + return header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + var body unsafe.Pointer + var bodyLen C.int + if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) { + if body != nil && bodyLen > 0 { + r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen))) + } else { + r.body = http.NoBody + } + } else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) { + r.body = &requestBodyStreamReader{task: r.task} + } + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{r: r} + return r.rw +} + +func (r *request) Close() error { + var err error + if r.body != nil { + err = r.body.Close() + } + r.Response().Finish() + C.URLSchemeTaskRelease(r.task) + return err +} + +var _ io.ReadCloser = &requestBodyStreamReader{} + +type requestBodyStreamReader struct { + task unsafe.Pointer + closed bool +} + +// Read implements io.Reader +func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) { + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen)) + if res > 0 { + return int(res), nil + } + + switch res { + case 0: + return 0, io.EOF + case -1: + return 0, fmt.Errorf("body: stream error") + case -2: + return 0, fmt.Errorf("body: no stream defined") + case -3: + return 0, io.ErrClosedPipe + default: + return 0, fmt.Errorf("body: unknown error %d", res) + } +} + +func (r *requestBodyStreamReader) Close() error { + if r.closed { + return nil + } + r.closed = true + + C.URLSchemeTaskRequestBodyStreamClose(r.task) + return nil +} diff --git a/v3/pkg/assetserver/webview/request_finalizer.go b/v3/pkg/assetserver/webview/request_finalizer.go new file mode 100644 index 000000000..6a8c6a928 --- /dev/null +++ b/v3/pkg/assetserver/webview/request_finalizer.go @@ -0,0 +1,40 @@ +package webview + +import ( + "runtime" + "sync/atomic" +) + +var _ Request = &requestFinalizer{} + +type requestFinalizer struct { + Request + closed int32 +} + +// newRequestFinalizer returns a request with a runtime finalizer to make sure it will be closed from the finalizer +// if it has not been already closed. +// It also makes sure Close() of the wrapping request is only called once. +func newRequestFinalizer(r Request) Request { + rf := &requestFinalizer{Request: r} + // Make sure to async release since it might block the finalizer goroutine for a longer period + runtime.SetFinalizer(rf, func(obj *requestFinalizer) { rf.close(true) }) + return rf +} + +func (r *requestFinalizer) Close() error { + return r.close(false) +} + +func (r *requestFinalizer) close(asyncRelease bool) error { + if atomic.CompareAndSwapInt32(&r.closed, 0, 1) { + runtime.SetFinalizer(r, nil) + if asyncRelease { + go r.Request.Close() + return nil + } else { + return r.Request.Close() + } + } + return nil +} diff --git a/v3/pkg/assetserver/webview/request_linux.go b/v3/pkg/assetserver/webview/request_linux.go new file mode 100644 index 000000000..101ee12fb --- /dev/null +++ b/v3/pkg/assetserver/webview/request_linux.go @@ -0,0 +1,83 @@ +//go:build linux +// +build linux + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "io" + "net/http" + "unsafe" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest` +func NewRequest(webKitURISchemeRequest unsafe.Pointer) Request { + webkitReq := (*C.WebKitURISchemeRequest)(webKitURISchemeRequest) + C.g_object_ref(C.gpointer(webkitReq)) + + req := &request{req: webkitReq} + return newRequestFinalizer(req) +} + +var _ Request = &request{} + +type request struct { + req *C.WebKitURISchemeRequest + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) URL() (string, error) { + return C.GoString(C.webkit_uri_scheme_request_get_uri(r.req)), nil +} + +func (r *request) Method() (string, error) { + return webkit_uri_scheme_request_get_http_method(r.req), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + r.header = webkit_uri_scheme_request_get_http_headers(r.req) + return r.header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + r.body = webkit_uri_scheme_request_get_http_body(r.req) + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{req: r.req} + return r.rw +} + +func (r *request) Close() error { + var err error + if r.body != nil { + err = r.body.Close() + } + r.Response().Finish() + C.g_object_unref(C.gpointer(r.req)) + return err +} diff --git a/v3/pkg/assetserver/webview/request_linux_purego.go b/v3/pkg/assetserver/webview/request_linux_purego.go new file mode 100644 index 000000000..bf724a55b --- /dev/null +++ b/v3/pkg/assetserver/webview/request_linux_purego.go @@ -0,0 +1,94 @@ +//go:build linux && purego +// +build linux,purego + +package webview + +import ( + "io" + "net/http" + + "github.com/ebitengine/purego" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `WebKitURISchemeRequest` +// +// Please make sure to call Release() when finished using the request. +func NewRequest(webKitURISchemeRequest uintptr) Request { + webkitReq := webKitURISchemeRequest + req := &request{req: webkitReq} + req.AddRef() + return req +} + +var _ Request = &request{} + +type request struct { + req uintptr + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) AddRef() error { + var objectRef func(uintptr) + purego.RegisterLibFunc(&objectRef, gtk, "g_object_ref") + objectRef(r.req) + return nil +} + +func (r *request) Release() error { + var objectUnref func(uintptr) + purego.RegisterLibFunc(&objectUnref, gtk, "g_object_unref") + objectUnref(r.req) + return nil +} + +func (r *request) URL() (string, error) { + var getUri func(uintptr) string + purego.RegisterLibFunc(&getUri, webkit, "webkit_uri_scheme_request_get_uri") + return getUri(r.req), nil +} + +func (r *request) Method() (string, error) { + return webkit_uri_scheme_request_get_http_method(r.req), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + r.header = webkit_uri_scheme_request_get_http_headers(r.req) + return r.header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + // WebKit2GTK has currently no support for request bodies. + r.body = http.NoBody + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{req: r.req} + return r.rw +} + +func (r *request) Close() error { + var err error + if r.body != nil { + err = r.body.Close() + } + r.Response().Finish() + r.Release() + return err +} diff --git a/v3/pkg/assetserver/webview/responsewriter.go b/v3/pkg/assetserver/webview/responsewriter.go new file mode 100644 index 000000000..d67802a05 --- /dev/null +++ b/v3/pkg/assetserver/webview/responsewriter.go @@ -0,0 +1,25 @@ +package webview + +import ( + "errors" + "net/http" +) + +const ( + HeaderContentLength = "Content-Length" + HeaderContentType = "Content-Type" +) + +var ( + errRequestStopped = errors.New("request has been stopped") + errResponseFinished = errors.New("response has been finished") +) + +// A ResponseWriter interface is used by an HTTP handler to +// construct an HTTP response for the WebView. +type ResponseWriter interface { + http.ResponseWriter + + // Finish the response and flush all data. A Finish after the request has already been finished has no effect. + Finish() +} diff --git a/v3/pkg/assetserver/webview/responsewriter_darwin.go b/v3/pkg/assetserver/webview/responsewriter_darwin.go new file mode 100644 index 000000000..1c0cbee72 --- /dev/null +++ b/v3/pkg/assetserver/webview/responsewriter_darwin.go @@ -0,0 +1,147 @@ +//go:build darwin + +package webview + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework WebKit + +#import +#import + +typedef void (^schemeTaskCaller)(id); + +static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) { + id urlSchemeTask = (id) wkUrlSchemeTask; + if (urlSchemeTask == nil) { + return false; + } + + @autoreleasepool { + @try { + fn(urlSchemeTask); + } @catch (NSException *exception) { + // This is very bad to detect a stopped schemeTask this should be implemented in a better way + // But it seems to be very tricky to not deadlock when keeping a lock curing executing fn() + // It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want + // to get the lock again to start another request or stop it. + if ([exception.reason isEqualToString: @"This task has already been stopped"]) { + return false; + } + + @throw exception; + } + + return true; + } +} + +static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + NSData *nsdata = [NSData dataWithBytes:data length:datalength]; + [urlSchemeTask didReceiveData:nsdata]; + }); +} + +static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + [urlSchemeTask didFinish]; + }); +} + +static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength]; + NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options: NSJSONReadingMutableContainers error: nil]; + NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease]; + + [urlSchemeTask didReceiveResponse:response]; + }); +} +*/ +import "C" + +import ( + "encoding/json" + "net/http" + "unsafe" +) + +var _ ResponseWriter = &responseWriter{} + +type responseWriter struct { + r *request + + header http.Header + wroteHeader bool + + finished bool +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + + var content unsafe.Pointer + var contentLen int + if buf != nil { + content = unsafe.Pointer(&buf[0]) + contentLen = len(buf) + } + + if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) { + return 0, errRequestStopped + } + return contentLen, nil +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + header := map[string]string{} + for k := range rw.Header() { + header[k] = rw.Header().Get(k) + } + headerData, _ := json.Marshal(header) + + var headers unsafe.Pointer + var headersLen int + if len(headerData) != 0 { + headers = unsafe.Pointer(&headerData[0]) + headersLen = len(headerData) + } + + C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen)) +} + +func (rw *responseWriter) Finish() { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return + } + rw.finished = true + + C.URLSchemeTaskDidFinish(rw.r.task) +} diff --git a/v3/pkg/assetserver/webview/responsewriter_linux.go b/v3/pkg/assetserver/webview/responsewriter_linux.go new file mode 100644 index 000000000..9b3f53a78 --- /dev/null +++ b/v3/pkg/assetserver/webview/responsewriter_linux.go @@ -0,0 +1,129 @@ +//go:build linux +// +build linux + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +#include "gio/gunixinputstream.h" + +*/ +import "C" +import ( + "fmt" + "io" + "net/http" + "os" + "strconv" + "syscall" + "unsafe" +) + +type responseWriter struct { + req *C.WebKitURISchemeRequest + + header http.Header + wroteHeader bool + finished bool + + w io.WriteCloser + wErr error +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + if rw.wErr != nil { + return 0, rw.wErr + } + return rw.w.Write(buf) +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + contentLength := int64(-1) + if sLen := rw.Header().Get(HeaderContentLength); sLen != "" { + if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { + contentLength = pLen + } + } + + // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the + // read FD is given to the InputStream and will be closed there. + // Furthermore we especially don't want to have the FD_CLOEXEC + rFD, w, err := pipe() + if err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) + return + } + rw.w = w + + stream := C.g_unix_input_stream_new(C.int(rFD), C.gboolean(1)) + defer C.g_object_unref(C.gpointer(stream)) + + if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + return + } +} + +func (rw *responseWriter) Finish() { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return + } + rw.finished = true + if rw.w != nil { + rw.w.Close() + } +} + +func (rw *responseWriter) finishWithError(code int, err error) { + if rw.w != nil { + rw.w.Close() + rw.w = &nopCloser{io.Discard} + } + rw.wErr = err + + msg := C.CString(err.Error()) + gerr := C.g_error_new_literal(C.g_quark_from_string(msg), C.int(code), msg) + C.webkit_uri_scheme_request_finish_error(rw.req, gerr) + C.g_error_free(gerr) + C.free(unsafe.Pointer(msg)) +} + +type nopCloser struct { + io.Writer +} + +func (nopCloser) Close() error { return nil } + +func pipe() (r int, w *os.File, err error) { + var p [2]int + e := syscall.Pipe2(p[0:], 0) + if e != nil { + return 0, nil, fmt.Errorf("pipe2: %s", e) + } + + return p[0], os.NewFile(uintptr(p[1]), "|1"), nil +} diff --git a/v3/pkg/assetserver/webview/responsewriter_linux_purego.go b/v3/pkg/assetserver/webview/responsewriter_linux_purego.go new file mode 100644 index 000000000..c62f54a55 --- /dev/null +++ b/v3/pkg/assetserver/webview/responsewriter_linux_purego.go @@ -0,0 +1,174 @@ +//go:build linux && purego +// +build linux,purego + +package webview + +import ( + "fmt" + "io" + "net/http" + "os" + "strconv" + "syscall" + + "github.com/ebitengine/purego" +) + +const ( + gtk3 = "libgtk-3.so" + gtk4 = "libgtk-4.so" +) + +var ( + gtk uintptr + webkit uintptr + version int +) + +func init() { + var err error + // gtk, err = purego.Dlopen(gtk4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + // if err == nil { + // version = 4 + // return + // } + // log.Println("Failed to open GTK4: Falling back to GTK3") + gtk, err = purego.Dlopen(gtk3, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + version = 3 + + var webkit4 string = "libwebkit2gtk-4.1.so" + webkit, err = purego.Dlopen(webkit4, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } +} + +type responseWriter struct { + req uintptr + + header http.Header + wroteHeader bool + finished bool + + w io.WriteCloser + wErr error +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + if rw.wErr != nil { + return 0, rw.wErr + } + return rw.w.Write(buf) +} + +func (rw *responseWriter) WriteHeader(code int) { + // TODO? Is this ever called? I don't think so! + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + contentLength := int64(-1) + if sLen := rw.Header().Get(HeaderContentLength); sLen != "" { + if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { + contentLength = pLen + } + } + // We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the + // read FD is given to the InputStream and will be closed there. + // Furthermore we especially don't want to have the FD_CLOEXEC + rFD, w, err := pipe() + if err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) + return + } + rw.w = w + + var newStream func(int, bool) uintptr + purego.RegisterLibFunc(&newStream, gtk, "g_unix_input_stream_new") + var unRef func(uintptr) + purego.RegisterLibFunc(&unRef, gtk, "g_object_unref") + stream := newStream(rFD, true) + + /* var reqFinish func(uintptr, uintptr, uintptr, uintptr, int64) int + purego.RegisterLibFunc(&reqFinish, webkit, "webkit_uri_scheme_request_finish") + + header := rw.Header() + defer unRef(stream) + if err := reqFinish(rw.req, code, header, stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + } + */ + if err := webkit_uri_scheme_request_finish(rw.req, code, rw.Header(), stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + return + } +} + +func (rw *responseWriter) Finish() { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return + } + rw.finished = true + if rw.w != nil { + rw.w.Close() + } +} + +func (rw *responseWriter) finishWithError(code int, err error) { + if rw.w != nil { + rw.w.Close() + rw.w = &nopCloser{io.Discard} + } + rw.wErr = err + + var newLiteral func(uint32, string, int, string) uintptr // is this correct? + purego.RegisterLibFunc(&newLiteral, gtk, "g_error_new_literal") + var newQuark func(string) uintptr + purego.RegisterLibFunc(&newQuark, gtk, "g_quark_from_string") + var freeError func(uintptr) + purego.RegisterLibFunc(&freeError, gtk, "g_error_free") + var finishError func(uintptr, uintptr) + purego.RegisterLibFunc(&finishError, webkit, "webkit_uri_scheme_request_finish_error") + + msg := string(err.Error()) + //gquark := newQuark(msg) + gerr := newLiteral(1, msg, code, msg) + finishError(rw.req, gerr) + freeError(gerr) +} + +type nopCloser struct { + io.Writer +} + +func (nopCloser) Close() error { return nil } + +func pipe() (r int, w *os.File, err error) { + var p [2]int + e := syscall.Pipe2(p[0:], 0) + if e != nil { + return 0, nil, fmt.Errorf("pipe2: %s", e) + } + + return p[0], os.NewFile(uintptr(p[1]), "|1"), nil +} diff --git a/v3/pkg/assetserver/webview/webkit2_36+.go b/v3/pkg/assetserver/webview/webkit2_36+.go new file mode 100644 index 000000000..2c1a79c43 --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_36+.go @@ -0,0 +1,69 @@ +//go:build linux && (webkit2_36 || webkit2_40) + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 libsoup-2.4 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +#include "libsoup/soup.h" +*/ +import "C" + +import ( + "net/http" + "strings" + "unsafe" +) + +func webkit_uri_scheme_request_get_http_method(req *C.WebKitURISchemeRequest) string { + method := C.GoString(C.webkit_uri_scheme_request_get_http_method(req)) + return strings.ToUpper(method) +} + +func webkit_uri_scheme_request_get_http_headers(req *C.WebKitURISchemeRequest) http.Header { + hdrs := C.webkit_uri_scheme_request_get_http_headers(req) + + var iter C.SoupMessageHeadersIter + C.soup_message_headers_iter_init(&iter, hdrs) + + var name *C.char + var value *C.char + + h := http.Header{} + for C.soup_message_headers_iter_next(&iter, &name, &value) != 0 { + h.Add(C.GoString(name), C.GoString(value)) + } + + return h +} + +func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error { + resp := C.webkit_uri_scheme_response_new(stream, C.gint64(streamLength)) + defer C.g_object_unref(C.gpointer(resp)) + + cReason := C.CString(http.StatusText(code)) + C.webkit_uri_scheme_response_set_status(resp, C.guint(code), cReason) + C.free(unsafe.Pointer(cReason)) + + cMimeType := C.CString(header.Get(HeaderContentType)) + C.webkit_uri_scheme_response_set_content_type(resp, cMimeType) + C.free(unsafe.Pointer(cMimeType)) + + hdrs := C.soup_message_headers_new(C.SOUP_MESSAGE_HEADERS_RESPONSE) + for name, values := range header { + cName := C.CString(name) + for _, value := range values { + cValue := C.CString(value) + C.soup_message_headers_append(hdrs, cName, cValue) + C.free(unsafe.Pointer(cValue)) + } + C.free(unsafe.Pointer(cName)) + } + + C.webkit_uri_scheme_response_set_http_headers(resp, hdrs) + + C.webkit_uri_scheme_request_finish_with_response(req, resp) + return nil +} diff --git a/v3/pkg/assetserver/webview/webkit2_36+_purego.go b/v3/pkg/assetserver/webview/webkit2_36+_purego.go new file mode 100644 index 000000000..2386868c3 --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_36+_purego.go @@ -0,0 +1,94 @@ +//go:build linux && (webkit2_36 || webkit2_40) && purego + +package webview + +import ( + "net/http" + "strings" + + "github.com/ebitengine/purego" +) + +func webkit_uri_scheme_request_get_http_method(req uintptr) string { + var getMethod func(uintptr) string + purego.RegisterLibFunc(&getMethod, gtk, "webkit_uri_scheme_request_get_http_method") + return strings.ToUpper(getMethod(req)) +} + +func webkit_uri_scheme_request_get_http_headers(req uintptr) http.Header { + var getHeaders func(uintptr) uintptr + purego.RegisterLibFunc(&getUri, webkit, "webkit_uri_scheme_request_get_http_headers") + + hdrs := getHeaders(req) + + var headersIterInit func(uintptr, uintptr) uintptr + purego.RegisterLibFunc(&headersIterInit, gtk, "soup_message_headers_iter_init") + + // TODO: How do we get a struct? + /* + typedef struct { + SoupMessageHeaders *hdrs; + int index_common; + int index_uncommon; + } SoupMessageHeadersIterReal; + */ + iter := make([]byte, 12) + headersIterInit(&iter, hdrs) + + var iterNext func(uintptr, *string, *string) int + purego.RegisterLibFunc(&iterNext, gtk, "soup_message_headers_iter_next") + + var name string + var value string + h := http.Header{} + + for iterNext(&iter, &name, &value) != 0 { + h.Add(name, value) + } + + return h +} + +func webkit_uri_scheme_request_finish(req uintptr, code int, header http.Header, stream uintptr, streamLength int64) error { + + var newResponse func(uintptr, int64) string + purego.RegisterLibFunc(&newResponse, webkit, "webkit_uri_scheme_response_new") + var unRef func(uintptr) + purego.RegisterLibFunc(&unRef, gtk, "g_object_unref") + + resp := newResponse(stream, streamLength) + defer unRef(resp) + + var setStatus func(uintptr, int, string) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_status") + + setStatus(resp, code, cReason) + + var setContentType func(uintptr, string) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_content_type") + + setContentType(resp, header.Get(HeaderContentType)) + + soup := gtk + var soupHeadersNew func(int) uintptr + purego.RegisterLibFunc(&unRef, soup, "soup_message_headers_new") + var soupHeadersAppend func(uintptr, string, string) + purego.RegisterLibFunc(&unRef, soup, "soup_message_headers_append") + + hdrs := soupHeadersNew(SOUP_MESSAGE_HEADERS_RESPONSE) + for name, values := range header { + for _, value := range values { + soupHeadersAppend(hdrs, name, value) + } + } + + var setHttpHeaders func(uintptr, uintptr) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_response_set_http_headers") + + setHttpHeaders(resp, hdrs) + var finishWithResponse func(uintptr, uintptr) + purego.RegisterLibFunc(&unRef, webkit, "webkit_uri_scheme_request_finish_with_response") + finishWithResponse(req, resp) + + return nil +} diff --git a/v3/pkg/assetserver/webview/webkit2_36.go b/v3/pkg/assetserver/webview/webkit2_36.go new file mode 100644 index 000000000..cd200af8e --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_36.go @@ -0,0 +1,21 @@ +//go:build linux && webkit2_36 + +package webview + +/* +#cgo linux pkg-config: webkit2gtk-4.0 + +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "io" + "net/http" +) + +const Webkit2MinMinorVersion = 36 + +func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser { + return http.NoBody +} diff --git a/v3/pkg/assetserver/webview/webkit2_40+.go b/v3/pkg/assetserver/webview/webkit2_40+.go new file mode 100644 index 000000000..dceb0803d --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_40+.go @@ -0,0 +1,83 @@ +//go:build linux && webkit2_40 + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +#include "gio/gunixinputstream.h" +*/ +import "C" + +import ( + "fmt" + "io" + "net/http" + "unsafe" +) + +func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser { + stream := C.webkit_uri_scheme_request_get_http_body(req) + if stream == nil { + return http.NoBody + } + return &webkitRequestBody{stream: stream} +} + +type webkitRequestBody struct { + stream *C.GInputStream + closed bool +} + +// Read implements io.Reader +func (r *webkitRequestBody) Read(p []byte) (int, error) { + if r.closed { + return 0, io.ErrClosedPipe + } + + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + var n C.gsize + var gErr *C.GError + res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr) + if res == 0 { + return 0, formatGError("stream read failed", gErr) + } else if n == 0 { + return 0, io.EOF + } + return int(n), nil +} + +func (r *webkitRequestBody) Close() error { + if r.closed { + return nil + } + r.closed = true + + // https://docs.gtk.org/gio/method.InputStream.close.html + // Streams will be automatically closed when the last reference is dropped, but you might want to call this function + // to make sure resources are released as early as possible. + var err error + var gErr *C.GError + if C.g_input_stream_close(r.stream, nil, &gErr) == 0 { + err = formatGError("stream close failed", gErr) + } + C.g_object_unref(C.gpointer(r.stream)) + r.stream = nil + return err +} + +func formatGError(msg string, gErr *C.GError, args ...any) error { + if gErr != nil && gErr.message != nil { + msg += ": " + C.GoString(gErr.message) + C.g_error_free(gErr) + } + return fmt.Errorf(msg, args...) +} diff --git a/v3/pkg/assetserver/webview/webkit2_40+_purego.go b/v3/pkg/assetserver/webview/webkit2_40+_purego.go new file mode 100644 index 000000000..1088be25e --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_40+_purego.go @@ -0,0 +1,74 @@ +//go:build linux && webkit2_40 && purego + +package webview + +import ( + "fmt" + "io" + "net/http" + "unsafe" +) + +func webkit_uri_scheme_request_get_http_body(req *C.WebKitURISchemeRequest) io.ReadCloser { + stream := C.webkit_uri_scheme_request_get_http_body(req) + if stream == nil { + return http.NoBody + } + return &webkitRequestBody{stream: stream} +} + +type webkitRequestBody struct { + stream *C.GInputStream + closed bool +} + +// Read implements io.Reader +func (r *webkitRequestBody) Read(p []byte) (int, error) { + if r.closed { + return 0, io.ErrClosedPipe + } + + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + var n C.gsize + var gErr *C.GError + res := C.g_input_stream_read_all(r.stream, content, C.gsize(contentLen), &n, nil, &gErr) + if res == 0 { + return 0, formatGError("stream read failed", gErr) + } else if n == 0 { + return 0, io.EOF + } + return int(n), nil +} + +func (r *webkitRequestBody) Close() error { + if r.closed { + return nil + } + r.closed = true + + // https://docs.gtk.org/gio/method.InputStream.close.html + // Streams will be automatically closed when the last reference is dropped, but you might want to call this function + // to make sure resources are released as early as possible. + var err error + var gErr *C.GError + if C.g_input_stream_close(r.stream, nil, &gErr) == 0 { + err = formatGError("stream close failed", gErr) + } + C.g_object_unref(C.gpointer(r.stream)) + r.stream = nil + return err +} + +func formatGError(msg string, gErr *C.GError, args ...any) error { + if gErr != nil && gErr.message != nil { + msg += ": " + C.GoString(gErr.message) + C.g_error_free(gErr) + } + return fmt.Errorf(msg, args...) +} diff --git a/v3/pkg/assetserver/webview/webkit2_40.go b/v3/pkg/assetserver/webview/webkit2_40.go new file mode 100644 index 000000000..47b504383 --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_40.go @@ -0,0 +1,5 @@ +//go:build linux && webkit2_40 + +package webview + +const Webkit2MinMinorVersion = 40 diff --git a/v3/pkg/assetserver/webview/webkit2_legacy.go b/v3/pkg/assetserver/webview/webkit2_legacy.go new file mode 100644 index 000000000..1a87fe96a --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_legacy.go @@ -0,0 +1,48 @@ +//go:build linux && !(webkit2_36 || webkit2_40) + +package webview + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "fmt" + "io" + "net/http" + "unsafe" +) + +const Webkit2MinMinorVersion = 0 + +func webkit_uri_scheme_request_get_http_method(_ *C.WebKitURISchemeRequest) string { + return http.MethodGet +} + +func webkit_uri_scheme_request_get_http_headers(_ *C.WebKitURISchemeRequest) http.Header { + // Fake some basic default headers that are needed if e.g. request are being proxied to the an external sever, like + // we do in the devserver. + h := http.Header{} + h.Add("Accept", "*/*") + h.Add("User-Agent", "wails.io/605.1.15") + return h +} + +func webkit_uri_scheme_request_get_http_body(_ *C.WebKitURISchemeRequest) io.ReadCloser { + return http.NoBody +} + +func webkit_uri_scheme_request_finish(req *C.WebKitURISchemeRequest, code int, header http.Header, stream *C.GInputStream, streamLength int64) error { + if code != http.StatusOK { + return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code)) + } + + cMimeType := C.CString(header.Get(HeaderContentType)) + C.webkit_uri_scheme_request_finish(req, stream, C.gint64(streamLength), cMimeType) + C.free(unsafe.Pointer(cMimeType)) + return nil +} diff --git a/v3/pkg/assetserver/webview/webkit2_legacy_purego.go b/v3/pkg/assetserver/webview/webkit2_legacy_purego.go new file mode 100644 index 000000000..2e88864c8 --- /dev/null +++ b/v3/pkg/assetserver/webview/webkit2_legacy_purego.go @@ -0,0 +1,36 @@ +//go:build linux && !(webkit2_36 || webkit2_40) && purego + +package webview + +import ( + "fmt" + "io" + "net/http" + + "github.com/ebitengine/purego" +) + +const Webkit2MinMinorVersion = 0 + +func webkit_uri_scheme_request_get_http_method(_ uintptr) string { + return http.MethodGet +} + +func webkit_uri_scheme_request_get_http_headers(_ uintptr) http.Header { + return http.Header{} +} + +func webkit_uri_scheme_request_get_http_body(_ uintptr) io.ReadCloser { + return http.NoBody +} + +func webkit_uri_scheme_request_finish(req uintptr, code int, header http.Header, stream uintptr, streamLength int64) error { + if code != http.StatusOK { + return fmt.Errorf("StatusCodes not supported: %d - %s", code, http.StatusText(code)) + } + + var requestFinish func(uintptr, uintptr, int64, string) + purego.RegisterLibFunc(&requestFinish, webkit, "webkit_uri_scheme_request_finish") + requestFinish(req, stream, streamLength, header.Get(HeaderContentType)) + return nil +}