mirror of https://github.com/jetkvm/kvm.git
refactor(ota): improve OTA state management
This commit is contained in:
parent
005505a2da
commit
8d085a6071
|
|
@ -22,9 +22,6 @@ func (s *State) componentUpdateError(prefix string, err error, l *zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus) error {
|
func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus) error {
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
l := s.l.With().Str("path", appUpdatePath).Logger()
|
l := s.l.With().Str("path", appUpdatePath).Logger()
|
||||||
|
|
||||||
if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, "app"); err != nil {
|
if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, "app"); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateReleaseAPIEndpoint updates the release API endpoint
|
// UpdateReleaseAPIEndpoint updates the release API endpoint
|
||||||
|
|
@ -32,26 +31,18 @@ func (s *State) getUpdateURL(params UpdateParams) (string, error, bool) {
|
||||||
|
|
||||||
isCustomVersion := false
|
isCustomVersion := false
|
||||||
|
|
||||||
appTargetVersion := s.GetTargetVersion("app")
|
|
||||||
if appTargetVersion != "" && params.AppTargetVersion == "" {
|
|
||||||
params.AppTargetVersion = appTargetVersion
|
|
||||||
}
|
|
||||||
systemTargetVersion := s.GetTargetVersion("system")
|
|
||||||
if systemTargetVersion != "" && params.SystemTargetVersion == "" {
|
|
||||||
params.SystemTargetVersion = systemTargetVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
query := updateURL.Query()
|
query := updateURL.Query()
|
||||||
query.Set("deviceId", params.DeviceID)
|
query.Set("deviceId", params.DeviceID)
|
||||||
query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease))
|
query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease))
|
||||||
if params.AppTargetVersion != "" {
|
|
||||||
query.Set("appVersion", params.AppTargetVersion)
|
// set the custom versions if they are specified
|
||||||
isCustomVersion = true
|
for component, constraint := range params.Components {
|
||||||
}
|
if constraint != "" {
|
||||||
if params.SystemTargetVersion != "" {
|
query.Set(component+"Version", constraint)
|
||||||
query.Set("systemVersion", params.SystemTargetVersion)
|
}
|
||||||
isCustomVersion = true
|
isCustomVersion = true
|
||||||
}
|
}
|
||||||
|
|
||||||
updateURL.RawQuery = query.Encode()
|
updateURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
return updateURL.String(), nil, isCustomVersion
|
return updateURL.String(), nil, isCustomVersion
|
||||||
|
|
@ -98,10 +89,6 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) TryUpdate(ctx context.Context, params UpdateParams) error {
|
|
||||||
return s.doUpdate(ctx, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) triggerStateUpdate() {
|
func (s *State) triggerStateUpdate() {
|
||||||
s.onStateUpdate(s.ToRPCState())
|
s.onStateUpdate(s.ToRPCState())
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +98,22 @@ func (s *State) triggerComponentUpdateState(component string, update *componentU
|
||||||
s.triggerStateUpdate()
|
s.triggerStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TryUpdate tries to update the given components
|
||||||
|
// if the update is already in progress, it returns an error
|
||||||
|
func (s *State) TryUpdate(ctx context.Context, params UpdateParams) error {
|
||||||
|
locked := s.mu.TryLock()
|
||||||
|
if !locked {
|
||||||
|
return fmt.Errorf("update already in progress")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.doUpdate(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// before calling doUpdate, the caller must have locked the mutex
|
||||||
|
// otherwise a runtime error will occur
|
||||||
func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
scopedLogger := s.l.With().
|
scopedLogger := s.l.With().
|
||||||
Interface("params", params).
|
Interface("params", params).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
@ -122,10 +124,11 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(params.Components) == 0 {
|
if len(params.Components) == 0 {
|
||||||
params.Components = []string{"app", "system"}
|
params.Components = defaultComponents
|
||||||
}
|
}
|
||||||
shouldUpdateApp := slices.Contains(params.Components, "app")
|
|
||||||
shouldUpdateSystem := slices.Contains(params.Components, "system")
|
_, shouldUpdateApp := params.Components["app"]
|
||||||
|
_, shouldUpdateSystem := params.Components["system"]
|
||||||
|
|
||||||
if !shouldUpdateApp && !shouldUpdateSystem {
|
if !shouldUpdateApp && !shouldUpdateSystem {
|
||||||
return fmt.Errorf("no components to update")
|
return fmt.Errorf("no components to update")
|
||||||
|
|
@ -211,13 +214,11 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
||||||
|
|
||||||
// UpdateParams represents the parameters for the update
|
// UpdateParams represents the parameters for the update
|
||||||
type UpdateParams struct {
|
type UpdateParams struct {
|
||||||
DeviceID string `json:"deviceID"`
|
DeviceID string `json:"deviceID"`
|
||||||
AppTargetVersion string `json:"appTargetVersion"`
|
Components map[string]string `json:"components,omitempty"`
|
||||||
SystemTargetVersion string `json:"systemTargetVersion"`
|
IncludePreRelease bool `json:"includePreRelease"`
|
||||||
Components []string `json:"components,omitempty"`
|
CheckOnly bool `json:"checkOnly"`
|
||||||
IncludePreRelease bool `json:"includePreRelease"`
|
ResetConfig bool `json:"resetConfig"`
|
||||||
CheckOnly bool `json:"checkOnly"`
|
|
||||||
ResetConfig bool `json:"resetConfig"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUpdateStatus gets the update status for the given components
|
// getUpdateStatus gets the update status for the given components
|
||||||
|
|
@ -259,7 +260,7 @@ func (s *State) checkUpdateStatus(
|
||||||
appUpdateStatus *componentUpdateStatus,
|
appUpdateStatus *componentUpdateStatus,
|
||||||
systemUpdateStatus *componentUpdateStatus,
|
systemUpdateStatus *componentUpdateStatus,
|
||||||
) error {
|
) error {
|
||||||
// Get local versions
|
// get the local versions
|
||||||
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
|
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting local version: %w", err)
|
return fmt.Errorf("error getting local version: %w", err)
|
||||||
|
|
@ -267,7 +268,12 @@ func (s *State) checkUpdateStatus(
|
||||||
appUpdateStatus.localVersion = appVersionLocal.String()
|
appUpdateStatus.localVersion = appVersionLocal.String()
|
||||||
systemUpdateStatus.localVersion = systemVersionLocal.String()
|
systemUpdateStatus.localVersion = systemVersionLocal.String()
|
||||||
|
|
||||||
// Get remote metadata
|
s.l.Trace().
|
||||||
|
Str("appVersionLocal", appVersionLocal.String()).
|
||||||
|
Str("systemVersionLocal", systemVersionLocal.String()).
|
||||||
|
Msg("checkUpdateStatus: getLocalVersion")
|
||||||
|
|
||||||
|
// fetch the remote metadata
|
||||||
remoteMetadata, err := s.fetchUpdateMetadata(ctx, params)
|
remoteMetadata, err := s.fetchUpdateMetadata(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == ErrVersionNotFound || errors.Unwrap(err) == ErrVersionNotFound {
|
if err == ErrVersionNotFound || errors.Unwrap(err) == ErrVersionNotFound {
|
||||||
|
|
@ -277,61 +283,33 @@ func (s *State) checkUpdateStatus(
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appUpdateStatus.url = remoteMetadata.AppURL
|
|
||||||
appUpdateStatus.hash = remoteMetadata.AppHash
|
|
||||||
appUpdateStatus.version = remoteMetadata.AppVersion
|
|
||||||
|
|
||||||
systemUpdateStatus.url = remoteMetadata.SystemURL
|
s.l.Trace().
|
||||||
systemUpdateStatus.hash = remoteMetadata.SystemHash
|
Interface("remoteMetadata", remoteMetadata).
|
||||||
systemUpdateStatus.version = remoteMetadata.SystemVersion
|
Msg("checkUpdateStatus: fetchUpdateMetadata")
|
||||||
|
|
||||||
// Get remote versions
|
// parse the remote metadata to the componentUpdateStatuses
|
||||||
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
if err := remoteMetadataToComponentStatus(
|
||||||
if err != nil {
|
remoteMetadata,
|
||||||
err = fmt.Errorf("error parsing remote system version: %w", err)
|
"app",
|
||||||
return err
|
appUpdateStatus,
|
||||||
}
|
params,
|
||||||
systemUpdateStatus.available = systemVersionRemote.GreaterThan(systemVersionLocal)
|
); err != nil {
|
||||||
|
return fmt.Errorf("error parsing remote app version: %w", err)
|
||||||
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
appUpdateStatus.available = appVersionRemote.GreaterThan(appVersionLocal)
|
|
||||||
|
|
||||||
// Handle pre-release updates
|
|
||||||
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
|
||||||
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
|
||||||
|
|
||||||
if isRemoteSystemPreRelease && !params.IncludePreRelease {
|
|
||||||
systemUpdateStatus.available = false
|
|
||||||
}
|
|
||||||
if isRemoteAppPreRelease && !params.IncludePreRelease {
|
|
||||||
appUpdateStatus.available = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
components := params.Components
|
if err := remoteMetadataToComponentStatus(
|
||||||
// skip check if no components are specified
|
remoteMetadata,
|
||||||
if len(components) == 0 {
|
"system",
|
||||||
return nil
|
systemUpdateStatus,
|
||||||
|
params,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("error parsing remote system version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: simplify this
|
if s.l.GetLevel() <= zerolog.TraceLevel {
|
||||||
if slices.Contains(components, "app") {
|
appUpdateStatus.getZerologLogger(s.l).Trace().Msg("checkUpdateStatus: remoteMetadataToComponentStatus [app]")
|
||||||
if params.AppTargetVersion != "" {
|
systemUpdateStatus.getZerologLogger(s.l).Trace().Msg("checkUpdateStatus: remoteMetadataToComponentStatus [system]")
|
||||||
appUpdateStatus.available = appVersionRemote.String() != appVersionLocal.String()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appUpdateStatus.available = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(components, "system") {
|
|
||||||
if params.SystemTargetVersion != "" {
|
|
||||||
systemUpdateStatus.available = systemVersionRemote.String() != systemVersionLocal.String()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
systemUpdateStatus.available = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,9 @@ func newOtaState() *State {
|
||||||
func TestCheckUpdateComponents(t *testing.T) {
|
func TestCheckUpdateComponents(t *testing.T) {
|
||||||
otaState := newOtaState()
|
otaState := newOtaState()
|
||||||
updateParams := UpdateParams{
|
updateParams := UpdateParams{
|
||||||
DeviceID: "test",
|
DeviceID: "test",
|
||||||
IncludePreRelease: false,
|
IncludePreRelease: false,
|
||||||
SystemTargetVersion: "0.2.2",
|
Components: map[string]string{"system": "0.2.2"},
|
||||||
Components: []string{"system"},
|
|
||||||
}
|
}
|
||||||
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
|
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
|
||||||
t.Logf("update status: %+v", info)
|
t.Logf("update status: %+v", info)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
package ota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// to make the field names consistent with the RPCState struct
|
||||||
|
var componentFieldMap = map[string]string{
|
||||||
|
"app": "App",
|
||||||
|
"system": "System",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCState represents the current OTA state for the RPC API
|
||||||
|
type RPCState struct {
|
||||||
|
Updating bool `json:"updating"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
MetadataFetchedAt *time.Time `json:"metadataFetchedAt,omitempty"`
|
||||||
|
AppUpdatePending bool `json:"appUpdatePending"`
|
||||||
|
SystemUpdatePending bool `json:"systemUpdatePending"`
|
||||||
|
AppDownloadProgress *float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar
|
||||||
|
AppDownloadFinishedAt *time.Time `json:"appDownloadFinishedAt,omitempty"`
|
||||||
|
SystemDownloadProgress *float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar
|
||||||
|
SystemDownloadFinishedAt *time.Time `json:"systemDownloadFinishedAt,omitempty"`
|
||||||
|
AppVerificationProgress *float32 `json:"appVerificationProgress,omitempty"`
|
||||||
|
AppVerifiedAt *time.Time `json:"appVerifiedAt,omitempty"`
|
||||||
|
SystemVerificationProgress *float32 `json:"systemVerificationProgress,omitempty"`
|
||||||
|
SystemVerifiedAt *time.Time `json:"systemVerifiedAt,omitempty"`
|
||||||
|
AppUpdateProgress *float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar
|
||||||
|
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
||||||
|
SystemUpdateProgress *float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
||||||
|
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyComponentStatusToRPCState uses reflection to map componentUpdateStatus fields to RPCState
|
||||||
|
func applyComponentStatusToRPCState(component string, status componentUpdateStatus, rpcState *RPCState) {
|
||||||
|
prefix := componentFieldMap[component]
|
||||||
|
if prefix == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcVal := reflect.ValueOf(rpcState).Elem()
|
||||||
|
|
||||||
|
// it's really inefficient, but hey we do not need to use this often
|
||||||
|
// componentUpdateStatus is for internal use only, and all fields are unexported
|
||||||
|
for i := 0; i < rpcVal.NumField(); i++ {
|
||||||
|
rpcFieldName, hasPrefix := strings.CutPrefix(rpcVal.Type().Field(i).Name, prefix)
|
||||||
|
if !hasPrefix {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rpcFieldName {
|
||||||
|
case "DownloadProgress":
|
||||||
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.downloadProgress))
|
||||||
|
case "DownloadFinishedAt":
|
||||||
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.downloadFinishedAt))
|
||||||
|
case "VerificationProgress":
|
||||||
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.verificationProgress))
|
||||||
|
case "VerifiedAt":
|
||||||
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.verifiedAt))
|
||||||
|
case "UpdateProgress":
|
||||||
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.updateProgress))
|
||||||
|
case "UpdatedAt":
|
||||||
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.updatedAt))
|
||||||
|
case "UpdatePending":
|
||||||
|
rpcVal.Field(i).SetBool(status.pending)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRPCState converts the State to the RPCState
|
||||||
|
func (s *State) ToRPCState() *RPCState {
|
||||||
|
r := &RPCState{
|
||||||
|
Updating: s.updating,
|
||||||
|
Error: s.error,
|
||||||
|
MetadataFetchedAt: &s.metadataFetchedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
for component, status := range s.componentUpdateStatuses {
|
||||||
|
applyComponentStatusToRPCState(component, status, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func remoteMetadataToComponentStatus(
|
||||||
|
remoteMetadata *UpdateMetadata,
|
||||||
|
component string,
|
||||||
|
componentStatus *componentUpdateStatus,
|
||||||
|
params UpdateParams,
|
||||||
|
) error {
|
||||||
|
prefix := componentFieldMap[component]
|
||||||
|
if prefix == "" {
|
||||||
|
return fmt.Errorf("unknown component: %s", component)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteMetadataVal := reflect.ValueOf(remoteMetadata).Elem()
|
||||||
|
for i := 0; i < remoteMetadataVal.NumField(); i++ {
|
||||||
|
fieldName, hasPrefix := strings.CutPrefix(remoteMetadataVal.Type().Field(i).Name, prefix)
|
||||||
|
if !hasPrefix {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fieldName {
|
||||||
|
case "URL":
|
||||||
|
componentStatus.url = remoteMetadataVal.Field(i).String()
|
||||||
|
case "Hash":
|
||||||
|
componentStatus.hash = remoteMetadataVal.Field(i).String()
|
||||||
|
case "Version":
|
||||||
|
componentStatus.version = remoteMetadataVal.Field(i).String()
|
||||||
|
default:
|
||||||
|
// fmt.Printf("unknown field %s", fieldName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVersion, err := semver.NewVersion(componentStatus.localVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing local version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteVersion, err := semver.NewVersion(componentStatus.version)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing remote version: %w", err)
|
||||||
|
}
|
||||||
|
componentStatus.available = remoteVersion.GreaterThan(localVersion)
|
||||||
|
componentStatus.availableReason = fmt.Sprintf("remote version %s is greater than local version %s", remoteVersion.String(), localVersion.String())
|
||||||
|
|
||||||
|
// Handle pre-release updates
|
||||||
|
if remoteVersion.Prerelease() != "" && params.IncludePreRelease && componentStatus.available {
|
||||||
|
componentStatus.availableReason += " (pre-release)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a custom version is specified, use it to determine if the update is available
|
||||||
|
constraint, componentExists := params.Components[component]
|
||||||
|
// we don't need to check again if it's already available
|
||||||
|
if componentExists && constraint != "" {
|
||||||
|
componentStatus.available = componentStatus.version != componentStatus.localVersion
|
||||||
|
if componentStatus.available {
|
||||||
|
componentStatus.availableReason = fmt.Sprintf("custom version %s is not equal to local version %s", constraint, componentStatus.localVersion)
|
||||||
|
}
|
||||||
|
} else if !componentExists {
|
||||||
|
componentStatus.available = false
|
||||||
|
componentStatus.availableReason = "component not specified in update parameters"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// appUpdateStatus.url = remoteMetadata.AppURL
|
||||||
|
// appUpdateStatus.hash = remoteMetadata.AppHash
|
||||||
|
// appUpdateStatus.version = remoteMetadata.AppVersion
|
||||||
|
|
||||||
|
// systemUpdateStatus.url = remoteMetadata.SystemURL
|
||||||
|
// systemUpdateStatus.hash = remoteMetadata.SystemHash
|
||||||
|
// systemUpdateStatus.version = remoteMetadata.SystemVersion
|
||||||
|
|
||||||
|
// // Get remote versions
|
||||||
|
// systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// err = fmt.Errorf("error parsing remote system version: %w", err)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// systemUpdateStatus.available = systemVersionRemote.GreaterThan(systemVersionLocal)
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package ota
|
package ota
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -10,6 +9,14 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
availableComponents = []string{"app", "system"}
|
||||||
|
defaultComponents = map[string]string{
|
||||||
|
"app": "",
|
||||||
|
"system": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// UpdateMetadata represents the metadata of an update
|
// UpdateMetadata represents the metadata of an update
|
||||||
type UpdateMetadata struct {
|
type UpdateMetadata struct {
|
||||||
AppVersion string `json:"appVersion"`
|
AppVersion string `json:"appVersion"`
|
||||||
|
|
@ -48,9 +55,9 @@ type PostRebootAction struct {
|
||||||
type componentUpdateStatus struct {
|
type componentUpdateStatus struct {
|
||||||
pending bool
|
pending bool
|
||||||
available bool
|
available bool
|
||||||
|
availableReason string // why the component is available or not available
|
||||||
version string
|
version string
|
||||||
localVersion string
|
localVersion string
|
||||||
targetVersion string
|
|
||||||
url string
|
url string
|
||||||
hash string
|
hash string
|
||||||
downloadProgress float32
|
downloadProgress float32
|
||||||
|
|
@ -59,30 +66,27 @@ type componentUpdateStatus struct {
|
||||||
verifiedAt time.Time
|
verifiedAt time.Time
|
||||||
updateProgress float32
|
updateProgress float32
|
||||||
updatedAt time.Time
|
updatedAt time.Time
|
||||||
dependsOn []string //nolint:unused
|
dependsOn []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPCState represents the current OTA state for the RPC API
|
func (c *componentUpdateStatus) getZerologLogger(l *zerolog.Logger) *zerolog.Logger {
|
||||||
type RPCState struct {
|
logger := l.With().
|
||||||
Updating bool `json:"updating"`
|
Bool("pending", c.pending).
|
||||||
Error string `json:"error,omitempty"`
|
Bool("available", c.available).
|
||||||
MetadataFetchedAt *time.Time `json:"metadataFetchedAt,omitempty"`
|
Str("availableReason", c.availableReason).
|
||||||
AppUpdatePending bool `json:"appUpdatePending"`
|
Str("version", c.version).
|
||||||
SystemUpdatePending bool `json:"systemUpdatePending"`
|
Str("localVersion", c.localVersion).
|
||||||
AppDownloadProgress *float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar
|
Str("url", c.url).
|
||||||
AppDownloadFinishedAt *time.Time `json:"appDownloadFinishedAt,omitempty"`
|
Str("hash", c.hash).
|
||||||
SystemDownloadProgress *float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar
|
Float32("downloadProgress", c.downloadProgress).
|
||||||
SystemDownloadFinishedAt *time.Time `json:"systemDownloadFinishedAt,omitempty"`
|
Time("downloadFinishedAt", c.downloadFinishedAt).
|
||||||
AppVerificationProgress *float32 `json:"appVerificationProgress,omitempty"`
|
Float32("verificationProgress", c.verificationProgress).
|
||||||
AppVerifiedAt *time.Time `json:"appVerifiedAt,omitempty"`
|
Time("verifiedAt", c.verifiedAt).
|
||||||
SystemVerificationProgress *float32 `json:"systemVerificationProgress,omitempty"`
|
Float32("updateProgress", c.updateProgress).
|
||||||
SystemVerifiedAt *time.Time `json:"systemVerifiedAt,omitempty"`
|
Time("updatedAt", c.updatedAt).
|
||||||
AppUpdateProgress *float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar
|
Strs("dependsOn", c.dependsOn).
|
||||||
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
Logger()
|
||||||
SystemUpdateProgress *float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
return &logger
|
||||||
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
|
||||||
SystemTargetVersion *string `json:"systemTargetVersion,omitempty"`
|
|
||||||
AppTargetVersion *string `json:"appTargetVersion,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HwRebootFunc is a function that reboots the hardware
|
// HwRebootFunc is a function that reboots the hardware
|
||||||
|
|
@ -120,39 +124,6 @@ type State struct {
|
||||||
resetConfig ResetConfigFunc
|
resetConfig ResetConfigFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTargetVersion sets the target version for a component
|
|
||||||
func (s *State) SetTargetVersion(component string, version string) error {
|
|
||||||
parsedVersion := version
|
|
||||||
if version != "" {
|
|
||||||
// validate if it's a valid semver string first
|
|
||||||
semverVersion, err := semver.NewVersion(version)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("not a valid semantic version: %w", err)
|
|
||||||
}
|
|
||||||
parsedVersion = semverVersion.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the component exists
|
|
||||||
componentUpdate, ok := s.componentUpdateStatuses[component]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("component %s not found", component)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentUpdate.targetVersion = parsedVersion
|
|
||||||
s.componentUpdateStatuses[component] = componentUpdate
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTargetVersion returns the target version for a component
|
|
||||||
func (s *State) GetTargetVersion(component string) string {
|
|
||||||
componentUpdate, ok := s.componentUpdateStatuses[component]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return componentUpdate.targetVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpdateStatus, error string) *UpdateStatus {
|
func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpdateStatus, error string) *UpdateStatus {
|
||||||
return &UpdateStatus{
|
return &UpdateStatus{
|
||||||
Local: &LocalMetadata{
|
Local: &LocalMetadata{
|
||||||
|
|
@ -209,8 +180,9 @@ type Options struct {
|
||||||
// NewState creates a new OTA state
|
// NewState creates a new OTA state
|
||||||
func NewState(opts Options) *State {
|
func NewState(opts Options) *State {
|
||||||
components := make(map[string]componentUpdateStatus)
|
components := make(map[string]componentUpdateStatus)
|
||||||
components["app"] = componentUpdateStatus{}
|
for _, component := range availableComponents {
|
||||||
components["system"] = componentUpdateStatus{}
|
components[component] = componentUpdateStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
s := &State{
|
s := &State{
|
||||||
l: opts.Logger,
|
l: opts.Logger,
|
||||||
|
|
@ -228,50 +200,20 @@ func NewState(opts Options) *State {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRPCState converts the State to the RPCState
|
// appUpdateStatus.url = remoteMetadata.AppURL
|
||||||
// probably we need a generator for this ...
|
// appUpdateStatus.hash = remoteMetadata.AppHash
|
||||||
func (s *State) ToRPCState() *RPCState {
|
// appUpdateStatus.version = remoteMetadata.AppVersion
|
||||||
r := &RPCState{
|
|
||||||
Updating: s.updating,
|
|
||||||
Error: s.error,
|
|
||||||
MetadataFetchedAt: &s.metadataFetchedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
app, ok := s.componentUpdateStatuses["app"]
|
// systemUpdateStatus.url = remoteMetadata.SystemURL
|
||||||
if ok {
|
// systemUpdateStatus.hash = remoteMetadata.SystemHash
|
||||||
r.AppUpdatePending = app.pending
|
// systemUpdateStatus.version = remoteMetadata.SystemVersion
|
||||||
r.AppDownloadProgress = &app.downloadProgress
|
|
||||||
if !app.downloadFinishedAt.IsZero() {
|
|
||||||
r.AppDownloadFinishedAt = &app.downloadFinishedAt
|
|
||||||
}
|
|
||||||
r.AppVerificationProgress = &app.verificationProgress
|
|
||||||
if !app.verifiedAt.IsZero() {
|
|
||||||
r.AppVerifiedAt = &app.verifiedAt
|
|
||||||
}
|
|
||||||
r.AppUpdateProgress = &app.updateProgress
|
|
||||||
if !app.updatedAt.IsZero() {
|
|
||||||
r.AppUpdatedAt = &app.updatedAt
|
|
||||||
}
|
|
||||||
r.AppTargetVersion = &app.targetVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
system, ok := s.componentUpdateStatuses["system"]
|
// // Get remote versions
|
||||||
if ok {
|
// systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
||||||
r.SystemUpdatePending = system.pending
|
//
|
||||||
r.SystemDownloadProgress = &system.downloadProgress
|
// if err != nil {
|
||||||
if !system.downloadFinishedAt.IsZero() {
|
// err = fmt.Errorf("error parsing remote system version: %w", err)
|
||||||
r.SystemDownloadFinishedAt = &system.downloadFinishedAt
|
// return err
|
||||||
}
|
// }
|
||||||
r.SystemVerificationProgress = &system.verificationProgress
|
//
|
||||||
if !system.verifiedAt.IsZero() {
|
// systemUpdateStatus.available = systemVersionRemote.GreaterThan(systemVersionLocal)
|
||||||
r.SystemVerifiedAt = &system.verifiedAt
|
|
||||||
}
|
|
||||||
r.SystemUpdateProgress = &system.updateProgress
|
|
||||||
if !system.updatedAt.IsZero() {
|
|
||||||
r.SystemUpdatedAt = &system.updatedAt
|
|
||||||
}
|
|
||||||
r.SystemTargetVersion = &system.targetVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
|
||||||
25
ota.go
25
ota.go
|
|
@ -135,26 +135,21 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateParams struct {
|
type updateParams struct {
|
||||||
AppTargetVersion string `json:"appTargetVersion"`
|
Components map[string]string `json:"components,omitempty"`
|
||||||
SystemTargetVersion string `json:"systemTargetVersion"`
|
|
||||||
Components []string `json:"components,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcTryUpdate() error {
|
func rpcTryUpdate() error {
|
||||||
return rpcTryUpdateComponents(updateParams{
|
return rpcTryUpdateComponents(updateParams{
|
||||||
AppTargetVersion: "",
|
Components: make(map[string]string),
|
||||||
SystemTargetVersion: "",
|
|
||||||
}, config.IncludePreRelease, false)
|
}, config.IncludePreRelease, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rpcCheckUpdateComponents checks the update status for the given components
|
// rpcCheckUpdateComponents checks the update status for the given components
|
||||||
func rpcCheckUpdateComponents(params updateParams, includePreRelease bool) (*ota.UpdateStatus, error) {
|
func rpcCheckUpdateComponents(params updateParams, includePreRelease bool) (*ota.UpdateStatus, error) {
|
||||||
updateParams := ota.UpdateParams{
|
updateParams := ota.UpdateParams{
|
||||||
DeviceID: GetDeviceID(),
|
DeviceID: GetDeviceID(),
|
||||||
IncludePreRelease: includePreRelease,
|
IncludePreRelease: includePreRelease,
|
||||||
AppTargetVersion: params.AppTargetVersion,
|
Components: params.Components,
|
||||||
SystemTargetVersion: params.SystemTargetVersion,
|
|
||||||
Components: params.Components,
|
|
||||||
}
|
}
|
||||||
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
|
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -171,16 +166,6 @@ func rpcTryUpdateComponents(params updateParams, includePreRelease bool, resetCo
|
||||||
Components: params.Components,
|
Components: params.Components,
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams.AppTargetVersion = params.AppTargetVersion
|
|
||||||
if err := otaState.SetTargetVersion("app", params.AppTargetVersion); err != nil {
|
|
||||||
return fmt.Errorf("failed to set app target version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParams.SystemTargetVersion = params.SystemTargetVersion
|
|
||||||
if err := otaState.SetTargetVersion("system", params.SystemTargetVersion); err != nil {
|
|
||||||
return fmt.Errorf("failed to set system target version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := otaState.TryUpdate(context.Background(), updateParams)
|
err := otaState.TryUpdate(context.Background(), updateParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { isOnDevice } from "@/main";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
import { sleep } from "@/utils";
|
import { sleep } from "@/utils";
|
||||||
import { checkUpdateComponents } from "@/utils/jsonrpc";
|
import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc";
|
||||||
import { SystemVersionInfo } from "@hooks/useVersion";
|
import { SystemVersionInfo } from "@hooks/useVersion";
|
||||||
|
|
||||||
export default function SettingsAdvancedRoute() {
|
export default function SettingsAdvancedRoute() {
|
||||||
|
|
@ -196,16 +196,17 @@ export default function SettingsAdvancedRoute() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleVersionUpdate = useCallback(async () => {
|
const handleVersionUpdate = useCallback(async () => {
|
||||||
const components = updateTarget === "both" ? ["app", "system"] : [updateTarget];
|
const components: UpdateComponents = {};
|
||||||
|
if (["app", "both"].includes(updateTarget)) components.app = appVersion;
|
||||||
|
if (["system", "both"].includes(updateTarget)) components.system = systemVersion;
|
||||||
let versionInfo: SystemVersionInfo | undefined;
|
let versionInfo: SystemVersionInfo | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// we do not need to set it to false if check succeeds,
|
// we do not need to set it to false if check succeeds,
|
||||||
// because it will be redirected to the update page later
|
// because it will be redirected to the update page later
|
||||||
setVersionUpdateLoading(true);
|
setVersionUpdateLoading(true);
|
||||||
versionInfo = await checkUpdateComponents({
|
versionInfo = await checkUpdateComponents({
|
||||||
components,
|
components,
|
||||||
appTargetVersion: appVersion,
|
|
||||||
systemTargetVersion: systemVersion,
|
|
||||||
}, devChannel);
|
}, devChannel);
|
||||||
console.log("versionInfo", versionInfo);
|
console.log("versionInfo", versionInfo);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|
@ -214,21 +215,14 @@ export default function SettingsAdvancedRoute() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("versionInfo", versionInfo, components.includes("app") && versionInfo.remote?.appVersion && versionInfo?.appUpdateAvailable, components.includes("system") && versionInfo.remote?.systemVersion && versionInfo?.systemUpdateAvailable);
|
|
||||||
console.debug("components", components);
|
|
||||||
console.debug("versionInfo.remote?.appVersion", versionInfo.remote?.appVersion);
|
|
||||||
console.debug("versionInfo.appUpdateAvailable", versionInfo?.appUpdateAvailable);
|
|
||||||
console.debug("versionInfo.remote?.systemVersion", versionInfo.remote?.systemVersion);
|
|
||||||
console.debug("versionInfo.systemUpdateAvailable", versionInfo?.systemUpdateAvailable);
|
|
||||||
|
|
||||||
let hasUpdate = false;
|
let hasUpdate = false;
|
||||||
|
|
||||||
const pageParams = new URLSearchParams();
|
const pageParams = new URLSearchParams();
|
||||||
if (components.includes("app") && versionInfo.remote?.appVersion && versionInfo.appUpdateAvailable) {
|
if (components.app && versionInfo?.remote?.appVersion && versionInfo?.appUpdateAvailable) {
|
||||||
hasUpdate = true;
|
hasUpdate = true;
|
||||||
pageParams.set("custom_app_version", versionInfo.remote?.appVersion);
|
pageParams.set("custom_app_version", versionInfo.remote?.appVersion);
|
||||||
}
|
}
|
||||||
if (components.includes("system") && versionInfo.remote?.systemVersion && versionInfo.systemUpdateAvailable) {
|
if (components.system && versionInfo?.remote?.systemVersion && versionInfo?.systemUpdateAvailable) {
|
||||||
hasUpdate = true;
|
hasUpdate = true;
|
||||||
pageParams.set("custom_system_version", versionInfo.remote?.systemVersion);
|
pageParams.set("custom_system_version", versionInfo.remote?.systemVersion);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard";
|
import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
import { sleep } from "@/utils";
|
import { sleep } from "@/utils";
|
||||||
import { checkUpdateComponents, SystemVersionInfo, updateParams } from "@/utils/jsonrpc";
|
import { checkUpdateComponents, SystemVersionInfo, UpdateComponents, updateParams } from "@/utils/jsonrpc";
|
||||||
|
|
||||||
export default function SettingsGeneralUpdateRoute() {
|
export default function SettingsGeneralUpdateRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -43,15 +43,13 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
}, [send, setModalView, setShouldReload]);
|
}, [send, setModalView, setShouldReload]);
|
||||||
|
|
||||||
const onConfirmCustomUpdate = useCallback((appTargetVersion?: string, systemTargetVersion?: string) => {
|
const onConfirmCustomUpdate = useCallback((appTargetVersion?: string, systemTargetVersion?: string) => {
|
||||||
const components = [];
|
const components: UpdateComponents = {};
|
||||||
if (appTargetVersion) components.push("app");
|
if (appTargetVersion) components.app = appTargetVersion;
|
||||||
if (systemTargetVersion) components.push("system");
|
if (systemTargetVersion) components.system = systemTargetVersion;
|
||||||
|
|
||||||
send("tryUpdateComponents", {
|
send("tryUpdateComponents", {
|
||||||
params: {
|
params: {
|
||||||
components,
|
components,
|
||||||
appTargetVersion,
|
|
||||||
systemTargetVersion,
|
|
||||||
},
|
},
|
||||||
includePreRelease: false,
|
includePreRelease: false,
|
||||||
resetConfig,
|
resetConfig,
|
||||||
|
|
@ -195,13 +193,9 @@ function LoadingState({
|
||||||
if (!customAppVersion && !customSystemVersion) {
|
if (!customAppVersion && !customSystemVersion) {
|
||||||
return await getVersionInfo();
|
return await getVersionInfo();
|
||||||
}
|
}
|
||||||
const params : updateParams = {
|
const params: updateParams = { components: {} as UpdateComponents };
|
||||||
components: [],
|
if (customAppVersion) params.components!.app = customAppVersion;
|
||||||
appTargetVersion: customAppVersion,
|
if (customSystemVersion) params.components!.system = customSystemVersion;
|
||||||
systemTargetVersion: customSystemVersion,
|
|
||||||
};
|
|
||||||
if (customAppVersion) params.components?.push("app");
|
|
||||||
if (customSystemVersion) params.components?.push("system");
|
|
||||||
|
|
||||||
return await checkUpdateComponents(params, false);
|
return await checkUpdateComponents(params, false);
|
||||||
}, [customAppVersion, customSystemVersion, getVersionInfo]);
|
}, [customAppVersion, customSystemVersion, getVersionInfo]);
|
||||||
|
|
|
||||||
|
|
@ -243,10 +243,11 @@ export async function getLocalVersion() {
|
||||||
return response.result;
|
return response.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateComponent = "app" | "system";
|
||||||
|
export type UpdateComponents = Partial<Record<UpdateComponent, string>>;
|
||||||
|
|
||||||
export interface updateParams {
|
export interface updateParams {
|
||||||
appTargetVersion?: string;
|
components?: UpdateComponents;
|
||||||
systemTargetVersion?: string;
|
|
||||||
components?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUpdateComponents(params: updateParams, includePreRelease: boolean) {
|
export async function checkUpdateComponents(params: updateParams, includePreRelease: boolean) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue