316-multi-bridge-conn (#317)

* [316-multi-bridge-conn] feat: use callback func for bridge response

* [316-multi-bridge-conn] feat: implement multiple session support

* split client handling portion into 'session'
* keep track of sessions by remote address (ip & port)
* notify each of the sessions anytime an event comes across the bus

* [316-multi-bridge-conn] chore: move bridge files to package

* [316-multi-bridge-conn] chore: remove deprecated Callback function

The Callback function is no longer needed for the operation of
the frontend callback since the ipc.Dispatch function now requires
a callback function to be provided as an argument.
This function can be a private function since it is passed by reference.

* [316-multi-bridge-conn] chore: make webview.Callback private

* [316-multi-bridge-conn] chore: remove unused injectCSS function

I believe a slightly better method of doing this might need to be devised
if it is needed in the future. I presume it should collect the values
into a cache and then inject it into each sesssion as it appears.

* [316-multi-bridge-conn] ensure wails:ready event is emitted

Event is only emitted for the first session created from the Bridge.

* [316-multi-bridge-conn] emit events for session lifecycle

Emit an event for each session started and ended.

* [316-multi-bridge-conn] fix: session handling fixes

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
Travis McLane
2020-01-25 18:07:53 -06:00
committed by Lea Anthony
parent 030bd629df
commit 8a7c098041
8 changed files with 329 additions and 272 deletions

View File

@@ -1,262 +1,10 @@
package renderer
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"github.com/dchest/htmlmin"
"github.com/gorilla/websocket"
"github.com/leaanthony/mewn"
"github.com/wailsapp/wails/lib/interfaces"
"github.com/wailsapp/wails/lib/logger"
"github.com/wailsapp/wails/lib/messages"
bridge "github.com/wailsapp/wails/lib/renderer/bridge"
)
type messageType int
const (
jsMessage messageType = iota
cssMessage
htmlMessage
notifyMessage
bindingMessage
callbackMessage
wailsRuntimeMessage
)
func (m messageType) toString() string {
return [...]string{"j", "s", "h", "n", "b", "c", "w"}[m]
}
// Bridge is a backend that opens a local web server
// and renders the files over a websocket
type Bridge struct {
// Common
log *logger.CustomLogger
ipcManager interfaces.IPCManager
appConfig interfaces.AppConfig
eventManager interfaces.EventManager
bindingCache []string
// Bridge specific
initialisationJS []string
server *http.Server
theConnection *websocket.Conn
// Mutex for writing to the socket
lock sync.Mutex
}
// Initialise the Bridge Renderer
func (h *Bridge) Initialise(appConfig interfaces.AppConfig, ipcManager interfaces.IPCManager, eventManager interfaces.EventManager) error {
h.ipcManager = ipcManager
h.appConfig = appConfig
h.eventManager = eventManager
ipcManager.BindRenderer(h)
h.log = logger.NewCustomLogger("Bridge")
return nil
}
func (h *Bridge) evalJS(js string, mtype messageType) error {
message := mtype.toString() + js
if h.theConnection == nil {
h.initialisationJS = append(h.initialisationJS, message)
} else {
// Prepend message type to message
h.sendMessage(h.theConnection, message)
}
return nil
}
// EnableConsole not needed for bridge!
func (h *Bridge) EnableConsole() {
}
func (h *Bridge) injectCSS(css string) {
// Minify css to overcome issues in the browser with carriage returns
minified, err := htmlmin.Minify([]byte(css), &htmlmin.Options{
MinifyStyles: true,
})
if err != nil {
h.log.Fatal("Unable to minify CSS: " + css)
}
minifiedCSS := string(minified)
minifiedCSS = strings.Replace(minifiedCSS, "\\", "\\\\", -1)
minifiedCSS = strings.Replace(minifiedCSS, "'", "\\'", -1)
minifiedCSS = strings.Replace(minifiedCSS, "\n", " ", -1)
inject := fmt.Sprintf("wails._.InjectCSS('%s')", minifiedCSS)
h.evalJS(inject, cssMessage)
}
func (h *Bridge) wsBridgeHandler(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
if err != nil {
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
}
h.theConnection = conn
h.log.Infof("Connection from frontend accepted [%p].", h.theConnection)
conn.SetCloseHandler(func(int, string) error {
h.log.Infof("Connection dropped [%p].", h.theConnection)
h.theConnection = nil
return nil
})
go h.start(conn)
}
func (h *Bridge) sendMessage(conn *websocket.Conn, msg string) {
h.lock.Lock()
defer h.lock.Unlock()
if err := conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
h.log.Error(err.Error())
}
}
func (h *Bridge) start(conn *websocket.Conn) {
// set external.invoke
h.log.Infof("Connected to frontend.")
wailsRuntime := mewn.String("../../runtime/assets/wails.js")
h.evalJS(wailsRuntime, wailsRuntimeMessage)
// Inject bindings
for _, binding := range h.bindingCache {
h.evalJS(binding, bindingMessage)
}
// Emit that everything is loaded and ready
h.eventManager.Emit("wails:ready")
for {
messageType, buffer, err := conn.ReadMessage()
if messageType == -1 {
return
}
if err != nil {
h.log.Errorf("Error reading message: ", err)
continue
}
h.log.Debugf("Got message: %#v\n", string(buffer))
h.ipcManager.Dispatch(string(buffer))
}
}
// Run the app in Bridge mode!
func (h *Bridge) Run() error {
h.server = &http.Server{Addr: ":34115"}
http.HandleFunc("/bridge", h.wsBridgeHandler)
h.log.Info("Bridge mode started.")
h.log.Info("The frontend will connect automatically.")
err := h.server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
h.log.Fatal(err.Error())
}
return err
}
// NewBinding creates a new binding with the frontend
func (h *Bridge) NewBinding(methodName string) error {
h.bindingCache = append(h.bindingCache, methodName)
return nil
}
// SelectFile is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SelectFile() string {
h.log.Warn("SelectFile() unsupported in bridge mode")
return ""
}
// SelectDirectory is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SelectDirectory() string {
h.log.Warn("SelectDirectory() unsupported in bridge mode")
return ""
}
// SelectSaveFile is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SelectSaveFile() string {
h.log.Warn("SelectSaveFile() unsupported in bridge mode")
return ""
}
// Callback sends a callback to the frontend
func (h *Bridge) Callback(data string) error {
return h.evalJS(data, callbackMessage)
}
// NotifyEvent notifies the frontend of an event
func (h *Bridge) NotifyEvent(event *messages.EventData) error {
// Look out! Nils about!
var err error
if event == nil {
err = fmt.Errorf("Sent nil event to renderer.webViewRenderer")
h.log.Error(err.Error())
return err
}
// Default data is a blank array
data := []byte("[]")
// Process event data
if event.Data != nil {
// Marshall the data
data, err = json.Marshal(event.Data)
if err != nil {
h.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error())
return err
}
}
message := fmt.Sprintf("window.wails._.Notify('%s','%s')", event.Name, data)
return h.evalJS(message, notifyMessage)
}
// SetColour is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SetColour(colour string) error {
h.log.WarnFields("SetColour ignored for Bridge more", logger.Fields{"col": colour})
return nil
}
// Fullscreen is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) Fullscreen() {
h.log.Warn("Fullscreen() unsupported in bridge mode")
}
// UnFullscreen is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) UnFullscreen() {
h.log.Warn("UnFullscreen() unsupported in bridge mode")
}
// SetTitle is currently unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SetTitle(title string) {
h.log.WarnFields("SetTitle() unsupported in bridge mode", logger.Fields{"title": title})
}
// Close is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) Close() {
h.log.Debug("Shutting down")
err := h.server.Close()
if err != nil {
h.log.Errorf(err.Error())
}
// NewBridge returns a new Bridge struct
func NewBridge() *bridge.Bridge {
return &bridge.Bridge{}
}

