fix: update components

This commit is contained in:
Siyuan 2025-10-31 16:47:15 +00:00
parent 2b3f392f0f
commit f6b0b7297d
8 changed files with 87 additions and 36 deletions

View File

@ -1,5 +0,0 @@
package ota
import "github.com/jetkvm/kvm/internal/logging"
var logger = logging.GetSubsystemLogger("ota")

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"time" "time"
"github.com/Masterminds/semver/v3" "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) 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) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating request: %w", err) 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") 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 { if !params.CheckOnly {
s.updating = true s.updating = true
s.triggerStateUpdate() s.triggerStateUpdate()
@ -128,12 +143,12 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
return nil return nil
} }
if appUpdate.available || appUpdate.downgradeAvailable { if shouldUpdateApp && (appUpdate.available || appUpdate.downgradeAvailable) {
appUpdate.pending = true appUpdate.pending = true
s.triggerComponentUpdateState("app", appUpdate) s.triggerComponentUpdateState("app", appUpdate)
} }
if systemUpdate.available || systemUpdate.downgradeAvailable { if shouldUpdateSystem && (systemUpdate.available || systemUpdate.downgradeAvailable) {
systemUpdate.pending = true systemUpdate.pending = true
s.triggerComponentUpdateState("system", systemUpdate) 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 // UpdateParams represents the parameters for the update
type UpdateParams struct { type UpdateParams struct {
DeviceID string `json:"deviceID"` DeviceID string `json:"deviceID"`
AppTargetVersion string `json:"appTargetVersion"` AppTargetVersion string `json:"appTargetVersion"`
SystemTargetVersion string `json:"systemTargetVersion"` SystemTargetVersion string `json:"systemTargetVersion"`
IncludePreRelease bool `json:"includePreRelease"` Components []string `json:"components,omitempty"`
CheckOnly bool `json:"checkOnly"` IncludePreRelease bool `json:"includePreRelease"`
CheckOnly bool `json:"checkOnly"`
} }
func (s *State) getUpdateStatus( func (s *State) getUpdateStatus(

View File

@ -62,7 +62,7 @@ type componentUpdateStatus struct {
verifiedAt time.Time verifiedAt time.Time
updateProgress float32 updateProgress float32
updatedAt time.Time updatedAt time.Time
dependsOn []string dependsOn []string //nolint:unused
} }
// RPCState represents the current OTA state for the RPC API // RPCState represents the current OTA state for the RPC API

View File

@ -84,6 +84,8 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS
return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger) return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger)
} }
rkLogger.Info().Msg("rk_ota success") rkLogger.Info().Msg("rk_ota success")
s.rebootNeeded = true
systemUpdate.updateProgress = 1 systemUpdate.updateProgress = 1
systemUpdate.updatedAt = verifyFinished systemUpdate.updatedAt = verifyFinished
s.triggerComponentUpdateState("system", systemUpdate) s.triggerComponentUpdateState("system", systemUpdate)

22
ota.go
View File

