diff --git a/app.go b/app.go index e987e6c4..7048e04f 100644 --- a/app.go +++ b/app.go @@ -97,7 +97,7 @@ func (a *App) start() error { // Check if we are to run in bridge mode if BuildMode == cmd.BuildModeBridge { - a.renderer = &renderer.Bridge{} + a.renderer = renderer.NewBridge() } // Initialise the renderer diff --git a/lib/interfaces/ipcmanager.go b/lib/interfaces/ipcmanager.go index 9d077203..bc6a3cc0 100644 --- a/lib/interfaces/ipcmanager.go +++ b/lib/interfaces/ipcmanager.go @@ -1,9 +1,13 @@ package interfaces +// CallbackFunc defines the signature of a function required to be provided to the +// Dispatch function so that the response may be returned +type CallbackFunc func(string) error + // IPCManager is the event manager interface type IPCManager interface { BindRenderer(Renderer) - Dispatch(message string) + Dispatch(message string, f CallbackFunc) Start(eventManager EventManager, bindingManager BindingManager) Shutdown() } diff --git a/lib/interfaces/renderer.go b/lib/interfaces/renderer.go index 1a56dc1f..f8d127ba 100644 --- a/lib/interfaces/renderer.go +++ b/lib/interfaces/renderer.go @@ -12,7 +12,6 @@ type Renderer interface { // Binding NewBinding(bindingName string) error - Callback(data string) error // Events NotifyEvent(eventData *messages.EventData) error diff --git a/lib/ipc/manager.go b/lib/ipc/manager.go index a4170406..8cff6e3d 100644 --- a/lib/ipc/manager.go +++ b/lib/ipc/manager.go @@ -135,10 +135,10 @@ func (i *Manager) Start(eventManager interfaces.EventManager, bindingManager int // Dispatch receives JSON encoded messages from the renderer. // It processes the message to ensure that it is valid and places // the processed message on the message queue -func (i *Manager) Dispatch(message string) { +func (i *Manager) Dispatch(message string, cb interfaces.CallbackFunc) { // Create a new IPC Message - incomingMessage, err := newIPCMessage(message, i.SendResponse) + incomingMessage, err := newIPCMessage(message, i.SendResponse(cb)) if err != nil { i.log.ErrorFields("Could not understand incoming message! ", map[string]interface{}{ "message": message, @@ -158,17 +158,19 @@ func (i *Manager) Dispatch(message string) { } // SendResponse sends the given response back to the frontend -func (i *Manager) SendResponse(response *ipcResponse) error { +// It sends the data back to the correct renderer by way of the provided callback function +func (i *Manager) SendResponse(cb interfaces.CallbackFunc) func(i *ipcResponse) error { - // Serialise the Message - data, err := response.Serialise() - if err != nil { - fmt.Printf(err.Error()) - return err + return func(response *ipcResponse) error { + // Serialise the Message + data, err := response.Serialise() + if err != nil { + fmt.Printf(err.Error()) + return err + } + return cb(data) } - // Call back to the front end - return i.renderer.Callback(data) } // Shutdown is called when exiting the Application diff --git a/lib/renderer/bridge.go b/lib/renderer/bridge.go index 2b74f868..44da7849 100644 --- a/lib/renderer/bridge.go +++ b/lib/renderer/bridge.go @@ -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{} } diff --git a/lib/renderer/bridge/bridge.go b/lib/renderer/bridge/bridge.go new file mode 100644 index 00000000..9666b465 --- /dev/null +++ b/lib/renderer/bridge/bridge.go @@ -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()) + } +} diff --git a/lib/renderer/bridge/session.go b/lib/renderer/bridge/session.go new file mode 100644 index 00000000..fd4d9d59 --- /dev/null +++ b/lib/renderer/bridge/session.go @@ -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 :. +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) + +} diff --git a/lib/renderer/webview.go b/lib/renderer/webview.go index 8ab31a8c..6589d0fb 100644 --- a/lib/renderer/webview.go +++ b/lib/renderer/webview.go @@ -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) }