Compare commits

...

6 Commits

9 changed files with 130 additions and 167 deletions

View File

@ -152,12 +152,12 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
return nil
}
if shouldUpdateApp && (appUpdate.available || appUpdate.downgradeAvailable) {
if shouldUpdateApp && appUpdate.available {
appUpdate.pending = true
s.triggerComponentUpdateState("app", appUpdate)
}
if shouldUpdateSystem && (systemUpdate.available || systemUpdate.downgradeAvailable) {
if shouldUpdateSystem && systemUpdate.available {
systemUpdate.pending = true
s.triggerComponentUpdateState("system", systemUpdate)
}
@ -220,6 +220,8 @@ type UpdateParams struct {
ResetConfig bool `json:"resetConfig"`
}
// getUpdateStatus gets the update status for the given components
// and updates the componentUpdateStatuses map
func (s *State) getUpdateStatus(
ctx context.Context,
params UpdateParams,
@ -239,7 +241,7 @@ func (s *State) getUpdateStatus(
systemUpdate = &currentSystemUpdate
}
err = s.doGetUpdateStatus(ctx, params, appUpdate, systemUpdate)
err = s.checkUpdateStatus(ctx, params, appUpdate, systemUpdate)
if err != nil {
return nil, nil, err
}
@ -250,21 +252,20 @@ func (s *State) getUpdateStatus(
return appUpdate, systemUpdate, nil
}
// doGetUpdateStatus is the internal function that gets the update status
// it WON'T change the state of the OTA state
func (s *State) doGetUpdateStatus(
// checkUpdateStatus checks the update status for the given components
func (s *State) checkUpdateStatus(
ctx context.Context,
params UpdateParams,
appUpdate *componentUpdateStatus,
systemUpdate *componentUpdateStatus,
appUpdateStatus *componentUpdateStatus,
systemUpdateStatus *componentUpdateStatus,
) error {
// Get local versions
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
if err != nil {
return fmt.Errorf("error getting local version: %w", err)
}
appUpdate.localVersion = appVersionLocal.String()
systemUpdate.localVersion = systemVersionLocal.String()
appUpdateStatus.localVersion = appVersionLocal.String()
systemUpdateStatus.localVersion = systemVersionLocal.String()
// Get remote metadata
remoteMetadata, err := s.fetchUpdateMetadata(ctx, params)
@ -276,13 +277,13 @@ func (s *State) doGetUpdateStatus(
}
return err
}
appUpdate.url = remoteMetadata.AppURL
appUpdate.hash = remoteMetadata.AppHash
appUpdate.version = remoteMetadata.AppVersion
appUpdateStatus.url = remoteMetadata.AppURL
appUpdateStatus.hash = remoteMetadata.AppHash
appUpdateStatus.version = remoteMetadata.AppVersion
systemUpdate.url = remoteMetadata.SystemURL
systemUpdate.hash = remoteMetadata.SystemHash
systemUpdate.version = remoteMetadata.SystemVersion
systemUpdateStatus.url = remoteMetadata.SystemURL
systemUpdateStatus.hash = remoteMetadata.SystemHash
systemUpdateStatus.version = remoteMetadata.SystemVersion
// Get remote versions
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
@ -290,26 +291,33 @@ func (s *State) doGetUpdateStatus(
err = fmt.Errorf("error parsing remote system version: %w", err)
return err
}
systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal)
systemUpdate.downgradeAvailable = systemVersionRemote.LessThan(systemVersionLocal)
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
}
appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal)
appUpdate.downgradeAvailable = appVersionRemote.LessThan(appVersionLocal)
appUpdateStatus.available = appVersionRemote.GreaterThan(appVersionLocal)
// Handle pre-release updates
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
if isRemoteSystemPreRelease && !params.IncludePreRelease {
systemUpdate.available = false
systemUpdateStatus.available = false
}
if isRemoteAppPreRelease && !params.IncludePreRelease {
appUpdate.available = false
appUpdateStatus.available = false
}
// Handle custom target versions
if slices.Contains(params.Components, "app") && params.AppTargetVersion != "" {
appUpdateStatus.available = appVersionRemote.String() != appUpdateStatus.localVersion
}
if slices.Contains(params.Components, "system") && params.SystemTargetVersion != "" {
systemUpdateStatus.available = systemVersionRemote.String() != systemUpdateStatus.localVersion
}
return nil
@ -317,12 +325,12 @@ func (s *State) doGetUpdateStatus(
// GetUpdateStatus returns the current update status (for backwards compatibility)
func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) {
appUpdate := &componentUpdateStatus{}
systemUpdate := &componentUpdateStatus{}
err := s.doGetUpdateStatus(ctx, params, appUpdate, systemUpdate)
appUpdateStatus := componentUpdateStatus{}
systemUpdateStatus := componentUpdateStatus{}
err := s.checkUpdateStatus(ctx, params, &appUpdateStatus, &systemUpdateStatus)
if err != nil {
return nil, fmt.Errorf("error getting update status: %w", err)
}
return toUpdateStatus(appUpdate, systemUpdate, ""), nil
return toUpdateStatus(&appUpdateStatus, &systemUpdateStatus, ""), nil
}

View File

@ -28,12 +28,10 @@ type LocalMetadata struct {
// UpdateStatus represents the current update status
type UpdateStatus struct {
Local *LocalMetadata `json:"local"`
Remote *UpdateMetadata `json:"remote"`
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
SystemDowngradeAvailable bool `json:"systemDowngradeAvailable"`
AppUpdateAvailable bool `json:"appUpdateAvailable"`
AppDowngradeAvailable bool `json:"appDowngradeAvailable"`
Local *LocalMetadata `json:"local"`
Remote *UpdateMetadata `json:"remote"`
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
AppUpdateAvailable bool `json:"appUpdateAvailable"`
// for backwards compatibility
Error string `json:"error,omitempty"`
@ -50,7 +48,6 @@ type PostRebootAction struct {
type componentUpdateStatus struct {
pending bool
available bool
downgradeAvailable bool
version string
localVersion string
targetVersion string
@ -170,11 +167,9 @@ func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpd
SystemURL: systemUpdate.url,
SystemHash: systemUpdate.hash,
},
SystemUpdateAvailable: systemUpdate.available,
SystemDowngradeAvailable: systemUpdate.downgradeAvailable,
AppUpdateAvailable: appUpdate.available,
AppDowngradeAvailable: appUpdate.downgradeAvailable,
Error: error,
SystemUpdateAvailable: systemUpdate.available,
AppUpdateAvailable: appUpdate.available,
Error: error,
}
}

View File

@ -1155,7 +1155,6 @@ var rpcHandlers = map[string]RPCHandler{
"getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel},
"tryUpdate": {Func: rpcTryUpdate},
"tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"params", "includePreRelease", "resetConfig"}},
"cancelDowngrade": {Func: rpcCancelDowngrade},
"getDevModeState": {Func: rpcGetDevModeState},
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
"getSSHKeyState": {Func: rpcGetSSHKeyState},

14
ota.go
View File

@ -135,8 +135,8 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) {
}
type updateParams struct {
AppTargetVersion string `json:"app"`
SystemTargetVersion string `json:"system"`
AppTargetVersion string `json:"appTargetVersion"`
SystemTargetVersion string `json:"systemTargetVersion"`
Components string `json:"components,omitempty"` // components is a comma-separated list of components to update
}
@ -194,13 +194,3 @@ func rpcTryUpdateComponents(params updateParams, includePreRelease bool, resetCo
}()
return nil
}
func rpcCancelDowngrade() error {
if err := otaState.SetTargetVersion("app", ""); err != nil {
return fmt.Errorf("failed to set app target version: %w", err)
}
if err := otaState.SetTargetVersion("system", ""); err != nil {
return fmt.Errorf("failed to set system target version: %w", err)
}
return nil
}

View File

@ -554,7 +554,6 @@ export type UpdateModalViews =
| "updating"
| "upToDate"
| "updateAvailable"
| "updateDowngradeAvailable"
| "updateCompleted"
| "error";

View File

@ -15,9 +15,7 @@ export interface SystemVersionInfo {
local: VersionInfo;
remote?: VersionInfo;
systemUpdateAvailable: boolean;
systemDowngradeAvailable: boolean;
appUpdateAvailable: boolean;
appDowngradeAvailable: boolean;
error?: string;
}

View File

@ -185,10 +185,10 @@ export default function SettingsAdvancedRoute() {
setShowLoopbackWarning(false);
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
const handleVersionUpdateError = useCallback((error?: JsonRpcError) => {
const handleVersionUpdateError = useCallback((error?: JsonRpcError | string) => {
notifications.error(
m.advanced_error_version_update({
error: error?.data ?? error?.message ?? m.unknown_error()
error: typeof error === "string" ? error : (error?.data ?? error?.message ?? m.unknown_error())
}),
{ duration: 1000 * 15 } // 15 seconds
);
@ -204,30 +204,40 @@ export default function SettingsAdvancedRoute() {
setVersionUpdateLoading(true);
versionInfo = await checkUpdateComponents({
components: components.join(","),
app: appVersion,
system: systemVersion,
appTargetVersion: appVersion,
systemTargetVersion: systemVersion,
}, devChannel);
console.log("versionInfo", versionInfo);
} catch (error: unknown) {
const jsonRpcError = error as JsonRpcError;
handleVersionUpdateError(jsonRpcError);
return ;
}
if (!versionInfo) {
handleVersionUpdateError();
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();
pageParams.set("downgrade", "true");
if (components.includes("app") && versionInfo.remote?.appVersion && versionInfo.appDowngradeAvailable) {
pageParams.set("app", versionInfo.remote?.appVersion);
if (components.includes("app") && versionInfo.remote?.appVersion && versionInfo.appUpdateAvailable) {
hasUpdate = true;
pageParams.set("custom_app_version", versionInfo.remote?.appVersion);
}
if (components.includes("system") && versionInfo.remote?.systemVersion && versionInfo.systemDowngradeAvailable) {
pageParams.set("system", versionInfo.remote?.systemVersion);
if (components.includes("system") && versionInfo.remote?.systemVersion && versionInfo.systemUpdateAvailable) {
hasUpdate = true;
pageParams.set("custom_system_version", versionInfo.remote?.systemVersion);
}
pageParams.set("reset_config", resetConfig.toString());
if (!hasUpdate) {
handleVersionUpdateError("No update available");
return;
}
pageParams.set("resetConfig", resetConfig.toString());
// Navigate to update page
navigateTo(`/settings/general/update?${pageParams.toString()}`);

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 { SystemVersionInfo } from "@/utils/jsonrpc";
import { checkUpdateComponents, SystemVersionInfo, updateParams } from "@/utils/jsonrpc";
export default function SettingsGeneralUpdateRoute() {
const navigate = useNavigate();
@ -22,10 +22,9 @@ export default function SettingsGeneralUpdateRoute() {
const { setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc();
const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]);
const customAppVersion = useMemo(() => searchParams.get("app") || "", [searchParams]);
const customSystemVersion = useMemo(() => searchParams.get("system") || "", [searchParams]);
const resetConfig = useMemo(() => searchParams.get("resetConfig") === "true", [searchParams]);
const customAppVersion = useMemo(() => searchParams.get("custom_app_version") || undefined, [searchParams]);
const customSystemVersion = useMemo(() => searchParams.get("custom_system_version") || undefined, [searchParams]);
const resetConfig = useMemo(() => searchParams.get("reset_config") === "true", [searchParams]);
const onClose = useCallback(async () => {
navigate(".."); // back to the devices.$id.settings page
@ -39,20 +38,16 @@ export default function SettingsGeneralUpdateRoute() {
setModalView("updating");
}, [send, setModalView]);
const onConfirmDowngrade = useCallback(() => {
const onConfirmCustomUpdate = useCallback((appTargetVersion?: string, systemTargetVersion?: string) => {
const components = [];
if (customSystemVersion) {
components.push("system");
}
if (customAppVersion) {
components.push("app");
}
if (appTargetVersion) components.push("app");
if (systemTargetVersion) components.push("system");
send("tryUpdateComponents", {
params: {
components: components.join(","),
app: customAppVersion,
system: customSystemVersion,
appTargetVersion,
systemTargetVersion,
},
includePreRelease: false,
resetConfig,
@ -60,7 +55,7 @@ export default function SettingsGeneralUpdateRoute() {
if ("error" in resp) return;
setModalView("updating");
});
}, [send, setModalView, customAppVersion, customSystemVersion, resetConfig]);
}, [send, setModalView, resetConfig]);
useEffect(() => {
if (otaState.updating) {
@ -77,8 +72,7 @@ export default function SettingsGeneralUpdateRoute() {
return <Dialog
onClose={onClose}
onConfirmUpdate={onConfirmUpdate}
onConfirmDowngrade={onConfirmDowngrade}
downgrade={downgrade}
onConfirmCustomUpdate={onConfirmCustomUpdate}
customAppVersion={customAppVersion}
customSystemVersion={customSystemVersion}
/>;
@ -87,15 +81,13 @@ export default function SettingsGeneralUpdateRoute() {
export function Dialog({
onClose,
onConfirmUpdate,
onConfirmDowngrade,
downgrade,
onConfirmCustomUpdate: onConfirmCustomUpdateCallback,
customAppVersion,
customSystemVersion,
}: Readonly<{
downgrade: boolean;
onClose: () => void;
onConfirmUpdate: () => void;
onConfirmDowngrade: () => void;
onConfirmCustomUpdate: (appVersion?: string, systemVersion?: string) => void;
customAppVersion?: string;
customSystemVersion?: string;
}>) {
@ -103,32 +95,31 @@ export function Dialog({
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
const { modalView, setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc();
const forceCustomUpdate = customSystemVersion !== undefined || customAppVersion !== undefined;
const onConfirmCustomUpdate = useCallback(() => {
console.debug("onConfirmCustomUpdate", customAppVersion, customSystemVersion, versionInfo);
onConfirmCustomUpdateCallback(
customAppVersion !== undefined ? customAppVersion : versionInfo?.remote?.appVersion,
customSystemVersion !== undefined ? customSystemVersion : versionInfo?.remote?.systemVersion,
);
}, [onConfirmCustomUpdateCallback, customAppVersion, customSystemVersion, versionInfo]);
const onFinishedLoading = useCallback(
(versionInfo: SystemVersionInfo) => {
const hasUpdate =
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
const hasDowngrade = customSystemVersion !== undefined || customAppVersion !== undefined;
setVersionInfo(versionInfo);
if (hasDowngrade && downgrade) {
setModalView("updateDowngradeAvailable");
} else if (hasUpdate) {
if (hasUpdate || forceCustomUpdate) {
setModalView("updateAvailable");
} else {
setModalView("upToDate");
}
},
[setModalView, downgrade, customAppVersion, customSystemVersion],
[setModalView, forceCustomUpdate],
);
const onCancelDowngrade = useCallback(() => {
send("cancelDowngrade", {});
onClose();
}, [onClose, send]);
return (
<div className="pointer-events-auto relative mx-auto text-left">
<div>
@ -141,24 +132,22 @@ export function Dialog({
)}
{modalView === "loading" && (
<LoadingState onFinished={onFinishedLoading} onCancelCheck={onClose} />
<LoadingState
onFinished={onFinishedLoading}
onCancelCheck={onClose}
customAppVersion={customAppVersion}
customSystemVersion={customSystemVersion}
/>
)}
{modalView === "updateAvailable" && (
<UpdateAvailableState
onConfirmUpdate={onConfirmUpdate}
forceCustomUpdate={forceCustomUpdate}
onConfirm={forceCustomUpdate ? onConfirmCustomUpdate : onConfirmUpdate}
onClose={onClose}
versionInfo={versionInfo!}
/>
)}
{modalView === "updateDowngradeAvailable" && (
<UpdateDowngradeAvailableState
appVersion={customAppVersion}
systemVersion={customSystemVersion}
onConfirmDowngrade={onConfirmDowngrade}
onCancelDowngrade={onCancelDowngrade}
/>
)}
{modalView === "updating" && (
<UpdatingDeviceState
@ -183,9 +172,13 @@ export function Dialog({
function LoadingState({
onFinished,
onCancelCheck,
customAppVersion,
customSystemVersion,
}: {
onFinished: (versionInfo: SystemVersionInfo) => void;
onCancelCheck: () => void;
customAppVersion?: string;
customSystemVersion?: string;
}) {
const [progressWidth, setProgressWidth] = useState("0%");
const abortControllerRef = useRef<AbortController | null>(null);
@ -194,6 +187,23 @@ function LoadingState({
const { setModalView } = useUpdateStore();
const progressBarRef = useRef<HTMLDivElement>(null);
const checkUpdate = useCallback(async () => {
if (!customAppVersion && !customSystemVersion) {
return await getVersionInfo();
}
const params : updateParams = {
components: "",
appTargetVersion: customAppVersion,
systemTargetVersion: customSystemVersion,
};
if (customAppVersion) params.components += ",app";
if (customSystemVersion) params.components += ",system";
params.components = params.components?.replace(/^,+/, "");
return await checkUpdateComponents(params, false);
}, [customAppVersion, customSystemVersion, getVersionInfo]);
useEffect(() => {
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
@ -203,7 +213,7 @@ function LoadingState({
setProgressWidth("100%");
}, 0);
getVersionInfo()
checkUpdate()
.then(async versionInfo => {
// Add a small delay to ensure it's not just flickering
await sleep(600);
@ -225,7 +235,7 @@ function LoadingState({
clearTimeout(animationTimer);
abortControllerRef.current?.abort();
};
}, [getVersionInfo, onFinished, setModalView]);
}, [checkUpdate, onFinished, setModalView]);
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
@ -433,11 +443,12 @@ function SystemUpToDateState({
function UpdateAvailableState({
versionInfo,
onConfirmUpdate,
onConfirm,
onClose,
}: {
versionInfo: SystemVersionInfo;
onConfirmUpdate: () => void;
forceCustomUpdate: boolean;
onConfirm: () => void;
onClose: () => void;
}) {
return (
@ -452,18 +463,18 @@ function UpdateAvailableState({
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
{versionInfo?.systemUpdateAvailable ? (
<>
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion}
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.local?.systemVersion} <span className="text-slate-600 dark:text-slate-300"></span> {versionInfo?.remote?.systemVersion}
<br />
</>
) : null}
{versionInfo?.appUpdateAvailable ? (
<>
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.remote?.appVersion}
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.local?.appVersion} <span className="text-slate-600 dark:text-slate-300"></span> {versionInfo?.remote?.appVersion}
</>
) : null}
</p>
<div className="flex items-center justify-start gap-x-2">
<Button size="SM" theme="primary" text={m.general_update_now_button()} onClick={onConfirmUpdate} />
<Button size="SM" theme="primary" text={m.general_update_now_button()} onClick={onConfirm} />
<Button size="SM" theme="light" text={m.general_update_later_button()} onClick={onClose} />
</div>
</div>
@ -471,51 +482,6 @@ function UpdateAvailableState({
);
}
function UpdateDowngradeAvailableState({
appVersion,
systemVersion,
onConfirmDowngrade,
onCancelDowngrade,
}: {
appVersion?: string;
systemVersion?: string;
onConfirmDowngrade: () => void;
onCancelDowngrade: () => void;
}) {
const confirmDowngrade = useCallback(() => {
onConfirmDowngrade();
}, [onConfirmDowngrade]);
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left">
<p className="text-base font-semibold text-black dark:text-white">
{m.general_update_downgrade_available_title()}
</p>
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
{m.general_update_downgrade_available_description()}
</p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
{systemVersion ? (
<>
<span className="font-semibold">{m.general_update_system_type()}</span>: {systemVersion}
<br />
</>
) : null}
{appVersion ? (
<>
<span className="font-semibold">{m.general_update_application_type()}</span>: {appVersion}
</>
) : null}
</p>
<div className="flex items-center justify-start gap-x-2">
<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} />
</div>
</div>
</div>
);
}
function UpdateCompletedState({ onClose }: { onClose: () => void }) {
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">

View File

@ -220,9 +220,7 @@ export interface SystemVersionInfo {
local: VersionInfo;
remote?: VersionInfo;
systemUpdateAvailable: boolean;
systemDowngradeAvailable: boolean;
appUpdateAvailable: boolean;
appDowngradeAvailable: boolean;
error?: string;
}
@ -246,8 +244,8 @@ export async function getLocalVersion() {
}
export interface updateParams {
app?: string;
system?: string;
appTargetVersion?: string;
systemTargetVersion?: string;
components?: string;
}