mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 18:13:20 +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 IsMinimised(void *ctx);
|
||||||
const bool IsMaximised(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 */
|
/* 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);
|
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;
|
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) {
|
void ExecJS(void* inctx, const char *script) {
|
||||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||||
NSString *nsscript = safeInit(script);
|
NSString *nsscript = safeInit(script);
|
||||||
|
@ -47,9 +47,6 @@
|
|||||||
@property bool debug;
|
@property bool debug;
|
||||||
|
|
||||||
@property (retain) WKUserContentController* userContentController;
|
@property (retain) WKUserContentController* userContentController;
|
||||||
@property (retain) NSLock *urlRequestsLock;
|
|
||||||
@property unsigned long long urlRequestsId;
|
|
||||||
@property (retain) NSMutableDictionary *urlRequests;
|
|
||||||
|
|
||||||
@property (retain) NSMenu* applicationMenu;
|
@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) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
|
||||||
|
|
||||||
- (void) loadRequest:(NSString*)url;
|
- (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;
|
- (void) ExecJS:(NSString*)script;
|
||||||
- (NSScreen*) getCurrentScreen;
|
- (NSScreen*) getCurrentScreen;
|
||||||
|
|
||||||
|
@ -108,7 +108,6 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
|
|||||||
[self.mainWindow release];
|
[self.mainWindow release];
|
||||||
[self.mouseEvent release];
|
[self.mouseEvent release];
|
||||||
[self.userContentController release];
|
[self.userContentController release];
|
||||||
[self.urlRequests release];
|
|
||||||
[self.applicationMenu release];
|
[self.applicationMenu release];
|
||||||
[super dealloc];
|
[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 {
|
- (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;
|
NSWindowStyleMask styleMask = 0;
|
||||||
|
|
||||||
if( !frameless ) {
|
if( !frameless ) {
|
||||||
@ -431,156 +427,19 @@ 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 {
|
- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
|
||||||
// This callback is run with an autorelease pool
|
// This callback is run with an autorelease pool
|
||||||
const char *url = [urlSchemeTask.request.URL.absoluteString UTF8String];
|
processURLRequest(self, urlSchemeTask);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)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;
|
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
|
||||||
if (stream) {
|
if (stream) {
|
||||||
[stream close];
|
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 {
|
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
|
||||||
|
@ -19,11 +19,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/assetserver"
|
"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/binding"
|
||||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||||
@ -35,7 +36,7 @@ import (
|
|||||||
const startURL = "wails://wails/"
|
const startURL = "wails://wails/"
|
||||||
|
|
||||||
var messageBuffer = make(chan string, 100)
|
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)
|
var callbackBuffer = make(chan uint, 10)
|
||||||
|
|
||||||
type Frontend struct {
|
type Frontend struct {
|
||||||
@ -93,12 +94,10 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
assets.ExpectedWebViewHost = result.startURL.Host
|
||||||
result.assets = assets
|
result.assets = assets
|
||||||
|
|
||||||
// Start 10 processors to handle requests in parallel
|
go result.startRequestProcessor()
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
go result.startRequestProcessor()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go result.startMessageProcessor()
|
go result.startMessageProcessor()
|
||||||
@ -114,7 +113,8 @@ func (f *Frontend) startMessageProcessor() {
|
|||||||
}
|
}
|
||||||
func (f *Frontend) startRequestProcessor() {
|
func (f *Frontend) startRequestProcessor() {
|
||||||
for request := range requestBuffer {
|
for request := range requestBuffer {
|
||||||
f.processRequest(request)
|
f.assets.ServeWebViewRequest(request)
|
||||||
|
request.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (f *Frontend) startCallbackProcessor() {
|
func (f *Frontend) startCallbackProcessor() {
|
||||||
@ -344,31 +344,6 @@ func (f *Frontend) ExecJS(js string) {
|
|||||||
f.mainWindow.ExecJS(js)
|
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) {
|
//func (f *Frontend) processSystemEvent(message string) {
|
||||||
// sl := strings.Split(message, ":")
|
// sl := strings.Split(message, ":")
|
||||||
// if len(sl) != 2 {
|
// if len(sl) != 2 {
|
||||||
@ -395,3 +370,8 @@ func processMessage(message *C.char) {
|
|||||||
func processCallback(callbackID uint) {
|
func processCallback(callbackID uint) {
|
||||||
callbackBuffer <- callbackID
|
callbackBuffer <- callbackID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//export processURLRequest
|
||||||
|
func processURLRequest(ctx unsafe.Pointer, wkURLSchemeTask unsafe.Pointer) {
|
||||||
|
requestBuffer <- webview.NewRequest(wkURLSchemeTask)
|
||||||
|
}
|
||||||
|
@ -15,7 +15,7 @@ extern "C"
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void processMessage(const char *);
|
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 processMessageDialogResponse(int);
|
||||||
void processOpenFileDialogResponse(const char*);
|
void processOpenFileDialogResponse(const char*);
|
||||||
void processSaveFileDialogResponse(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}
|
rw := &webKitResponseWriter{req: req}
|
||||||
defer rw.Close()
|
defer rw.Close()
|
||||||
|
|
||||||
f.assets.ProcessHTTPRequest(
|
f.assets.ProcessHTTPRequestLegacy(
|
||||||
goURI,
|
|
||||||
rw,
|
rw,
|
||||||
func() (*http.Request, error) {
|
func() (*http.Request, error) {
|
||||||
method := webkit_uri_scheme_request_get_http_method(req)
|
method := webkit_uri_scheme_request_get_http_method(req)
|
||||||
|
@ -547,10 +547,8 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo := strings.Replace(uri, f.startURL.String(), "", 1)
|
|
||||||
|
|
||||||
rw := httptest.NewRecorder()
|
rw := httptest.NewRecorder()
|
||||||
f.assets.ProcessHTTPRequest(logInfo, rw, coreWebview2RequestToHttpRequest(req))
|
f.assets.ProcessHTTPRequestLegacy(rw, coreWebview2RequestToHttpRequest(req))
|
||||||
|
|
||||||
headers := []string{}
|
headers := []string{}
|
||||||
for k, v := range rw.Header() {
|
for k, v := range rw.Header() {
|
||||||
|
@ -2,10 +2,8 @@ package assetserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
|
|
||||||
@ -35,6 +33,8 @@ type AssetServer struct {
|
|||||||
|
|
||||||
servingFromDisk bool
|
servingFromDisk bool
|
||||||
appendSpinnerToBody bool
|
appendSpinnerToBody bool
|
||||||
|
|
||||||
|
assetServerWebView
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
|
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) {
|
func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
|
||||||
htmlNode, err := getHTMLNode(indexHTML)
|
htmlNode, err := getHTMLNode(indexHTML)
|
||||||
if err != nil {
|
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