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 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
l := s.l.With().Str("path", appUpdatePath).Logger()
|
||||
|
||||
if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, "app"); err != nil {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// UpdateReleaseAPIEndpoint updates the release API endpoint
|
||||
|
|
@ -32,26 +31,18 @@ func (s *State) getUpdateURL(params UpdateParams) (string, error, bool) {
|
|||
|
||||
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.Set("deviceId", params.DeviceID)
|
||||
query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease))
|
||||
if params.AppTargetVersion != "" {
|
||||
query.Set("appVersion", params.AppTargetVersion)
|
||||
isCustomVersion = true
|
||||
}
|
||||
if params.SystemTargetVersion != "" {
|
||||
query.Set("systemVersion", params.SystemTargetVersion)
|
||||
|
||||
// set the custom versions if they are specified
|
||||
for component, constraint := range params.Components {
|
||||
if constraint != "" {
|
||||
query.Set(component+"Version", constraint)
|
||||
}
|
||||
isCustomVersion = true
|
||||
}
|
||||
|
||||
updateURL.RawQuery = query.Encode()
|
||||
|
||||
return updateURL.String(), nil, isCustomVersion
|
||||
|
|
@ -98,10 +89,6 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*
|
|||
return metadata, nil
|
||||
}
|
||||
|
||||
func (s *State) TryUpdate(ctx context.Context, params UpdateParams) error {
|
||||
return s.doUpdate(ctx, params)
|
||||
}
|
||||
|
||||
func (s *State) triggerStateUpdate() {
|
||||
s.onStateUpdate(s.ToRPCState())
|
||||
}
|
||||
|
|
@ -111,7 +98,22 @@ func (s *State) triggerComponentUpdateState(component string, update *componentU
|
|||
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 {
|
||||
defer s.mu.Unlock()
|
||||
|
||||
scopedLogger := s.l.With().
|
||||
Interface("params", params).
|
||||
Logger()
|
||||
|
|
@ -122,10 +124,11 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
|||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
type UpdateParams struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
AppTargetVersion string `json:"appTargetVersion"`
|
||||
SystemTargetVersion string `json:"systemTargetVersion"`
|
||||
Components []string `json:"components,omitempty"`
|
||||
IncludePreRelease bool `json:"includePreRelease"`
|
||||
CheckOnly bool `json:"checkOnly"`
|
||||
ResetConfig bool `json:"resetConfig"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
Components map[string]string `json:"components,omitempty"`
|
||||
IncludePreRelease bool `json:"includePreRelease"`
|
||||
CheckOnly bool `json:"checkOnly"`
|
||||
ResetConfig bool `json:"resetConfig"`
|
||||
}
|
||||
|
||||
// getUpdateStatus gets the update status for the given components
|
||||
|
|
@ -259,7 +260,7 @@ func (s *State) checkUpdateStatus(
|
|||
appUpdateStatus *componentUpdateStatus,
|
||||
systemUpdateStatus *componentUpdateStatus,
|
||||
) error {
|
||||
// Get local versions
|
||||
// get the local versions
|
||||
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting local version: %w", err)
|
||||
|
|
@ -267,7 +268,12 @@ func (s *State) checkUpdateStatus(
|
|||
appUpdateStatus.localVersion = appVersionLocal.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)
|
||||
if err != nil {
|
||||
if err == ErrVersionNotFound || errors.Unwrap(err) == ErrVersionNotFound {
|
||||
|
|
@ -277,61 +283,33 @@ func (s *State) checkUpdateStatus(
|
|||
}
|
||||
return err
|
||||
}
|
||||
appUpdateStatus.url = remoteMetadata.AppURL
|
||||
appUpdateStatus.hash = remoteMetadata.AppHash
|
||||
appUpdateStatus.version = remoteMetadata.AppVersion
|
||||
|
||||
systemUpdateStatus.url = remoteMetadata.SystemURL
|
||||
systemUpdateStatus.hash = remoteMetadata.SystemHash
|
||||
systemUpdateStatus.version = remoteMetadata.SystemVersion
|
||||
s.l.Trace().
|
||||
Interface("remoteMetadata", remoteMetadata).
|
||||
Msg("checkUpdateStatus: fetchUpdateMetadata")
|
||||
|
||||
// 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)
|
||||
|
||||
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
|
||||
// parse the remote metadata to the componentUpdateStatuses
|
||||
if err := remoteMetadataToComponentStatus(
|
||||
remoteMetadata,
|
||||
"app",
|
||||
appUpdateStatus,
|
||||
params,
|
||||
); err != nil {
|
||||
return fmt.Errorf("error parsing remote app version: %w", err)
|
||||
}
|
||||
|
||||
components := params.Components
|
||||
// skip check if no components are specified
|
||||
if len(components) == 0 {
|
||||
return nil
|
||||
if err := remoteMetadataToComponentStatus(
|
||||
remoteMetadata,
|
||||
"system",
|
||||
systemUpdateStatus,
|
||||
params,
|
||||
); err != nil {
|
||||
return fmt.Errorf("error parsing remote system version: %w", err)
|
||||
}
|
||||
|
||||
// TODO: simplify this
|
||||
if slices.Contains(components, "app") {
|
||||
if params.AppTargetVersion != "" {
|
||||
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
|
||||
if s.l.GetLevel() <= zerolog.TraceLevel {
|
||||
appUpdateStatus.getZerologLogger(s.l).Trace().Msg("checkUpdateStatus: remoteMetadataToComponentStatus [app]")
|
||||
systemUpdateStatus.getZerologLogger(s.l).Trace().Msg("checkUpdateStatus: remoteMetadataToComponentStatus [system]")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -43,10 +43,9 @@ func newOtaState() *State {
|
|||
func TestCheckUpdateComponents(t *testing.T) {
|
||||
otaState := newOtaState()
|
||||
updateParams := UpdateParams{
|
||||
DeviceID: "test",
|
||||
IncludePreRelease: false,
|
||||
SystemTargetVersion: "0.2.2",
|
||||
Components: []string{"system"},
|
||||
DeviceID: "test",
|
||||
IncludePreRelease: false,
|
||||
Components: map[string]string{"system": "0.2.2"},
|
||||
}
|
||||
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -10,6 +9,14 @@ import (
|
|||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
availableComponents = []string{"app", "system"}
|
||||
defaultComponents = map[string]string{
|
||||
"app": "",
|
||||
"system": "",
|
||||
}
|
||||
)
|
||||
|
||||
// UpdateMetadata represents the metadata of an update
|
||||
type UpdateMetadata struct {
|
||||
AppVersion string `json:"appVersion"`
|
||||
|
|
@ -48,9 +55,9 @@ type PostRebootAction struct {
|
|||
type componentUpdateStatus struct {
|
||||
pending bool
|
||||
available bool
|
||||
availableReason string // why the component is available or not available
|
||||
version string
|
||||
localVersion string
|
||||
targetVersion string
|
||||
url string
|
||||
hash string
|
||||
downloadProgress float32
|
||||
|
|
@ -59,30 +66,27 @@ type componentUpdateStatus struct {
|
|||
verifiedAt time.Time
|
||||
updateProgress float32
|
||||
updatedAt time.Time
|
||||
dependsOn []string //nolint:unused
|
||||
dependsOn []string
|
||||
}
|
||||
|
||||
// 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"`
|
||||
SystemTargetVersion *string `json:"systemTargetVersion,omitempty"`
|
||||
AppTargetVersion *string `json:"appTargetVersion,omitempty"`
|
||||
func (c *componentUpdateStatus) getZerologLogger(l *zerolog.Logger) *zerolog.Logger {
|
||||
logger := l.With().
|
||||
Bool("pending", c.pending).
|
||||
Bool("available", c.available).
|
||||
Str("availableReason", c.availableReason).
|
||||
Str("version", c.version).
|
||||
Str("localVersion", c.localVersion).
|
||||
Str("url", c.url).
|
||||
Str("hash", c.hash).
|
||||
Float32("downloadProgress", c.downloadProgress).
|
||||
Time("downloadFinishedAt", c.downloadFinishedAt).
|
||||
Float32("verificationProgress", c.verificationProgress).
|
||||
Time("verifiedAt", c.verifiedAt).
|
||||
Float32("updateProgress", c.updateProgress).
|
||||
Time("updatedAt", c.updatedAt).
|
||||
Strs("dependsOn", c.dependsOn).
|
||||
Logger()
|
||||
return &logger
|
||||
}
|
||||
|
||||
// HwRebootFunc is a function that reboots the hardware
|
||||
|
|
@ -120,39 +124,6 @@ type State struct {
|
|||
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 {
|
||||
return &UpdateStatus{
|
||||
Local: &LocalMetadata{
|
||||
|
|
@ -209,8 +180,9 @@ type Options struct {
|
|||
// NewState creates a new OTA state
|
||||
func NewState(opts Options) *State {
|
||||
components := make(map[string]componentUpdateStatus)
|
||||
components["app"] = componentUpdateStatus{}
|
||||
components["system"] = componentUpdateStatus{}
|
||||
for _, component := range availableComponents {
|
||||
components[component] = componentUpdateStatus{}
|
||||
}
|
||||
|
||||
s := &State{
|
||||
l: opts.Logger,
|
||||
|
|
@ -228,50 +200,20 @@ func NewState(opts Options) *State {
|
|||
return s
|
||||
}
|
||||
|
||||
// ToRPCState converts the State to the RPCState
|
||||
// probably we need a generator for this ...
|
||||
func (s *State) ToRPCState() *RPCState {
|
||||
r := &RPCState{
|
||||
Updating: s.updating,
|
||||
Error: s.error,
|
||||
MetadataFetchedAt: &s.metadataFetchedAt,
|
||||
}
|
||||
// appUpdateStatus.url = remoteMetadata.AppURL
|
||||
// appUpdateStatus.hash = remoteMetadata.AppHash
|
||||
// appUpdateStatus.version = remoteMetadata.AppVersion
|
||||
|
||||
app, ok := s.componentUpdateStatuses["app"]
|
||||
if ok {
|
||||
r.AppUpdatePending = app.pending
|
||||
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
|
||||
}
|
||||
// systemUpdateStatus.url = remoteMetadata.SystemURL
|
||||
// systemUpdateStatus.hash = remoteMetadata.SystemHash
|
||||
// systemUpdateStatus.version = remoteMetadata.SystemVersion
|
||||
|
||||
system, ok := s.componentUpdateStatuses["system"]
|
||||
if ok {
|
||||
r.SystemUpdatePending = system.pending
|
||||
r.SystemDownloadProgress = &system.downloadProgress
|
||||
if !system.downloadFinishedAt.IsZero() {
|
||||
r.SystemDownloadFinishedAt = &system.downloadFinishedAt
|
||||
}
|
||||
r.SystemVerificationProgress = &system.verificationProgress
|
||||
if !system.verifiedAt.IsZero() {
|
||||
r.SystemVerifiedAt = &system.verifiedAt
|
||||
}
|
||||
r.SystemUpdateProgress = &system.updateProgress
|
||||
if !system.updatedAt.IsZero() {
|
||||
r.SystemUpdatedAt = &system.updatedAt
|
||||
}
|
||||
r.SystemTargetVersion = &system.targetVersion
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
// // 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)
|
||||
|
|
|
|||
25
ota.go
25
ota.go
|
|
@ -135,26 +135,21 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) {
|
|||
}
|
||||
|
||||
type updateParams struct {
|
||||
AppTargetVersion string `json:"appTargetVersion"`
|
||||
SystemTargetVersion string `json:"systemTargetVersion"`
|
||||
Components []string `json:"components,omitempty"`
|
||||
Components map[string]string `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
func rpcTryUpdate() error {
|
||||
return rpcTryUpdateComponents(updateParams{
|
||||
AppTargetVersion: "",
|
||||
SystemTargetVersion: "",
|
||||
Components: make(map[string]string),
|
||||
}, config.IncludePreRelease, false)
|
||||
}
|
||||
|
||||
// rpcCheckUpdateComponents checks the update status for the given components
|
||||
func rpcCheckUpdateComponents(params updateParams, includePreRelease bool) (*ota.UpdateStatus, error) {
|
||||
updateParams := ota.UpdateParams{
|
||||
DeviceID: GetDeviceID(),
|
||||
IncludePreRelease: includePreRelease,
|
||||
AppTargetVersion: params.AppTargetVersion,
|
||||
SystemTargetVersion: params.SystemTargetVersion,
|
||||
Components: params.Components,
|
||||
DeviceID: GetDeviceID(),
|
||||
IncludePreRelease: includePreRelease,
|
||||
Components: params.Components,
|
||||
}
|
||||
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
|
||||
if err != nil {
|
||||
|
|
@ -171,16 +166,6 @@ func rpcTryUpdateComponents(params updateParams, includePreRelease bool, resetCo
|
|||
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() {
|
||||
err := otaState.TryUpdate(context.Background(), updateParams)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { isOnDevice } from "@/main";
|
|||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
import { checkUpdateComponents } from "@/utils/jsonrpc";
|
||||
import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc";
|
||||
import { SystemVersionInfo } from "@hooks/useVersion";
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
|
|
@ -196,16 +196,17 @@ export default function SettingsAdvancedRoute() {
|
|||
}, []);
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
// we do not need to set it to false if check succeeds,
|
||||
// because it will be redirected to the update page later
|
||||
setVersionUpdateLoading(true);
|
||||
versionInfo = await checkUpdateComponents({
|
||||
components,
|
||||
appTargetVersion: appVersion,
|
||||
systemTargetVersion: systemVersion,
|
||||
}, devChannel);
|
||||
console.log("versionInfo", versionInfo);
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -214,21 +215,14 @@ export default function SettingsAdvancedRoute() {
|
|||
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;
|
||||
|
||||
const pageParams = new URLSearchParams();
|
||||
if (components.includes("app") && versionInfo.remote?.appVersion && versionInfo.appUpdateAvailable) {
|
||||
if (components.app && versionInfo?.remote?.appVersion && versionInfo?.appUpdateAvailable) {
|
||||
hasUpdate = true;
|
||||
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;
|
||||
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 { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
import { checkUpdateComponents, SystemVersionInfo, updateParams } from "@/utils/jsonrpc";
|
||||
import { checkUpdateComponents, SystemVersionInfo, UpdateComponents, updateParams } from "@/utils/jsonrpc";
|
||||
|
||||
export default function SettingsGeneralUpdateRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -43,15 +43,13 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
}, [send, setModalView, setShouldReload]);
|
||||
|
||||
const onConfirmCustomUpdate = useCallback((appTargetVersion?: string, systemTargetVersion?: string) => {
|
||||
const components = [];
|
||||
if (appTargetVersion) components.push("app");
|
||||
if (systemTargetVersion) components.push("system");
|
||||
const components: UpdateComponents = {};
|
||||
if (appTargetVersion) components.app = appTargetVersion;
|
||||
if (systemTargetVersion) components.system = systemTargetVersion;
|
||||
|
||||
send("tryUpdateComponents", {
|
||||
params: {
|
||||
components,
|
||||
appTargetVersion,
|
||||
systemTargetVersion,
|
||||
},
|
||||
includePreRelease: false,
|
||||
resetConfig,
|
||||
|
|
@ -195,13 +193,9 @@ function LoadingState({
|
|||
if (!customAppVersion && !customSystemVersion) {
|
||||
return await getVersionInfo();
|
||||
}
|
||||
const params : updateParams = {
|
||||
components: [],
|
||||
appTargetVersion: customAppVersion,
|
||||
systemTargetVersion: customSystemVersion,
|
||||
};
|
||||
if (customAppVersion) params.components?.push("app");
|
||||
if (customSystemVersion) params.components?.push("system");
|
||||
const params: updateParams = { components: {} as UpdateComponents };
|
||||
if (customAppVersion) params.components!.app = customAppVersion;
|
||||
if (customSystemVersion) params.components!.system = customSystemVersion;
|
||||
|
||||
return await checkUpdateComponents(params, false);
|
||||
}, [customAppVersion, customSystemVersion, getVersionInfo]);
|
||||
|
|
|
|||
|
|
@ -243,10 +243,11 @@ export async function getLocalVersion() {
|
|||
return response.result;
|
||||
}
|
||||
|
||||
export type UpdateComponent = "app" | "system";
|
||||
export type UpdateComponents = Partial<Record<UpdateComponent, string>>;
|
||||
|
||||
export interface updateParams {
|
||||
appTargetVersion?: string;
|
||||
systemTargetVersion?: string;
|
||||
components?: string[];
|
||||
components?: UpdateComponents;
|
||||
}
|
||||
|
||||
export async function checkUpdateComponents(params: updateParams, includePreRelease: boolean) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue