mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 07:21:32 +08:00

* [assetserver] Add support for HTTP Middlewares * [dev] Disable frontend DevServer if no Assets has been defined and inform user * [dev] Consistent WebSocket behaviour in dev and prod mode for assets handler and middleware In prod mode we can't support WebSockets so make sure the assets handler and middleware never see WebSockets in dev mode. * [templates] Migrate to new AssetServer option * [docs] Add assetserver.Options to the reference
317 lines
8.0 KiB
Go
317 lines
8.0 KiB
Go
//go:build dev
|
|
// +build dev
|
|
|
|
// Package devserver provides a web-based frontend so that
|
|
// it is possible to run a Wails app in a browsers.
|
|
package devserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/wailsapp/wails/v2/internal/binding"
|
|
"github.com/wailsapp/wails/v2/internal/frontend"
|
|
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
|
"github.com/wailsapp/wails/v2/internal/logger"
|
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
|
"github.com/wailsapp/wails/v2/pkg/options"
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
type Screen = frontend.Screen
|
|
|
|
type DevWebServer struct {
|
|
server *echo.Echo
|
|
ctx context.Context
|
|
appoptions *options.App
|
|
logger *logger.Logger
|
|
appBindings *binding.Bindings
|
|
dispatcher frontend.Dispatcher
|
|
socketMutex sync.Mutex
|
|
websocketClients map[*websocket.Conn]*sync.Mutex
|
|
menuManager *menumanager.Manager
|
|
starttime string
|
|
|
|
// Desktop frontend
|
|
frontend.Frontend
|
|
|
|
devServerAddr string
|
|
}
|
|
|
|
func (d *DevWebServer) Run(ctx context.Context) error {
|
|
d.ctx = ctx
|
|
|
|
d.server.GET("/wails/reload", d.handleReload)
|
|
d.server.GET("/wails/ipc", d.handleIPCWebSocket)
|
|
|
|
assetServerConfig := assetserver.BuildAssetServerConfig(d.appoptions)
|
|
|
|
var assetHandler http.Handler
|
|
var wsHandler http.Handler
|
|
_fronendDevServerURL, _ := ctx.Value("frontenddevserverurl").(string)
|
|
if _fronendDevServerURL == "" {
|
|
assetdir, _ := ctx.Value("assetdir").(string)
|
|
d.server.GET("/wails/assetdir", func(c echo.Context) error {
|
|
return c.String(http.StatusOK, assetdir)
|
|
})
|
|
|
|
var err error
|
|
assetHandler, err = assetserver.NewAssetHandler(ctx, assetServerConfig)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else {
|
|
externalURL, err := url.Parse(_fronendDevServerURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if externalURL.Host == "" {
|
|
return fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?")
|
|
}
|
|
|
|
waitCb := func() { d.LogDebug("Waiting for frontend DevServer '%s' to be ready", externalURL) }
|
|
if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) {
|
|
d.logger.Error("Timeout waiting for frontend DevServer")
|
|
}
|
|
|
|
assetHandler = newExternalDevServerAssetHandler(d.logger, externalURL, assetServerConfig)
|
|
// WebSockets aren't currently supported in prod mode, so a WebSocket connection is the result of the
|
|
// FrontendDevServer e.g. Vite to support auto reloads.
|
|
// Therefore we direct WebSockets directly to the FrontendDevServer instead of returning a NotImplementedStatus.
|
|
wsHandler = httputil.NewSingleHostReverseProxy(externalURL)
|
|
}
|
|
|
|
// Setup internal dev server
|
|
bindingsJSON, err := d.appBindings.ToJSON()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
assetServer, err := assetserver.NewDevAssetServer(ctx, assetHandler, wsHandler, bindingsJSON)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
d.server.Any("/*", func(c echo.Context) error {
|
|
assetServer.ServeHTTP(c.Response(), c.Request())
|
|
return nil
|
|
})
|
|
|
|
if devServerAddr := d.devServerAddr; devServerAddr != "" {
|
|
// Start server
|
|
go func(server *echo.Echo, log *logger.Logger) {
|
|
err := server.Start(devServerAddr)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
}
|
|
d.LogDebug("Shutdown completed")
|
|
}(d.server, d.logger)
|
|
|
|
d.LogDebug("Serving DevServer at http://%s", devServerAddr)
|
|
}
|
|
|
|
// Launch desktop app
|
|
err = d.Frontend.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (d *DevWebServer) WindowReload() {
|
|
d.broadcast("reload")
|
|
d.Frontend.WindowReload()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowReloadApp() {
|
|
d.broadcast("reloadapp")
|
|
d.Frontend.WindowReloadApp()
|
|
}
|
|
|
|
func (d *DevWebServer) Notify(name string, data ...interface{}) {
|
|
d.notify(name, data...)
|
|
}
|
|
|
|
func (d *DevWebServer) handleReload(c echo.Context) error {
|
|
d.WindowReload()
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (d *DevWebServer) handleReloadApp(c echo.Context) error {
|
|
d.WindowReloadApp()
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (d *DevWebServer) handleIPCWebSocket(c echo.Context) error {
|
|
websocket.Handler(func(c *websocket.Conn) {
|
|
d.LogDebug(fmt.Sprintf("Websocket client %p connected", c))
|
|
d.socketMutex.Lock()
|
|
d.websocketClients[c] = &sync.Mutex{}
|
|
locker := d.websocketClients[c]
|
|
d.socketMutex.Unlock()
|
|
|
|
defer func() {
|
|
d.socketMutex.Lock()
|
|
delete(d.websocketClients, c)
|
|
d.socketMutex.Unlock()
|
|
d.LogDebug(fmt.Sprintf("Websocket client %p disconnected", c))
|
|
}()
|
|
|
|
var msg string
|
|
defer c.Close()
|
|
for {
|
|
if err := websocket.Message.Receive(c, &msg); err != nil {
|
|
break
|
|
}
|
|
// We do not support drag in browsers
|
|
if msg == "drag" {
|
|
continue
|
|
}
|
|
|
|
// Notify the other browsers of "EventEmit"
|
|
if len(msg) > 2 && strings.HasPrefix(string(msg), "EE") {
|
|
d.notifyExcludingSender([]byte(msg), c)
|
|
}
|
|
|
|
// Send the message to dispatch to the frontend
|
|
result, err := d.dispatcher.ProcessMessage(string(msg), d)
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
}
|
|
if result != "" {
|
|
locker.Lock()
|
|
if err = websocket.Message.Send(c, result); err != nil {
|
|
locker.Unlock()
|
|
break
|
|
}
|
|
locker.Unlock()
|
|
}
|
|
}
|
|
}).ServeHTTP(c.Response(), c.Request())
|
|
return nil
|
|
}
|
|
|
|
func (d *DevWebServer) LogDebug(message string, args ...interface{}) {
|
|
d.logger.Debug("[DevWebServer] "+message, args...)
|
|
}
|
|
|
|
type EventNotify struct {
|
|
Name string `json:"name"`
|
|
Data []interface{} `json:"data"`
|
|
}
|
|
|
|
func (d *DevWebServer) broadcast(message string) {
|
|
d.socketMutex.Lock()
|
|
defer d.socketMutex.Unlock()
|
|
for client, locker := range d.websocketClients {
|
|
go func(client *websocket.Conn, locker *sync.Mutex) {
|
|
if client == nil {
|
|
d.logger.Error("Lost connection to websocket server")
|
|
return
|
|
}
|
|
locker.Lock()
|
|
err := websocket.Message.Send(client, message)
|
|
if err != nil {
|
|
locker.Unlock()
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
locker.Unlock()
|
|
}(client, locker)
|
|
}
|
|
}
|
|
|
|
func (d *DevWebServer) notify(name string, data ...interface{}) {
|
|
// Notify
|
|
notification := EventNotify{
|
|
Name: name,
|
|
Data: data,
|
|
}
|
|
payload, err := json.Marshal(notification)
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
d.broadcast("n" + string(payload))
|
|
}
|
|
|
|
func (d *DevWebServer) broadcastExcludingSender(message string, sender *websocket.Conn) {
|
|
d.socketMutex.Lock()
|
|
defer d.socketMutex.Unlock()
|
|
for client, locker := range d.websocketClients {
|
|
go func(client *websocket.Conn, locker *sync.Mutex) {
|
|
if client == sender {
|
|
return
|
|
}
|
|
locker.Lock()
|
|
err := websocket.Message.Send(client, message)
|
|
if err != nil {
|
|
locker.Unlock()
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
locker.Unlock()
|
|
}(client, locker)
|
|
}
|
|
}
|
|
|
|
func (d *DevWebServer) notifyExcludingSender(eventMessage []byte, sender *websocket.Conn) {
|
|
message := "n" + string(eventMessage[2:])
|
|
d.broadcastExcludingSender(message, sender)
|
|
|
|
var notifyMessage EventNotify
|
|
err := json.Unmarshal(eventMessage[2:], ¬ifyMessage)
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
d.Frontend.Notify(notifyMessage.Name, notifyMessage.Data...)
|
|
}
|
|
|
|
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher, menuManager *menumanager.Manager, desktopFrontend frontend.Frontend) *DevWebServer {
|
|
result := &DevWebServer{
|
|
ctx: ctx,
|
|
Frontend: desktopFrontend,
|
|
appoptions: appoptions,
|
|
logger: myLogger,
|
|
appBindings: appBindings,
|
|
dispatcher: dispatcher,
|
|
server: echo.New(),
|
|
menuManager: menuManager,
|
|
websocketClients: make(map[*websocket.Conn]*sync.Mutex),
|
|
}
|
|
|
|
result.devServerAddr, _ = ctx.Value("devserver").(string)
|
|
result.server.HideBanner = true
|
|
result.server.HidePort = true
|
|
return result
|
|
}
|
|
|
|
func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) {
|
|
if timeout == 0 {
|
|
timeout = time.Minute
|
|
}
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
conn, _ := net.DialTimeout("tcp", host, 2*time.Second)
|
|
if conn != nil {
|
|
conn.Close()
|
|
return true
|
|
}
|
|
|
|
waitCB()
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
return false
|
|
}
|