loader logic revised

This commit is contained in:
IliaDenisov
2026-03-16 15:48:00 +02:00
parent cc7ecf6667
commit e6c6970947
13 changed files with 530 additions and 82 deletions
+25
View File
@@ -0,0 +1,25 @@
package loader
// onConnectionError обёртка над событием OnConnectionError для UI
func (l *loader) onConnectionError(err error) {
if err == nil || l.client == nil {
return
}
l.client.OnConnectionError(err)
}
// onStorageError обёртка над событием OnStorageError для UI
func (l *loader) onStorageError(err error) {
if err == nil || l.client == nil {
return
}
l.client.OnStorageError(err)
}
// onServiceError обёртка над событием OnServiceError для UI
func (l *loader) onServiceError(err error) {
if err == nil || l.client == nil {
return
}
l.client.OnServiceError(err)
}
+73 -8
View File
@@ -1,19 +1,84 @@
package loader
import (
"errors"
"fmt"
"galaxy/connector"
"galaxy/util"
)
func (l *loader) newerVersion(version string) bool {
return util.CompareSemver(util.MustParseSemver(l.client.Version()), util.MustParseSemver(version)) > 0
func (l *loader) checkAndDownloadPluginVersion() error {
versions, err := l.connector.CheckVersion()
if err != nil {
return err
} else if latest, ok, err := latestVersion(versions); err != nil {
return err
} else if ok {
return l.preparePluginVersion(latest)
} else {
return errors.New("Server did not respond with a suitable client version")
}
}
// downloadVersion fetches given version artifact, when newer to the current version,
// and stores at the App's local storage with a pre-defined name with semver suffix
func (l *loader) downloadVersion(v connector.VersionInfo) {
if !l.newerVersion(v.Version) {
return
// preparePluginVersion fetches and stores given version of plugin artifact, if newer to the current known version.
func (l *loader) preparePluginVersion(v connector.VersionInfo) error {
s, exists, err := l.loadCurrentState()
if err != nil {
return err
}
l.connector.DownloadVersion(v.URL)
version, err := util.ParseSemver(v.Version)
if err != nil {
return err
}
if exists {
currentVersion, err := util.ParseSemver(s.ClientCurrentVersion)
if err != nil {
return err
}
if util.CompareSemver(currentVersion, version) > 0 {
return nil
}
}
err = l.downloadPlugin(v.URL, v.Version, v.Checksum)
if err != nil {
return err
}
return nil
}
// downloadPlugin downloads and stores at the App's local storage with a pre-defined name with semver suffix.
func (l *loader) downloadPlugin(url, version string, checksum [32]byte) error {
data, err := l.connector.DownloadVersion(url)
if err != nil {
return fmt.Errorf("Plugin v%s download error: %w", version, err)
}
dataChecksum := SumSHA256(data)
if !EqualSHA256(checksum, dataChecksum) {
return errors.New("Downloaded pligin checksum incorrect")
}
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 {
storedData, err := l.storage.ReadFile(versionFileName)
if err != nil {
return err
}
if EqualSHA256(dataChecksum, SumSHA256(storedData)) {
return nil
}
if err := l.storage.DeleteFile(versionFileName); err != nil {
return fmt.Errorf("Plugin v%s delete error: %w", version, err)
}
}
err = l.storage.WriteFile(versionFileName, data)
if err != nil {
return fmt.Errorf("Plugin file write error: %w", err)
}
if err := l.setNextVersion(&version); err != nil {
return err
}
return nil
}
+75 -60
View File
@@ -1,3 +1,11 @@
/*
Пакет loader - основная точка входа в клиенское приложение.
Задачи:
- Загрузка и выполнение плагина, в котором сосредоточена логика основного UI,
- Выполнение операций с локальным Storage,
- Выполнение операций обмена данными с сервером.
*/
package loader
import (
@@ -64,50 +72,78 @@ func NewLoader(s storage.Storage, conn connector.Connector, app fyne.App) (*load
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) {
exists, pluginPath, err := l.storage.FileExists(libUIPluginFile)
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 !exists {
if !pluginExists {
l.logText("Client plugin file not found, fetching available versions")
v, err := l.connector.CheckVersion()
err = l.checkAndDownloadPluginVersion()
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
}
}
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() {
@@ -161,9 +197,15 @@ func (l *loader) appendFatalError(err error) {
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
@@ -172,50 +214,23 @@ func (l *loader) Run(ctx context.Context) error {
}
}
// 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.")
}
final := make(chan struct{}, 1)
go l.backgroundLoop(ctx, final)
defer func() { final <- struct{}{} }()
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
}
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)
}
}
return nil
}
// loadClientPlugin loads a Client implementation from a shared plugin file at the specified path.
+34
View File
@@ -0,0 +1,34 @@
package loader
import (
"context"
"time"
)
// backgroundLoop периодически проверяет доступность соединения и наличие новых версий на сервере
func (l *loader) backgroundLoop(ctx context.Context, stop <-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 <-stop:
return
case <-checkConnTimer.C:
isGood := l.connector.CheckConnection()
l.client.OnConnection(isGood)
checkConnTimer.Reset(checkConnectionTimeout)
case <-checkVersionTimer.C:
if err := l.checkAndDownloadPluginVersion(); err != nil {
l.onConnectionError(err) // TODO: separate error types for: onConnectionError / onStorageError / onServiceError
}
checkVersionTimer.Reset(checkVersionTimeout)
}
}
}
+61
View File
@@ -0,0 +1,61 @@
package loader
import (
"errors"
"fmt"
mc "galaxy/model/client"
)
func (l *loader) loadCurrentState() (*mc.State, bool, error) {
stateExists, err := l.storage.StateExists()
if err != nil {
return nil, false, fmt.Errorf("State file lookup error: %w", err)
}
if !stateExists {
return nil, false, nil
}
if s, err := l.storage.LoadState(); err != nil {
return nil, false, fmt.Errorf("State read error: %w", err)
} else {
return &s, true, nil
}
}
func (l *loader) updateCurrentVersion(v string) error {
state, stateExists, err := l.loadCurrentState()
if err != nil {
return fmt.Errorf("Load State error: %w", err)
}
if !stateExists {
state = &mc.State{}
}
if state.ClientCurrentVersion == v {
return nil
}
state.ClientCurrentVersion = v
if err := l.storage.SaveState(*state); err != nil {
return fmt.Errorf("State write error: %w", err)
}
return nil
}
func (l *loader) setNextVersion(v *string) error {
state, stateExists, err := l.loadCurrentState()
if err != nil {
return fmt.Errorf("Load State error: %w", err)
}
if !stateExists {
return errors.New("State was never saved, unable to set next version")
}
if state.ClientNextVersion != nil && v != nil && *state.ClientNextVersion == *v {
return nil
}
if state.ClientNextVersion == v {
return nil
}
state.ClientNextVersion = v
if err := l.storage.SaveState(*state); err != nil {
return fmt.Errorf("State write error: %w", err)
}
return nil
}
+21 -1
View File
@@ -1,6 +1,7 @@
package loader
import (
"crypto/sha256"
"fmt"
"galaxy/connector"
"galaxy/util"
@@ -12,7 +13,7 @@ func resolvePluginFile(version string) string {
return libUIPluginFile + "-" + version
}
// latestVersion should return VersionInfo with the latest Version for the current OD
// latestVersion should return VersionInfo with the latest Version for the current OS
func latestVersion(versions []connector.VersionInfo) (connector.VersionInfo, bool, error) {
os := runtime.GOOS
versions = slices.DeleteFunc(versions, func(v connector.VersionInfo) bool { return v.OS != os })
@@ -37,3 +38,22 @@ func latestVersion(versions []connector.VersionInfo) (connector.VersionInfo, boo
slices.SortFunc(semvers, func(a, b *v) int { return util.CompareSemver(*b.sv, *a.sv) })
return *semvers[0].vi, true, nil
}
// SumSHA256 calculates SHA-256 for the provided byte slice and returns
// the raw 32-byte digest as a fixed-size array.
//
// The function does not modify the input data.
// The returned value is the binary digest, not a hex string.
// Use digest[:] if a []byte is needed.
func SumSHA256(data []byte) [32]byte {
return sha256.Sum256(data)
}
// EqualSHA256 returns true when both SHA-256 digests are identical.
//
// Since SHA-256 digest is represented as a fixed-size array [32]byte,
// Go allows direct value comparison with ==.
// This is the simplest and fastest approach for ordinary equality checks.
func EqualSHA256(a, b [32]byte) bool {
return a == b
}
+56
View File
@@ -0,0 +1,56 @@
package loader
import (
"crypto/sha256"
"testing"
"github.com/stretchr/testify/require"
)
// TestSumSHA256 verifies that SumSHA256 returns the same digest
// as the standard library implementation for a non-empty payload.
func TestSumSHA256(t *testing.T) {
t.Parallel()
data := []byte("hello world")
expected := sha256.Sum256(data)
actual := SumSHA256(data)
require.Equal(t, expected, actual)
}
// TestSumSHA256Empty verifies that SumSHA256 correctly handles
// an empty byte slice.
func TestSumSHA256Empty(t *testing.T) {
t.Parallel()
data := []byte{}
expected := sha256.Sum256(data)
actual := SumSHA256(data)
require.Equal(t, expected, actual)
}
// TestEqualSHA256Same verifies that two identical digests
// are considered equal.
func TestEqualSHA256Same(t *testing.T) {
t.Parallel()
data := []byte("hello")
digest := sha256.Sum256(data)
require.True(t, EqualSHA256(digest, digest))
}
// TestEqualSHA256Different verifies that different digests
// are considered not equal.
func TestEqualSHA256Different(t *testing.T) {
t.Parallel()
digestA := sha256.Sum256([]byte("hello"))
digestB := sha256.Sum256([]byte("world"))
require.False(t, EqualSHA256(digestA, digestB))
}