View File

@@ -0,0 +1,214 @@
package renderer
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"github.com/gorilla/websocket"
"github.com/wailsapp/wails/lib/interfaces"
"github.com/wailsapp/wails/lib/logger"
"github.com/wailsapp/wails/lib/messages"
)
type messageType int
const (
jsMessage messageType = iota
cssMessage
htmlMessage
notifyMessage
bindingMessage
callbackMessage
wailsRuntimeMessage
)
func (m messageType) toString() string {
return [...]string{"j", "s", "h", "n", "b", "c", "w"}[m]
}
// Bridge is a backend that opens a local web server
// and renders the files over a websocket
type Bridge struct {
// Common
log *logger.CustomLogger
ipcManager interfaces.IPCManager
appConfig interfaces.AppConfig
eventManager interfaces.EventManager
bindingCache []string
// Bridge specific
server *http.Server
lock sync.Mutex
sessions map[string]session
}
// Initialise the Bridge Renderer
func (h *Bridge) Initialise(appConfig interfaces.AppConfig, ipcManager interfaces.IPCManager, eventManager interfaces.EventManager) error {
h.sessions = map[string]session{}
h.ipcManager = ipcManager
h.appConfig = appConfig
h.eventManager = eventManager
ipcManager.BindRenderer(h)
h.log = logger.NewCustomLogger("Bridge")
return nil
}
// EnableConsole not needed for bridge!
func (h *Bridge) EnableConsole() {
}
func (h *Bridge) wsBridgeHandler(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
if err != nil {
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
}
h.log.Infof("Connection from frontend accepted [%s].", conn.RemoteAddr().String())
h.startSession(conn)
}
func (h *Bridge) startSession(conn *websocket.Conn) {
s := session{
conn: conn,
bindingCache: h.bindingCache,
ipc: h.ipcManager,
log: h.log,
eventManager: h.eventManager,
}
conn.SetCloseHandler(func(int, string) error {
h.log.Infof("Connection dropped [%s].", s.Identifier())
h.eventManager.Emit("wails:bridge:session:closed", s.Identifier())
h.lock.Lock()
defer h.lock.Unlock()
delete(h.sessions, s.Identifier())
return nil
})
h.lock.Lock()
defer h.lock.Unlock()
go s.start(len(h.sessions) == 0)
h.sessions[s.Identifier()] = s
}
// Run the app in Bridge mode!
func (h *Bridge) Run() error {
h.server = &http.Server{Addr: ":34115"}
http.HandleFunc("/bridge", h.wsBridgeHandler)
h.log.Info("Bridge mode started.")
h.log.Info("The frontend will connect automatically.")
err := h.server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
h.log.Fatal(err.Error())
}
return err
}
// NewBinding creates a new binding with the frontend
func (h *Bridge) NewBinding(methodName string) error {
h.bindingCache = append(h.bindingCache, methodName)
return nil
}
// SelectFile is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SelectFile() string {
h.log.Warn("SelectFile() unsupported in bridge mode")
return ""
}
// SelectDirectory is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SelectDirectory() string {
h.log.Warn("SelectDirectory() unsupported in bridge mode")
return ""
}
// SelectSaveFile is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SelectSaveFile() string {
h.log.Warn("SelectSaveFile() unsupported in bridge mode")
return ""
}
// NotifyEvent notifies the frontend of an event
func (h *Bridge) NotifyEvent(event *messages.EventData) error {
// Look out! Nils about!
var err error
if event == nil {
err = fmt.Errorf("Sent nil event to renderer.webViewRenderer")
h.log.Error(err.Error())
return err
}
// Default data is a blank array
data := []byte("[]")
// Process event data
if event.Data != nil {
// Marshall the data
data, err = json.Marshal(event.Data)
if err != nil {
h.log.Errorf("Cannot unmarshall JSON data in event: %s ", err.Error())
return err
}
}
message := fmt.Sprintf("window.wails._.Notify('%s','%s')", event.Name, data)
dead := []session{}
for _, session := range h.sessions {
err := session.evalJS(message, notifyMessage)
if err != nil {
h.log.Debugf("Failed to send message to %s - Removing listener : %v", session.Identifier(), err)
h.log.Infof("Connection from [%v] unresponsive - dropping", session.Identifier())
dead = append(dead, session)
}
}
h.lock.Lock()
defer h.lock.Unlock()
for _, session := range dead {
delete(h.sessions, session.Identifier())
}
return nil
}
// SetColour is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SetColour(colour string) error {
h.log.WarnFields("SetColour ignored for Bridge more", logger.Fields{"col": colour})
return nil
}
// Fullscreen is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) Fullscreen() {
h.log.Warn("Fullscreen() unsupported in bridge mode")
}
// UnFullscreen is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) UnFullscreen() {
h.log.Warn("UnFullscreen() unsupported in bridge mode")
}
// SetTitle is currently unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) SetTitle(title string) {
h.log.WarnFields("SetTitle() unsupported in bridge mode", logger.Fields{"title": title})
}
// Close is unsupported for Bridge but required
// for the Renderer interface
func (h *Bridge) Close() {
h.log.Debug("Shutting down")
err := h.server.Close()
if err != nil {
h.log.Errorf(err.Error())
}
}

