github.com/secoba/wails/v2@v2.6.4/internal/frontend/devserver/devserver.go (about)

     1  //go:build dev
     2  // +build dev
     3  
     4  // Package devserver provides a web-based frontend so that
     5  // it is possible to run a Wails app in a browsers.
     6  package devserver
     7  
     8  import (
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"log"
    13  	"net/http"
    14  	"net/http/httputil"
    15  	"net/url"
    16  	"strings"
    17  	"sync"
    18  
    19  	"github.com/secoba/wails/v2/pkg/assetserver"
    20  
    21  	"github.com/secoba/wails/v2/internal/frontend/runtime"
    22  
    23  	"github.com/labstack/echo/v4"
    24  	"github.com/secoba/wails/v2/internal/binding"
    25  	"github.com/secoba/wails/v2/internal/frontend"
    26  	"github.com/secoba/wails/v2/internal/logger"
    27  	"github.com/secoba/wails/v2/internal/menumanager"
    28  	"github.com/secoba/wails/v2/pkg/options"
    29  	"golang.org/x/net/websocket"
    30  )
    31  
    32  type Screen = frontend.Screen
    33  
    34  type DevWebServer struct {
    35  	server           *echo.Echo
    36  	ctx              context.Context
    37  	appoptions       *options.App
    38  	logger           *logger.Logger
    39  	appBindings      *binding.Bindings
    40  	dispatcher       frontend.Dispatcher
    41  	socketMutex      sync.Mutex
    42  	websocketClients map[*websocket.Conn]*sync.Mutex
    43  	menuManager      *menumanager.Manager
    44  	starttime        string
    45  
    46  	// Desktop frontend
    47  	frontend.Frontend
    48  
    49  	devServerAddr string
    50  }
    51  
    52  func (d *DevWebServer) Run(ctx context.Context) error {
    53  	d.ctx = ctx
    54  
    55  	d.server.GET("/wails/reload", d.handleReload)
    56  	d.server.GET("/wails/ipc", d.handleIPCWebSocket)
    57  
    58  	assetServerConfig, err := assetserver.BuildAssetServerConfig(d.appoptions)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	var myLogger assetserver.Logger
    64  	if _logger := ctx.Value("logger"); _logger != nil {
    65  		myLogger = _logger.(*logger.Logger)
    66  	}
    67  
    68  	var wsHandler http.Handler
    69  
    70  	_fronendDevServerURL, _ := ctx.Value("frontenddevserverurl").(string)
    71  	if _fronendDevServerURL == "" {
    72  		assetdir, _ := ctx.Value("assetdir").(string)
    73  		d.server.GET("/wails/assetdir", func(c echo.Context) error {
    74  			return c.String(http.StatusOK, assetdir)
    75  		})
    76  
    77  	} else {
    78  		externalURL, err := url.Parse(_fronendDevServerURL)
    79  		if err != nil {
    80  			return err
    81  		}
    82  
    83  		// WebSockets aren't currently supported in prod mode, so a WebSocket connection is the result of the
    84  		// FrontendDevServer e.g. Vite to support auto reloads.
    85  		// Therefore we direct WebSockets directly to the FrontendDevServer instead of returning a NotImplementedStatus.
    86  		wsHandler = httputil.NewSingleHostReverseProxy(externalURL)
    87  	}
    88  
    89  	assetHandler, err := assetserver.NewAssetHandler(assetServerConfig, myLogger)
    90  	if err != nil {
    91  		log.Fatal(err)
    92  	}
    93  
    94  	// Setup internal dev server
    95  	bindingsJSON, err := d.appBindings.ToJSON()
    96  	if err != nil {
    97  		log.Fatal(err)
    98  	}
    99  
   100  	assetServer, err := assetserver.NewDevAssetServer(assetHandler, bindingsJSON, ctx.Value("assetdir") != nil, myLogger, runtime.RuntimeAssetsBundle)
   101  	if err != nil {
   102  		log.Fatal(err)
   103  	}
   104  
   105  	d.server.Any("/*", func(c echo.Context) error {
   106  		if c.IsWebSocket() {
   107  			wsHandler.ServeHTTP(c.Response(), c.Request())
   108  		} else {
   109  			assetServer.ServeHTTP(c.Response(), c.Request())
   110  		}
   111  		return nil
   112  	})
   113  
   114  	if devServerAddr := d.devServerAddr; devServerAddr != "" {
   115  		// Start server
   116  		go func(server *echo.Echo, log *logger.Logger) {
   117  			err := server.Start(devServerAddr)
   118  			if err != nil {
   119  				log.Error(err.Error())
   120  			}
   121  			d.LogDebug("Shutdown completed")
   122  		}(d.server, d.logger)
   123  
   124  		d.LogDebug("Serving DevServer at http://%s", devServerAddr)
   125  	}
   126  
   127  	// Launch desktop app
   128  	err = d.Frontend.Run(ctx)
   129  
   130  	return err
   131  }
   132  
   133  func (d *DevWebServer) WindowReload() {
   134  	d.broadcast("reload")
   135  	d.Frontend.WindowReload()
   136  }
   137  
   138  func (d *DevWebServer) WindowReloadApp() {
   139  	d.broadcast("reloadapp")
   140  	d.Frontend.WindowReloadApp()
   141  }
   142  
   143  func (d *DevWebServer) Notify(name string, data ...interface{}) {
   144  	d.notify(name, data...)
   145  }
   146  
   147  func (d *DevWebServer) handleReload(c echo.Context) error {
   148  	d.WindowReload()
   149  	return c.NoContent(http.StatusNoContent)
   150  }
   151  
   152  func (d *DevWebServer) handleReloadApp(c echo.Context) error {
   153  	d.WindowReloadApp()
   154  	return c.NoContent(http.StatusNoContent)
   155  }
   156  
   157  func (d *DevWebServer) handleIPCWebSocket(c echo.Context) error {
   158  	websocket.Handler(func(c *websocket.Conn) {
   159  		d.LogDebug(fmt.Sprintf("Websocket client %p connected", c))
   160  		d.socketMutex.Lock()
   161  		d.websocketClients[c] = &sync.Mutex{}
   162  		locker := d.websocketClients[c]
   163  		d.socketMutex.Unlock()
   164  
   165  		defer func() {
   166  			d.socketMutex.Lock()
   167  			delete(d.websocketClients, c)
   168  			d.socketMutex.Unlock()
   169  			d.LogDebug(fmt.Sprintf("Websocket client %p disconnected", c))
   170  		}()
   171  
   172  		var msg string
   173  		defer c.Close()
   174  		for {
   175  			if err := websocket.Message.Receive(c, &msg); err != nil {
   176  				break
   177  			}
   178  			// We do not support drag in browsers
   179  			if msg == "drag" {
   180  				continue
   181  			}
   182  
   183  			// Notify the other browsers of "EventEmit"
   184  			if len(msg) > 2 && strings.HasPrefix(string(msg), "EE") {
   185  				d.notifyExcludingSender([]byte(msg), c)
   186  			}
   187  
   188  			// Send the message to dispatch to the frontend
   189  			result, err := d.dispatcher.ProcessMessage(string(msg), d)
   190  			if err != nil {
   191  				d.logger.Error(err.Error())
   192  			}
   193  			if result != "" {
   194  				locker.Lock()
   195  				if err = websocket.Message.Send(c, result); err != nil {
   196  					locker.Unlock()
   197  					break
   198  				}
   199  				locker.Unlock()
   200  			}
   201  		}
   202  	}).ServeHTTP(c.Response(), c.Request())
   203  	return nil
   204  }
   205  
   206  func (d *DevWebServer) LogDebug(message string, args ...interface{}) {
   207  	d.logger.Debug("[DevWebServer] "+message, args...)
   208  }
   209  
   210  type EventNotify struct {
   211  	Name string        `json:"name"`
   212  	Data []interface{} `json:"data"`
   213  }
   214  
   215  func (d *DevWebServer) broadcast(message string) {
   216  	d.socketMutex.Lock()
   217  	defer d.socketMutex.Unlock()
   218  	for client, locker := range d.websocketClients {
   219  		go func(client *websocket.Conn, locker *sync.Mutex) {
   220  			if client == nil {
   221  				d.logger.Error("Lost connection to websocket server")
   222  				return
   223  			}
   224  			locker.Lock()
   225  			err := websocket.Message.Send(client, message)
   226  			if err != nil {
   227  				locker.Unlock()
   228  				d.logger.Error(err.Error())
   229  				return
   230  			}
   231  			locker.Unlock()
   232  		}(client, locker)
   233  	}
   234  }
   235  
   236  func (d *DevWebServer) notify(name string, data ...interface{}) {
   237  	// Notify
   238  	notification := EventNotify{
   239  		Name: name,
   240  		Data: data,
   241  	}
   242  	payload, err := json.Marshal(notification)
   243  	if err != nil {
   244  		d.logger.Error(err.Error())
   245  		return
   246  	}
   247  	d.broadcast("n" + string(payload))
   248  }
   249  
   250  func (d *DevWebServer) broadcastExcludingSender(message string, sender *websocket.Conn) {
   251  	d.socketMutex.Lock()
   252  	defer d.socketMutex.Unlock()
   253  	for client, locker := range d.websocketClients {
   254  		go func(client *websocket.Conn, locker *sync.Mutex) {
   255  			if client == sender {
   256  				return
   257  			}
   258  			locker.Lock()
   259  			err := websocket.Message.Send(client, message)
   260  			if err != nil {
   261  				locker.Unlock()
   262  				d.logger.Error(err.Error())
   263  				return
   264  			}
   265  			locker.Unlock()
   266  		}(client, locker)
   267  	}
   268  }
   269  
   270  func (d *DevWebServer) notifyExcludingSender(eventMessage []byte, sender *websocket.Conn) {
   271  	message := "n" + string(eventMessage[2:])
   272  	d.broadcastExcludingSender(message, sender)
   273  
   274  	var notifyMessage EventNotify
   275  	err := json.Unmarshal(eventMessage[2:], &notifyMessage)
   276  	if err != nil {
   277  		d.logger.Error(err.Error())
   278  		return
   279  	}
   280  	d.Frontend.Notify(notifyMessage.Name, notifyMessage.Data...)
   281  }
   282  
   283  func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher, menuManager *menumanager.Manager, desktopFrontend frontend.Frontend) *DevWebServer {
   284  	result := &DevWebServer{
   285  		ctx:              ctx,
   286  		Frontend:         desktopFrontend,
   287  		appoptions:       appoptions,
   288  		logger:           myLogger,
   289  		appBindings:      appBindings,
   290  		dispatcher:       dispatcher,
   291  		server:           echo.New(),
   292  		menuManager:      menuManager,
   293  		websocketClients: make(map[*websocket.Conn]*sync.Mutex),
   294  	}
   295  
   296  	result.devServerAddr, _ = ctx.Value("devserver").(string)
   297  	result.server.HideBanner = true
   298  	result.server.HidePort = true
   299  	return result
   300  }