mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 09:00:38 +08:00

* [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.
301 lines
7.5 KiB
Go
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:], ¬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
|
|
}
|