mirror of https://github.com/jetkvm/kvm.git
feat: downgrade
This commit is contained in:
parent
ab06e376d0
commit
0a98a73275
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
68
jsonrpc.go
68
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"
|
||||
)
|
||||
|
|
@ -237,71 +236,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 {
|
||||
|
|
@ -1219,6 +1153,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},
|
||||
|
|
|
|||
6
main.go
6
main.go
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gwatts/rootcerts"
|
||||
"github.com/jetkvm/kvm/internal/ota"
|
||||
)
|
||||
|
||||
var appCtx context.Context
|
||||
|
|
@ -107,7 +108,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")
|
||||
}
|
||||
|
|
|
|||
112
ota.go
112
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -910,5 +898,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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ export type UpdateModalViews =
|
|||
| "updating"
|
||||
| "upToDate"
|
||||
| "updateAvailable"
|
||||
| "updateDowngradeAvailable"
|
||||
| "updateCompleted"
|
||||
| "error";
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,22 @@ import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc";
|
|||
import { getUpdateStatus, getLocalVersion as getLocalVersionRpc } from "@/utils/jsonrpc";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import semver from "semver";
|
||||
|
||||
export interface VersionInfo {
|
||||
appVersion: string;
|
||||
systemVersion: string;
|
||||
}
|
||||
|
||||
export interface SystemVersionInfo {
|
||||
local: VersionInfo;
|
||||
remote?: VersionInfo;
|
||||
systemUpdateAvailable: boolean;
|
||||
systemDowngradeAvailable: boolean;
|
||||
appUpdateAvailable: boolean;
|
||||
appDowngradeAvailable: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function useVersion() {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -182,9 +182,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() })
|
||||
|
|
@ -194,7 +200,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 (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -59,16 +59,21 @@ export function Dialog({
|
|||
|
||||
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
||||
const { modalView, setModalView, otaState } = useUpdateStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onFinishedLoading = useCallback(
|
||||
(versionInfo: SystemVersionInfo) => {
|
||||
const hasUpdate =
|
||||
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
|
||||
const hasDowngrade =
|
||||
versionInfo?.systemDowngradeAvailable || versionInfo?.appDowngradeAvailable;
|
||||
|
||||
setVersionInfo(versionInfo);
|
||||
|
||||
if (hasUpdate) {
|
||||
setModalView("updateAvailable");
|
||||
} else if (hasDowngrade) {
|
||||
setModalView("updateDowngradeAvailable");
|
||||
} else {
|
||||
setModalView("upToDate");
|
||||
}
|
||||
|
|
@ -76,6 +81,11 @@ export function Dialog({
|
|||
[setModalView],
|
||||
);
|
||||
|
||||
const onCancelDowngrade = useCallback(() => {
|
||||
send("cancelDowngrade", {});
|
||||
onClose();
|
||||
}, [onClose, send]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
|
|
@ -98,6 +108,13 @@ export function Dialog({
|
|||
versionInfo={versionInfo!}
|
||||
/>
|
||||
)}
|
||||
{modalView === "updateDowngradeAvailable" && (
|
||||
<UpdateDowngradeAvailableState
|
||||
onConfirmUpdate={onConfirmUpdate}
|
||||
onCancelDowngrade={onCancelDowngrade}
|
||||
versionInfo={versionInfo!}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "updating" && (
|
||||
<UpdatingDeviceState
|
||||
|
|
@ -410,6 +427,46 @@ function UpdateAvailableState({
|
|||
);
|
||||
}
|
||||
|
||||
function UpdateDowngradeAvailableState({
|
||||
versionInfo,
|
||||
onConfirmUpdate,
|
||||
onCancelDowngrade,
|
||||
}: {
|
||||
versionInfo: SystemVersionInfo;
|
||||
onConfirmUpdate: () => void;
|
||||
onCancelDowngrade: () => void;
|
||||
}) {
|
||||
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">
|
||||
{versionInfo?.systemDowngradeAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion}
|
||||
<br />
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.appDowngradeAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">{m.general_update_application_type()}</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_downgrade_button()} onClick={onConfirmUpdate} />
|
||||
<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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue