diff --git a/v2/internal/frontend/assetserver/assetserver.go b/v2/internal/frontend/assetserver/assetserver.go index 71eaaf9cb..f191fb06d 100644 --- a/v2/internal/frontend/assetserver/assetserver.go +++ b/v2/internal/frontend/assetserver/assetserver.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/leaanthony/debme" "github.com/leaanthony/slicer" + "github.com/wailsapp/wails/v2/internal/frontend/runtime" "io/fs" "path/filepath" "strings" @@ -14,18 +15,21 @@ import ( type AssetServer struct { assets debme.Debme indexFile []byte + runtimeJS string +} + +func NewAssetServer(assets embed.FS, bindingsJSON string) (*AssetServer, error) { + result := &AssetServer{ + runtimeJS: `window.wailsbindings='` + bindingsJSON + `';` + runtime.RuntimeJS, + } + err := result.init(assets) + return result, err } func (a *AssetServer) IndexHTML() string { return string(a.indexFile) } -func NewAssetServer(assets embed.FS) (*AssetServer, error) { - result := &AssetServer{} - err := result.init(assets) - return result, err -} - func injectScript(input string, script string) ([]byte, error) { splits := strings.Split(input, "
") if len(splits) != 2 { diff --git a/v2/internal/frontend/assetserver/assetserver_desktop.go b/v2/internal/frontend/assetserver/assetserver_desktop.go index 7b09b8a3f..27431efe2 100644 --- a/v2/internal/frontend/assetserver/assetserver_desktop.go +++ b/v2/internal/frontend/assetserver/assetserver_desktop.go @@ -4,7 +4,6 @@ package assetserver import ( "embed" - "github.com/wailsapp/wails/v2/internal/frontend/runtime" "net/http" ) @@ -19,7 +18,7 @@ func (a *AssetServer) init(assets embed.FS) error { if err != nil { return err } - a.indexFile, err = injectScript(string(indexHTML), "") + a.indexFile, err = injectScript(string(indexHTML), "") if err != nil { return err } diff --git a/v2/internal/frontend/calls.go b/v2/internal/frontend/calls.go new file mode 100644 index 000000000..3983c24bf --- /dev/null +++ b/v2/internal/frontend/calls.go @@ -0,0 +1,5 @@ +package frontend + +type Calls interface { + Callback(string) +} diff --git a/v2/internal/frontend/dispatcher.go b/v2/internal/frontend/dispatcher.go index 55a5f74fb..a15ad76e8 100644 --- a/v2/internal/frontend/dispatcher.go +++ b/v2/internal/frontend/dispatcher.go @@ -2,4 +2,5 @@ package frontend type Dispatcher interface { ProcessMessage(message string) error + SetCallbackHandler(func(string)) } diff --git a/v2/internal/frontend/dispatcher/calls.go b/v2/internal/frontend/dispatcher/calls.go new file mode 100644 index 000000000..183e1b4b8 --- /dev/null +++ b/v2/internal/frontend/dispatcher/calls.go @@ -0,0 +1,59 @@ +package dispatcher + +import ( + "encoding/json" + "fmt" +) + +type callMessage struct { + Name string `json:"name"` + Args []json.RawMessage `json:"args"` + CallbackID string `json:"callbackID"` +} + +func (d *Dispatcher) processCallMessage(message string) error { + + var payload callMessage + err := json.Unmarshal([]byte(message[1:]), &payload) + if err != nil { + return err + } + // Lookup method + registeredMethod := d.bindingsDB.GetMethod(payload.Name) + + // Check we have it + if registeredMethod == nil { + return fmt.Errorf("method '%s' not registered", payload.Name) + } + + args, err := registeredMethod.ParseArgs(payload.Args) + if err != nil { + return fmt.Errorf("error parsing arguments: %s", err.Error()) + } + + result, err := registeredMethod.Call(args) + callbackMessage := &CallbackMessage{ + CallbackID: payload.CallbackID, + } + if err != nil { + callbackMessage.Err = err.Error() + } else { + callbackMessage.Result = result + } + messageData, err := json.Marshal(callbackMessage) + d.log.Trace("json call result data: %+v\n", string(messageData)) + if err != nil { + // what now? + d.log.Fatal(err.Error()) + } + d.resultCallback(string(messageData)) + + return nil +} + +// CallbackMessage defines a message that contains the result of a call +type CallbackMessage struct { + Result interface{} `json:"result"` + Err string `json:"error"` + CallbackID string `json:"callbackid"` +} diff --git a/v2/internal/frontend/dispatcher/dispatcher.go b/v2/internal/frontend/dispatcher/dispatcher.go index 3ca9fbaa1..4d9a0824d 100644 --- a/v2/internal/frontend/dispatcher/dispatcher.go +++ b/v2/internal/frontend/dispatcher/dispatcher.go @@ -8,19 +8,26 @@ import ( ) type Dispatcher struct { - log *logger.Logger - bindings *binding.Bindings - events frontend.Events + log *logger.Logger + bindings *binding.Bindings + events frontend.Events + bindingsDB *binding.DB + resultCallback func(string) } func NewDispatcher(log *logger.Logger, bindings *binding.Bindings, events frontend.Events) *Dispatcher { return &Dispatcher{ - log: log, - bindings: bindings, - events: events, + log: log, + bindings: bindings, + events: events, + bindingsDB: bindings.DB(), } } +func (d *Dispatcher) SetCallbackHandler(handler func(string)) { + d.resultCallback = handler +} + func (d *Dispatcher) ProcessMessage(message string) error { if message == "" { return errors.New("No message to process") @@ -30,6 +37,8 @@ func (d *Dispatcher) ProcessMessage(message string) error { return d.processLogMessage(message) case 'E': return d.processEventMessage(message) + case 'C': + return d.processCallMessage(message) default: return errors.New("Unknown message from front end: " + message) } diff --git a/v2/internal/frontend/runtime/desktop/bindings.js b/v2/internal/frontend/runtime/desktop/bindings.js new file mode 100644 index 000000000..8a3acac15 --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/bindings.js @@ -0,0 +1,66 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The lightweight framework for web-like apps +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +import { Call } from './calls'; + +window.backend = {}; + +export function SetBindings(bindingsMap) { + try { + bindingsMap = JSON.parse(bindingsMap); + } catch (e) { + console.error(e); + } + + // Initialise the backend map + window.backend = window.backend || {}; + + // Iterate package names + Object.keys(bindingsMap).forEach((packageName) => { + + // Create inner map if it doesn't exist + window.backend[packageName] = window.backend[packageName] || {}; + + // Iterate struct names + Object.keys(bindingsMap[packageName]).forEach((structName) => { + + // Create inner map if it doesn't exist + window.backend[packageName][structName] = window.backend[packageName][structName] || {}; + + Object.keys(bindingsMap[packageName][structName]).forEach((methodName) => { + + window.backend[packageName][structName][methodName] = function () { + + // No timeout by default + let timeout = 0; + + // Actual function + function dynamic() { + const args = [].slice.call(arguments); + return Call([packageName, structName, methodName].join('.'), args, timeout); + } + + // Allow setting timeout to function + dynamic.setTimeout = function (newTimeout) { + timeout = newTimeout; + }; + + // Allow getting timeout to function + dynamic.getTimeout = function () { + return timeout; + }; + + return dynamic; + }(); + }); + }); + }); +} diff --git a/v2/internal/frontend/runtime/desktop/calls.js b/v2/internal/frontend/runtime/desktop/calls.js new file mode 100644 index 000000000..b7a79743a --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/calls.js @@ -0,0 +1,143 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The lightweight framework for web-like apps +(c) Lea Anthony 2019-present +*/ +/* jshint esversion: 6 */ + +import { SendMessage } from './ipc'; + +var callbacks = {}; + +/** + * Returns a number from the native browser random function + * + * @returns number + */ +function cryptoRandom() { + var array = new Uint32Array(1); + return window.crypto.getRandomValues(array)[0]; +} + +/** + * Returns a number using da old-skool Math.Random + * I likes to call it LOLRandom + * + * @returns number + */ +function basicRandom() { + return Math.random() * 9007199254740991; +} + +// Pick a random number function based on browser capability +var randomFunc; +if (window.crypto) { + randomFunc = cryptoRandom; +} else { + randomFunc = basicRandom; +} + + +/** + * Call sends a message to the backend to call the binding with the + * given data. A promise is returned and will be completed when the + * backend responds. This will be resolved when the call was successful + * or rejected if an error is passed back. + * There is a timeout mechanism. If the call doesn't respond in the given + * time (in milliseconds) then the promise is rejected. + * + * @export + * @param {string} name + * @param {string} args + * @param {number=} timeout + * @returns + */ +export function Call(name, args, timeout) { + + // Timeout infinite by default + if (timeout == null) { + timeout = 0; + } + + // Create a promise + return new Promise(function (resolve, reject) { + + // Create a unique callbackID + var callbackID; + do { + callbackID = name + '-' + randomFunc(); + } while (callbacks[callbackID]); + + // Set timeout + if (timeout > 0) { + var timeoutHandle = setTimeout(function () { + reject(Error('Call to ' + name + ' timed out. Request ID: ' + callbackID)); + }, timeout); + } + + // Store callback + callbacks[callbackID] = { + timeoutHandle: timeoutHandle, + reject: reject, + resolve: resolve + }; + + try { + const payload = { + name, + args, + callbackID, + }; + + // Make the call + SendMessage('C' + JSON.stringify(payload)); + } catch (e) { + // eslint-disable-next-line + console.error(e); + } + }); +} + + + +/** + * Called by the backend to return data to a previously called + * binding invocation + * + * @export + * @param {string} incomingMessage + */ +export function Callback(incomingMessage) { + // Decode the message - Credit: https://stackoverflow.com/a/13865680 + //incomingMessage = decodeURIComponent(incomingMessage.replace(/\s+/g, '').replace(/[0-9a-f]{2}/g, '%$&')); + + // Parse the message + var message; + try { + message = JSON.parse(incomingMessage); + } catch (e) { + const error = `Invalid JSON passed to callback: ${e.message}. Message: ${incomingMessage}`; + wails.LogDebug(error); + throw new Error(error); + } + var callbackID = message.callbackid; + var callbackData = callbacks[callbackID]; + if (!callbackData) { + const error = `Callback '${callbackID}' not registered!!!`; + console.error(error); // eslint-disable-line + throw new Error(error); + } + clearTimeout(callbackData.timeoutHandle); + + delete callbacks[callbackID]; + + if (message.error) { + callbackData.reject(message.error); + } else { + callbackData.resolve(message.result); + } +} diff --git a/v2/internal/frontend/runtime/desktop/main.js b/v2/internal/frontend/runtime/desktop/main.js index 75209a24f..abd8611c0 100644 --- a/v2/internal/frontend/runtime/desktop/main.js +++ b/v2/internal/frontend/runtime/desktop/main.js @@ -10,7 +10,8 @@ The lightweight framework for web-like apps /* jshint esversion: 9 */ import * as Log from './log'; import {EventsEmit, EventsNotify, EventsOff, EventsOn, EventsOnce, EventsOnMultiple} from './events'; -// import {Callback, SystemCall} from './calls'; +import {Callback} from './calls'; +import {SetBindings} from "./bindings"; // import {AddScript, DisableDefaultContextMenu, InjectCSS} from './utils'; // import {AddIPCListener, SendMessage} from 'ipc'; @@ -28,8 +29,9 @@ window.runtime = { // Initialise global if not already window.wails = { - // Callback, + Callback, EventsNotify, + SetBindings, // AddScript, // InjectCSS, // DisableDefaultContextMenu, @@ -39,3 +41,6 @@ window.wails = { // SendMessage, }; +window.wails.SetBindings(window.wailsbindings); +delete window.wails['SetBindings']; +delete window['wailsbindings']; diff --git a/v2/internal/frontend/windows/frontend.go b/v2/internal/frontend/windows/frontend.go index 970a5dc88..e11d2d2c8 100644 --- a/v2/internal/frontend/windows/frontend.go +++ b/v2/internal/frontend/windows/frontend.go @@ -12,6 +12,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/options" "log" "runtime" + "strconv" "strings" ) @@ -213,13 +214,19 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg } func (f *Frontend) processMessage(message string) { + println("msg:", message) err := f.dispatcher.ProcessMessage(message) if err != nil { - // TODO: Work out what this means - return + f.logger.Error(err.Error()) } } +func (f *Frontend) Callback(message string) { + f.mainWindow.Dispatch(func() { + f.chromium.Eval(`window.wails.Callback(` + strconv.Quote(message) + `);`) + }) +} + func NewFrontend(appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { result := &Frontend{ @@ -229,8 +236,15 @@ func NewFrontend(appoptions *options.App, myLogger *logger.Logger, appBindings * dispatcher: dispatcher, } + // Setup the callback handler (Go -> JS) + dispatcher.SetCallbackHandler(result.Callback) + if appoptions.Windows.Assets != nil { - assets, err := assetserver.NewAssetServer(*appoptions.Windows.Assets) + bindingsJSON, err := appBindings.ToJSON() + if err != nil { + log.Fatal(err) + } + assets, err := assetserver.NewAssetServer(*appoptions.Windows.Assets, bindingsJSON) if err != nil { log.Fatal(err) }