/* Пакет loader - основная точка входа в клиенское приложение. Задачи: - Загрузка и выполнение плагина, в котором сосредоточена логика основного UI, - Выполнение операций с локальным Storage, - Выполнение операций обмена данными с сервером. */ 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 } // updatePluginFromVersion заменяет файл {libUIPluginFile} с плагином на новую версию, которая была ранее загружена и сохранена в State func (l *loader) updatePluginFromVersion() error { state, stateExists, err := l.loadCurrentState() if err != nil { return fmt.Errorf("Load State error: %w", err) } if !stateExists { return nil } if state.ClientNextVersion == nil { return nil } version := *state.ClientNextVersion versionFileName := resolvePluginFile(version) versionFileExists, _, err := l.storage.FileExists(versionFileName) if err != nil { return fmt.Errorf("Check plugin v%s exists error: %w", version, err) } if !versionFileExists { return fmt.Errorf("Requested plugin v%s does not exists at local storage", version) } storedData, err := l.storage.ReadFile(versionFileName) if err != nil { return err } if err := l.storage.WriteFile(libUIPluginFile, storedData); err != nil { return fmt.Errorf("Plugin file write error: %w", err) } state.ClientCurrentVersion = version state.ClientNextVersion = nil if err := l.storage.SaveState(*state); err != nil { return fmt.Errorf("State write error: %w", err) } return nil } func (l *loader) initPlugin() (mc.Client, error) { pluginExists, pluginPath, err := l.storage.FileExists(libUIPluginFile) if err != nil { l.appendFatalError(fmt.Errorf("Client plugin file lookup error: %w", err)) return nil, err } if !pluginExists { l.logText("Client plugin file not found, fetching available versions") err = l.checkAndDownloadPluginVersion() if err != nil { l.logError(err) return nil, err } } err = l.updatePluginFromVersion() if err != nil { 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 } // need to store current plugin version for the very first app start if err := l.updateCurrentVersion(cli.Version()); err != nil { return nil, err } return cli, nil } // init инициализирует плагин клиента и запускает его, либо отображает пользователю лог с ошибками 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 } // Run is the main entry point for start Client func (l *loader) Run(ctx context.Context) error { // start initializing process go l.init() // run UI engine, not the main Client app l.app.Run() // wait for successfull 'loaded' signal from init() select { case <-ctx.Done(): return nil case <-l.loaded: return l.runClient(ctx) } } // runClient запусукает основного Client и блокирует выполнение в текущем потоке 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.") } bgSignal := make(chan struct{}, 1) go l.backgroundLoop(ctx, bgSignal) // stop backbround loop before exin func defer func() { bgSignal <- struct{}{} }() // Client's Run() blocks execution until window is closed, or error is returned immediately if err := l.client.Run(); err != nil { return err } return nil } // 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) }