Files
galaxy-game/client/loader/loader.go
T
2026-03-15 21:57:42 +02:00

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)
}