refactor(ota): improve OTA state management

This commit is contained in:
Siyuan 2025-11-14 11:55:22 +00:00
parent 005505a2da
commit 8d085a6071
9 changed files with 304 additions and 242 deletions

View File

@ -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 {

View File

@ -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

View File

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

172
internal/ota/rpc.go Normal file
View File

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

View File

@ -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
View File

@ -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 {

View File

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

View File

@ -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]);

View File

@ -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) {