mirror of https://github.com/jetkvm/kvm.git
fix: update components
This commit is contained in:
parent
2b3f392f0f
commit
f6b0b7297d
|
|
@ -1,5 +0,0 @@
|
||||||
package ota
|
|
||||||
|
|
||||||
import "github.com/jetkvm/kvm/internal/logging"
|
|
||||||
|
|
||||||
var logger = logging.GetSubsystemLogger("ota")
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -180,6 +195,7 @@ 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"`
|
||||||
|
Components []string `json:"components,omitempty"`
|
||||||
IncludePreRelease bool `json:"includePreRelease"`
|
IncludePreRelease bool `json:"includePreRelease"`
|
||||||
CheckOnly bool `json:"checkOnly"`
|
CheckOnly bool `json:"checkOnly"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
8
ota.go
8
ota.go
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue