diff --git a/.vscode/settings.json b/.vscode/settings.json index ba3550bf..2fc7b8b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,5 @@ ] }, "git.ignoreLimitWarning": true, - "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo" + "cmake.sourceDirectory": "/workspaces/kvm-sleep-mode/internal/native/cgo" } \ No newline at end of file diff --git a/internal/ota/app.go b/internal/ota/app.go index 482a07de..5554549c 100644 --- a/internal/ota/app.go +++ b/internal/ota/app.go @@ -34,7 +34,7 @@ func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus) downloadFinished := time.Now() appUpdate.downloadFinishedAt = downloadFinished appUpdate.downloadProgress = 1 - s.onProgressUpdate() + s.triggerStateUpdate() if err := s.verifyFile( appUpdatePath, @@ -48,7 +48,7 @@ func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus) appUpdate.verificationProgress = 1 appUpdate.updatedAt = verifyFinished appUpdate.updateProgress = 1 - s.onProgressUpdate() + s.triggerStateUpdate() l.Info().Msg("App update downloaded") diff --git a/internal/ota/ota.go b/internal/ota/ota.go index b45909ec..21b4705c 100644 --- a/internal/ota/ota.go +++ b/internal/ota/ota.go @@ -21,22 +21,45 @@ func (s *State) GetReleaseAPIEndpoint() string { return s.releaseAPIEndpoint } -func (s *State) fetchUpdateMetadata(ctx context.Context, deviceID string, includePreRelease bool) (*UpdateMetadata, error) { - metadata := &UpdateMetadata{} - +// getUpdateURL returns the update URL for the given parameters +func (s *State) getUpdateURL(params UpdateParams) (string, error) { updateURL, err := url.Parse(s.releaseAPIEndpoint) if err != nil { - return nil, fmt.Errorf("error parsing update metadata URL: %w", err) + return "", fmt.Errorf("error parsing update metadata URL: %w", err) + } + + appTargetVersion := s.GetTargetVersion("app") + if appTargetVersion != "" && params.AppTargetVersion == "" { + params.AppTargetVersion = appTargetVersion + } + systemTargetVersion := s.GetTargetVersion("system") + if systemTargetVersion != "" && params.SystemTargetVersion == "" { + params.SystemTargetVersion = systemTargetVersion } query := updateURL.Query() - query.Set("deviceId", deviceID) - query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) + query.Set("deviceId", params.DeviceID) + query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease)) + if params.AppTargetVersion != "" { + query.Set("appVersion", params.AppTargetVersion) + } + if params.SystemTargetVersion != "" { + query.Set("systemVersion", params.SystemTargetVersion) + } updateURL.RawQuery = query.Encode() - logger.Info().Str("url", updateURL.String()).Msg("Checking for updates") + return updateURL.String(), nil +} - req, err := http.NewRequestWithContext(ctx, "GET", updateURL.String(), nil) +func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*UpdateMetadata, error) { + metadata := &UpdateMetadata{} + + url, err := s.getUpdateURL(params) + if err != nil { + return nil, fmt.Errorf("error getting update URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } @@ -61,38 +84,49 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, deviceID string, includ return metadata, nil } -func (s *State) TryUpdate(ctx context.Context, deviceID string, includePreRelease bool) error { +func (s *State) TryUpdate(ctx context.Context, params UpdateParams) error { + return s.doUpdate(ctx, params) +} + +func (s *State) triggerStateUpdate() { + s.onStateUpdate(s.ToRPCState()) +} + +func (s *State) doUpdate(ctx context.Context, params UpdateParams) error { scopedLogger := s.l.With(). - Str("deviceID", deviceID). - Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)). + Interface("params", params). Logger() - scopedLogger.Info().Msg("Trying to update...") + scopedLogger.Info().Msg("checking for updates") if s.updating { return fmt.Errorf("update already in progress") } s.updating = true - s.onProgressUpdate() + s.triggerStateUpdate() defer func() { s.updating = false - s.onProgressUpdate() + s.triggerStateUpdate() }() - appUpdate, systemUpdate, err := s.getUpdateStatus(ctx, deviceID, includePreRelease) + appUpdate, systemUpdate, err := s.getUpdateStatus(ctx, params) if err != nil { return s.componentUpdateError("Error checking for updates", err, &scopedLogger) } - s.metadataFetchedAt = time.Now() - s.onProgressUpdate() + if params.CheckOnly { + return nil + } - if appUpdate.available { + s.metadataFetchedAt = time.Now() + s.triggerStateUpdate() + + if appUpdate.available || appUpdate.downgradeAvailable { appUpdate.pending = true } - if systemUpdate.available { + if systemUpdate.available || systemUpdate.downgradeAvailable { systemUpdate.pending = true } @@ -133,10 +167,18 @@ func (s *State) TryUpdate(ctx context.Context, deviceID string, includePreReleas return nil } +// 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"` +} + func (s *State) getUpdateStatus( ctx context.Context, - deviceID string, - includePreRelease bool, + params UpdateParams, ) ( appUpdate *componentUpdateStatus, systemUpdate *componentUpdateStatus, @@ -144,7 +186,14 @@ func (s *State) getUpdateStatus( ) { appUpdate = &componentUpdateStatus{} systemUpdate = &componentUpdateStatus{} - err = nil + + if currentAppUpdate, ok := s.componentUpdateStatuses["app"]; ok { + appUpdate = ¤tAppUpdate + } + + if currentSystemUpdate, ok := s.componentUpdateStatuses["system"]; ok { + systemUpdate = ¤tSystemUpdate + } // Get local versions systemVersionLocal, appVersionLocal, err := s.getLocalVersion() @@ -155,7 +204,7 @@ func (s *State) getUpdateStatus( systemUpdate.localVersion = systemVersionLocal.String() // Get remote metadata - remoteMetadata, err := s.fetchUpdateMetadata(ctx, deviceID, includePreRelease) + remoteMetadata, err := s.fetchUpdateMetadata(ctx, params) if err != nil { err = fmt.Errorf("error checking for updates: %w", err) return @@ -175,6 +224,7 @@ func (s *State) getUpdateStatus( return } systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal) + systemUpdate.downgradeAvailable = systemVersionRemote.LessThan(systemVersionLocal) appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion) if err != nil { @@ -182,15 +232,16 @@ func (s *State) getUpdateStatus( return } appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal) + appUpdate.downgradeAvailable = appVersionRemote.LessThan(appVersionLocal) // Handle pre-release updates isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != "" isRemoteAppPreRelease := appVersionRemote.Prerelease() != "" - if isRemoteSystemPreRelease && !includePreRelease { + if isRemoteSystemPreRelease && !params.IncludePreRelease { systemUpdate.available = false } - if isRemoteAppPreRelease && !includePreRelease { + if isRemoteAppPreRelease && !params.IncludePreRelease { appUpdate.available = false } @@ -201,8 +252,8 @@ func (s *State) getUpdateStatus( } // GetUpdateStatus returns the current update status (for backwards compatibility) -func (s *State) GetUpdateStatus(ctx context.Context, deviceID string, includePreRelease bool) (*UpdateStatus, error) { - _, _, err := s.getUpdateStatus(ctx, deviceID, includePreRelease) +func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) { + _, _, err := s.getUpdateStatus(ctx, params) if err != nil { return nil, fmt.Errorf("error getting update status: %w", err) } diff --git a/internal/ota/state.go b/internal/ota/state.go index 4375a08f..6374edc9 100644 --- a/internal/ota/state.go +++ b/internal/ota/state.go @@ -1,6 +1,7 @@ package ota import ( + "fmt" "net/http" "sync" "time" @@ -27,10 +28,12 @@ type LocalMetadata struct { // UpdateStatus represents the current update status type UpdateStatus struct { - Local *LocalMetadata `json:"local"` - Remote *UpdateMetadata `json:"remote"` - SystemUpdateAvailable bool `json:"systemUpdateAvailable"` - AppUpdateAvailable bool `json:"appUpdateAvailable"` + Local *LocalMetadata `json:"local"` + Remote *UpdateMetadata `json:"remote"` + SystemUpdateAvailable bool `json:"systemUpdateAvailable"` + SystemDowngradeAvailable bool `json:"systemDowngradeAvailable"` + AppUpdateAvailable bool `json:"appUpdateAvailable"` + AppDowngradeAvailable bool `json:"appDowngradeAvailable"` // for backwards compatibility Error string `json:"error,omitempty"` @@ -47,8 +50,10 @@ type PostRebootAction struct { type componentUpdateStatus struct { pending bool available bool + downgradeAvailable bool version string localVersion string + targetVersion string url string hash string downloadProgress float32 @@ -79,6 +84,8 @@ type RPCState struct { AppUpdatedAt time.Time `json:"appUpdatedAt,omitempty"` SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement SystemUpdatedAt time.Time `json:"systemUpdatedAt,omitempty"` + SystemTargetVersion string `json:"systemTargetVersion,omitempty"` + AppTargetVersion string `json:"appTargetVersion,omitempty"` } // HwRebootFunc is a function that reboots the hardware @@ -109,6 +116,40 @@ type State struct { client GetHTTPClientFunc reboot HwRebootFunc getLocalVersion GetLocalVersionFunc + onStateUpdate OnStateUpdateFunc +} + +// SetTargetVersion sets the target version for a component +func (s *State) SetTargetVersion(component string, version string) error { + parsedVersion := version + if version != "" { + // validate if it's a valid semver string first + semverVersion, err := semver.NewVersion(version) + if err != nil { + return fmt.Errorf("not a valid semantic version: %w", err) + } + parsedVersion = semverVersion.String() + } + + // check if the component exists + componentUpdate, ok := s.componentUpdateStatuses[component] + if !ok { + return fmt.Errorf("component %s not found", component) + } + + componentUpdate.targetVersion = parsedVersion + s.componentUpdateStatuses[component] = componentUpdate + + return nil +} + +// GetTargetVersion returns the target version for a component +func (s *State) GetTargetVersion(component string) string { + componentUpdate, ok := s.componentUpdateStatuses[component] + if !ok { + return "" + } + return componentUpdate.targetVersion } // ToUpdateStatus converts the State to the UpdateStatus @@ -136,9 +177,11 @@ func (s *State) ToUpdateStatus() *UpdateStatus { SystemURL: systemUpdate.url, SystemHash: systemUpdate.hash, }, - SystemUpdateAvailable: systemUpdate.available, - AppUpdateAvailable: appUpdate.available, - Error: s.error, + SystemUpdateAvailable: systemUpdate.available, + SystemDowngradeAvailable: systemUpdate.downgradeAvailable, + AppUpdateAvailable: appUpdate.available, + AppDowngradeAvailable: appUpdate.downgradeAvailable, + Error: s.error, } } @@ -160,12 +203,17 @@ type Options struct { // NewState creates a new OTA state func NewState(opts Options) *State { + components := make(map[string]componentUpdateStatus) + components["app"] = componentUpdateStatus{} + components["system"] = componentUpdateStatus{} + s := &State{ l: opts.Logger, client: opts.GetHTTPClient, reboot: opts.HwReboot, + onStateUpdate: opts.OnStateUpdate, getLocalVersion: opts.GetLocalVersion, - componentUpdateStatuses: make(map[string]componentUpdateStatus), + componentUpdateStatuses: components, releaseAPIEndpoint: opts.ReleaseAPIEndpoint, } go s.confirmCurrentSystem() @@ -189,6 +237,7 @@ func (s *State) ToRPCState() *RPCState { r.AppVerifiedAt = app.verifiedAt r.AppUpdateProgress = app.updateProgress r.AppUpdatedAt = app.updatedAt + r.AppTargetVersion = app.targetVersion } system, ok := s.componentUpdateStatuses["system"] @@ -200,10 +249,8 @@ func (s *State) ToRPCState() *RPCState { r.SystemVerifiedAt = system.verifiedAt r.SystemUpdateProgress = system.updateProgress r.SystemUpdatedAt = system.updatedAt + r.SystemTargetVersion = system.targetVersion } return r } - -func (s *State) onProgressUpdate() { -} diff --git a/internal/ota/sys.go b/internal/ota/sys.go index 2426c353..334fa1eb 100644 --- a/internal/ota/sys.go +++ b/internal/ota/sys.go @@ -24,7 +24,7 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS downloadFinished := time.Now() systemUpdate.downloadFinishedAt = downloadFinished systemUpdate.downloadProgress = 1 - s.onProgressUpdate() + s.triggerStateUpdate() if err := s.verifyFile( systemUpdatePath, @@ -38,7 +38,7 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS systemUpdate.verificationProgress = 1 systemUpdate.updatedAt = verifyFinished systemUpdate.updateProgress = 1 - s.onProgressUpdate() + s.triggerStateUpdate() l.Info().Msg("System update downloaded") @@ -68,7 +68,7 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS if systemUpdate.updateProgress > 0.99 { systemUpdate.updateProgress = 0.99 } - s.onProgressUpdate() + s.triggerStateUpdate() case <-ctx.Done(): return } @@ -86,7 +86,7 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS rkLogger.Info().Msg("rk_ota success") systemUpdate.updateProgress = 1 systemUpdate.updatedAt = verifyFinished - s.onProgressUpdate() + s.triggerStateUpdate() return nil } diff --git a/internal/ota/utils.go b/internal/ota/utils.go index caa03384..88d99e4d 100644 --- a/internal/ota/utils.go +++ b/internal/ota/utils.go @@ -82,7 +82,7 @@ func (s *State) downloadFile(ctx context.Context, path string, url string, downl progress := float32(written) / float32(totalSize) if progress-*downloadProgress >= 0.01 { *downloadProgress = progress - s.onProgressUpdate() + s.triggerStateUpdate() } } if er != nil { @@ -136,7 +136,7 @@ func (s *State) verifyFile(path string, expectedHash string, verifyProgress *flo progress := float32(verified) / float32(totalSize) if progress-*verifyProgress >= 0.01 { *verifyProgress = progress - s.onProgressUpdate() + s.triggerStateUpdate() } } if er != nil { diff --git a/jsonrpc.go b/jsonrpc.go index 02d80d57..980fac0c 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -19,7 +19,6 @@ import ( "go.bug.st/serial" "github.com/jetkvm/kvm/internal/hidrpc" - "github.com/jetkvm/kvm/internal/ota" "github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/utils" ) @@ -236,71 +235,6 @@ func rpcGetVideoLogStatus() (string, error) { return nativeInstance.VideoLogStatus() } -func rpcGetDevChannelState() (bool, error) { - return config.IncludePreRelease, nil -} - -func rpcSetDevChannelState(enabled bool) error { - config.IncludePreRelease = enabled - if err := SaveConfig(); err != nil { - return fmt.Errorf("failed to save config: %w", err) - } - return nil -} - -func getUpdateStatus(includePreRelease bool) (*ota.UpdateStatus, error) { - updateStatus, err := otaState.GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease) - // to ensure backwards compatibility, - // if there's an error, we won't return an error, but we will set the error field - if err != nil { - if updateStatus == nil { - return nil, fmt.Errorf("error checking for updates: %w", err) - } - updateStatus.Error = err.Error() - } - - logger.Info().Interface("updateStatus", updateStatus).Msg("Update status") - - return updateStatus, nil -} - -func rpcGetUpdateStatus() (*ota.UpdateStatus, error) { - return getUpdateStatus(config.IncludePreRelease) -} - -func rpcGetUpdateStatusChannel(channel string) (*ota.UpdateStatus, error) { - switch channel { - case "stable": - return getUpdateStatus(false) - case "dev": - return getUpdateStatus(true) - default: - return nil, fmt.Errorf("invalid channel: %s", channel) - } -} - -func rpcGetLocalVersion() (*ota.LocalMetadata, error) { - systemVersion, appVersion, err := GetLocalVersion() - if err != nil { - return nil, fmt.Errorf("error getting local version: %w", err) - } - return &ota.LocalMetadata{ - AppVersion: appVersion.String(), - SystemVersion: systemVersion.String(), - }, nil -} - -func rpcTryUpdate() error { - includePreRelease := config.IncludePreRelease - go func() { - err := otaState.TryUpdate(context.Background(), GetDeviceID(), includePreRelease) - if err != nil { - logger.Warn().Err(err).Msg("failed to try update") - } - }() - return nil -} - func rpcSetDisplayRotation(params DisplayRotationSettings) error { currentRotation := config.DisplayRotation if currentRotation == params.Rotation { @@ -1218,6 +1152,8 @@ var rpcHandlers = map[string]RPCHandler{ "getUpdateStatus": {Func: rpcGetUpdateStatus}, "getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel}, "tryUpdate": {Func: rpcTryUpdate}, + "tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"components", "includePreRelease", "checkOnly"}}, + "cancelDowngrade": {Func: rpcCancelDowngrade}, "getDevModeState": {Func: rpcGetDevModeState}, "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, "getSSHKeyState": {Func: rpcGetSSHKeyState}, diff --git a/main.go b/main.go index 893c086a..43784614 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gwatts/rootcerts" + "github.com/jetkvm/kvm/internal/ota" ) var appCtx context.Context @@ -100,7 +101,10 @@ func Main() { } includePreRelease := config.IncludePreRelease - err = otaState.TryUpdate(context.Background(), GetDeviceID(), includePreRelease) + err = otaState.TryUpdate(context.Background(), ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + }) if err != nil { logger.Warn().Err(err).Msg("failed to auto update") } diff --git a/ota.go b/ota.go index 90bd8f28..921cd353 100644 --- a/ota.go +++ b/ota.go @@ -1,6 +1,7 @@ package kvm import ( + "context" "fmt" "net/http" "os" @@ -74,3 +75,114 @@ func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Versio return systemVersion, appVersion, nil } + +func getUpdateStatus(includePreRelease bool) (*ota.UpdateStatus, error) { + updateStatus, err := otaState.GetUpdateStatus(context.Background(), ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + }) + // to ensure backwards compatibility, + // if there's an error, we won't return an error, but we will set the error field + if err != nil { + if updateStatus == nil { + return nil, fmt.Errorf("error checking for updates: %w", err) + } + updateStatus.Error = err.Error() + } + + logger.Info().Interface("updateStatus", updateStatus).Msg("Update status") + + return updateStatus, nil +} + +func rpcGetDevChannelState() (bool, error) { + return config.IncludePreRelease, nil +} + +func rpcSetDevChannelState(enabled bool) error { + config.IncludePreRelease = enabled + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcGetUpdateStatus() (*ota.UpdateStatus, error) { + return getUpdateStatus(config.IncludePreRelease) +} + +func rpcGetUpdateStatusChannel(channel string) (*ota.UpdateStatus, error) { + switch channel { + case "stable": + return getUpdateStatus(false) + case "dev": + return getUpdateStatus(true) + default: + return nil, fmt.Errorf("invalid channel: %s", channel) + } +} + +func rpcGetLocalVersion() (*ota.LocalMetadata, error) { + systemVersion, appVersion, err := GetLocalVersion() + if err != nil { + return nil, fmt.Errorf("error getting local version: %w", err) + } + return &ota.LocalMetadata{ + AppVersion: appVersion.String(), + SystemVersion: systemVersion.String(), + }, nil +} + +// ComponentName represents the name of a component +type tryUpdateComponents struct { + AppTargetVersion string `json:"app"` + SystemTargetVersion string `json:"system"` +} + +func rpcTryUpdate() error { + return rpcTryUpdateComponents(tryUpdateComponents{ + AppTargetVersion: "", + SystemTargetVersion: "", + }, config.IncludePreRelease, false) +} + +func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bool, checkOnly bool) error { + updateParams := ota.UpdateParams{ + DeviceID: GetDeviceID(), + IncludePreRelease: includePreRelease, + CheckOnly: checkOnly, + } + + 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) + } + } + 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) + } + } + + go func() { + err := otaState.TryUpdate(context.Background(), updateParams) + if err != nil { + logger.Warn().Err(err).Msg("failed to try update") + } + }() + 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 +} diff --git a/scripts/dev_deploy.sh b/scripts/dev_deploy.sh index d99f6974..c2afb5cf 100755 --- a/scripts/dev_deploy.sh +++ b/scripts/dev_deploy.sh @@ -26,6 +26,31 @@ show_help() { echo " $0 -r 192.168.0.17 -u admin" } +# Function to check if device is pingable +check_ping() { + local host=$1 + msg_info "▶ Checking if device is reachable at ${host}..." + if ! ping -c 3 -W 5 "${host}" > /dev/null 2>&1; then + msg_err "Error: Cannot reach device at ${host}" + msg_err "Please verify the IP address and network connectivity" + exit 1 + fi + msg_info "✓ Device is reachable" +} + +# Function to check if SSH is accessible +check_ssh() { + local user=$1 + local host=$2 + msg_info "▶ Checking SSH connectivity to ${user}@${host}..." + if ! ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${user}@${host}" "echo 'SSH connection successful'" > /dev/null 2>&1; then + msg_err "Error: Cannot establish SSH connection to ${user}@${host}" + msg_err "Please verify SSH access and credentials" + exit 1 + fi + msg_info "✓ SSH connection successful" +} + # Default values SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") REMOTE_USER="root" @@ -113,6 +138,10 @@ if [ -z "$REMOTE_HOST" ]; then exit 1 fi +# Check device connectivity before proceeding +check_ping "${REMOTE_HOST}" +check_ssh "${REMOTE_USER}" "${REMOTE_HOST}" + # check if the current CPU architecture is x86_64 if [ "$(uname -m)" != "x86_64" ]; then msg_warn "Warning: This script is only supported on x86_64 architecture" @@ -131,7 +160,7 @@ if [[ "$SKIP_UI_BUILD" = true && ! -f "static/index.html" ]]; then SKIP_UI_BUILD=false fi -if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then +if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then msg_info "▶ Building frontend" make frontend SKIP_UI_BUILD=0 SKIP_UI_BUILD_RELEASE=1 @@ -144,13 +173,13 @@ fi if [ "$RUN_GO_TESTS" = true ]; then msg_info "▶ Building go tests" - make build_dev_test + make build_dev_test msg_info "▶ Copying device-tests.tar.gz to remote host" - ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz msg_info "▶ Running go tests" - ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF' + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF' set -e TMP_DIR=$(mktemp -d) cd ${TMP_DIR} @@ -191,35 +220,35 @@ then SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \ SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \ ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} - + # Copy the binary to the remote host as if we were the OTA updater. - ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app - + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app + # Reboot the device, the new app will be deployed by the startup process. - ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot" + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot" else msg_info "▶ Building development binary" do_make build_dev \ SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \ SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \ ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} - + # Kill any existing instances of the application - ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" - + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" + # Copy the binary to the remote host - ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app - + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app + if [ "$RESET_USB_HID_DEVICE" = true ]; then msg_info "▶ Resetting USB HID device" msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed" # Remove the old USB gadget configuration - ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" - ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" fi - + # Deploy and run the application on the remote host - ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF set -e # Set the library path to include the directory where librockit.so is located @@ -229,6 +258,17 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH killall jetkvm_app || true killall jetkvm_app_debug || true +# Wait until both binaries are killed, max 10 seconds +i=1 +while [ \$i -le 10 ]; do + echo "Waiting for jetkvm_app and jetkvm_app_debug to be killed, \$i/10 ..." + if ! pgrep -f "jetkvm_app" > /dev/null && ! pgrep -f "jetkvm_app_debug" > /dev/null; then + break + fi + sleep 1 + i=\$((i + 1)) +done + # Navigate to the directory where the binary will be stored cd "${REMOTE_PATH}" @@ -240,4 +280,4 @@ PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug | tee -a /tmp/jetkvm_app_d EOF fi -echo "Deployment complete." +echo "Deployment complete." \ No newline at end of file diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index 200a766f..67f4033a 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -100,18 +100,6 @@ "advanced_update_ssh_key_button": "Update SSH Key", "advanced_usb_emulation_description": "Control the USB emulation state", "advanced_usb_emulation_title": "USB Emulation", - "advanced_version_update_app_label": "App Version", - "advanced_version_update_button": "Update to Version", - "advanced_version_update_description": "Install a specific version from GitHub releases", - "advanced_version_update_github_link": "JetKVM releases page", - "advanced_version_update_helper": "Find available versions on the", - "advanced_version_update_system_label": "System Version", - "advanced_version_update_target_app": "App only", - "advanced_version_update_target_both": "Both App and System", - "advanced_version_update_target_label": "What to update", - "advanced_version_update_target_system": "System only", - "advanced_version_update_title": "Update to Specific Version", - "advanced_error_version_update": "Failed to initiate version update: {error}", "already_adopted_new_owner": "If you're the new owner, please ask the previous owner to de-register the device from their account in the cloud dashboard. If you believe this is an error, contact our support team for assistance.", "already_adopted_other_user": "This device is currently registered to another user in our cloud dashboard.", "already_adopted_return_to_dashboard": "Return to Dashboard", @@ -904,5 +892,21 @@ "wake_on_lan_invalid_mac": "Invalid MAC address", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "welcome_to_jetkvm": "Welcome to JetKVM", - "welcome_to_jetkvm_description": "Control any computer remotely" + "welcome_to_jetkvm_description": "Control any computer remotely", + "advanced_version_update_app_label": "App Version", + "advanced_version_update_button": "Update to Version", + "advanced_version_update_description": "Install a specific version from GitHub releases", + "advanced_version_update_github_link": "JetKVM releases page", + "advanced_version_update_helper": "Find available versions on the", + "advanced_version_update_system_label": "System Version", + "advanced_version_update_target_app": "App only", + "advanced_version_update_target_both": "Both App and System", + "advanced_version_update_target_label": "What to update", + "advanced_version_update_target_system": "System only", + "advanced_version_update_title": "Update to Specific Version", + "advanced_error_version_update": "Failed to initiate version update: {error}", + "general_update_downgrade_available_description": "A downgrade is available to revert to a previous version.", + "general_update_downgrade_available_title": "Downgrade Available", + "general_update_downgrade_button": "Downgrade Now", + "general_update_keep_current_button": "Keep Current Version" } diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 052c8d9a..6dd17d20 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -536,6 +536,7 @@ export type UpdateModalViews = | "updating" | "upToDate" | "updateAvailable" + | "updateDowngradeAvailable" | "updateCompleted" | "error"; diff --git a/ui/src/hooks/useVersion.tsx b/ui/src/hooks/useVersion.tsx index 7f67e858..af634dec 100644 --- a/ui/src/hooks/useVersion.tsx +++ b/ui/src/hooks/useVersion.tsx @@ -15,7 +15,9 @@ export interface SystemVersionInfo { local: VersionInfo; remote?: VersionInfo; systemUpdateAvailable: boolean; + systemDowngradeAvailable: boolean; appUpdateAvailable: boolean; + appDowngradeAvailable: boolean; error?: string; } diff --git a/ui/src/routes/devices.$id.settings.advanced.tsx b/ui/src/routes/devices.$id.settings.advanced.tsx index 88946f5d..9660274b 100644 --- a/ui/src/routes/devices.$id.settings.advanced.tsx +++ b/ui/src/routes/devices.$id.settings.advanced.tsx @@ -181,9 +181,15 @@ export default function SettingsAdvancedRoute() { }, [applyLoopbackOnlyMode, setShowLoopbackWarning]); const handleVersionUpdate = useCallback(() => { - // TODO: Add version params to tryUpdate - console.log("tryUpdate", updateTarget, appVersion, systemVersion); - send("tryUpdate", {}, (resp: JsonRpcResponse) => { + const params = { + components: { + app: appVersion, + system: systemVersion, + }, + includePreRelease: devChannel, + checkOnly: true, + }; + send("tryUpdateComponents", params, (resp: JsonRpcResponse) => { if ("error" in resp) { notifications.error( m.advanced_error_version_update({ error: resp.error.data || m.unknown_error() }) @@ -193,7 +199,7 @@ export default function SettingsAdvancedRoute() { // Navigate to update page navigateTo("/settings/general/update"); }); - }, [updateTarget, appVersion, systemVersion, send, navigateTo]); + }, [updateTarget, appVersion, systemVersion, devChannel, send, navigateTo]); return (
+ {m.general_update_downgrade_available_title()} +
++ {m.general_update_downgrade_available_description()} +
+
+ {versionInfo?.systemDowngradeAvailable ? (
+ <>
+ {m.general_update_system_type()}: {versionInfo?.remote?.systemVersion}
+
+ >
+ ) : null}
+ {versionInfo?.appDowngradeAvailable ? (
+ <>
+ {m.general_update_application_type()}: {versionInfo?.remote?.appVersion}
+ >
+ ) : null}
+