package loader import ( "context" "errors" "fmt" "galaxy/connector" mc "galaxy/model/client" "galaxy/storage" "plugin" "time" "fyne.io/fyne/v2" ) type ClientInit func(context.Context, storage.UIStorage, connector.UIConnector, fyne.App) (mc.Client, error) type loader struct { conn connector.Connector cli mc.Client storagePath string } const ( clientLibraryFile = "client" ) var ( checkConnectionTimeout = time.Second * 5 checkVersionTimeout = time.Minute * 60 ) func NewLoader(ctx context.Context, app fyne.App, conn connector.Connector) (*loader, error) { storagePath, err := initStorage(app) if err != nil { return nil, err } var s storage.Storage = nil cli, err := loadClientPlugin(ctx, s, conn, app, "./client.so", "NewClient") if err != nil { return nil, err } l := &loader{ conn: conn, cli: cli, storagePath: storagePath, } return l, nil } func (l *loader) Run(ctx context.Context) error { final := make(chan struct{}, 1) go l.backgroundLoop(ctx, final) if err := l.cli.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.cli.Shutdown() return case <-final: return case <-checkConnTimer.C: isGood := l.conn.CheckConnection() l.cli.OnConnection(isGood) checkConnTimer.Reset(checkConnectionTimeout) case <-checkVersionTimer.C: versions, err := l.conn.CheckVersion() if err != nil { // propagate error to the UI } else if latest, ok := latestVersion(versions); ok { l.downloadVersion(latest) } checkVersionTimer.Reset(checkVersionTimeout) } } } // loadClientPlugin loads a Client implementation from a shared object (.so) file at the specified path. // It calls the constructor function by name, passing the necessary dependencies, and returns the initialized Client. func loadClientPlugin(ctx context.Context, 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)(ctx, s, conn, app) }