5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 04:40:41 +08:00
wails/v2/internal/app/app_dev.go

297 lines
8.1 KiB
Go

//go:build dev
package app
import (
"context"
"embed"
"flag"
"fmt"
iofs "io/fs"
"net"
"net/url"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v2/pkg/assetserver"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend/desktop"
"github.com/wailsapp/wails/v2/internal/frontend/devserver"
"github.com/wailsapp/wails/v2/internal/frontend/dispatcher"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
pkglogger "github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *App) Run() error {
err := a.frontend.Run(a.ctx)
a.frontend.RunMainLoop()
a.frontend.WindowClose()
if a.shutdownCallback != nil {
a.shutdownCallback(a.ctx)
}
return err
}
// CreateApp creates the app!
func CreateApp(appoptions *options.App) (*App, error) {
var err error
ctx := context.Background()
ctx = context.WithValue(ctx, "debug", true)
ctx = context.WithValue(ctx, "devtoolsEnabled", true)
// Set up logger if the appoptions.LogLevel is an invalid value, set it to the default log level
appoptions.LogLevel, err = pkglogger.StringToLogLevel(appoptions.LogLevel.String())
if err != nil {
return nil, err
}
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
// Check for CLI Flags
devFlags := flag.NewFlagSet("dev", flag.ContinueOnError)
var assetdirFlag *string
var devServerFlag *string
var frontendDevServerURLFlag *string
var loglevelFlag *string
assetdir := os.Getenv("assetdir")
if assetdir == "" {
assetdirFlag = devFlags.String("assetdir", "", "Directory to serve assets")
}
devServer := os.Getenv("devserver")
if devServer == "" {
devServerFlag = devFlags.String("devserver", "", "Address to bind the wails dev server to")
}
frontendDevServerURL := os.Getenv("frontenddevserverurl")
if frontendDevServerURL == "" {
frontendDevServerURLFlag = devFlags.String("frontenddevserverurl", "", "URL of the external frontend dev server")
}
loglevel := os.Getenv("loglevel")
if loglevel == "" {
loglevelFlag = devFlags.String("loglevel", appoptions.LogLevel.String(), "Loglevel to use - Trace, Debug, Info, Warning, Error")
}
// If we weren't given the assetdir in the environment variables
if assetdir == "" {
// Parse args but ignore errors in case -appargs was used to pass in args for the app.
_ = devFlags.Parse(os.Args[1:])
if assetdirFlag != nil {
assetdir = *assetdirFlag
}
if devServerFlag != nil {
devServer = *devServerFlag
}
if frontendDevServerURLFlag != nil {
frontendDevServerURL = *frontendDevServerURLFlag
}
if loglevelFlag != nil {
loglevel = *loglevelFlag
}
}
assetConfig, err := assetserver.BuildAssetServerConfig(appoptions)
if err != nil {
return nil, err
}
if assetConfig.Assets == nil && frontendDevServerURL != "" {
myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.")
frontendDevServerURL = ""
assetdir = ""
}
if frontendDevServerURL != "" {
_, port, err := net.SplitHostPort(devServer)
if err != nil {
return nil, fmt.Errorf("unable to determine port of DevServer: %s", err)
}
ctx = context.WithValue(ctx, "assetserverport", port)
ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL)
externalURL, err := url.Parse(frontendDevServerURL)
if err != nil {
return nil, err
}
if externalURL.Host == "" {
return nil, fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?")
}
waitCb := func() { myLogger.Debug("Waiting for frontend DevServer '%s' to be ready", externalURL) }
if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) {
myLogger.Error("Timeout waiting for frontend DevServer")
}
handler := assetserver.NewExternalAssetsHandler(myLogger, assetConfig, externalURL)
assetConfig.Assets = nil
assetConfig.Handler = handler
assetConfig.Middleware = nil
myLogger.Info("Serving assets from frontend DevServer URL: %s", frontendDevServerURL)
} else {
if assetdir == "" {
// If no assetdir has been defined, let's try to infer it from the project root and the asset FS.
assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets)
if err != nil {
return nil, fmt.Errorf("unable to infer the AssetDir from your Assets fs.FS: %w", err)
}
}
if assetdir != "" {
// Let's override the assets to serve from on disk, if needed
absdir, err := filepath.Abs(assetdir)
if err != nil {
return nil, err
}
myLogger.Info("Serving assets from disk: %s", absdir)
assetConfig.Assets = os.DirFS(absdir)
ctx = context.WithValue(ctx, "assetdir", assetdir)
}
}
// Migrate deprecated options to the new AssetServer option
appoptions.Assets = nil
appoptions.AssetsHandler = nil
appoptions.AssetServer = &assetConfig
if devServer != "" {
ctx = context.WithValue(ctx, "devserver", devServer)
}
if loglevel != "" {
level, err := pkglogger.StringToLogLevel(loglevel)
if err != nil {
return nil, err
}
// Only set the log level if it's different from the appoptions.LogLevel
if level != appoptions.LogLevel {
myLogger.SetLogLevel(level)
}
}
// Attach logger to context
ctx = context.WithValue(ctx, "logger", myLogger)
ctx = context.WithValue(ctx, "buildtype", "dev")
// Preflight checks
err = PreflightChecks(appoptions, myLogger)
if err != nil {
return nil, err
}
// Merge default options
options.MergeDefaults(appoptions)
var menuManager *menumanager.Manager
// Process the application menu
if appoptions.Menu != nil {
// Create the menu manager
menuManager = menumanager.NewManager()
err = menuManager.SetApplicationMenu(appoptions.Menu)
if err != nil {
return nil, err
}
}
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{
appoptions.OnStartup,
appoptions.OnShutdown,
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind)
eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery)
// Create the frontends and register to event handler
desktopFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher)
appFrontend := devserver.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher, menuManager, desktopFrontend)
eventHandler.AddFrontend(appFrontend)
eventHandler.AddFrontend(desktopFrontend)
ctx = context.WithValue(ctx, "frontend", appFrontend)
result := &App{
ctx: ctx,
frontend: appFrontend,
logger: myLogger,
menuManager: menuManager,
startupCallback: appoptions.OnStartup,
shutdownCallback: appoptions.OnShutdown,
debug: true,
devtoolsEnabled: true,
}
result.options = appoptions
return result, nil
}
func tryInferAssetDirFromFS(assets iofs.FS) (string, error) {
if _, isEmbedFs := assets.(embed.FS); !isEmbedFs {
// We only infer the assetdir for embed.FS assets
return "", nil
}
path, err := fs.FindPathToFile(assets, "index.html")
if err != nil {
return "", err
}
path, err = filepath.Abs(path)
if err != nil {
return "", err
}
if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil {
if os.IsNotExist(err) {
err = fmt.Errorf(
"inferred assetdir '%s' does not exist or does not contain an 'index.html' file, "+
"please specify it with -assetdir or set it in wails.json",
path)
}
return "", err
}
return path, nil
}
func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) {
if timeout == 0 {
timeout = time.Minute
}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
conn, _ := net.DialTimeout("tcp", host, 2*time.Second)
if conn != nil {
conn.Close()
return true
}
waitCB()
time.Sleep(1 * time.Second)
}
return false
}