From 6aa57b6d829a7b490e0e536a16747eb3b328ea07 Mon Sep 17 00:00:00 2001 From: stffabi Date: Sun, 27 Nov 2022 22:27:30 +0100 Subject: [PATCH] [linux] Add support for WebKit2GTK 2.36+ features --- .../frontend/desktop/linux/frontend.go | 26 +++---- v2/internal/frontend/desktop/linux/webkit2.go | 29 ++++++++ .../frontend/desktop/linux/webkit2_36.go | 73 +++++++++++++++++++ .../frontend/desktop/linux/webkit2_legacy.go | 40 ++++++++++ ...nsewriter.go => webkit2_responsewriter.go} | 39 +++++----- v2/internal/frontend/desktop/linux/window.go | 18 +++++ v2/pkg/options/linux/linux.go | 13 ++++ website/docs/reference/options.mdx | 36 ++++----- website/src/pages/changelog.mdx | 1 + 9 files changed, 224 insertions(+), 51 deletions(-) create mode 100644 v2/internal/frontend/desktop/linux/webkit2.go create mode 100644 v2/internal/frontend/desktop/linux/webkit2_36.go create mode 100644 v2/internal/frontend/desktop/linux/webkit2_legacy.go rename v2/internal/frontend/desktop/linux/{responsewriter.go => webkit2_responsewriter.go} (76%) diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 072cfdf2324..0e1c1de39c7 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -126,15 +126,17 @@ func (f *Frontend) WindowClose() { func init() { runtime.LockOSThread() -} - -func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { // Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings if os.Getenv("GDK_BACKEND") == "" && (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") { _ = os.Setenv("GDK_BACKEND", "x11") } + C.gtk_init(nil, nil) +} + +func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { + result := &Frontend{ frontendOptions: appoptions, logger: myLogger, @@ -171,8 +173,6 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. go result.startMessageProcessor() - C.gtk_init(nil, nil) - var _debug = ctx.Value("debug") if _debug != nil { result.debug = _debug.(bool) @@ -480,8 +480,6 @@ func (f *Frontend) processRequest(request unsafe.Pointer) { uri := C.webkit_uri_scheme_request_get_uri(req) goURI := C.GoString(uri) - // WebKitGTK stable < 2.36 API does not support request method, request headers and request. - // Apart from request bodies, this is only available beginning with 2.36: https://webkitgtk.org/reference/webkit2gtk/stable/WebKitURISchemeResponse.html rw := &webKitResponseWriter{req: req} defer rw.Close() @@ -489,20 +487,22 @@ func (f *Frontend) processRequest(request unsafe.Pointer) { goURI, rw, func() (*http.Request, error) { - req, err := http.NewRequest(http.MethodGet, goURI, nil) + method := webkit_uri_scheme_request_get_http_method(req) + r, err := http.NewRequest(method, goURI, nil) if err != nil { return nil, err } + r.Header = webkit_uri_scheme_request_get_http_headers(req) - if req.URL.Host != f.startURL.Host { - if req.Body != nil { - req.Body.Close() + if r.URL.Host != f.startURL.Host { + if r.Body != nil { + r.Body.Close() } - return nil, fmt.Errorf("Expected host '%s' in request, but was '%s'", f.startURL.Host, req.URL.Host) + return nil, fmt.Errorf("Expected host '%s' in request, but was '%s'", f.startURL.Host, r.URL.Host) } - return req, nil + return r, nil }) } diff --git a/v2/internal/frontend/desktop/linux/webkit2.go b/v2/internal/frontend/desktop/linux/webkit2.go new file mode 100644 index 00000000000..9d64104c405 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/webkit2.go @@ -0,0 +1,29 @@ +//go:build linux + +package linux + +/* +#cgo linux pkg-config: webkit2gtk-4.0 +#include "webkit2/webkit2.h" +*/ +import "C" +import ( + "fmt" + + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/linux" +) + +func validateWebKit2Version(options *options.App) { + if C.webkit_get_major_version() == 2 && C.webkit_get_minor_version() >= webkit2MinMinorVersion { + return + } + + msg := linux.DefaultMessages() + if options.Linux != nil && options.Linux.Messages != nil { + msg = options.Linux.Messages + } + + v := fmt.Sprintf("2.%d.0", webkit2MinMinorVersion) + showModalDialogAndExit("WebKit2GTK", fmt.Sprintf(msg.WebKit2GTKMinRequired, v)) +} diff --git a/v2/internal/frontend/desktop/linux/webkit2_36.go b/v2/internal/frontend/desktop/linux/webkit2_36.go new file mode 100644 index 00000000000..a5669ba0890 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/webkit2_36.go @@ -0,0 +1,73 @@ +//go:build linux && webkit2_36 + +package linux + +/* +#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" + + "github.com/wailsapp/wails/v2/internal/frontend/assetserver" +) + +const webkit2MinMinorVersion = 36 + +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(assetserver.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/v2/internal/frontend/desktop/linux/webkit2_legacy.go b/v2/internal/frontend/desktop/linux/webkit2_legacy.go new file mode 100644 index 00000000000..d3c1b581b29 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/webkit2_legacy.go @@ -0,0 +1,40 @@ +//go:build linux && !webkit2_36 + +package linux + +/* +#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 + +#include "gtk/gtk.h" +#include "webkit2/webkit2.h" +*/ +import "C" + +import ( + "fmt" + "net/http" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/assetserver" +) + +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 { + return http.Header{} +} + +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(assetserver.HeaderContentType)) + C.webkit_uri_scheme_request_finish(req, stream, C.gint64(streamLength), cMimeType) + C.free(unsafe.Pointer(cMimeType)) + return nil +} diff --git a/v2/internal/frontend/desktop/linux/responsewriter.go b/v2/internal/frontend/desktop/linux/webkit2_responsewriter.go similarity index 76% rename from v2/internal/frontend/desktop/linux/responsewriter.go rename to v2/internal/frontend/desktop/linux/webkit2_responsewriter.go index 3b08a3845a5..0f858b805fb 100644 --- a/v2/internal/frontend/desktop/linux/responsewriter.go +++ b/v2/internal/frontend/desktop/linux/webkit2_responsewriter.go @@ -55,11 +55,11 @@ func (rw *webKitResponseWriter) WriteHeader(code int) { } rw.wroteHeader = true - if code != http.StatusOK { - // WebKitGTK stable < 2.36 API does not support response headers and response statuscodes - rw.w = &nopCloser{io.Discard} - rw.finishWithError(http.StatusText(code), code) - return + contentLength := int64(-1) + if sLen := rw.Header().Get(assetserver.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 @@ -67,25 +67,18 @@ func (rw *webKitResponseWriter) WriteHeader(code int) { // Furthermore we especially don't want to have the FD_CLOEXEC rFD, w, err := pipe() if err != nil { - rw.wErr = fmt.Errorf("Unable opening pipe: %s", err) - rw.finishWithError(rw.wErr.Error(), http.StatusInternalServerError) + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) return } rw.w = w - cMimeType := C.CString(rw.Header().Get(assetserver.HeaderContentType)) - defer C.free(unsafe.Pointer(cMimeType)) + stream := C.g_unix_input_stream_new(C.int(rFD), gtkBool(true)) + defer C.g_object_unref(C.gpointer(stream)) - contentLength := int64(-1) - if sLen := rw.Header().Get(assetserver.HeaderContentLength); sLen != "" { - if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { - contentLength = pLen - } + 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 } - - stream := C.g_unix_input_stream_new(C.int(rFD), gtkBool(true)) - C.webkit_uri_scheme_request_finish(rw.req, stream, C.gint64(contentLength), cMimeType) - C.g_object_unref(C.gpointer(stream)) } func (rw *webKitResponseWriter) Close() { @@ -94,8 +87,14 @@ func (rw *webKitResponseWriter) Close() { } } -func (rw *webKitResponseWriter) finishWithError(message string, code int) { - msg := C.CString(http.StatusText(code)) +func (rw *webKitResponseWriter) 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) diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go index efde2d68e05..782083d9bd3 100644 --- a/v2/internal/frontend/desktop/linux/window.go +++ b/v2/internal/frontend/desktop/linux/window.go @@ -642,6 +642,7 @@ static void SetWindowTransparency(GtkWidget *widget) */ import "C" import ( + "log" "strings" "sync" "unsafe" @@ -679,6 +680,7 @@ func bool2Cint(value bool) C.int { } func NewWindow(appoptions *options.App, debug bool) *Window { + validateWebKit2Version(appoptions) result := &Window{ appoptions: appoptions, @@ -1034,3 +1036,19 @@ func (w *Window) ToggleMaximise() { w.Maximise() } } + +// showModalDialogAndExit shows a modal dialog and exits the app. +func showModalDialogAndExit(title, message string) { + go func() { + data := C.MessageDialogOptions{ + title: C.CString(title), + message: C.CString(message), + messageType: C.int(1), + } + + C.messageDialog(unsafe.Pointer(&data)) + }() + + <-messageDialogResult + log.Fatal(message) +} diff --git a/v2/pkg/options/linux/linux.go b/v2/pkg/options/linux/linux.go index b44186b4ceb..9a9fa56f9d4 100644 --- a/v2/pkg/options/linux/linux.go +++ b/v2/pkg/options/linux/linux.go @@ -4,4 +4,17 @@ package linux type Options struct { Icon []byte WindowIsTranslucent bool + + // User messages that can be customised + Messages *Messages +} + +type Messages struct { + WebKit2GTKMinRequired string +} + +func DefaultMessages() *Messages { + return &Messages{ + WebKit2GTKMinRequired: "This application requires at least WebKit2GTK %s to be installed.", + } } diff --git a/website/docs/reference/options.mdx b/website/docs/reference/options.mdx index 13426d00972..dd0d6e38c0a 100644 --- a/website/docs/reference/options.mdx +++ b/website/docs/reference/options.mdx @@ -249,24 +249,24 @@ dynamically with an `http.Handler` or hook into the request chain with an `asset Not all features of an `http.Request` are currently supported, please see the following feature matrix: -| Feature | Win | Mac | Lin | -| ----------------------- | --- | --- | --- | -| GET | ✅ | ✅ | ✅ | -| POST | ✅ | ✅ | ❌ | -| PUT | ✅ | ✅ | ❌ | -| PATCH | ✅ | ✅ | ❌ | -| DELETE | ✅ | ✅ | ❌ | -| Request Headers | ✅ | ✅ | ❌ | -| Request Body | ✅ | ✅ | ❌ | -| Request Body Streaming | ❌ | ❌ | ❌ | -| Response StatusCodes | ✅ | ✅ | ❌ | -| Response Headers | ✅ | ✅ | ❌ | -| Response Body | ✅ | ✅ | ✅ | -| Response Body Streaming | ❌ | ❌ | ✅ | -| WebSockets | ❌ | ❌ | ❌ | - -NOTE: Linux is currently very limited due to targeting a WebKit2GTK Version < 2.36.0. In the future some features will be -supported by the introduction of WebKit2GTK 2.36.0+ support. +| Feature | Win | Mac | Lin | +| ----------------------- | --- | --- | ------ | +| GET | ✅ | ✅ | ✅ | +| POST | ✅ | ✅ | ✅ [^1] | +| PUT | ✅ | ✅ | ✅ [^1] | +| PATCH | ✅ | ✅ | ✅ [^1] | +| DELETE | ✅ | ✅ | ✅ [^1] | +| Request Headers | ✅ | ✅ | ✅ [^1] | +| Request Body | ✅ | ✅ | ❌ | +| Request Body Streaming | ❌ | ❌ | ❌ | +| Response StatusCodes | ✅ | ✅ | ✅ [^1] | +| Response Headers | ✅ | ✅ | ✅ [^1] | +| Response Body | ✅ | ✅ | ✅ | +| Response Body Streaming | ❌ | ❌ | ✅ | +| WebSockets | ❌ | ❌ | ❌ | +| HTTP Redirects 30x | ✅ | ❌ | ❌ | + +[^1]: This requires WebKit2GTK 2.36+ support and your app needs to be build with the build tag `webkit2_36` to activate support for this feature. This also bumps the minimum requirement of WebKit2GTK to 2.36 for your app. Name: AssetServer
Type: `*assetserver.Options` diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 8faa7d705cf..6cf1f61c88a 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add new property for the `wails.json` config file - `bindings`. More information on the new property can be found in the updated [schema](/schemas/config.v2.json). Properties `prefix` and `suffix` allow you to control the generated TypeScript entity name in the `model.ts` file. Added by @OlegGulevskyy in [PR](https://github.com/wailsapp/wails/pull/2101) - The `WindowSetAlwaysOnTop` method is now exposed in the JS runtime. Fixed by @gotid in [PR](https://github.com/wailsapp/wails/pull/2128) - The [AssetServer](/docs/reference/options#assetserver) now supports serving the index.html file when requesting a directory. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2110) +- Added support for WebKit2GTK 2.36+ on Linux. This brings additional features for the [AssetServer](/docs/reference/options#assetserver), like support for HTTP methods and Headers. The app must be compiled with the Go build tag `webkit2_36` to activate support for this features. This also bumps the minimum requirement of WebKit2GTK to 2.36 for your app. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2151) ### Fixed - The `noreload` flag in wails dev wasn't applied. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2081)