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 }