From 21a0245985f93856540f376fe47345587dc0ca50 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Tue, 9 Feb 2021 21:10:06 +1100 Subject: [PATCH] Generate bindings in package. Support dialogs in dev mode. --- v2/cmd/wails/internal/commands/dev/dev.go | 2 +- v2/go.mod | 1 + v2/go.sum | 2 + v2/internal/app/dev.go | 4 + v2/internal/binding/assets/package.json | 11 ++ v2/internal/binding/generate.go | 150 ++++++++++++++++++ v2/internal/bridge/bridge.go | 15 +- .../bridge/{client.go => client_darwin.go} | 52 +++++- v2/internal/bridge/session.go | 8 +- v2/internal/ffenestri/ffenestri_darwin.c | 2 +- .../messagedispatcher/message/dialog.go | 2 +- v2/internal/runtime/js/runtime/bridge.js | 14 ++ v2/internal/runtime/js/runtime/package.json | 2 +- v2/internal/subsystem/runtime.go | 8 +- 14 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 v2/internal/binding/assets/package.json create mode 100644 v2/internal/binding/generate.go rename v2/internal/bridge/{client.go => client_darwin.go} (70%) diff --git a/v2/cmd/wails/internal/commands/dev/dev.go b/v2/cmd/wails/internal/commands/dev/dev.go index 1a71b5dc..3201fdcb 100644 --- a/v2/cmd/wails/internal/commands/dev/dev.go +++ b/v2/cmd/wails/internal/commands/dev/dev.go @@ -105,7 +105,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error { } if !rebuild { - logger.Println("Filename change: %s did not match extension list %s", event.Name, extensions) + logger.Println("Filename change: %s did not match extension list (%s)", event.Name, extensions) return } diff --git a/v2/go.mod b/v2/go.mod index 055f06ec..a89cbcbe 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -22,6 +22,7 @@ require ( github.com/tdewolff/test v1.0.6 // indirect github.com/xyproto/xpm v1.2.1 golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/sync v0.0.0-20201207232520-09787c993a3a golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 nhooyr.io/websocket v1.8.6 diff --git a/v2/go.sum b/v2/go.sum index 05e611cc..b8cc83b8 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -92,6 +92,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/v2/internal/app/dev.go b/v2/internal/app/dev.go index a32c7974..253e5909 100644 --- a/v2/internal/app/dev.go +++ b/v2/internal/app/dev.go @@ -97,6 +97,7 @@ func CreateApp(appoptions *options.App) (*App, error) { startupCallback: appoptions.Startup, shutdownCallback: appoptions.Shutdown, bridge: bridge.NewBridge(myLogger), + menuManager: menuManager, } result.options = appoptions @@ -208,6 +209,9 @@ func (a *App) Run() error { return err } + // Generate backend.js + a.bindings.GenerateBackendJS() + err = a.bridge.Run(dispatcher, bindingDump, a.debug) a.logger.Trace("Bridge.Run() exited") if err != nil { diff --git a/v2/internal/binding/assets/package.json b/v2/internal/binding/assets/package.json new file mode 100644 index 00000000..90949813 --- /dev/null +++ b/v2/internal/binding/assets/package.json @@ -0,0 +1,11 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "Package to wrap backend method calls", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go new file mode 100644 index 00000000..80c70172 --- /dev/null +++ b/v2/internal/binding/generate.go @@ -0,0 +1,150 @@ +package binding + +import ( + "bytes" + _ "embed" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/wailsapp/wails/v2/internal/fs" + + "github.com/leaanthony/slicer" +) + +const _comment = ` + +const backend = { + main: { + "xbarApp": { + "GetCategories": () => { + window.backend.main.xbarApp.GetCategories.call(arguments); + }, + + /** + * @param {string} arg1 + */ + "InstallPlugin": (arg1) => { + window.backend.main.xbarApp.InstallPlugin.call(arguments); + }, + "GetPlugins": () => { + window.backend.main.xbarApp.GetPlugins.call(arguments); + } + } + } +} + +export default backend;` + +//go:embed assets/package.json +var packageJSON []byte + +func (b *Bindings) GenerateBackendJS() { + + store := b.db.store + var output bytes.Buffer + + output.WriteString(`// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +const backend = {`) + output.WriteString("\n") + + for packageName, packages := range store { + output.WriteString(fmt.Sprintf(" \"%s\": {", packageName)) + output.WriteString("\n") + for structName, structs := range packages { + output.WriteString(fmt.Sprintf(" \"%s\": {", structName)) + output.WriteString("\n") + for methodName, methodDetails := range structs { + output.WriteString(" /**\n") + output.WriteString(" * " + methodName + "\n") + var args slicer.StringSlicer + for count, input := range methodDetails.Inputs { + arg := fmt.Sprintf("arg%d", count+1) + args.Add(arg) + output.WriteString(fmt.Sprintf(" * @param {%s} %s - Go Type: %s\n", goTypeToJSDocType(input.TypeName), arg, input.TypeName)) + } + returnType := "Promise" + returnTypeDetails := "" + if methodDetails.OutputCount() > 0 { + firstType := goTypeToJSDocType(methodDetails.Outputs[0].TypeName) + returnType += "<" + firstType + if methodDetails.OutputCount() == 2 { + secondType := goTypeToJSDocType(methodDetails.Outputs[1].TypeName) + returnType += "|" + secondType + } + returnType += ">" + returnTypeDetails = " - Go Type: " + methodDetails.Outputs[0].TypeName + } + output.WriteString(" * @returns {" + returnType + "} " + returnTypeDetails + "\n") + output.WriteString(" */\n") + argsString := args.Join(", ") + output.WriteString(fmt.Sprintf(" \"%s\": (%s) => {", methodName, argsString)) + output.WriteString("\n") + output.WriteString(fmt.Sprintf(" return window.backend.%s.%s.%s(%s);", packageName, structName, methodName, argsString)) + output.WriteString("\n") + output.WriteString(fmt.Sprintf(" },")) + output.WriteString("\n") + } + output.WriteString(fmt.Sprintf(" }")) + output.WriteString("\n") + } + output.WriteString(fmt.Sprintf(" }\n")) + output.WriteString("\n") + } + + output.WriteString(`}; +export default backend;`) + output.WriteString("\n") + + dirname, err := fs.RelativeToCwd("frontend/src/backend") + if err != nil { + log.Fatal(err) + } + + if !fs.DirExists(dirname) { + err := fs.Mkdir(dirname) + if err != nil { + log.Fatal(err) + } + } + + packageJsonFile := filepath.Join(dirname, "package.json") + if !fs.FileExists(packageJsonFile) { + err := os.WriteFile(packageJsonFile, packageJSON, 0755) + if err != nil { + log.Fatal(err) + } + } + + filename := filepath.Join(dirname, "index.js") + err = os.WriteFile(filename, output.Bytes(), 0755) + if err != nil { + log.Fatal(err) + } +} + +func goTypeToJSDocType(input string) string { + switch true { + case input == "string": + return "string" + case input == "error": + return "Error" + case + strings.HasPrefix(input, "int"), + strings.HasPrefix(input, "uint"), + strings.HasPrefix(input, "float"): + return "number" + case input == "bool": + return "boolean" + case strings.HasPrefix(input, "[]"): + arrayType := goTypeToJSDocType(input[2:]) + return "Array.<" + arrayType + ">" + default: + return "any" + } +} diff --git a/v2/internal/bridge/bridge.go b/v2/internal/bridge/bridge.go index c6405408..fb34f182 100644 --- a/v2/internal/bridge/bridge.go +++ b/v2/internal/bridge/bridge.go @@ -6,6 +6,8 @@ import ( "net/http" "sync" + "golang.org/x/sync/semaphore" + "github.com/wailsapp/wails/v2/internal/messagedispatcher" "github.com/gorilla/websocket" @@ -25,13 +27,16 @@ type Bridge struct { ctx context.Context cancel context.CancelFunc + + dialogSemaphore *semaphore.Weighted } func NewBridge(myLogger *logger.Logger) *Bridge { result := &Bridge{ - myLogger: myLogger, - upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}, - sessions: make(map[string]*session), + myLogger: myLogger, + upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}, + sessions: make(map[string]*session), + dialogSemaphore: semaphore.NewWeighted(1), } myLogger.SetLogLevel(1) @@ -80,12 +85,12 @@ func (b *Bridge) wsBridgeHandler(w http.ResponseWriter, r *http.Request) { func (b *Bridge) startSession(conn *websocket.Conn) { // Create a new session for this connection - s := newSession(conn, b.bindings, b.dispatcher, b.myLogger, b.ctx) + s := newSession(conn, b.bindings, b.dispatcher, b.myLogger, b.ctx, b.dialogSemaphore) // Setup the close handler conn.SetCloseHandler(func(int, string) error { b.myLogger.Info("Connection dropped [%s].", s.Identifier()) - + b.dispatcher.RemoveClient(s.client) b.mu.Lock() delete(b.sessions, s.Identifier()) b.mu.Unlock() diff --git a/v2/internal/bridge/client.go b/v2/internal/bridge/client_darwin.go similarity index 70% rename from v2/internal/bridge/client.go rename to v2/internal/bridge/client_darwin.go index 9eeb357e..d2fb7a49 100644 --- a/v2/internal/bridge/client.go +++ b/v2/internal/bridge/client_darwin.go @@ -1,11 +1,19 @@ package bridge import ( + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/leaanthony/slicer" "github.com/wailsapp/wails/v2/pkg/options/dialog" + "golang.org/x/sync/semaphore" ) type BridgeClient struct { - session *session + session *session + dialogSemaphore *semaphore.Weighted } func (b BridgeClient) Quit() { @@ -31,7 +39,42 @@ func (b BridgeClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID st } func (b BridgeClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) { - b.session.log.Info("MessageDialog unsupported in Bridge mode") + + // Check there aren't other dialogs going on + if !b.dialogSemaphore.TryAcquire(1) { + return + } + defer b.dialogSemaphore.Release(1) + + osa, err := exec.LookPath("osascript") + if err != nil { + b.session.log.Info("MessageDialog unavailable (osascript not found)") + return + } + + var btns slicer.StringSlicer + defaultButton := "" + for index, btn := range dialogOptions.Buttons { + btns.Add(strconv.Quote(btn)) + if btn == dialogOptions.DefaultButton { + defaultButton = fmt.Sprintf("default button %d", index+1) + } + } + buttons := "{" + btns.Join(",") + "}" + script := fmt.Sprintf("display dialog \"%s\" buttons %s %s with title \"%s\"", dialogOptions.Message, buttons, defaultButton, dialogOptions.Title) + + b.session.log.Info("OSASCRIPT: %s", script) + go func() { + out, err := exec.Command(osa, "-e", script).Output() + if err != nil { + b.session.log.Error(err.Error()) + return + } + + b.session.log.Info(string(out)) + buttonPressed := strings.TrimSpace(strings.TrimPrefix(string(out), "button returned:")) + b.session.client.DispatchMessage("DM" + callbackID + "|" + buttonPressed) + }() } func (b BridgeClient) WindowSetTitle(title string) { @@ -114,8 +157,9 @@ func (b BridgeClient) UpdateContextMenu(contextMenuJSON string) { b.session.log.Info("UpdateContextMenu unsupported in Bridge mode") } -func newBridgeClient(session *session) *BridgeClient { +func newBridgeClient(session *session, dialogSemaphore *semaphore.Weighted) *BridgeClient { return &BridgeClient{ - session: session, + session: session, + dialogSemaphore: dialogSemaphore, } } diff --git a/v2/internal/bridge/session.go b/v2/internal/bridge/session.go index 4b00af70..5965ceeb 100644 --- a/v2/internal/bridge/session.go +++ b/v2/internal/bridge/session.go @@ -8,6 +8,7 @@ import ( "time" "github.com/wailsapp/wails/v2/internal/messagedispatcher" + "golang.org/x/sync/semaphore" "github.com/gorilla/websocket" "github.com/wailsapp/wails/v2/internal/logger" @@ -37,7 +38,7 @@ type session struct { client *messagedispatcher.DispatchClient } -func newSession(conn *websocket.Conn, bindings string, dispatcher *messagedispatcher.Dispatcher, logger *logger.Logger, ctx context.Context) *session { +func newSession(conn *websocket.Conn, bindings string, dispatcher *messagedispatcher.Dispatcher, logger *logger.Logger, ctx context.Context, dialogSemaphore *semaphore.Weighted) *session { result := &session{ conn: conn, bindings: bindings, @@ -47,7 +48,7 @@ func newSession(conn *websocket.Conn, bindings string, dispatcher *messagedispat ctx: ctx, } - result.client = dispatcher.RegisterClient(newBridgeClient(result)) + result.client = dispatcher.RegisterClient(newBridgeClient(result, dialogSemaphore)) return result @@ -95,7 +96,8 @@ func (s *session) start(firstSession bool) { } if err != nil { s.log.Error("Error reading message: %v", err) - continue + err = s.conn.Close() + return } message := string(buffer) diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index d8b202ad..3a22066e 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -785,7 +785,7 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type, buttonPressed = button4; } - // Construct callback message. Format "DS|" + // Construct callback message. Format "DM|" const char *callback = concat("DM", callbackID); const char *header = concat(callback, "|"); const char *responseMessage = concat(header, buttonPressed); diff --git a/v2/internal/messagedispatcher/message/dialog.go b/v2/internal/messagedispatcher/message/dialog.go index 0b227651..7fdf6695 100644 --- a/v2/internal/messagedispatcher/message/dialog.go +++ b/v2/internal/messagedispatcher/message/dialog.go @@ -27,7 +27,7 @@ func dialogMessageParser(message string) (*parsedMessage, error) { if idx < 0 { return nil, fmt.Errorf("Invalid dialog response message format: %+v", message) } - callbackID := message[:idx+1] + callbackID := message[:idx] payloadData := message[idx+1:] switch dialogType { diff --git a/v2/internal/runtime/js/runtime/bridge.js b/v2/internal/runtime/js/runtime/bridge.js index ecd998ea..50d3070b 100644 --- a/v2/internal/runtime/js/runtime/bridge.js +++ b/v2/internal/runtime/js/runtime/bridge.js @@ -10,6 +10,7 @@ The lightweight framework for web-like apps /* jshint esversion: 6 */ function init() { + // Bridge object window.wailsbridge = { reconnectOverlay: null, @@ -32,6 +33,15 @@ function init() { ); } }; + + window.onbeforeunload = function() { + if( window.wails.websocket ) { + window.wails.websocket.onclose = function () { }; + window.wails.websocket.close(); + window.wails.websocket = null; + } + } + } function setupIPCBridge() { @@ -173,6 +183,10 @@ function startBridge() { addScript(message); window.wailsbridge.log('Loaded Wails Runtime'); + // We need to now send a message to the backend telling it + // we have loaded (System Start) + window.webkit.messageHandlers.external.postMessage("SS"); + // Now wails runtime is loaded, wails for the ready event // and callback to the main app // window.wails.Events.On('wails:loaded', function () { diff --git a/v2/internal/runtime/js/runtime/package.json b/v2/internal/runtime/js/runtime/package.json index ee6b895c..d26ce9c2 100644 --- a/v2/internal/runtime/js/runtime/package.json +++ b/v2/internal/runtime/js/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@wails/runtime", - "version": "1.3.7", + "version": "1.3.8", "description": "Wails V2 Javascript runtime library", "main": "main.js", "types": "runtime.d.ts", diff --git a/v2/internal/subsystem/runtime.go b/v2/internal/subsystem/runtime.go index 9d624b54..c42bf5ff 100644 --- a/v2/internal/subsystem/runtime.go +++ b/v2/internal/subsystem/runtime.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/runtime" @@ -30,6 +31,9 @@ type Runtime struct { //ctx ctx context.Context + + // Startup Hook + startupOnce sync.Once } // NewRuntime creates a new runtime subsystem @@ -75,7 +79,9 @@ func (r *Runtime) Start() error { switch hook { case "startup": if r.startupCallback != nil { - go r.startupCallback(r.runtime) + r.startupOnce.Do(func() { + go r.startupCallback(r.runtime) + }) } else { r.logger.Warning("no startup callback registered!") }