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:], ¬ifyMessage) 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 }