diff --git a/internal/ota/logger.go b/internal/ota/logger.go deleted file mode 100644 index a13036de..00000000 --- a/internal/ota/logger.go +++ /dev/null @@ -1,5 +0,0 @@ -package ota - -import "github.com/jetkvm/kvm/internal/logging" - -var logger = logging.GetSubsystemLogger("ota") diff --git a/internal/ota/ota.go b/internal/ota/ota.go index 366ea922..9e40e840 100644 --- a/internal/ota/ota.go +++ b/internal/ota/ota.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "time" "github.com/Masterminds/semver/v3" @@ -59,6 +60,10 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (* return nil, fmt.Errorf("error getting update URL: %w", err) } + s.l.Trace(). + Str("url", url). + Msg("fetching update metadata") + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) @@ -107,6 +112,16 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error { return fmt.Errorf("update already in progress") } + if len(params.Components) == 0 { + params.Components = []string{"app", "system"} + } + shouldUpdateApp := slices.Contains(params.Components, "app") + shouldUpdateSystem := slices.Contains(params.Components, "system") + + if !shouldUpdateApp && !shouldUpdateSystem { + return fmt.Errorf("no components to update") + } + if !params.CheckOnly { s.updating = true s.triggerStateUpdate() @@ -128,12 +143,12 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error { return nil } - if appUpdate.available || appUpdate.downgradeAvailable { + if shouldUpdateApp && (appUpdate.available || appUpdate.downgradeAvailable) { appUpdate.pending = true s.triggerComponentUpdateState("app", appUpdate) } - if systemUpdate.available || systemUpdate.downgradeAvailable { + if shouldUpdateSystem && (systemUpdate.available || systemUpdate.downgradeAvailable) { systemUpdate.pending = true s.triggerComponentUpdateState("system", systemUpdate) } @@ -177,11 +192,12 @@ 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"` - IncludePreRelease bool `json:"includePreRelease"` - CheckOnly bool `json:"checkOnly"` + 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"` } func (s *State) getUpdateStatus( diff --git a/internal/ota/state.go b/internal/ota/state.go index 9d9b8c01..0406b926 100644 --- a/internal/ota/state.go +++ b/internal/ota/state.go @@ -62,7 +62,7 @@ type componentUpdateStatus struct { verifiedAt time.Time updateProgress float32 updatedAt time.Time - dependsOn []string + dependsOn []string //nolint:unused } // RPCState represents the current OTA state for the RPC API diff --git a/internal/ota/sys.go b/internal/ota/sys.go index 575cf634..465b9a4d 100644 --- a/internal/ota/sys.go +++ b/internal/ota/sys.go @@ -84,6 +84,8 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger) } rkLogger.Info().Msg("rk_ota success") + + s.rebootNeeded = true systemUpdate.updateProgress = 1 systemUpdate.updatedAt = verifyFinished s.triggerComponentUpdateState("system", systemUpdate) diff --git a/ota.go b/ota.go index 921cd353..dd761bdc 100644 --- a/ota.go +++ b/ota.go @@ -137,6 +137,7 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) { type tryUpdateComponents struct { AppTargetVersion string `json:"app"` SystemTargetVersion string `json:"system"` + Components string `json:"components,omitempty"` // components is a comma-separated list of components to update } func rpcTryUpdate() error { @@ -155,17 +156,18 @@ func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bo logger.Info().Interface("components", components).Msg("components") - if 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) - } + updateParams.AppTargetVersion = components.AppTargetVersion + if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil { + return fmt.Errorf("failed to set app target version: %w", err) } - if 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) - } + + updateParams.SystemTargetVersion = components.SystemTargetVersion + if err := otaState.SetTargetVersion("system", components.SystemTargetVersion); err != nil { + return fmt.Errorf("failed to set system target version: %w", err) + } + + if components.Components != "" { + updateParams.Components = strings.Split(components.Components, ",") } go func() { diff --git a/ui/src/hooks/useVersion.tsx b/ui/src/hooks/useVersion.tsx index af634dec..cdde3a0c 100644 --- a/ui/src/hooks/useVersion.tsx +++ b/ui/src/hooks/useVersion.tsx @@ -1,10 +1,10 @@ import { useCallback, useMemo } from "react"; +import semver from "semver"; import { useDeviceStore } from "@/hooks/stores"; import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { m } from "@localizations/messages.js"; -import semver from "semver"; export interface VersionInfo { appVersion: string; diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index 7c6bcaf7..14d3d35c 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -196,10 +196,14 @@ export default function SettingsAdvancedRoute() { ); return; } + const pageParams = new URLSearchParams(); + pageParams.set("downgrade", "true"); + pageParams.set("components", updateTarget == "both" ? "app,system" : updateTarget); + // Navigate to update page - navigateTo("/settings/general/update"); + navigateTo(`/settings/general/update?${pageParams.toString()}`); }); - }, [appVersion, systemVersion, devChannel, send, navigateTo]); + }, [updateTarget,appVersion, systemVersion, devChannel, send, navigateTo]); return (
diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 14f2dedf..e4045183 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useLocation, useNavigate } from "react-router"; +import { useLocation, useNavigate, useSearchParams } from "react-router"; import { useJsonRpc } from "@hooks/useJsonRpc"; import { UpdateState, useUpdateStore } from "@hooks/stores"; @@ -14,16 +14,33 @@ import { m } from "@localizations/messages.js"; export default function SettingsGeneralUpdateRoute() { const navigate = useNavigate(); const location = useLocation(); + //@ts-ignore + const [searchParams, setSearchParams] = useSearchParams(); const { updateSuccess } = location.state || {}; const { setModalView, otaState } = useUpdateStore(); const { send } = useJsonRpc(); + const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]); + const updateComponents = useMemo(() => searchParams.get("components") || "", [searchParams]); + const onConfirmUpdate = useCallback(() => { send("tryUpdate", {}); setModalView("updating"); }, [send, setModalView]); + const onConfirmDowngrade = useCallback((system?: string, app?: string) => { + send("tryUpdateComponents", { + components: { + system, app, + components: updateComponents + }, + includePreRelease: true, + checkOnly: false, + }); + setModalView("updating"); + }, [send, setModalView, updateComponents]); + useEffect(() => { if (otaState.updating) { setModalView("updating"); @@ -36,15 +53,24 @@ export default function SettingsGeneralUpdateRoute() { } }, [otaState.updating, otaState.error, setModalView, updateSuccess]); - return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; + return navigate("..")} + onConfirmUpdate={onConfirmUpdate} + onConfirmDowngrade={onConfirmDowngrade} + downgrade={downgrade} + />; } export function Dialog({ onClose, onConfirmUpdate, + onConfirmDowngrade, + downgrade, }: Readonly<{ + downgrade: boolean; onClose: () => void; onConfirmUpdate: () => void; + onConfirmDowngrade: () => void; }>) { const { navigateTo } = useDeviceUiNavigation(); @@ -61,15 +87,15 @@ export function Dialog({ setVersionInfo(versionInfo); - if (hasUpdate) { - setModalView("updateAvailable"); - } else if (hasDowngrade) { + if (hasDowngrade && downgrade) { setModalView("updateDowngradeAvailable"); + } else if (hasUpdate) { + setModalView("updateAvailable"); } else { setModalView("upToDate"); } }, - [setModalView], + [setModalView, downgrade], ); const onCancelDowngrade = useCallback(() => { @@ -101,7 +127,7 @@ export function Dialog({ )} {modalView === "updateDowngradeAvailable" && ( @@ -419,13 +445,19 @@ function UpdateAvailableState({ function UpdateDowngradeAvailableState({ versionInfo, - onConfirmUpdate, + onConfirmDowngrade, onCancelDowngrade, }: { versionInfo: SystemVersionInfo; - onConfirmUpdate: () => void; + onConfirmDowngrade: (system?: string, app?: string) => void; onCancelDowngrade: () => void; }) { + const confirmDowngrade = useCallback(() => { + onConfirmDowngrade( + versionInfo?.remote?.systemVersion || undefined, + versionInfo?.remote?.appVersion || undefined, + ); + }, [versionInfo, onConfirmDowngrade]); return (
@@ -449,7 +481,7 @@ function UpdateDowngradeAvailableState({ ) : null}

-