5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 03:40:12 +08:00
wails/v2/internal/frontend/devserver/devserver.go
stffabi 496461920f
[v2] DevServer improvements and fixes (#2664)
* [assetserver, darwin] Fix copying request headers by using strdup

* [assetserver, linux] Fake some basic http headers for legacy webkit2 versions to support proxying requests to other servers

This fixes the devserver on v2 for newer vite versions that use the custom
scheme.

* [v2, windows] 304 responses are going to hang the WebView2 so prevent them by removing cache related headers in the request.

* [v2, dev] Now uses the custom schemes `wails://` on macOS and Linux for all Vite versions.

Prevent missing reload after fast multiple savings on Linux and Windows.
2023-05-16 09:35:48 +02:00

301 lines
7.5 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/http"
"net/http/httputil"
"net/url"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/assetserver"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"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/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, err := assetserver.BuildAssetServerConfig(d.appoptions)
if err != nil {
return err
}
var myLogger assetserver.Logger
if _logger := ctx.Value("logger"); _logger != nil {
myLogger = _logger.(*logger.Logger)
}
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)
})
} else {
externalURL, err := url.Parse(_fronendDevServerURL)
if err != nil {
return err
}
// 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)
}
assetHandler, err := assetserver.NewAssetHandler(assetServerConfig, myLogger)
if err != nil {
log.Fatal(err)
}
// Setup internal dev server
bindingsJSON, err := d.appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
assetServer, err := assetserver.NewDevAssetServer(assetHandler, bindingsJSON, ctx.Value("assetdir") != nil, myLogger, runtime.RuntimeAssetsBundle)
if err != nil {
log.Fatal(err)
}
d.server.Any("/*", func(c echo.Context) error {
if c.IsWebSocket() {
wsHandler.ServeHTTP(c.Response(), c.Request())
} else {
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
}