package loader import ( "context" "errors" "fmt" "galaxy/connector" mc "galaxy/model/client" "galaxy/storage" "plugin" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) type ClientInit func(storage.UIStorage, connector.UIConnector, fyne.App) (mc.Client, error) type loader struct { app fyne.App storage storage.Storage connector connector.Connector client mc.Client window fyne.Window textGrid *widget.TextGrid btn *widget.Button fatalError bool loaded chan struct{} } const ( pluginInitSymbol = "Factory" libUIPluginFile = "libui" ) var ( checkConnectionTimeout = time.Second * 5 checkVersionTimeout = time.Minute * 60 ) func NewLoader(s storage.Storage, conn connector.Connector, app fyne.App) (*loader, error) { l := &loader{ app: app, connector: conn, storage: s, textGrid: widget.NewTextGrid(), window: app.NewWindow("Loader"), loaded: make(chan struct{}), } l.btn = widget.NewButton("OK", l.onButtonAction) l.btn.Disable() content := container.NewStack( l.textGrid, container.NewHBox( container.NewCenter( l.btn, ), ), ) l.window.SetContent(content) return l, nil } func (l *loader) initPlugin() (mc.Client, error) { exists, pluginPath, err := l.storage.FileExists(libUIPluginFile) if err != nil { l.appendFatalError(fmt.Errorf("Client plugin file lookup error: %w", err)) return nil, err } if !exists { l.logText("Client plugin file not found, fetching available versions") v, err := l.connector.CheckVersion() if err != nil { l.logError(err) return nil, err } l.logText(fmt.Sprintf("Received %d versions", len(v))) latest, ok, err := latestVersion(v) if err != nil { l.logError(err) return nil, err } if !ok { l.logError(errors.New("Server did not responded with a suitable client version")) return nil, err } l.logText(fmt.Sprintf("Downloading version %s", latest.Version)) data, err := l.connector.DownloadVersion(latest.URL) if err != nil { l.logError(fmt.Errorf("Version %s download error: %w", latest.Version, err)) return nil, err } err = l.storage.WriteFile(libUIPluginFile, data) if err != nil { l.appendFatalError(fmt.Errorf("Plugin file write error: %w", err)) return nil, err } } l.logText(fmt.Sprintf("Loading client plugin from %s", pluginPath)) cli, err := loadClientPlugin(l.storage, l.connector, l.app, pluginPath, pluginInitSymbol) if err != nil { l.appendFatalError(err) return nil, err } return cli, nil } func (l *loader) init() { l.fatalError = false fyne.Do(func() { l.textGrid.SetText("") l.btn.Hide() l.btn.Disable() }) var err error l.client, err = l.initPlugin() if err == nil { fyne.Do(func() { l.window.Hide() err = l.client.Run() }) } if err != nil { fyne.Do(func() { if l.fatalError { l.btn.SetText("Quit") l.logText("Please re-install application.") } else { l.btn.SetText("Retry") } l.btn.Enable() l.btn.Show() l.window.Show() }) } else { l.loaded <- struct{}{} } } func (l *loader) onButtonAction() { if l.fatalError { l.app.Quit() } else { go l.init() } } func (l *loader) logText(v string) { fyne.Do(func() { l.textGrid.Append(v) }) } func (l *loader) logError(err error) { l.logText(fmt.Sprintf("❌ %s", err)) } func (l *loader) appendFatalError(err error) { l.logError(err) l.fatalError = true } func (l *loader) Run(ctx context.Context) error { go l.init() l.app.Run() select { case <-ctx.Done(): return nil case <-l.loaded: return l.runClient(ctx) } } func (l *loader) runClient(ctx context.Context) error { if l.client == nil { return errors.New("run: client wasn't initialized, this is an program fatal error.") } final := make(chan struct{}, 1) go l.backgroundLoop(ctx, final) defer func() { final <- struct{}{} }() if err := l.client.Run(); err != nil { return err } final <- struct{}{} return nil } func (l *loader) backgroundLoop(ctx context.Context, final <-chan struct{}) { checkConnTimer := time.NewTimer(checkConnectionTimeout) checkVersionTimer := time.NewTimer(checkVersionTimeout) defer func() { checkConnTimer.Stop() checkVersionTimer.Stop() }() for { select { case <-ctx.Done(): l.client.Shutdown() return case <-final: return case <-checkConnTimer.C: isGood := l.connector.CheckConnection() l.client.OnConnection(isGood) checkConnTimer.Reset(checkConnectionTimeout) case <-checkVersionTimer.C: versions, err := l.connector.CheckVersion() if err != nil { // propagate error to the UI } else if latest, ok, err := latestVersion(versions); err != nil { // propagate error to the UI } else if ok { l.downloadVersion(latest) } checkVersionTimer.Reset(checkVersionTimeout) } } } // loadClientPlugin loads a Client implementation from a shared plugin file at the specified path. // It calls the constructor function by name, passing the necessary dependencies, and returns the initialized Client. func loadClientPlugin(s storage.UIStorage, conn connector.UIConnector, app fyne.App, path, name string) (mc.Client, error) { if path == "" { return nil, errors.New("no plugin path given") } plug, err := plugin.Open(path) if err != nil { return nil, fmt.Errorf("open plugin %q: %w", path, err) } sym, err := plug.Lookup(name) if err != nil { return nil, fmt.Errorf("lookup symbol %q: %w", name, err) } initializerPtr, ok := sym.(*ClientInit) if !ok { return nil, fmt.Errorf("unexpected type %T; want %T", sym, initializerPtr) } return (*initializerPtr)(s, conn, app) }