Skip to content

Commit

Permalink
[linux] Add support for WebKit2GTK 2.36+ features
Browse files Browse the repository at this point in the history
  • Loading branch information
stffabi committed Nov 30, 2022
1 parent 7f8952e commit 6aa57b6
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 51 deletions.
26 changes: 13 additions & 13 deletions v2/internal/frontend/desktop/linux/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -480,29 +480,29 @@ 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()

f.assets.ProcessHTTPRequest(
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
})

}
29 changes: 29 additions & 0 deletions v2/internal/frontend/desktop/linux/webkit2.go
Original file line number Diff line number Diff line change
@@ -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))
}
73 changes: 73 additions & 0 deletions v2/internal/frontend/desktop/linux/webkit2_36.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions v2/internal/frontend/desktop/linux/webkit2_legacy.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,37 +55,30 @@ 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
// 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.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() {
Expand All @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions v2/internal/frontend/desktop/linux/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ static void SetWindowTransparency(GtkWidget *widget)
*/
import "C"
import (
"log"
"strings"
"sync"
"unsafe"
Expand Down Expand Up @@ -679,6 +680,7 @@ func bool2Cint(value bool) C.int {
}

func NewWindow(appoptions *options.App, debug bool) *Window {
validateWebKit2Version(appoptions)

result := &Window{
appoptions: appoptions,
Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions v2/pkg/options/linux/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
}
}
36 changes: 18 additions & 18 deletions website/docs/reference/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>
Type: `*assetserver.Options`
Expand Down
1 change: 1 addition & 0 deletions website/src/pages/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 6aa57b6

Please sign in to comment.