@ -137,6 +137,7 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) {
type tryUpdateComponents struct { type tryUpdateComponents struct {
AppTargetVersion string `json:"app"` AppTargetVersion string `json:"app"`
SystemTargetVersion string `json:"system"` SystemTargetVersion string `json:"system"`
Components string `json:"components,omitempty"` // components is a comma-separated list of components to update
} }
func rpcTryUpdate() error { func rpcTryUpdate() error {
@ -155,17 +156,18 @@ func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bo
logger.Info().Interface("components", components).Msg("components") logger.Info().Interface("components", components).Msg("components")
if components.AppTargetVersion != "" { updateParams.AppTargetVersion = components.AppTargetVersion
updateParams.AppTargetVersion = components.AppTargetVersion if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil {
if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil { return fmt.Errorf("failed to set app target version: %w", err)
return fmt.Errorf("failed to set app target version: %w", err)
}
} }
if components.SystemTargetVersion != "" {
updateParams.SystemTargetVersion = components.SystemTargetVersion updateParams.SystemTargetVersion = components.SystemTargetVersion
if err := otaState.SetTargetVersion("system", components.SystemTargetVersion); err != nil { if err := otaState.SetTargetVersion("system", components.SystemTargetVersion); err != nil {
return fmt.Errorf("failed to set system target version: %w", err) return fmt.Errorf("failed to set system target version: %w", err)
} }
if components.Components != "" {
updateParams.Components = strings.Split(components.Components, ",")
} }
go func() { go func() {

View File

@ -1,11 +1,11 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import semver from "semver";
import { useDeviceStore } from "@/hooks/stores"; import { useDeviceStore } from "@/hooks/stores";
import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc"; import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc";
import { getUpdateStatus, getLocalVersion as getLocalVersionRpc } from "@/utils/jsonrpc"; import { getUpdateStatus, getLocalVersion as getLocalVersionRpc } from "@/utils/jsonrpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
import semver from "semver";
export interface VersionInfo { export interface VersionInfo {
appVersion: string; appVersion: string;

View File

@ -197,10 +197,14 @@ export default function SettingsAdvancedRoute() {
); );
return; return;
} }
const pageParams = new URLSearchParams();
pageParams.set("downgrade", "true");
pageParams.set("components", updateTarget == "both" ? "app,system" : updateTarget);
// Navigate to update page // 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 ( return (
<div className="space-y-4"> <div className="space-y-4">

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 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 { useJsonRpc } from "@hooks/useJsonRpc";
import { UpdateState, useUpdateStore } from "@hooks/stores"; import { UpdateState, useUpdateStore } from "@hooks/stores";
@ -16,11 +16,16 @@ import { SystemVersionInfo } from "@/utils/jsonrpc";
export default function SettingsGeneralUpdateRoute() { export default function SettingsGeneralUpdateRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
//@ts-ignore
const [searchParams, setSearchParams] = useSearchParams();
const { updateSuccess } = location.state || {}; const { updateSuccess } = location.state || {};
const { setModalView, otaState } = useUpdateStore(); const { setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]);
const updateComponents = useMemo(() => searchParams.get("components") || "", [searchParams]);
const onClose = useCallback(async () => { const onClose = useCallback(async () => {
navigate(".."); // back to the devices.$id.settings page navigate(".."); // back to the devices.$id.settings page
// Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation. // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
@ -33,6 +38,18 @@ export default function SettingsGeneralUpdateRoute() {
setModalView("updating"); setModalView("updating");
}, [send, setModalView]); }, [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(() => { useEffect(() => {
if (otaState.updating) { if (otaState.updating) {
setModalView("updating"); setModalView("updating");
@ -45,15 +62,24 @@ export default function SettingsGeneralUpdateRoute() {
} }
}, [otaState.error, otaState.updating, setModalView, updateSuccess]); }, [otaState.error, otaState.updating, setModalView, updateSuccess]);
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />; return <Dialog
onClose={onClose}
onConfirmUpdate={onConfirmUpdate}
onConfirmDowngrade={onConfirmDowngrade}
downgrade={downgrade}
/>;
} }
export function Dialog({ export function Dialog({
onClose, onClose,
onConfirmUpdate, onConfirmUpdate,
onConfirmDowngrade,
downgrade,
}: Readonly<{ }: Readonly<{
downgrade: boolean;
onClose: () => void; onClose: () => void;
onConfirmUpdate: () => void; onConfirmUpdate: () => void;
onConfirmDowngrade: () => void;
}>) { }>) {
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
@ -70,15 +96,15 @@ export function Dialog({
setVersionInfo(versionInfo); setVersionInfo(versionInfo);
if (hasUpdate) { if (hasDowngrade && downgrade) {
setModalView("updateAvailable");
} else if (hasDowngrade) {
setModalView("updateDowngradeAvailable"); setModalView("updateDowngradeAvailable");
} else if (hasUpdate) {
setModalView("updateAvailable");
} else { } else {
setModalView("upToDate"); setModalView("upToDate");
} }
}, },
[setModalView], [setModalView, downgrade],
); );
const onCancelDowngrade = useCallback(() => { const onCancelDowngrade = useCallback(() => {
@ -110,7 +136,7 @@ export function Dialog({
)} )}
{modalView === "updateDowngradeAvailable" && ( {modalView === "updateDowngradeAvailable" && (
<UpdateDowngradeAvailableState <UpdateDowngradeAvailableState
onConfirmUpdate={onConfirmUpdate} onConfirmDowngrade={onConfirmDowngrade}
onCancelDowngrade={onCancelDowngrade} onCancelDowngrade={onCancelDowngrade}
versionInfo={versionInfo!} versionInfo={versionInfo!}
/> />
@ -429,13 +455,19 @@ function UpdateAvailableState({
function UpdateDowngradeAvailableState({ function UpdateDowngradeAvailableState({
versionInfo, versionInfo,
onConfirmUpdate, onConfirmDowngrade,
onCancelDowngrade, onCancelDowngrade,
}: { }: {
versionInfo: SystemVersionInfo; versionInfo: SystemVersionInfo;
onConfirmUpdate: () => void; onConfirmDowngrade: (system?: string, app?: string) => void;
onCancelDowngrade: () => void; onCancelDowngrade: () => void;
}) { }) {
const confirmDowngrade = useCallback(() => {
onConfirmDowngrade(
versionInfo?.remote?.systemVersion || undefined,
versionInfo?.remote?.appVersion || undefined,
);
}, [versionInfo, onConfirmDowngrade]);
return ( return (
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
@ -459,7 +491,7 @@ function UpdateDowngradeAvailableState({
) : null} ) : null}
</p> </p>
<div className="flex items-center justify-start gap-x-2"> <div className="flex items-center justify-start gap-x-2">
<Button size="SM" theme="primary" text={m.general_update_downgrade_button()} onClick={onConfirmUpdate} /> <Button size="SM" theme="primary" text={m.general_update_downgrade_button()} onClick={confirmDowngrade} />
<Button size="SM" theme="light" text={m.general_update_keep_current_button()} onClick={onCancelDowngrade} /> <Button size="SM" theme="light" text={m.general_update_keep_current_button()} onClick={onCancelDowngrade} />
</div> </div>
</div> </div>