github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/internal/app/app_dev.go (about)

     1  //go:build dev
     2  
     3  package app
     4  
     5  import (
     6  	"context"
     7  	"embed"
     8  	"flag"
     9  	"fmt"
    10  	iofs "io/fs"
    11  	"net"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"time"
    16  
    17  	"github.com/AlpineAIO/wails/v2/pkg/assetserver"
    18  
    19  	"github.com/AlpineAIO/wails/v2/internal/binding"
    20  	"github.com/AlpineAIO/wails/v2/internal/frontend/desktop"
    21  	"github.com/AlpineAIO/wails/v2/internal/frontend/devserver"
    22  	"github.com/AlpineAIO/wails/v2/internal/frontend/dispatcher"
    23  	"github.com/AlpineAIO/wails/v2/internal/frontend/runtime"
    24  	"github.com/AlpineAIO/wails/v2/internal/fs"
    25  	"github.com/AlpineAIO/wails/v2/internal/logger"
    26  	"github.com/AlpineAIO/wails/v2/internal/menumanager"
    27  	pkglogger "github.com/AlpineAIO/wails/v2/pkg/logger"
    28  	"github.com/AlpineAIO/wails/v2/pkg/options"
    29  )
    30  
    31  func (a *App) Run() error {
    32  	err := a.frontend.Run(a.ctx)
    33  	a.frontend.RunMainLoop()
    34  	a.frontend.WindowClose()
    35  	if a.shutdownCallback != nil {
    36  		a.shutdownCallback(a.ctx)
    37  	}
    38  	return err
    39  }
    40  
    41  // CreateApp creates the app!
    42  func CreateApp(appoptions *options.App) (*App, error) {
    43  	var err error
    44  
    45  	ctx := context.Background()
    46  	ctx = context.WithValue(ctx, "debug", true)
    47  	ctx = context.WithValue(ctx, "devtoolsEnabled", true)
    48  
    49  	// Set up logger
    50  	myLogger := logger.New(appoptions.Logger)
    51  	myLogger.SetLogLevel(appoptions.LogLevel)
    52  
    53  	// Check for CLI Flags
    54  	devFlags := flag.NewFlagSet("dev", flag.ContinueOnError)
    55  
    56  	var assetdirFlag *string
    57  	var devServerFlag *string
    58  	var frontendDevServerURLFlag *string
    59  	var loglevelFlag *string
    60  
    61  	assetdir := os.Getenv("assetdir")
    62  	if assetdir == "" {
    63  		assetdirFlag = devFlags.String("assetdir", "", "Directory to serve assets")
    64  	}
    65  
    66  	devServer := os.Getenv("devserver")
    67  	if devServer == "" {
    68  		devServerFlag = devFlags.String("devserver", "", "Address to bind the wails dev server to")
    69  	}
    70  
    71  	frontendDevServerURL := os.Getenv("frontenddevserverurl")
    72  	if frontendDevServerURL == "" {
    73  		frontendDevServerURLFlag = devFlags.String("frontenddevserverurl", "", "URL of the external frontend dev server")
    74  	}
    75  
    76  	loglevel := os.Getenv("loglevel")
    77  	if loglevel == "" {
    78  		loglevelFlag = devFlags.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error")
    79  	}
    80  
    81  	// If we weren't given the assetdir in the environment variables
    82  	if assetdir == "" {
    83  		// Parse args but ignore errors in case -appargs was used to pass in args for the app.
    84  		_ = devFlags.Parse(os.Args[1:])
    85  		if assetdirFlag != nil {
    86  			assetdir = *assetdirFlag
    87  		}
    88  		if devServerFlag != nil {
    89  			devServer = *devServerFlag
    90  		}
    91  		if frontendDevServerURLFlag != nil {
    92  			frontendDevServerURL = *frontendDevServerURLFlag
    93  		}
    94  		if loglevelFlag != nil {
    95  			loglevel = *loglevelFlag
    96  		}
    97  	}
    98  
    99  	assetConfig, err := assetserver.BuildAssetServerConfig(appoptions)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	if assetConfig.Assets == nil && frontendDevServerURL != "" {
   105  		myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.")
   106  		frontendDevServerURL = ""
   107  		assetdir = ""
   108  	}
   109  
   110  	if frontendDevServerURL != "" {
   111  		_, port, err := net.SplitHostPort(devServer)
   112  		if err != nil {
   113  			return nil, fmt.Errorf("unable to determine port of DevServer: %s", err)
   114  		}
   115  
   116  		ctx = context.WithValue(ctx, "assetserverport", port)
   117  
   118  		ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL)
   119  
   120  		externalURL, err := url.Parse(frontendDevServerURL)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  
   125  		if externalURL.Host == "" {
   126  			return nil, fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?")
   127  		}
   128  
   129  		waitCb := func() { myLogger.Debug("Waiting for frontend DevServer '%s' to be ready", externalURL) }
   130  		if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) {
   131  			myLogger.Error("Timeout waiting for frontend DevServer")
   132  		}
   133  
   134  		handler := assetserver.NewExternalAssetsHandler(myLogger, assetConfig, externalURL)
   135  		assetConfig.Assets = nil
   136  		assetConfig.Handler = handler
   137  		assetConfig.Middleware = nil
   138  
   139  		myLogger.Info("Serving assets from frontend DevServer URL: %s", frontendDevServerURL)
   140  	} else {
   141  		if assetdir == "" {
   142  			// If no assetdir has been defined, let's try to infer it from the project root and the asset FS.
   143  			assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets)
   144  			if err != nil {
   145  				return nil, fmt.Errorf("unable to infer the AssetDir from your Assets fs.FS: %w", err)
   146  			}
   147  		}
   148  
   149  		if assetdir != "" {
   150  			// Let's override the assets to serve from on disk, if needed
   151  			absdir, err := filepath.Abs(assetdir)
   152  			if err != nil {
   153  				return nil, err
   154  			}
   155  
   156  			myLogger.Info("Serving assets from disk: %s", absdir)
   157  			assetConfig.Assets = os.DirFS(absdir)
   158  
   159  			ctx = context.WithValue(ctx, "assetdir", assetdir)
   160  		}
   161  	}
   162  
   163  	// Migrate deprecated options to the new AssetServer option
   164  	appoptions.Assets = nil
   165  	appoptions.AssetsHandler = nil
   166  	appoptions.AssetServer = &assetConfig
   167  
   168  	if devServer != "" {
   169  		ctx = context.WithValue(ctx, "devserver", devServer)
   170  	}
   171  
   172  	if loglevel != "" {
   173  		level, err := pkglogger.StringToLogLevel(loglevel)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		myLogger.SetLogLevel(level)
   178  	}
   179  
   180  	// Attach logger to context
   181  	ctx = context.WithValue(ctx, "logger", myLogger)
   182  	ctx = context.WithValue(ctx, "buildtype", "dev")
   183  
   184  	// Preflight checks
   185  	err = PreflightChecks(appoptions, myLogger)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	// Merge default options
   191  	options.MergeDefaults(appoptions)
   192  
   193  	var menuManager *menumanager.Manager
   194  
   195  	// Process the application menu
   196  	if appoptions.Menu != nil {
   197  		// Create the menu manager
   198  		menuManager = menumanager.NewManager()
   199  		err = menuManager.SetApplicationMenu(appoptions.Menu)
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  	}
   204  
   205  	// Create binding exemptions - Ugly hack. There must be a better way
   206  	bindingExemptions := []interface{}{
   207  		appoptions.OnStartup,
   208  		appoptions.OnShutdown,
   209  		appoptions.OnDomReady,
   210  		appoptions.OnBeforeClose,
   211  	}
   212  	appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind)
   213  
   214  	eventHandler := runtime.NewEvents(myLogger)
   215  	ctx = context.WithValue(ctx, "events", eventHandler)
   216  	messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter)
   217  
   218  	// Create the frontends and register to event handler
   219  	desktopFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher)
   220  	appFrontend := devserver.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher, menuManager, desktopFrontend)
   221  	eventHandler.AddFrontend(appFrontend)
   222  	eventHandler.AddFrontend(desktopFrontend)
   223  
   224  	ctx = context.WithValue(ctx, "frontend", appFrontend)
   225  	result := &App{
   226  		ctx:              ctx,
   227  		frontend:         appFrontend,
   228  		logger:           myLogger,
   229  		menuManager:      menuManager,
   230  		startupCallback:  appoptions.OnStartup,
   231  		shutdownCallback: appoptions.OnShutdown,
   232  		debug:            true,
   233  		devtoolsEnabled:  true,
   234  	}
   235  
   236  	result.options = appoptions
   237  
   238  	return result, nil
   239  
   240  }
   241  
   242  func tryInferAssetDirFromFS(assets iofs.FS) (string, error) {
   243  	if _, isEmbedFs := assets.(embed.FS); !isEmbedFs {
   244  		// We only infer the assetdir for embed.FS assets
   245  		return "", nil
   246  	}
   247  
   248  	path, err := fs.FindPathToFile(assets, "index.html")
   249  	if err != nil {
   250  		return "", err
   251  	}
   252  
   253  	path, err = filepath.Abs(path)
   254  	if err != nil {
   255  		return "", err
   256  	}
   257  
   258  	if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil {
   259  		if os.IsNotExist(err) {
   260  			err = fmt.Errorf(
   261  				"inferred assetdir '%s' does not exist or does not contain an 'index.html' file, "+
   262  					"please specify it with -assetdir or set it in wails.json",
   263  				path)
   264  		}
   265  		return "", err
   266  	}
   267  
   268  	return path, nil
   269  }
   270  
   271  func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) {
   272  	if timeout == 0 {
   273  		timeout = time.Minute
   274  	}
   275  
   276  	deadline := time.Now().Add(timeout)
   277  	for time.Now().Before(deadline) {
   278  		conn, _ := net.DialTimeout("tcp", host, 2*time.Second)
   279  		if conn != nil {
   280  			conn.Close()
   281  			return true
   282  		}
   283  
   284  		waitCB()
   285  		time.Sleep(1 * time.Second)
   286  	}
   287  	return false
   288  }