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 } 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, // client: cli, storage: s, textGrid: widget.NewTextGrid(), window: app.NewWindow("Loader"), } 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 } /* Комментарий временный, удалить после реализации. # Порядок инициализации - При ошибках Connector: не фатальное состояние, отображать состояние недоступности сети, предлагать повторить попытку. - При ошибках Storage: фатальное, отобразить рекомендацию переустановить клиента, выход после confirm от пользователя. 1. Проверяем полный путь libUIPluginFile с помощью Storage.FileExists; 2. Если libUIPluginFile не существует в Storage: 2.1. запрашиваем Connector.CheckVersion, 2.2. с помощью хэлпера latestVersion(...) определяем самую свежую версию; 2.3. загружаем полученную версию через Connector.DownloadVersion, 2.4. сохраняем libUIPluginFile в Storage.WriteFile. 3. Загружаем libUIPluginFile full path в loadClientPlugin; */ func (l *loader) initUIPlugin() (mc.Client, error) { // l.appendText(fmt.Sprintf("Checking client plugin at %s", libUIPluginFile)) // exists, err := l.storage.FileExists(libUIPluginFile) // if err != nil { // l.appendFatalError(err) // return nil, err // } // if !exists { // l.appendText("Client plugin not found, checking available versions") // v, err := l.connector.CheckVersion() // if err != nil { // l.appendError(err) // return nil, err // } // l.appendText(fmt.Sprintf("Received %d versions", len(v))) // latest, ok, err := latestVersion(v) // if err != nil { // l.appendError(err) // return nil, err // } // if !ok { // l.appendError(errors.New("Server did not responded with a suitable client version")) // return nil, err // } // l.appendText(fmt.Sprintf("Downloading version %s", latest.Version)) // data, err := l.connector.DownloadVersion(latest.URL) // if err != nil { // l.appendError(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("Write plugin file error: %w", err)) // return nil, err // } // } // l.appendText(fmt.Sprintf("Loading client plugin from %s", libUIPluginFile)) // cli, err := loadClientPlugin(s, conn, app, "./client.so", pluginInitSymbol) // if err != nil { // return nil, err // } return nil, nil } func (l *loader) startLoading() { l.fatalError = false fyne.Do(func() { l.textGrid.SetText("") l.btn.Hide() l.btn.Disable() }) l.appendText("Loading...") var err error l.client, err = l.initUIPlugin() if err != nil { fyne.Do(func() { if l.fatalError { l.btn.SetText("Quit") l.appendText("Please re-install application.") } else { l.btn.SetText("Retry") } l.btn.Enable() l.btn.Show() l.window.Show() }) } } func (l *loader) onButtonAction() { if l.fatalError { l.app.Quit() } else { go l.startLoading() } } func (l *loader) Run(ctx context.Context) error { go l.startLoading() l.app.Run() // final := make(chan struct{}, 1) // if l.connector != nil { // 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) appendText(v string) { fmt.Println(v) fyne.Do(func() { l.textGrid.Append(v) }) } func (l *loader) appendError(err error) { l.appendText(fmt.Sprintf("❌ %s", err)) } func (l *loader) appendFatalError(err error) { l.appendError(err) l.fatalError = true } 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) }