From 329ad025bfcdb40d9c3209f915c0a4b40b4b7b15 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Fri, 7 Nov 2025 08:48:26 +0000 Subject: [PATCH] fix: should return error if version is not available --- internal/ota/errors.go | 8 ++++++++ internal/ota/ota.go | 25 +++++++++++++++++++------ ota.go | 31 +++++++++++++++++++++++++++++++ ui/src/hooks/useVersion.tsx | 4 +--- ui/src/utils/jsonrpc.ts | 2 ++ 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 internal/ota/errors.go diff --git a/internal/ota/errors.go b/internal/ota/errors.go new file mode 100644 index 00000000..49804282 --- /dev/null +++ b/internal/ota/errors.go @@ -0,0 +1,8 @@ +package ota + +import "errors" + +var ( + // ErrVersionNotFound is returned when the specified version is not found + ErrVersionNotFound = errors.New("specified version not found") +) diff --git a/internal/ota/ota.go b/internal/ota/ota.go index 40ed3bba..d3f637bf 100644 --- a/internal/ota/ota.go +++ b/internal/ota/ota.go @@ -3,6 +3,7 @@ package ota import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -23,12 +24,14 @@ func (s *State) GetReleaseAPIEndpoint() string { } // getUpdateURL returns the update URL for the given parameters -func (s *State) getUpdateURL(params UpdateParams) (string, error) { +func (s *State) getUpdateURL(params UpdateParams) (string, error, bool) { updateURL, err := url.Parse(s.releaseAPIEndpoint) if err != nil { - return "", fmt.Errorf("error parsing update metadata URL: %w", err) + return "", fmt.Errorf("error parsing update metadata URL: %w", err), false } + isCustomVersion := false + appTargetVersion := s.GetTargetVersion("app") if appTargetVersion != "" && params.AppTargetVersion == "" { params.AppTargetVersion = appTargetVersion @@ -43,19 +46,21 @@ func (s *State) getUpdateURL(params UpdateParams) (string, error) { 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) + isCustomVersion = true } updateURL.RawQuery = query.Encode() - return updateURL.String(), nil + return updateURL.String(), nil, isCustomVersion } func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*UpdateMetadata, error) { metadata := &UpdateMetadata{} - url, err := s.getUpdateURL(params) + url, err, isCustomVersion := s.getUpdateURL(params) if err != nil { return nil, fmt.Errorf("error getting update URL: %w", err) } @@ -77,6 +82,10 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (* } defer resp.Body.Close() + if isCustomVersion && resp.StatusCode == http.StatusNotFound { + return nil, ErrVersionNotFound + } + if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } @@ -189,7 +198,7 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error { postRebootAction := &PostRebootAction{ HealthCheck: "/device/status", - RedirectUrl: redirectUrl, + RedirectTo: redirectUrl, } if err := s.reboot(true, postRebootAction, 10*time.Second); err != nil { @@ -241,7 +250,11 @@ func (s *State) getUpdateStatus( // Get remote metadata remoteMetadata, err := s.fetchUpdateMetadata(ctx, params) if err != nil { - err = fmt.Errorf("error checking for updates: %w", err) + if err == ErrVersionNotFound || errors.Unwrap(err) == ErrVersionNotFound { + err = ErrVersionNotFound + } else { + err = fmt.Errorf("error checking for updates: %w", err) + } return } appUpdate.url = remoteMetadata.AppURL diff --git a/ota.go b/ota.go index d85ce84b..f4a56415 100644 --- a/ota.go +++ b/ota.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/jetkvm/kvm/internal/ota" @@ -158,11 +159,15 @@ func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bo logger.Info().Interface("components", components).Msg("components") + currentAppTargetVersion := otaState.GetTargetVersion("app") + appTargetVersionChanged := currentAppTargetVersion != components.AppTargetVersion updateParams.AppTargetVersion = components.AppTargetVersion if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil { return fmt.Errorf("failed to set app target version: %w", err) } + currentSystemTargetVersion := otaState.GetTargetVersion("system") + systemTargetVersionChanged := currentSystemTargetVersion != components.SystemTargetVersion updateParams.SystemTargetVersion = components.SystemTargetVersion if err := otaState.SetTargetVersion("system", components.SystemTargetVersion); err != nil { return fmt.Errorf("failed to set system target version: %w", err) @@ -172,6 +177,32 @@ func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bo updateParams.Components = strings.Split(components.Components, ",") } + // if it's a check only update, we don't need to try to update, we just need to check if the version is available + // and return the error immediately then revert the previous target versions + if checkOnly { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err := otaState.GetUpdateStatus(ctx, updateParams) + if err == nil { + return nil + } + + // revert the previous target versions + if appTargetVersionChanged { + if err := otaState.SetTargetVersion("app", currentAppTargetVersion); err != nil { + return fmt.Errorf("failed to revert app target version: %w", err) + } + } + if systemTargetVersionChanged { + if err := otaState.SetTargetVersion("system", currentSystemTargetVersion); err != nil { + return fmt.Errorf("failed to revert system target version: %w", err) + } + } + + return err + } + go func() { err := otaState.TryUpdate(context.Background(), updateParams) if err != nil { diff --git a/ui/src/hooks/useVersion.tsx b/ui/src/hooks/useVersion.tsx index 64b0c617..48af1f46 100644 --- a/ui/src/hooks/useVersion.tsx +++ b/ui/src/hooks/useVersion.tsx @@ -1,5 +1,4 @@ -import { useCallback, useMemo } from "react"; -import semver from "semver"; +import { useCallback } from "react"; import { useDeviceStore } from "@/hooks/stores"; import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc"; @@ -69,6 +68,5 @@ export function useVersion() { getLocalVersion, appVersion, systemVersion, - isOnDevVersion, }; } diff --git a/ui/src/utils/jsonrpc.ts b/ui/src/utils/jsonrpc.ts index ae97be13..0ad4b5df 100644 --- a/ui/src/utils/jsonrpc.ts +++ b/ui/src/utils/jsonrpc.ts @@ -220,7 +220,9 @@ export interface SystemVersionInfo { local: VersionInfo; remote?: VersionInfo; systemUpdateAvailable: boolean; + systemDowngradeAvailable: boolean; appUpdateAvailable: boolean; + appDowngradeAvailable: boolean; error?: string; }