mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
* feat(v3): add server mode for headless HTTP deployment Server mode allows Wails applications to run as pure HTTP servers without native GUI dependencies. Enable with `-tags server` build tag. Features: - HTTP server with configurable host/port via ServerOptions - WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides - WebSocket event broadcasting to connected browsers - Browser clients represented as BrowserWindow (Window interface) - Health check endpoint at /health - Graceful shutdown with configurable timeout - Docker support with Dockerfile.server template and tasks Build and run: wails3 task build:server wails3 task run:server wails3 task build:docker wails3 task run:docker Documentation at docs/guides/server-build.mdx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(v3): add server mode for headless HTTP deployment Server mode allows Wails applications to run as pure HTTP servers without native GUI dependencies. Enable with `-tags server` build tag. Features: - HTTP server with configurable host/port via ServerOptions - WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides - WebSocket event broadcasting to connected browsers - Browser clients represented as BrowserWindow (Window interface) - Health check endpoint at /health - Graceful shutdown with configurable timeout - Docker support with Dockerfile.server template and tasks Build and run: wails3 task build:server wails3 task run:server wails3 task build:docker wails3 task run:docker Documentation at docs/guides/server-build.mdx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review comments - Fix corrupted test file with embedded terminal output - Fix module name mismatch in gin-routing (was gin-example) - Fix replace directive version mismatch in gin-service - Fix placeholder module name in ios example (was changeme) - Fix Dockerfile COPY path to work from both build contexts - Fix bare URL in README (MD034 compliance) - Fix comment accuracy in getScreens (returns error, not empty slice) - Remove deprecated docker-compose version field - Add port documentation in Taskfile template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address CodeRabbit review comments - Add note about healthcheck wget not being available in distroless images - Add !server build constraint to menu_windows.go and menu_darwin.go - Downgrade window-visibility-test go.mod from 1.25 to 1.24 to match CI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
137 lines
4 KiB
Go
137 lines
4 KiB
Go
//go:build server
|
|
|
|
package application
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/coder/websocket/wsjson"
|
|
)
|
|
|
|
// globalBroadcaster holds a reference to the WebSocket broadcaster for server mode.
|
|
// Used by HTTP handler to look up BrowserWindows from runtime client IDs.
|
|
var globalBroadcaster *WebSocketBroadcaster
|
|
|
|
// GetBrowserWindow returns the BrowserWindow for a given runtime clientId.
|
|
// Only available in server mode. Returns nil if not found.
|
|
func GetBrowserWindow(clientId string) *BrowserWindow {
|
|
if globalBroadcaster == nil {
|
|
return nil
|
|
}
|
|
return globalBroadcaster.GetBrowserWindow(clientId)
|
|
}
|
|
|
|
// clientInfo holds information about a connected WebSocket client.
|
|
type clientInfo struct {
|
|
conn *websocket.Conn
|
|
window *BrowserWindow
|
|
}
|
|
|
|
// WebSocketBroadcaster manages WebSocket connections and broadcasts events to all connected clients.
|
|
// It implements WailsEventListener to receive events from the application.
|
|
type WebSocketBroadcaster struct {
|
|
clients map[*websocket.Conn]*clientInfo
|
|
windows map[string]*BrowserWindow // maps runtime clientId (nanoid) to BrowserWindow
|
|
mu sync.RWMutex
|
|
app *App
|
|
nextID atomic.Uint64
|
|
}
|
|
|
|
// NewWebSocketBroadcaster creates a new WebSocket broadcaster.
|
|
func NewWebSocketBroadcaster(app *App) *WebSocketBroadcaster {
|
|
return &WebSocketBroadcaster{
|
|
clients: make(map[*websocket.Conn]*clientInfo),
|
|
windows: make(map[string]*BrowserWindow),
|
|
app: app,
|
|
}
|
|
}
|
|
|
|
// GetBrowserWindow returns the BrowserWindow for a given runtime clientId.
|
|
// Returns nil if not found.
|
|
func (b *WebSocketBroadcaster) GetBrowserWindow(clientId string) *BrowserWindow {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
return b.windows[clientId]
|
|
}
|
|
|
|
// ServeHTTP handles WebSocket upgrade requests.
|
|
func (b *WebSocketBroadcaster) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
|
// Allow connections from any origin in server mode
|
|
InsecureSkipVerify: true,
|
|
})
|
|
if err != nil {
|
|
b.app.error("WebSocket accept error", "error", err)
|
|
return
|
|
}
|
|
|
|
// Get runtime's clientId from query parameter
|
|
runtimeClientID := r.URL.Query().Get("clientId")
|
|
|
|
// Create BrowserWindow for this connection
|
|
browserWindow := NewBrowserWindow(uint(b.nextID.Add(1)), runtimeClientID)
|
|
|
|
// Store mapping if runtime clientId was provided
|
|
if runtimeClientID != "" {
|
|
b.mu.Lock()
|
|
b.windows[runtimeClientID] = browserWindow
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
b.register(conn, browserWindow)
|
|
defer b.unregister(conn, runtimeClientID)
|
|
|
|
// Keep connection alive - read loop for ping/pong and detecting disconnects
|
|
// Events from client to server are sent via HTTP, not WebSocket
|
|
for {
|
|
_, _, err := conn.Read(r.Context())
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// register adds a client connection with its BrowserWindow.
|
|
func (b *WebSocketBroadcaster) register(conn *websocket.Conn, window *BrowserWindow) {
|
|
client := &clientInfo{
|
|
conn: conn,
|
|
window: window,
|
|
}
|
|
b.mu.Lock()
|
|
b.clients[conn] = client
|
|
b.mu.Unlock()
|
|
b.app.info("WebSocket client connected", "id", window.Name(), "clients", len(b.clients))
|
|
}
|
|
|
|
// unregister removes a client connection and its BrowserWindow.
|
|
func (b *WebSocketBroadcaster) unregister(conn *websocket.Conn, runtimeClientID string) {
|
|
b.mu.Lock()
|
|
client := b.clients[conn]
|
|
delete(b.clients, conn)
|
|
if runtimeClientID != "" {
|
|
delete(b.windows, runtimeClientID)
|
|
}
|
|
b.mu.Unlock()
|
|
conn.Close(websocket.StatusNormalClosure, "")
|
|
if client != nil {
|
|
b.app.info("WebSocket client disconnected", "id", client.window.Name(), "clients", len(b.clients))
|
|
}
|
|
}
|
|
|
|
// DispatchWailsEvent implements WailsEventListener interface.
|
|
// It broadcasts the event to all connected WebSocket clients.
|
|
func (b *WebSocketBroadcaster) DispatchWailsEvent(event *CustomEvent) {
|
|
b.mu.RLock()
|
|
defer b.mu.RUnlock()
|
|
|
|
for _, client := range b.clients {
|
|
go func(c *clientInfo) {
|
|
if err := wsjson.Write(b.app.ctx, c.conn, event); err != nil {
|
|
b.app.debug("WebSocket write error", "error", err, "client", c.window.Name())
|
|
}
|
|
}(client)
|
|
}
|
|
}
|