5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 07:21:32 +08:00
wails/v2/internal/frontend/devserver/devserver.go
stffabi 638caf72f0
[assetserver] Introduce middleware and extract options (#2016)
* [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
2022-10-29 23:15:15 +02:00

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:], &notifyMessage)
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
}