mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 04:42:00 +08:00
[assetServer, darwin] Use AssetServer native WKWebView request handling (#2283)
This commit is contained in:
parent
8f92cf1074
commit
caa0b6c804
@ -48,11 +48,6 @@ const bool IsFullScreen(void *ctx);
|
||||
const bool IsMinimised(void *ctx);
|
||||
const bool IsMaximised(void *ctx);
|
||||
|
||||
void ProcessURLDidReceiveResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength);
|
||||
bool ProcessURLDidReceiveData(void *inctx, unsigned long long requestId, void* data, int datalength);
|
||||
void ProcessURLDidFinish(void *inctx, unsigned long long requestId);
|
||||
int ProcessURLRequestReadBodyStream(void *inctx, unsigned long long requestId, void *buf, int bufLen);
|
||||
|
||||
/* Dialogs */
|
||||
|
||||
void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength);
|
||||
|
@ -52,36 +52,6 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
|
||||
return result;
|
||||
}
|
||||
|
||||
void ProcessURLDidReceiveResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
@autoreleasepool {
|
||||
NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
|
||||
[ctx processURLDidReceiveResponse:requestId :statusCode :nsHeadersJSON];
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessURLDidReceiveData(void *inctx, unsigned long long requestId, void* data, int datalength) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
@autoreleasepool {
|
||||
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
|
||||
return [ctx processURLDidReceiveData:requestId :nsdata];
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessURLDidFinish(void *inctx, unsigned long long requestId) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
@autoreleasepool {
|
||||
[ctx processURLDidFinish:requestId];
|
||||
}
|
||||
}
|
||||
|
||||
int ProcessURLRequestReadBodyStream(void *inctx, unsigned long long requestId, void *buf, int bufLen) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
@autoreleasepool {
|
||||
return [ctx processURLRequestReadBodyStream:requestId :buf :bufLen];
|
||||
}
|
||||
}
|
||||
|
||||
void ExecJS(void* inctx, const char *script) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
NSString *nsscript = safeInit(script);
|
||||
|
@ -47,9 +47,6 @@
|
||||
@property bool debug;
|
||||
|
||||
@property (retain) WKUserContentController* userContentController;
|
||||
@property (retain) NSLock *urlRequestsLock;
|
||||
@property unsigned long long urlRequestsId;
|
||||
@property (retain) NSMutableDictionary *urlRequests;
|
||||
|
||||
@property (retain) NSMenu* applicationMenu;
|
||||
|
||||
@ -89,10 +86,6 @@
|
||||
- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
|
||||
|
||||
- (void) loadRequest:(NSString*)url;
|
||||
- (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON;
|
||||
- (bool) processURLDidReceiveData:(unsigned long long)requestId :(NSData *)data;
|
||||
- (void) processURLDidFinish:(unsigned long long)requestId;
|
||||
- (int) processURLRequestReadBodyStream:(unsigned long long)requestId :(void *)buf :(int)bufLen;
|
||||
- (void) ExecJS:(NSString*)script;
|
||||
- (NSScreen*) getCurrentScreen;
|
||||
|
||||
|
@ -108,7 +108,6 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
||||
[self.mainWindow release];
|
||||
[self.mouseEvent release];
|
||||
[self.userContentController release];
|
||||
[self.urlRequests release];
|
||||
[self.applicationMenu release];
|
||||
[super dealloc];
|
||||
}
|
||||
@ -138,9 +137,6 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
||||
}
|
||||
|
||||
- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled {
|
||||
self.urlRequestsId = 0;
|
||||
self.urlRequests = [NSMutableDictionary new];
|
||||
|
||||
NSWindowStyleMask styleMask = 0;
|
||||
|
||||
if( !frameless ) {
|
||||
@ -431,158 +427,21 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON {
|
||||
[self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
|
||||
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData: headersJSON options: NSJSONReadingMutableContainers error: nil];
|
||||
NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease];
|
||||
|
||||
[urlSchemeTask didReceiveResponse:response];
|
||||
}];
|
||||
}
|
||||
|
||||
- (bool) processURLDidReceiveData:(unsigned long long)requestId :(NSData *)data {
|
||||
return [self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
|
||||
[urlSchemeTask didReceiveData:data];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) processURLDidFinish:(unsigned long long)requestId {
|
||||
[self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
|
||||
[urlSchemeTask didFinish];
|
||||
}];
|
||||
|
||||
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
|
||||
[self removeURLSchemeTask:key];
|
||||
}
|
||||
|
||||
- (int) processURLRequestReadBodyStream:(unsigned long long)requestId :(void *)buf :(int)bufLen {
|
||||
int res = 0;
|
||||
int *pRes = &res;
|
||||
|
||||
bool hasRequest = [self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> urlSchemeTask) {
|
||||
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
|
||||
if (!stream) {
|
||||
*pRes = -3;
|
||||
} else {
|
||||
NSStreamStatus status = stream.streamStatus;
|
||||
if (status == NSStreamStatusAtEnd) {
|
||||
*pRes = 0;
|
||||
} else if (status != NSStreamStatusOpen) {
|
||||
*pRes = -4;
|
||||
} else if (!stream.hasBytesAvailable) {
|
||||
*pRes = 0;
|
||||
} else {
|
||||
*pRes = [stream read:buf maxLength:bufLen];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
if (!hasRequest) {
|
||||
res = -2;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
|
||||
// This callback is run with an autorelease pool
|
||||
const char *url = [urlSchemeTask.request.URL.absoluteString UTF8String];
|
||||
const char *method = [urlSchemeTask.request.HTTPMethod UTF8String];
|
||||
const char *headerJSON = "";
|
||||
const void *body = nil;
|
||||
int bodyLen = 0;
|
||||
int hasBodyStream = 0;
|
||||
|
||||
NSData *headers = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
|
||||
if (headers) {
|
||||
NSString* headerString = [[[NSString alloc] initWithData:headers encoding:NSUTF8StringEncoding] autorelease];
|
||||
headerJSON = [headerString UTF8String];
|
||||
}
|
||||
|
||||
if (urlSchemeTask.request.HTTPBody) {
|
||||
body = urlSchemeTask.request.HTTPBody.bytes;
|
||||
bodyLen = urlSchemeTask.request.HTTPBody.length;
|
||||
} else if (urlSchemeTask.request.HTTPBodyStream) {
|
||||
hasBodyStream = 1;
|
||||
[urlSchemeTask.request.HTTPBodyStream open];
|
||||
}
|
||||
|
||||
unsigned long long requestId;
|
||||
@synchronized(self.urlRequests) {
|
||||
self.urlRequestsId++;
|
||||
requestId = self.urlRequestsId;
|
||||
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
|
||||
self.urlRequests[key] = urlSchemeTask;
|
||||
}
|
||||
|
||||
processURLRequest(self, requestId, url, method, headerJSON, body, bodyLen, hasBodyStream);
|
||||
processURLRequest(self, urlSchemeTask);
|
||||
}
|
||||
|
||||
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
|
||||
NSArray<NSNumber*> *keys;
|
||||
@synchronized(self.urlRequests) {
|
||||
keys = [self.urlRequests allKeys];
|
||||
}
|
||||
|
||||
for (NSNumber *key in keys) {
|
||||
if (self.urlRequests[key] == urlSchemeTask) {
|
||||
[self removeURLSchemeTask:key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) removeURLSchemeTask:(NSNumber *)urlSchemeTaskKey {
|
||||
id<WKURLSchemeTask> urlSchemeTask = nil;
|
||||
@synchronized(self.urlRequests) {
|
||||
urlSchemeTask = self.urlRequests[urlSchemeTaskKey];
|
||||
}
|
||||
|
||||
if (!urlSchemeTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
|
||||
if (stream) {
|
||||
NSStreamStatus status = stream.streamStatus;
|
||||
if (status != NSStreamStatusClosed && status != NSStreamStatusNotOpen) {
|
||||
[stream close];
|
||||
}
|
||||
|
||||
@synchronized(self.urlRequests) {
|
||||
[self.urlRequests removeObjectForKey:urlSchemeTaskKey];
|
||||
}
|
||||
}
|
||||
|
||||
- (bool)processURLSchemeTaskCall:(unsigned long long)requestId :(schemeTaskCaller)fn {
|
||||
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
|
||||
|
||||
id<WKURLSchemeTask> urlSchemeTask;
|
||||
@synchronized(self.urlRequests) {
|
||||
urlSchemeTask = self.urlRequests[key];
|
||||
}
|
||||
|
||||
if (urlSchemeTask == nil) {
|
||||
// Stopped task, drop content...
|
||||
return false;
|
||||
}
|
||||
|
||||
@try {
|
||||
fn(urlSchemeTask);
|
||||
} @catch (NSException *exception) {
|
||||
[self removeURLSchemeTask:key];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
|
||||
processMessage("DomReady");
|
||||
}
|
||||
|
@ -19,11 +19,12 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
@ -35,7 +36,7 @@ import (
|
||||
const startURL = "wails://wails/"
|
||||
|
||||
var messageBuffer = make(chan string, 100)
|
||||
var requestBuffer = make(chan *wkWebViewRequest, 100)
|
||||
var requestBuffer = make(chan webview.Request, 100)
|
||||
var callbackBuffer = make(chan uint, 10)
|
||||
|
||||
type Frontend struct {
|
||||
@ -93,13 +94,11 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
assets.ExpectedWebViewHost = result.startURL.Host
|
||||
result.assets = assets
|
||||
|
||||
// Start 10 processors to handle requests in parallel
|
||||
for i := 0; i < 10; i++ {
|
||||
go result.startRequestProcessor()
|
||||
}
|
||||
}
|
||||
|
||||
go result.startMessageProcessor()
|
||||
go result.startCallbackProcessor()
|
||||
@ -114,7 +113,8 @@ func (f *Frontend) startMessageProcessor() {
|
||||
}
|
||||
func (f *Frontend) startRequestProcessor() {
|
||||
for request := range requestBuffer {
|
||||
f.processRequest(request)
|
||||
f.assets.ServeWebViewRequest(request)
|
||||
request.Release()
|
||||
}
|
||||
}
|
||||
func (f *Frontend) startCallbackProcessor() {
|
||||
@ -344,31 +344,6 @@ func (f *Frontend) ExecJS(js string) {
|
||||
f.mainWindow.ExecJS(js)
|
||||
}
|
||||
|
||||
func (f *Frontend) processRequest(r *wkWebViewRequest) {
|
||||
rw := &wkWebViewResponseWriter{r: r}
|
||||
defer rw.Close()
|
||||
|
||||
f.assets.ProcessHTTPRequest(
|
||||
r.url,
|
||||
rw,
|
||||
func() (*http.Request, error) {
|
||||
req, err := r.GetHttpRequest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.URL.Host != f.startURL.Host {
|
||||
if req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Expected host '%s' in request, but was '%s'", f.startURL.Host, req.URL.Host)
|
||||
}
|
||||
return req, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
//func (f *Frontend) processSystemEvent(message string) {
|
||||
// sl := strings.Split(message, ":")
|
||||
// if len(sl) != 2 {
|
||||
@ -395,3 +370,8 @@ func processMessage(message *C.char) {
|
||||
func processCallback(callbackID uint) {
|
||||
callbackBuffer <- callbackID
|
||||
}
|
||||
|
||||
//export processURLRequest
|
||||
func processURLRequest(ctx unsafe.Pointer, wkURLSchemeTask unsafe.Pointer) {
|
||||
requestBuffer <- webview.NewRequest(wkURLSchemeTask)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ extern "C"
|
||||
#endif
|
||||
|
||||
void processMessage(const char *);
|
||||
void processURLRequest(void*, unsigned long long, const char *, const char *, const char *, const void *, int, int);
|
||||
void processURLRequest(void *, void*);
|
||||
void processMessageDialogResponse(int);
|
||||
void processOpenFileDialogResponse(const char*);
|
||||
void processSaveFileDialogResponse(const char*);
|
||||
|
@ -1,106 +0,0 @@
|
||||
//go:build darwin
|
||||
|
||||
package darwin
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
|
||||
|
||||
#import "Application.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//export processURLRequest
|
||||
func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int, hasBodyStream C.int) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil && bodyLen != 0 {
|
||||
bodyReader = bytes.NewReader(C.GoBytes(body, bodyLen))
|
||||
} else if hasBodyStream != 0 {
|
||||
bodyReader = &bodyStreamReader{id: requestId, ctx: ctx}
|
||||
}
|
||||
|
||||
requestBuffer <- &wkWebViewRequest{
|
||||
id: requestId,
|
||||
url: C.GoString(url),
|
||||
method: C.GoString(method),
|
||||
headers: C.GoString(headers),
|
||||
body: bodyReader,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
type wkWebViewRequest struct {
|
||||
id C.ulonglong
|
||||
url string
|
||||
method string
|
||||
headers string
|
||||
body io.Reader
|
||||
|
||||
ctx unsafe.Pointer
|
||||
}
|
||||
|
||||
func (r *wkWebViewRequest) GetHttpRequest() (*http.Request, error) {
|
||||
req, err := http.NewRequest(r.method, r.url, r.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.headers != "" {
|
||||
var h map[string]string
|
||||
if err := json.Unmarshal([]byte(r.headers), &h); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal request headers: %s", err)
|
||||
}
|
||||
|
||||
for k, v := range h {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
var _ io.Reader = &bodyStreamReader{}
|
||||
|
||||
type bodyStreamReader struct {
|
||||
id C.ulonglong
|
||||
ctx unsafe.Pointer
|
||||
}
|
||||
|
||||
// Read implements io.Reader
|
||||
func (r *bodyStreamReader) 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.ProcessURLRequestReadBodyStream(r.ctx, r.id, 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, errRequestStopped
|
||||
case -3:
|
||||
return 0, fmt.Errorf("body: no stream defined")
|
||||
case -4:
|
||||
return 0, io.ErrClosedPipe
|
||||
default:
|
||||
return 0, fmt.Errorf("body: unknown error %d", res)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//go:build darwin
|
||||
|
||||
package darwin
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
|
||||
|
||||
#import "Application.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
errRequestStopped = errors.New("request has been stopped")
|
||||
)
|
||||
|
||||
type wkWebViewResponseWriter struct {
|
||||
r *wkWebViewRequest
|
||||
|
||||
header http.Header
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (rw *wkWebViewResponseWriter) Header() http.Header {
|
||||
if rw.header == nil {
|
||||
rw.header = http.Header{}
|
||||
}
|
||||
return rw.header
|
||||
}
|
||||
|
||||
func (rw *wkWebViewResponseWriter) Write(buf []byte) (int, error) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
|
||||
var content unsafe.Pointer
|
||||
var contentLen int
|
||||
if buf != nil {
|
||||
content = unsafe.Pointer(&buf[0])
|
||||
contentLen = len(buf)
|
||||
}
|
||||
|
||||
if !C.ProcessURLDidReceiveData(rw.r.ctx, rw.r.id, content, C.int(contentLen)) {
|
||||
return 0, errRequestStopped
|
||||
}
|
||||
return contentLen, nil
|
||||
}
|
||||
|
||||
func (rw *wkWebViewResponseWriter) WriteHeader(code int) {
|
||||
if rw.wroteHeader {
|
||||
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.ProcessURLDidReceiveResponse(rw.r.ctx, rw.r.id, C.int(code), headers, C.int(headersLen))
|
||||
}
|
||||
|
||||
func (rw *wkWebViewResponseWriter) Close() {
|
||||
if !rw.wroteHeader {
|
||||
rw.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
C.ProcessURLDidFinish(rw.r.ctx, rw.r.id)
|
||||
}
|
@ -486,8 +486,7 @@ func (f *Frontend) processRequest(request unsafe.Pointer) {
|
||||
rw := &webKitResponseWriter{req: req}
|
||||
defer rw.Close()
|
||||
|
||||
f.assets.ProcessHTTPRequest(
|
||||
goURI,
|
||||
f.assets.ProcessHTTPRequestLegacy(
|
||||
rw,
|
||||
func() (*http.Request, error) {
|
||||
method := webkit_uri_scheme_request_get_http_method(req)
|
||||
|
@ -547,10 +547,8 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg
|
||||
return
|
||||
}
|
||||
|
||||
logInfo := strings.Replace(uri, f.startURL.String(), "", 1)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
f.assets.ProcessHTTPRequest(logInfo, rw, coreWebview2RequestToHttpRequest(req))
|
||||
f.assets.ProcessHTTPRequestLegacy(rw, coreWebview2RequestToHttpRequest(req))
|
||||
|
||||
headers := []string{}
|
||||
for k, v := range rw.Header() {
|
||||
|
@ -2,10 +2,8 @@ package assetserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
|
||||
@ -35,6 +33,8 @@ type AssetServer struct {
|
||||
|
||||
servingFromDisk bool
|
||||
appendSpinnerToBody bool
|
||||
|
||||
assetServerWebView
|
||||
}
|
||||
|
||||
func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
|
||||
@ -134,47 +134,6 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) ProcessHTTPRequest(logInfo string, rw http.ResponseWriter, reqGetter func() (*http.Request, error)) {
|
||||
rw = &contentTypeSniffer{rw: rw} // Make sure we have a Content-Type sniffer
|
||||
|
||||
req, err := reqGetter()
|
||||
if err != nil {
|
||||
d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err)
|
||||
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Body == nil {
|
||||
req.Body = http.NoBody
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
d.ServeHTTP(rw, req)
|
||||
rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status
|
||||
}
|
||||
|
||||
func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
|
||||
htmlNode, err := getHTMLNode(indexHTML)
|
||||
if err != nil {
|
||||
|
86
v2/pkg/assetserver/assetserver_legacy.go
Normal file
86
v2/pkg/assetserver/assetserver_legacy.go
Normal file
@ -0,0 +1,86 @@
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/wailsapp/wails/v2/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) AddRef() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r legacyRequest) Release() 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() error {
|
||||
return nil
|
||||
}
|
163
v2/pkg/assetserver/assetserver_webview.go
Normal file
163
v2/pkg/assetserver/assetserver_webview.go
Normal file
@ -0,0 +1,163 @@
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/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.
|
||||
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 {
|
||||
d.processWebViewRequest(req)
|
||||
req.Release()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
dispatchC := make(chan webview.Request)
|
||||
go queueingDispatcher(50, dispatchC, workerC)
|
||||
|
||||
d.dispatchReqC = dispatchC
|
||||
})
|
||||
|
||||
if err := req.AddRef(); err != nil {
|
||||
uri, _ := req.URL()
|
||||
d.logError("Unable to call AddRef for request '%s'", uri)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
101
v2/pkg/assetserver/ringqueue.go
Normal file
101
v2/pkg/assetserver/ringqueue.go
Normal file
@ -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
|
||||
}
|
18
v2/pkg/assetserver/webview/request.go
Normal file
18
v2/pkg/assetserver/webview/request.go
Normal file
@ -0,0 +1,18 @@
|
||||
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
|
||||
|
||||
AddRef() error
|
||||
Release() error
|
||||
}
|
251
v2/pkg/assetserver/webview/request_darwin.go
Normal file
251
v2/pkg/assetserver/webview/request_darwin.go
Normal file
@ -0,0 +1,251 @@
|
||||
//go:build darwin
|
||||
|
||||
package webview
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework WebKit
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
|
||||
[urlSchemeTask retain];
|
||||
}
|
||||
|
||||
static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
|
||||
[urlSchemeTask release];
|
||||
}
|
||||
|
||||
static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
|
||||
@autoreleasepool {
|
||||
return [urlSchemeTask.request.URL.absoluteString UTF8String];
|
||||
}
|
||||
}
|
||||
|
||||
static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
|
||||
@autoreleasepool {
|
||||
return [urlSchemeTask.request.HTTPMethod UTF8String];
|
||||
}
|
||||
}
|
||||
|
||||
static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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];
|
||||
|
||||
char * headersOut = malloc(strlen(headerJSON));
|
||||
strcpy(headersOut, headerJSON);
|
||||
return headersOut;
|
||||
}
|
||||
}
|
||||
|
||||
static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
|
||||
@autoreleasepool {
|
||||
if (!urlSchemeTask.request.HTTPBodyStream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[urlSchemeTask.request.HTTPBodyStream open];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
|
||||
@autoreleasepool {
|
||||
if (!urlSchemeTask.request.HTTPBodyStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
[urlSchemeTask.request.HTTPBodyStream close];
|
||||
}
|
||||
}
|
||||
|
||||
static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask>`
|
||||
//
|
||||
// Please make sure to call Release() when finished using the request.
|
||||
func NewRequest(wkURLSchemeTask unsafe.Pointer) Request {
|
||||
C.URLSchemeTaskRetain(wkURLSchemeTask)
|
||||
return &request{task: wkURLSchemeTask}
|
||||
}
|
||||
|
||||
var _ Request = &request{}
|
||||
|
||||
type request struct {
|
||||
task unsafe.Pointer
|
||||
|
||||
header http.Header
|
||||
body io.ReadCloser
|
||||
rw *responseWriter
|
||||
}
|
||||
|
||||
func (r *request) AddRef() error {
|
||||
C.URLSchemeTaskRetain(r.task)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *request) Release() error {
|
||||
C.URLSchemeTaskRelease(r.task)
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
14
v2/pkg/assetserver/webview/responsewriter.go
Normal file
14
v2/pkg/assetserver/webview/responsewriter.go
Normal file
@ -0,0 +1,14 @@
|
||||
package webview
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 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.
|
||||
Finish() error
|
||||
}
|
154
v2/pkg/assetserver/webview/responsewriter_darwin.go
Normal file
154
v2/pkg/assetserver/webview/responsewriter_darwin.go
Normal file
@ -0,0 +1,154 @@
|
||||
//go:build darwin
|
||||
|
||||
package webview
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework WebKit
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
||||
|
||||
static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
|
||||
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask> urlSchemeTask) {
|
||||
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
|
||||
[urlSchemeTask didReceiveData:nsdata];
|
||||
});
|
||||
}
|
||||
|
||||
static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) {
|
||||
return urlSchemeTaskCall(
|
||||
wkUrlSchemeTask,
|
||||
^(id<WKURLSchemeTask> urlSchemeTask) {
|
||||
[urlSchemeTask didFinish];
|
||||
});
|
||||
}
|
||||
|
||||
static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) {
|
||||
return urlSchemeTaskCall(
|
||||
wkUrlSchemeTask,
|
||||
^(id<WKURLSchemeTask> 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"
|
||||
"errors"
|
||||
"net/http"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
errRequestStopped = errors.New("request has been stopped")
|
||||
errResponseFinished = errors.New("response has been finished")
|
||||
)
|
||||
|
||||
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() error {
|
||||
if !rw.wroteHeader {
|
||||
rw.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if rw.finished {
|
||||
return nil
|
||||
}
|
||||
rw.finished = true
|
||||
|
||||
C.URLSchemeTaskDidFinish(rw.r.task)
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user