diff --git a/internal/ota/app.go b/internal/ota/app.go index 301ea953..3ae206f9 100644 --- a/internal/ota/app.go +++ b/internal/ota/app.go @@ -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 { diff --git a/internal/ota/ota.go b/internal/ota/ota.go index 137ce023..1ea6a356 100644 --- a/internal/ota/ota.go +++ b/internal/ota/ota.go @@ -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 diff --git a/internal/ota/ota_test.go b/internal/ota/ota_test.go index 7a82b43e..d2b81cdb 100644 --- a/internal/ota/ota_test.go +++ b/internal/ota/ota_test.go @@ -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) diff --git a/internal/ota/rpc.go b/internal/ota/rpc.go new file mode 100644 index 00000000..e89c93d1 --- /dev/null +++ b/internal/ota/rpc.go @@ -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) diff --git a/internal/ota/state.go b/internal/ota/state.go index 58618902..d0a75e62 100644 --- a/internal/ota/state.go +++ b/internal/ota/state.go @@ -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) diff --git a/ota.go b/ota.go index ebea1800..b8ae47f1 100644 --- a/ota.go +++ b/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 { diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index 5eccce31..84f61a9d 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -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); } diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 17f576ca..c69d2140 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -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]); diff --git a/ui/src/utils/jsonrpc.ts b/ui/src/utils/jsonrpc.ts index f4cbc91f..42fd29f0 100644 --- a/ui/src/utils/jsonrpc.ts +++ b/ui/src/utils/jsonrpc.ts @@ -243,10 +243,11 @@ export async function getLocalVersion() { return response.result; } +export type UpdateComponent = "app" | "system"; +export type UpdateComponents = Partial>; + export interface updateParams { - appTargetVersion?: string; - systemTargetVersion?: string; - components?: string[]; + components?: UpdateComponents; } export async function checkUpdateComponents(params: updateParams, includePreRelease: boolean) {