245 lines
5.5 KiB
Go
245 lines
5.5 KiB
Go
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)
|
|
}
|