diff --git a/jsonrpc.go b/jsonrpc.go index 8bcc2654..3e3d9c94 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -282,6 +282,17 @@ func rpcGetUpdateStatus() (*UpdateStatus, error) { return updateStatus, nil } +func rpcGetLocalVersion() (*LocalMetadata, error) { + systemVersion, appVersion, err := GetLocalVersion() + if err != nil { + return nil, fmt.Errorf("error getting local version: %w", err) + } + return &LocalMetadata{ + AppVersion: appVersion.String(), + SystemVersion: systemVersion.String(), + }, nil +} + func rpcTryUpdate() error { includePreRelease := config.IncludePreRelease go func() { @@ -1191,6 +1202,7 @@ var rpcHandlers = map[string]RPCHandler{ "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, "getDevChannelState": {Func: rpcGetDevChannelState}, "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, + "getLocalVersion": {Func: rpcGetLocalVersion}, "getUpdateStatus": {Func: rpcGetUpdateStatus}, "tryUpdate": {Func: rpcTryUpdate}, "getDevModeState": {Func: rpcGetDevModeState}, diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index b4fcc8ef..5c52d59c 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -29,6 +29,8 @@ export interface JsonRpcErrorResponse { export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse; +export const RpcMethodNotFound = -32601; + const callbackStore = new Map void>(); let requestCounter = 0; diff --git a/ui/src/hooks/useVersion.tsx b/ui/src/hooks/useVersion.tsx new file mode 100644 index 00000000..7341dacb --- /dev/null +++ b/ui/src/hooks/useVersion.tsx @@ -0,0 +1,79 @@ +import { useCallback } from "react"; + +import { useDeviceStore } from "@/hooks/stores"; +import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc"; +import notifications from "@/notifications"; + +export interface VersionInfo { + appVersion: string; + systemVersion: string; +} + +export interface SystemVersionInfo { + local: VersionInfo; + remote?: VersionInfo; + systemUpdateAvailable: boolean; + appUpdateAvailable: boolean; + error?: string; +} + +export function useVersion() { + const { + appVersion, + systemVersion, + setAppVersion, + setSystemVersion, + } = useDeviceStore(); + const { send } = useJsonRpc(); + const getVersionInfo = useCallback(() => { + return new Promise((resolve, reject) => { + send("getUpdateStatus", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) { + notifications.error(`Failed to check for updates: ${resp.error}`); + reject(new Error("Failed to check for updates")); + } else { + const result = resp.result as SystemVersionInfo; + setAppVersion(result.local.appVersion); + setSystemVersion(result.local.systemVersion); + + if (result.error) { + notifications.error(`Failed to check for updates: ${result.error}`); + reject(new Error("Failed to check for updates")); + } else { + resolve(result); + } + } + }); + }); + }, [send, setAppVersion, setSystemVersion]); + + const getLocalVersion = useCallback(() => { + return new Promise((resolve, reject) => { + send("getLocalVersion", {}, (resp: JsonRpcResponse) => { + if ("error" in resp) { + console.log(resp.error) + if (resp.error.code === RpcMethodNotFound) { + console.warn("Failed to get device version, using legacy version"); + return getVersionInfo().then(result => resolve(result.local)).catch(reject); + } + console.error("Failed to get device version N", resp.error); + notifications.error(`Failed to get device version: ${resp.error}`); + reject(new Error("Failed to get device version")); + } else { + const result = resp.result as VersionInfo; + + setAppVersion(result.appVersion); + setSystemVersion(result.systemVersion); + resolve(result); + } + }); + }); + }, [send, setAppVersion, setSystemVersion, getVersionInfo]); + + return { + getVersionInfo, + getLocalVersion, + appVersion, + systemVersion, + }; +} \ No newline at end of file diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 80ba0f78..38c15412 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -3,12 +3,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import Card from "@/components/Card"; -import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; +import { useJsonRpc } from "@/hooks/useJsonRpc"; import { Button } from "@components/Button"; -import { UpdateState, useDeviceStore, useUpdateStore } from "@/hooks/stores"; -import notifications from "@/notifications"; +import { UpdateState, useUpdateStore } from "@/hooks/stores"; import LoadingSpinner from "@/components/LoadingSpinner"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; +import { SystemVersionInfo, useVersion } from "@/hooks/useVersion"; export default function SettingsGeneralUpdateRoute() { const navigate = useNavigate(); @@ -41,13 +41,7 @@ export default function SettingsGeneralUpdateRoute() { return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; } -export interface SystemVersionInfo { - local: { appVersion: string; systemVersion: string }; - remote?: { appVersion: string; systemVersion: string }; - systemUpdateAvailable: boolean; - appUpdateAvailable: boolean; - error?: string; -} + export function Dialog({ onClose, @@ -134,30 +128,8 @@ function LoadingState({ }) { const [progressWidth, setProgressWidth] = useState("0%"); const abortControllerRef = useRef(null); - const { setAppVersion, setSystemVersion } = useDeviceStore(); - const { send } = useJsonRpc(); - const getVersionInfo = useCallback(() => { - return new Promise((resolve, reject) => { - send("getUpdateStatus", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - notifications.error(`Failed to check for updates: ${resp.error}`); - reject(new Error("Failed to check for updates")); - } else { - const result = resp.result as SystemVersionInfo; - setAppVersion(result.local.appVersion); - setSystemVersion(result.local.systemVersion); - - if (result.error) { - notifications.error(`Failed to check for updates: ${result.error}`); - reject(new Error("Failed to check for updates")); - } else { - resolve(result); - } - } - }); - }); - }, [send, setAppVersion, setSystemVersion]); + const { getVersionInfo } = useVersion(); const progressBarRef = useRef(null); useEffect(() => { diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index fa9c4295..a1ace077 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -19,14 +19,12 @@ import { CLOUD_API, DEVICE_API } from "@/ui.config"; import api from "@/api"; import { checkAuth, isInCloud, isOnDevice } from "@/main"; import { cx } from "@/cva.config"; -import notifications from "@/notifications"; import { KeyboardLedState, KeysDownState, NetworkState, OtaState, USBStates, - useDeviceStore, useHidStore, useNetworkStateStore, User, @@ -42,7 +40,7 @@ const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectio const Terminal = lazy(() => import('@components/Terminal')); const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard")); import Modal from "@/components/Modal"; -import { JsonRpcRequest, JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; +import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc"; import { ConnectionFailedOverlay, LoadingConnectionOverlay, @@ -51,7 +49,7 @@ import { import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider"; import { DeviceStatus } from "@routes/welcome-local"; -import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update"; +import { useVersion } from "@/hooks/useVersion"; interface LocalLoaderResp { authMode: "password" | "noPassword" | null; @@ -715,7 +713,7 @@ export default function KvmIdRoute() { send("getKeyDownState", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { // -32601 means the method is not supported - if (resp.error.code === -32601) { + if (resp.error.code === RpcMethodNotFound) { // if we don't support key down state, we know key press is also not available console.warn("Failed to get key down state, switching to old-school", resp.error); setHidRpcDisabled(true); @@ -758,26 +756,13 @@ export default function KvmIdRoute() { if (location.pathname !== "/other-session") navigateTo("/"); }, [navigateTo, location.pathname]); - const { appVersion, setAppVersion, setSystemVersion} = useDeviceStore(); + const { appVersion, getLocalVersion} = useVersion(); useEffect(() => { if (appVersion) return; - send("getUpdateStatus", {}, (resp: JsonRpcResponse) => { - if ("error" in resp) { - notifications.error(`Failed to get device version: ${resp.error}`); - return - } - - const result = resp.result as SystemVersionInfo; - if (result.error) { - notifications.error(`Failed to get device version: ${result.error}`); - } - - setAppVersion(result.local.appVersion); - setSystemVersion(result.local.systemVersion); - }); - }, [appVersion, send, setAppVersion, setSystemVersion]); + getLocalVersion(); + }, [appVersion, getLocalVersion]); const ConnectionStatusElement = useMemo(() => { const hasConnectionFailed =