View File

@@ -0,0 +1,90 @@
package renderer
import (
"sync"
"github.com/gorilla/websocket"
"github.com/leaanthony/mewn"
"github.com/wailsapp/wails/lib/interfaces"
"github.com/wailsapp/wails/lib/logger"
)
// TODO Move this back into bridge.go
// session represents a single websocket session
type session struct {
bindingCache []string
conn *websocket.Conn
eventManager interfaces.EventManager
log *logger.CustomLogger
ipc interfaces.IPCManager
// Mutex for writing to the socket
lock sync.Mutex
}
// Identifier returns a string identifier for the remote connection.
// Taking the form of the client's <ip address>:<port>.
func (s *session) Identifier() string {
if s.conn != nil {
return s.conn.RemoteAddr().String()
}
return ""
}
func (s *session) sendMessage(msg string) error {
s.lock.Lock()
defer s.lock.Unlock()
if err := s.conn.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
s.log.Debug(err.Error())
return err
}
return nil
}
func (s *session) start(firstSession bool) {
s.log.Infof("Connected to frontend.")
wailsRuntime := mewn.String("../../runtime/assets/wails.js")
s.evalJS(wailsRuntime, wailsRuntimeMessage)
// Inject bindings
for _, binding := range s.bindingCache {
s.evalJS(binding, bindingMessage)
}
s.eventManager.Emit("wails:bridge:session:started", s.Identifier())
// Emit that everything is loaded and ready
if firstSession {
s.eventManager.Emit("wails:ready")
}
for {
messageType, buffer, err := s.conn.ReadMessage()
if messageType == -1 {
return
}
if err != nil {
s.log.Errorf("Error reading message: %v", err)
continue
}
s.log.Debugf("Got message: %#v\n", string(buffer))
s.ipc.Dispatch(string(buffer), s.Callback)
}
}
// Callback sends a callback to the frontend
func (s *session) Callback(data string) error {
return s.evalJS(data, callbackMessage)
}
func (s *session) evalJS(js string, mtype messageType) error {
// Prepend message type to message
return s.sendMessage(mtype.toString() + js)
}

View File

@@ -58,7 +58,7 @@ func (w *WebView) Initialise(config interfaces.AppConfig, ipc interfaces.IPCMana
URL: config.GetDefaultHTML(),
Debug: !config.GetDisableInspector(),
ExternalInvokeCallback: func(_ wv.WebView, message string) {
w.ipc.Dispatch(message)
w.ipc.Dispatch(message, w.callback)
},
})
@@ -299,8 +299,8 @@ func (w *WebView) SelectSaveFile() string {
return result
}
// Callback sends a callback to the frontend
func (w *WebView) Callback(data string) error {
// callback sends a callback to the frontend
func (w *WebView) callback(data string) error {
callbackCMD := fmt.Sprintf("window.wails._.Callback('%s');", data)
return w.evalJS(callbackCMD)
}