mirror of https://github.com/jetkvm/kvm.git
Compare commits
No commits in common. "bce968000c0b74015c56ec162d712e22e27dcb39" and "a69f7c9c504a286acaba43d03be190aee064112d" have entirely different histories.
bce968000c
...
a69f7c9c50
|
|
@ -10,5 +10,5 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"git.ignoreLimitWarning": true,
|
"git.ignoreLimitWarning": true,
|
||||||
"cmake.sourceDirectory": "/workspaces/kvm-sleep-mode/internal/native/cgo"
|
"cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo"
|
||||||
}
|
}
|
||||||
|
|
@ -27,14 +27,14 @@ func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus)
|
||||||
|
|
||||||
l := s.l.With().Str("path", appUpdatePath).Logger()
|
l := s.l.With().Str("path", appUpdatePath).Logger()
|
||||||
|
|
||||||
if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, "app"); err != nil {
|
if err := s.downloadFile(ctx, appUpdatePath, appUpdate.url, &appUpdate.downloadProgress); err != nil {
|
||||||
return s.componentUpdateError("Error downloading app update", err, &l)
|
return s.componentUpdateError("Error downloading app update", err, &l)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
appUpdate.downloadFinishedAt = downloadFinished
|
appUpdate.downloadFinishedAt = downloadFinished
|
||||||
appUpdate.downloadProgress = 1
|
appUpdate.downloadProgress = 1
|
||||||
s.triggerComponentUpdateState("app", appUpdate)
|
s.onProgressUpdate()
|
||||||
|
|
||||||
if err := s.verifyFile(
|
if err := s.verifyFile(
|
||||||
appUpdatePath,
|
appUpdatePath,
|
||||||
|
|
@ -48,7 +48,7 @@ func (s *State) updateApp(ctx context.Context, appUpdate *componentUpdateStatus)
|
||||||
appUpdate.verificationProgress = 1
|
appUpdate.verificationProgress = 1
|
||||||
appUpdate.updatedAt = verifyFinished
|
appUpdate.updatedAt = verifyFinished
|
||||||
appUpdate.updateProgress = 1
|
appUpdate.updateProgress = 1
|
||||||
s.triggerComponentUpdateState("app", appUpdate)
|
s.onProgressUpdate()
|
||||||
|
|
||||||
l.Info().Msg("App update downloaded")
|
l.Info().Msg("App update downloaded")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package ota
|
||||||
|
|
||||||
|
import "github.com/jetkvm/kvm/internal/logging"
|
||||||
|
|
||||||
|
var logger = logging.GetSubsystemLogger("ota")
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
|
@ -22,49 +21,22 @@ func (s *State) GetReleaseAPIEndpoint() string {
|
||||||
return s.releaseAPIEndpoint
|
return s.releaseAPIEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUpdateURL returns the update URL for the given parameters
|
func (s *State) fetchUpdateMetadata(ctx context.Context, deviceID string, includePreRelease bool) (*UpdateMetadata, error) {
|
||||||
func (s *State) getUpdateURL(params UpdateParams) (string, error) {
|
metadata := &UpdateMetadata{}
|
||||||
|
|
||||||
updateURL, err := url.Parse(s.releaseAPIEndpoint)
|
updateURL, err := url.Parse(s.releaseAPIEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error parsing update metadata URL: %w", err)
|
return nil, 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 := updateURL.Query()
|
||||||
query.Set("deviceId", params.DeviceID)
|
query.Set("deviceId", deviceID)
|
||||||
query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease))
|
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
|
||||||
if params.AppTargetVersion != "" {
|
|
||||||
query.Set("appVersion", params.AppTargetVersion)
|
|
||||||
}
|
|
||||||
if params.SystemTargetVersion != "" {
|
|
||||||
query.Set("systemVersion", params.SystemTargetVersion)
|
|
||||||
}
|
|
||||||
updateURL.RawQuery = query.Encode()
|
updateURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
return updateURL.String(), nil
|
logger.Info().Str("url", updateURL.String()).Msg("Checking for updates")
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*UpdateMetadata, error) {
|
req, err := http.NewRequestWithContext(ctx, "GET", updateURL.String(), nil)
|
||||||
metadata := &UpdateMetadata{}
|
|
||||||
|
|
||||||
url, err := s.getUpdateURL(params)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting update URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.l.Trace().
|
|
||||||
Str("url", url).
|
|
||||||
Msg("fetching update metadata")
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating request: %w", err)
|
return nil, fmt.Errorf("error creating request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -89,68 +61,39 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) TryUpdate(ctx context.Context, params UpdateParams) error {
|
func (s *State) TryUpdate(ctx context.Context, deviceID string, includePreRelease bool) error {
|
||||||
return s.doUpdate(ctx, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) triggerStateUpdate() {
|
|
||||||
s.onStateUpdate(s.ToRPCState())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) triggerComponentUpdateState(component string, update *componentUpdateStatus) {
|
|
||||||
s.componentUpdateStatuses[component] = *update
|
|
||||||
s.triggerStateUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
|
||||||
scopedLogger := s.l.With().
|
scopedLogger := s.l.With().
|
||||||
Interface("params", params).
|
Str("deviceID", deviceID).
|
||||||
|
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("checking for updates")
|
scopedLogger.Info().Msg("Trying to update...")
|
||||||
if s.updating {
|
if s.updating {
|
||||||
return fmt.Errorf("update already in progress")
|
return fmt.Errorf("update already in progress")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(params.Components) == 0 {
|
|
||||||
params.Components = []string{"app", "system"}
|
|
||||||
}
|
|
||||||
shouldUpdateApp := slices.Contains(params.Components, "app")
|
|
||||||
shouldUpdateSystem := slices.Contains(params.Components, "system")
|
|
||||||
|
|
||||||
if !shouldUpdateApp && !shouldUpdateSystem {
|
|
||||||
return fmt.Errorf("no components to update")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !params.CheckOnly {
|
|
||||||
s.updating = true
|
s.updating = true
|
||||||
s.triggerStateUpdate()
|
s.onProgressUpdate()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
s.updating = false
|
s.updating = false
|
||||||
s.triggerStateUpdate()
|
s.onProgressUpdate()
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
appUpdate, systemUpdate, err := s.getUpdateStatus(ctx, params)
|
appUpdate, systemUpdate, err := s.getUpdateStatus(ctx, deviceID, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s.componentUpdateError("Error checking for updates", err, &scopedLogger)
|
return s.componentUpdateError("Error checking for updates", err, &scopedLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.metadataFetchedAt = time.Now()
|
s.metadataFetchedAt = time.Now()
|
||||||
s.triggerStateUpdate()
|
s.onProgressUpdate()
|
||||||
|
|
||||||
if params.CheckOnly {
|
if appUpdate.available {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldUpdateApp && (appUpdate.available || appUpdate.downgradeAvailable) {
|
|
||||||
appUpdate.pending = true
|
appUpdate.pending = true
|
||||||
s.triggerComponentUpdateState("app", appUpdate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldUpdateSystem && (systemUpdate.available || systemUpdate.downgradeAvailable) {
|
if systemUpdate.available {
|
||||||
systemUpdate.pending = true
|
systemUpdate.pending = true
|
||||||
s.triggerComponentUpdateState("system", systemUpdate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if appUpdate.pending {
|
if appUpdate.pending {
|
||||||
|
|
@ -177,19 +120,9 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
||||||
if s.rebootNeeded {
|
if s.rebootNeeded {
|
||||||
scopedLogger.Info().Msg("System Rebooting due to OTA update")
|
scopedLogger.Info().Msg("System Rebooting due to OTA update")
|
||||||
|
|
||||||
redirectUrl := fmt.Sprintf("/settings/general/update?version=%s", systemUpdate.version)
|
|
||||||
|
|
||||||
if params.ResetConfig {
|
|
||||||
scopedLogger.Info().Msg("Resetting config")
|
|
||||||
if err := s.resetConfig(); err != nil {
|
|
||||||
return s.componentUpdateError("Error resetting config", err, &scopedLogger)
|
|
||||||
}
|
|
||||||
redirectUrl = "/device/setup"
|
|
||||||
}
|
|
||||||
|
|
||||||
postRebootAction := &PostRebootAction{
|
postRebootAction := &PostRebootAction{
|
||||||
HealthCheck: "/device/status",
|
HealthCheck: "/device/status",
|
||||||
RedirectUrl: redirectUrl,
|
RedirectUrl: fmt.Sprintf("/settings/general/update?version=%s", systemUpdate.version),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.reboot(true, postRebootAction, 10*time.Second); err != nil {
|
if err := s.reboot(true, postRebootAction, 10*time.Second); err != nil {
|
||||||
|
|
@ -200,20 +133,10 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateParams represents the parameters for the update
|
|
||||||
type UpdateParams struct {
|
|
||||||
DeviceID string `json:"deviceID"`
|
|
||||||
AppTargetVersion string `json:"appTargetVersion"`
|
|
||||||
SystemTargetVersion string `json:"systemTargetVersion"`
|
|
||||||
Components []string `json:"components,omitempty"`
|
|
||||||
IncludePreRelease bool `json:"includePreRelease"`
|
|
||||||
CheckOnly bool `json:"checkOnly"`
|
|
||||||
ResetConfig bool `json:"resetConfig"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) getUpdateStatus(
|
func (s *State) getUpdateStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
params UpdateParams,
|
deviceID string,
|
||||||
|
includePreRelease bool,
|
||||||
) (
|
) (
|
||||||
appUpdate *componentUpdateStatus,
|
appUpdate *componentUpdateStatus,
|
||||||
systemUpdate *componentUpdateStatus,
|
systemUpdate *componentUpdateStatus,
|
||||||
|
|
@ -221,14 +144,7 @@ func (s *State) getUpdateStatus(
|
||||||
) {
|
) {
|
||||||
appUpdate = &componentUpdateStatus{}
|
appUpdate = &componentUpdateStatus{}
|
||||||
systemUpdate = &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
|
// Get local versions
|
||||||
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
|
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
|
||||||
|
|
@ -239,7 +155,7 @@ func (s *State) getUpdateStatus(
|
||||||
systemUpdate.localVersion = systemVersionLocal.String()
|
systemUpdate.localVersion = systemVersionLocal.String()
|
||||||
|
|
||||||
// Get remote metadata
|
// Get remote metadata
|
||||||
remoteMetadata, err := s.fetchUpdateMetadata(ctx, params)
|
remoteMetadata, err := s.fetchUpdateMetadata(ctx, deviceID, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("error checking for updates: %w", err)
|
err = fmt.Errorf("error checking for updates: %w", err)
|
||||||
return
|
return
|
||||||
|
|
@ -259,7 +175,6 @@ func (s *State) getUpdateStatus(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal)
|
systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal)
|
||||||
systemUpdate.downgradeAvailable = systemVersionRemote.LessThan(systemVersionLocal)
|
|
||||||
|
|
||||||
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -267,16 +182,15 @@ func (s *State) getUpdateStatus(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal)
|
appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal)
|
||||||
appUpdate.downgradeAvailable = appVersionRemote.LessThan(appVersionLocal)
|
|
||||||
|
|
||||||
// Handle pre-release updates
|
// Handle pre-release updates
|
||||||
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
||||||
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
||||||
|
|
||||||
if isRemoteSystemPreRelease && !params.IncludePreRelease {
|
if isRemoteSystemPreRelease && !includePreRelease {
|
||||||
systemUpdate.available = false
|
systemUpdate.available = false
|
||||||
}
|
}
|
||||||
if isRemoteAppPreRelease && !params.IncludePreRelease {
|
if isRemoteAppPreRelease && !includePreRelease {
|
||||||
appUpdate.available = false
|
appUpdate.available = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,8 +201,8 @@ func (s *State) getUpdateStatus(
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUpdateStatus returns the current update status (for backwards compatibility)
|
// GetUpdateStatus returns the current update status (for backwards compatibility)
|
||||||
func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) {
|
func (s *State) GetUpdateStatus(ctx context.Context, deviceID string, includePreRelease bool) (*UpdateStatus, error) {
|
||||||
_, _, err := s.getUpdateStatus(ctx, params)
|
_, _, err := s.getUpdateStatus(ctx, deviceID, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting update status: %w", err)
|
return nil, fmt.Errorf("error getting update status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package ota
|
package ota
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -31,9 +30,7 @@ type UpdateStatus struct {
|
||||||
Local *LocalMetadata `json:"local"`
|
Local *LocalMetadata `json:"local"`
|
||||||
Remote *UpdateMetadata `json:"remote"`
|
Remote *UpdateMetadata `json:"remote"`
|
||||||
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
||||||
SystemDowngradeAvailable bool `json:"systemDowngradeAvailable"`
|
|
||||||
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
||||||
AppDowngradeAvailable bool `json:"appDowngradeAvailable"`
|
|
||||||
|
|
||||||
// for backwards compatibility
|
// for backwards compatibility
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|
@ -50,10 +47,8 @@ type PostRebootAction struct {
|
||||||
type componentUpdateStatus struct {
|
type componentUpdateStatus struct {
|
||||||
pending bool
|
pending bool
|
||||||
available bool
|
available bool
|
||||||
downgradeAvailable bool
|
|
||||||
version string
|
version string
|
||||||
localVersion string
|
localVersion string
|
||||||
targetVersion string
|
|
||||||
url string
|
url string
|
||||||
hash string
|
hash string
|
||||||
downloadProgress float32
|
downloadProgress float32
|
||||||
|
|
@ -62,38 +57,33 @@ type componentUpdateStatus struct {
|
||||||
verifiedAt time.Time
|
verifiedAt time.Time
|
||||||
updateProgress float32
|
updateProgress float32
|
||||||
updatedAt time.Time
|
updatedAt time.Time
|
||||||
dependsOn []string //nolint:unused
|
dependsOn []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPCState represents the current OTA state for the RPC API
|
// RPCState represents the current OTA state for the RPC API
|
||||||
type RPCState struct {
|
type RPCState struct {
|
||||||
Updating bool `json:"updating"`
|
Updating bool `json:"updating"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
MetadataFetchedAt *time.Time `json:"metadataFetchedAt,omitempty"`
|
MetadataFetchedAt time.Time `json:"metadataFetchedAt,omitempty"`
|
||||||
AppUpdatePending bool `json:"appUpdatePending"`
|
AppUpdatePending bool `json:"appUpdatePending"`
|
||||||
SystemUpdatePending bool `json:"systemUpdatePending"`
|
SystemUpdatePending bool `json:"systemUpdatePending"`
|
||||||
AppDownloadProgress *float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar
|
AppDownloadProgress float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar
|
||||||
AppDownloadFinishedAt *time.Time `json:"appDownloadFinishedAt,omitempty"`
|
AppDownloadFinishedAt time.Time `json:"appDownloadFinishedAt,omitempty"`
|
||||||
SystemDownloadProgress *float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar
|
SystemDownloadProgress float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar
|
||||||
SystemDownloadFinishedAt *time.Time `json:"systemDownloadFinishedAt,omitempty"`
|
SystemDownloadFinishedAt time.Time `json:"systemDownloadFinishedAt,omitempty"`
|
||||||
AppVerificationProgress *float32 `json:"appVerificationProgress,omitempty"`
|
AppVerificationProgress float32 `json:"appVerificationProgress,omitempty"`
|
||||||
AppVerifiedAt *time.Time `json:"appVerifiedAt,omitempty"`
|
AppVerifiedAt time.Time `json:"appVerifiedAt,omitempty"`
|
||||||
SystemVerificationProgress *float32 `json:"systemVerificationProgress,omitempty"`
|
SystemVerificationProgress float32 `json:"systemVerificationProgress,omitempty"`
|
||||||
SystemVerifiedAt *time.Time `json:"systemVerifiedAt,omitempty"`
|
SystemVerifiedAt time.Time `json:"systemVerifiedAt,omitempty"`
|
||||||
AppUpdateProgress *float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar
|
AppUpdateProgress float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar
|
||||||
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
AppUpdatedAt time.Time `json:"appUpdatedAt,omitempty"`
|
||||||
SystemUpdateProgress *float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
||||||
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
SystemUpdatedAt time.Time `json:"systemUpdatedAt,omitempty"`
|
||||||
SystemTargetVersion *string `json:"systemTargetVersion,omitempty"`
|
|
||||||
AppTargetVersion *string `json:"appTargetVersion,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HwRebootFunc is a function that reboots the hardware
|
// HwRebootFunc is a function that reboots the hardware
|
||||||
type HwRebootFunc func(force bool, postRebootAction *PostRebootAction, delay time.Duration) error
|
type HwRebootFunc func(force bool, postRebootAction *PostRebootAction, delay time.Duration) error
|
||||||
|
|
||||||
// ResetConfigFunc is a function that resets the config
|
|
||||||
type ResetConfigFunc func() error
|
|
||||||
|
|
||||||
// GetHTTPClientFunc is a function that returns the HTTP client
|
// GetHTTPClientFunc is a function that returns the HTTP client
|
||||||
type GetHTTPClientFunc func() *http.Client
|
type GetHTTPClientFunc func() *http.Client
|
||||||
|
|
||||||
|
|
@ -119,41 +109,6 @@ type State struct {
|
||||||
client GetHTTPClientFunc
|
client GetHTTPClientFunc
|
||||||
reboot HwRebootFunc
|
reboot HwRebootFunc
|
||||||
getLocalVersion GetLocalVersionFunc
|
getLocalVersion GetLocalVersionFunc
|
||||||
onStateUpdate OnStateUpdateFunc
|
|
||||||
resetConfig ResetConfigFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// ToUpdateStatus converts the State to the UpdateStatus
|
||||||
|
|
@ -182,9 +137,7 @@ func (s *State) ToUpdateStatus() *UpdateStatus {
|
||||||
SystemHash: systemUpdate.hash,
|
SystemHash: systemUpdate.hash,
|
||||||
},
|
},
|
||||||
SystemUpdateAvailable: systemUpdate.available,
|
SystemUpdateAvailable: systemUpdate.available,
|
||||||
SystemDowngradeAvailable: systemUpdate.downgradeAvailable,
|
|
||||||
AppUpdateAvailable: appUpdate.available,
|
AppUpdateAvailable: appUpdate.available,
|
||||||
AppDowngradeAvailable: appUpdate.downgradeAvailable,
|
|
||||||
Error: s.error,
|
Error: s.error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,73 +156,54 @@ type Options struct {
|
||||||
OnProgressUpdate OnProgressUpdateFunc
|
OnProgressUpdate OnProgressUpdateFunc
|
||||||
HwReboot HwRebootFunc
|
HwReboot HwRebootFunc
|
||||||
ReleaseAPIEndpoint string
|
ReleaseAPIEndpoint string
|
||||||
ResetConfig ResetConfigFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewState creates a new OTA state
|
// NewState creates a new OTA state
|
||||||
func NewState(opts Options) *State {
|
func NewState(opts Options) *State {
|
||||||
components := make(map[string]componentUpdateStatus)
|
|
||||||
components["app"] = componentUpdateStatus{}
|
|
||||||
components["system"] = componentUpdateStatus{}
|
|
||||||
|
|
||||||
s := &State{
|
s := &State{
|
||||||
l: opts.Logger,
|
l: opts.Logger,
|
||||||
client: opts.GetHTTPClient,
|
client: opts.GetHTTPClient,
|
||||||
reboot: opts.HwReboot,
|
reboot: opts.HwReboot,
|
||||||
onStateUpdate: opts.OnStateUpdate,
|
|
||||||
getLocalVersion: opts.GetLocalVersion,
|
getLocalVersion: opts.GetLocalVersion,
|
||||||
componentUpdateStatuses: components,
|
componentUpdateStatuses: make(map[string]componentUpdateStatus),
|
||||||
releaseAPIEndpoint: opts.ReleaseAPIEndpoint,
|
releaseAPIEndpoint: opts.ReleaseAPIEndpoint,
|
||||||
resetConfig: opts.ResetConfig,
|
|
||||||
}
|
}
|
||||||
go s.confirmCurrentSystem()
|
go s.confirmCurrentSystem()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRPCState converts the State to the RPCState
|
// ToRPCState converts the State to the RPCState
|
||||||
// probably we need a generator for this ...
|
|
||||||
func (s *State) ToRPCState() *RPCState {
|
func (s *State) ToRPCState() *RPCState {
|
||||||
r := &RPCState{
|
r := &RPCState{
|
||||||
Updating: s.updating,
|
Updating: s.updating,
|
||||||
Error: s.error,
|
Error: s.error,
|
||||||
MetadataFetchedAt: &s.metadataFetchedAt,
|
MetadataFetchedAt: s.metadataFetchedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
app, ok := s.componentUpdateStatuses["app"]
|
app, ok := s.componentUpdateStatuses["app"]
|
||||||
if ok {
|
if ok {
|
||||||
r.AppUpdatePending = app.pending
|
r.AppUpdatePending = app.pending
|
||||||
r.AppDownloadProgress = &app.downloadProgress
|
r.AppDownloadProgress = app.downloadProgress
|
||||||
if !app.downloadFinishedAt.IsZero() {
|
r.AppDownloadFinishedAt = app.downloadFinishedAt
|
||||||
r.AppDownloadFinishedAt = &app.downloadFinishedAt
|
r.AppVerificationProgress = app.verificationProgress
|
||||||
}
|
r.AppVerifiedAt = app.verifiedAt
|
||||||
r.AppVerificationProgress = &app.verificationProgress
|
r.AppUpdateProgress = app.updateProgress
|
||||||
if !app.verifiedAt.IsZero() {
|
r.AppUpdatedAt = app.updatedAt
|
||||||
r.AppVerifiedAt = &app.verifiedAt
|
|
||||||
}
|
|
||||||
r.AppUpdateProgress = &app.updateProgress
|
|
||||||
if !app.updatedAt.IsZero() {
|
|
||||||
r.AppUpdatedAt = &app.updatedAt
|
|
||||||
}
|
|
||||||
r.AppTargetVersion = &app.targetVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
system, ok := s.componentUpdateStatuses["system"]
|
system, ok := s.componentUpdateStatuses["system"]
|
||||||
if ok {
|
if ok {
|
||||||
r.SystemUpdatePending = system.pending
|
r.SystemUpdatePending = system.pending
|
||||||
r.SystemDownloadProgress = &system.downloadProgress
|
r.SystemDownloadProgress = system.downloadProgress
|
||||||
if !system.downloadFinishedAt.IsZero() {
|
r.SystemDownloadFinishedAt = system.downloadFinishedAt
|
||||||
r.SystemDownloadFinishedAt = &system.downloadFinishedAt
|
r.SystemVerificationProgress = system.verificationProgress
|
||||||
}
|
r.SystemVerifiedAt = system.verifiedAt
|
||||||
r.SystemVerificationProgress = &system.verificationProgress
|
r.SystemUpdateProgress = system.updateProgress
|
||||||
if !system.verifiedAt.IsZero() {
|
r.SystemUpdatedAt = system.updatedAt
|
||||||
r.SystemVerifiedAt = &system.verifiedAt
|
|
||||||
}
|
|
||||||
r.SystemUpdateProgress = &system.updateProgress
|
|
||||||
if !system.updatedAt.IsZero() {
|
|
||||||
r.SystemUpdatedAt = &system.updatedAt
|
|
||||||
}
|
|
||||||
r.SystemTargetVersion = &system.targetVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *State) onProgressUpdate() {
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS
|
||||||
|
|
||||||
l := s.l.With().Str("path", systemUpdatePath).Logger()
|
l := s.l.With().Str("path", systemUpdatePath).Logger()
|
||||||
|
|
||||||
if err := s.downloadFile(ctx, systemUpdatePath, systemUpdate.url, "system"); err != nil {
|
if err := s.downloadFile(ctx, systemUpdatePath, systemUpdate.url, &systemUpdate.downloadProgress); err != nil {
|
||||||
return s.componentUpdateError("Error downloading system update", err, &l)
|
return s.componentUpdateError("Error downloading system update", err, &l)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
systemUpdate.downloadFinishedAt = downloadFinished
|
systemUpdate.downloadFinishedAt = downloadFinished
|
||||||
systemUpdate.downloadProgress = 1
|
systemUpdate.downloadProgress = 1
|
||||||
s.triggerComponentUpdateState("system", systemUpdate)
|
s.onProgressUpdate()
|
||||||
|
|
||||||
if err := s.verifyFile(
|
if err := s.verifyFile(
|
||||||
systemUpdatePath,
|
systemUpdatePath,
|
||||||
|
|
@ -38,7 +38,7 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS
|
||||||
systemUpdate.verificationProgress = 1
|
systemUpdate.verificationProgress = 1
|
||||||
systemUpdate.updatedAt = verifyFinished
|
systemUpdate.updatedAt = verifyFinished
|
||||||
systemUpdate.updateProgress = 1
|
systemUpdate.updateProgress = 1
|
||||||
s.triggerComponentUpdateState("system", systemUpdate)
|
s.onProgressUpdate()
|
||||||
|
|
||||||
l.Info().Msg("System update downloaded")
|
l.Info().Msg("System update downloaded")
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS
|
||||||
if systemUpdate.updateProgress > 0.99 {
|
if systemUpdate.updateProgress > 0.99 {
|
||||||
systemUpdate.updateProgress = 0.99
|
systemUpdate.updateProgress = 0.99
|
||||||
}
|
}
|
||||||
s.triggerComponentUpdateState("system", systemUpdate)
|
s.onProgressUpdate()
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -84,11 +84,9 @@ func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateS
|
||||||
return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger)
|
return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger)
|
||||||
}
|
}
|
||||||
rkLogger.Info().Msg("rk_ota success")
|
rkLogger.Info().Msg("rk_ota success")
|
||||||
|
|
||||||
s.rebootNeeded = true
|
|
||||||
systemUpdate.updateProgress = 1
|
systemUpdate.updateProgress = 1
|
||||||
systemUpdate.updatedAt = verifyFinished
|
systemUpdate.updatedAt = verifyFinished
|
||||||
s.triggerComponentUpdateState("system", systemUpdate)
|
s.onProgressUpdate()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,7 @@ func syncFilesystem() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) downloadFile(ctx context.Context, path string, url string, component string) error {
|
func (s *State) downloadFile(ctx context.Context, path string, url string, downloadProgress *float32) error {
|
||||||
componentUpdate, ok := s.componentUpdateStatuses[component]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("component %s not found", component)
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadProgress := componentUpdate.downloadProgress
|
|
||||||
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
if _, err := os.Stat(path); err == nil {
|
||||||
if err := os.Remove(path); err != nil {
|
if err := os.Remove(path); err != nil {
|
||||||
return fmt.Errorf("error removing existing file: %w", err)
|
return fmt.Errorf("error removing existing file: %w", err)
|
||||||
|
|
@ -87,9 +80,9 @@ func (s *State) downloadFile(ctx context.Context, path string, url string, compo
|
||||||
return fmt.Errorf("error writing to file: %w", ew)
|
return fmt.Errorf("error writing to file: %w", ew)
|
||||||
}
|
}
|
||||||
progress := float32(written) / float32(totalSize)
|
progress := float32(written) / float32(totalSize)
|
||||||
if progress-downloadProgress >= 0.01 {
|
if progress-*downloadProgress >= 0.01 {
|
||||||
componentUpdate.downloadProgress = progress
|
*downloadProgress = progress
|
||||||
s.triggerComponentUpdateState(component, &componentUpdate)
|
s.onProgressUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if er != nil {
|
if er != nil {
|
||||||
|
|
@ -143,7 +136,7 @@ func (s *State) verifyFile(path string, expectedHash string, verifyProgress *flo
|
||||||
progress := float32(verified) / float32(totalSize)
|
progress := float32(verified) / float32(totalSize)
|
||||||
if progress-*verifyProgress >= 0.01 {
|
if progress-*verifyProgress >= 0.01 {
|
||||||
*verifyProgress = progress
|
*verifyProgress = progress
|
||||||
s.triggerStateUpdate()
|
s.onProgressUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if er != nil {
|
if er != nil {
|
||||||
|
|
|
||||||
68
jsonrpc.go
68
jsonrpc.go
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/hidrpc"
|
"github.com/jetkvm/kvm/internal/hidrpc"
|
||||||
|
"github.com/jetkvm/kvm/internal/ota"
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
"github.com/jetkvm/kvm/internal/utils"
|
"github.com/jetkvm/kvm/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -235,6 +236,71 @@ func rpcGetVideoLogStatus() (string, error) {
|
||||||
return nativeInstance.VideoLogStatus()
|
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 {
|
func rpcSetDisplayRotation(params DisplayRotationSettings) error {
|
||||||
currentRotation := config.DisplayRotation
|
currentRotation := config.DisplayRotation
|
||||||
if currentRotation == params.Rotation {
|
if currentRotation == params.Rotation {
|
||||||
|
|
@ -1152,8 +1218,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getUpdateStatus": {Func: rpcGetUpdateStatus},
|
"getUpdateStatus": {Func: rpcGetUpdateStatus},
|
||||||
"getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel},
|
"getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel},
|
||||||
"tryUpdate": {Func: rpcTryUpdate},
|
"tryUpdate": {Func: rpcTryUpdate},
|
||||||
"tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"components", "includePreRelease", "checkOnly", "resetConfig"}},
|
|
||||||
"cancelDowngrade": {Func: rpcCancelDowngrade},
|
|
||||||
"getDevModeState": {Func: rpcGetDevModeState},
|
"getDevModeState": {Func: rpcGetDevModeState},
|
||||||
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
|
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
|
||||||
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gwatts/rootcerts"
|
"github.com/gwatts/rootcerts"
|
||||||
"github.com/jetkvm/kvm/internal/ota"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var appCtx context.Context
|
var appCtx context.Context
|
||||||
|
|
@ -101,10 +100,7 @@ func Main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
includePreRelease := config.IncludePreRelease
|
includePreRelease := config.IncludePreRelease
|
||||||
err = otaState.TryUpdate(context.Background(), ota.UpdateParams{
|
err = otaState.TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
DeviceID: GetDeviceID(),
|
|
||||||
IncludePreRelease: includePreRelease,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to auto update")
|
logger.Warn().Err(err).Msg("failed to auto update")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
116
ota.go
116
ota.go
|
|
@ -1,7 +1,6 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -30,7 +29,6 @@ func initOta() {
|
||||||
},
|
},
|
||||||
GetLocalVersion: GetLocalVersion,
|
GetLocalVersion: GetLocalVersion,
|
||||||
HwReboot: hwReboot,
|
HwReboot: hwReboot,
|
||||||
ResetConfig: rpcResetConfig,
|
|
||||||
OnStateUpdate: func(state *ota.RPCState) {
|
OnStateUpdate: func(state *ota.RPCState) {
|
||||||
triggerOTAStateUpdate(state)
|
triggerOTAStateUpdate(state)
|
||||||
},
|
},
|
||||||
|
|
@ -76,117 +74,3 @@ func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Versio
|
||||||
|
|
||||||
return systemVersion, appVersion, nil
|
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"`
|
|
||||||
Components string `json:"components,omitempty"` // components is a comma-separated list of components to update
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcTryUpdate() error {
|
|
||||||
return rpcTryUpdateComponents(tryUpdateComponents{
|
|
||||||
AppTargetVersion: "",
|
|
||||||
SystemTargetVersion: "",
|
|
||||||
}, config.IncludePreRelease, false, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bool, checkOnly bool, resetConfig bool) error {
|
|
||||||
updateParams := ota.UpdateParams{
|
|
||||||
DeviceID: GetDeviceID(),
|
|
||||||
IncludePreRelease: includePreRelease,
|
|
||||||
CheckOnly: checkOnly,
|
|
||||||
ResetConfig: resetConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info().Interface("components", components).Msg("components")
|
|
||||||
|
|
||||||
updateParams.AppTargetVersion = components.AppTargetVersion
|
|
||||||
if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil {
|
|
||||||
return fmt.Errorf("failed to set app target version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParams.SystemTargetVersion = components.SystemTargetVersion
|
|
||||||
if err := otaState.SetTargetVersion("system", components.SystemTargetVersion); err != nil {
|
|
||||||
return fmt.Errorf("failed to set system target version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if components.Components != "" {
|
|
||||||
updateParams.Components = strings.Split(components.Components, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -26,31 +26,6 @@ show_help() {
|
||||||
echo " $0 -r 192.168.0.17 -u admin"
|
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
|
# Default values
|
||||||
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
|
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
|
||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
|
|
@ -138,10 +113,6 @@ if [ -z "$REMOTE_HOST" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# check if the current CPU architecture is x86_64
|
||||||
if [ "$(uname -m)" != "x86_64" ]; then
|
if [ "$(uname -m)" != "x86_64" ]; then
|
||||||
msg_warn "Warning: This script is only supported on x86_64 architecture"
|
msg_warn "Warning: This script is only supported on x86_64 architecture"
|
||||||
|
|
@ -176,10 +147,10 @@ if [ "$RUN_GO_TESTS" = true ]; then
|
||||||
make build_dev_test
|
make build_dev_test
|
||||||
|
|
||||||
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||||
|
|
||||||
msg_info "▶ Running go tests"
|
msg_info "▶ Running go tests"
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
||||||
set -e
|
set -e
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
cd ${TMP_DIR}
|
cd ${TMP_DIR}
|
||||||
|
|
@ -222,10 +193,10 @@ then
|
||||||
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
|
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
|
||||||
|
|
||||||
# Copy the binary to the remote host as if we were the OTA updater.
|
# Copy the binary to the remote host as if we were the OTA updater.
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
ssh "${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.
|
# Reboot the device, the new app will be deployed by the startup process.
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||||
else
|
else
|
||||||
msg_info "▶ Building development binary"
|
msg_info "▶ Building development binary"
|
||||||
do_make build_dev \
|
do_make build_dev \
|
||||||
|
|
@ -234,21 +205,21 @@ else
|
||||||
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
|
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
|
||||||
|
|
||||||
# Kill any existing instances of the application
|
# Kill any existing instances of the application
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||||
|
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||||
|
|
||||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||||
msg_info "▶ Resetting USB HID device"
|
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"
|
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
|
# Remove the old USB gadget configuration
|
||||||
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 "${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"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Deploy and run the application on the remote host
|
# Deploy and run the application on the remote host
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Set the library path to include the directory where librockit.so is located
|
# Set the library path to include the directory where librockit.so is located
|
||||||
|
|
@ -258,17 +229,6 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
|
||||||
killall jetkvm_app || true
|
killall jetkvm_app || true
|
||||||
killall jetkvm_app_debug || 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
|
# Navigate to the directory where the binary will be stored
|
||||||
cd "${REMOTE_PATH}"
|
cd "${REMOTE_PATH}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,18 @@
|
||||||
"advanced_update_ssh_key_button": "Update SSH Key",
|
"advanced_update_ssh_key_button": "Update SSH Key",
|
||||||
"advanced_usb_emulation_description": "Control the USB emulation state",
|
"advanced_usb_emulation_description": "Control the USB emulation state",
|
||||||
"advanced_usb_emulation_title": "USB Emulation",
|
"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_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_other_user": "This device is currently registered to another user in our cloud dashboard.",
|
||||||
"already_adopted_return_to_dashboard": "Return to Dashboard",
|
"already_adopted_return_to_dashboard": "Return to Dashboard",
|
||||||
|
|
@ -892,23 +904,5 @@
|
||||||
"wake_on_lan_invalid_mac": "Invalid MAC address",
|
"wake_on_lan_invalid_mac": "Invalid MAC address",
|
||||||
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
|
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
|
||||||
"welcome_to_jetkvm": "Welcome to JetKVM",
|
"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",
|
|
||||||
"advanced_version_update_reset_config_description": "Reset configuration after the update",
|
|
||||||
"advanced_version_update_reset_config_label": "Reset configuration"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -536,7 +536,6 @@ export type UpdateModalViews =
|
||||||
| "updating"
|
| "updating"
|
||||||
| "upToDate"
|
| "upToDate"
|
||||||
| "updateAvailable"
|
| "updateAvailable"
|
||||||
| "updateDowngradeAvailable"
|
|
||||||
| "updateCompleted"
|
| "updateCompleted"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import semver from "semver";
|
|
||||||
|
|
||||||
import { useDeviceStore } from "@/hooks/stores";
|
import { useDeviceStore } from "@/hooks/stores";
|
||||||
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import semver from "semver";
|
||||||
|
|
||||||
export interface VersionInfo {
|
export interface VersionInfo {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
|
|
@ -15,9 +15,7 @@ export interface SystemVersionInfo {
|
||||||
local: VersionInfo;
|
local: VersionInfo;
|
||||||
remote?: VersionInfo;
|
remote?: VersionInfo;
|
||||||
systemUpdateAvailable: boolean;
|
systemUpdateAvailable: boolean;
|
||||||
systemDowngradeAvailable: boolean;
|
|
||||||
appUpdateAvailable: boolean;
|
appUpdateAvailable: boolean;
|
||||||
appDowngradeAvailable: boolean;
|
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useSettingsStore } from "@hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Checkbox, { CheckboxWithLabel } from "@components/Checkbox";
|
import Checkbox from "@components/Checkbox";
|
||||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import { SettingsItem } from "@components/SettingsItem";
|
import { SettingsItem } from "@components/SettingsItem";
|
||||||
|
|
@ -30,7 +30,6 @@ export default function SettingsAdvancedRoute() {
|
||||||
const [updateTarget, setUpdateTarget] = useState<string>("app");
|
const [updateTarget, setUpdateTarget] = useState<string>("app");
|
||||||
const [appVersion, setAppVersion] = useState<string>("");
|
const [appVersion, setAppVersion] = useState<string>("");
|
||||||
const [systemVersion, setSystemVersion] = useState<string>("");
|
const [systemVersion, setSystemVersion] = useState<string>("");
|
||||||
const [resetConfig, setResetConfig] = useState(false);
|
|
||||||
|
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
|
|
@ -182,33 +181,19 @@ export default function SettingsAdvancedRoute() {
|
||||||
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
||||||
|
|
||||||
const handleVersionUpdate = useCallback(() => {
|
const handleVersionUpdate = useCallback(() => {
|
||||||
const params = {
|
// TODO: Add version params to tryUpdate
|
||||||
components: {
|
console.log("tryUpdate", updateTarget, appVersion, systemVersion);
|
||||||
app: appVersion,
|
send("tryUpdate", {}, (resp: JsonRpcResponse) => {
|
||||||
system: systemVersion,
|
|
||||||
},
|
|
||||||
includePreRelease: devChannel,
|
|
||||||
checkOnly: true,
|
|
||||||
// no need to reset config for a check only update
|
|
||||||
resetConfig: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
send("tryUpdateComponents", params, (resp: JsonRpcResponse) => {
|
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
m.advanced_error_version_update({ error: resp.error.data || m.unknown_error() })
|
m.advanced_error_version_update({ error: resp.error.data || m.unknown_error() })
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pageParams = new URLSearchParams();
|
|
||||||
pageParams.set("downgrade", "true");
|
|
||||||
pageParams.set("resetConfig", resetConfig.toString());
|
|
||||||
pageParams.set("components", updateTarget == "both" ? "app,system" : updateTarget);
|
|
||||||
|
|
||||||
// Navigate to update page
|
// Navigate to update page
|
||||||
navigateTo(`/settings/general/update?${pageParams.toString()}`);
|
navigateTo("/settings/general/update");
|
||||||
});
|
});
|
||||||
}, [updateTarget, appVersion, systemVersion, devChannel, send, navigateTo, resetConfig]);
|
}, [updateTarget, appVersion, systemVersion, send, navigateTo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -347,15 +332,6 @@ export default function SettingsAdvancedRoute() {
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
|
||||||
<CheckboxWithLabel
|
|
||||||
label={m.advanced_version_update_reset_config_label()}
|
|
||||||
description={m.advanced_version_update_reset_config_description()}
|
|
||||||
checked={resetConfig}
|
|
||||||
onChange={e => setResetConfig(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
|
|
||||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
||||||
|
|
@ -14,35 +14,16 @@ import { m } from "@localizations/messages.js";
|
||||||
export default function SettingsGeneralUpdateRoute() {
|
export default function SettingsGeneralUpdateRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
//@ts-ignore
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const { updateSuccess } = location.state || {};
|
const { updateSuccess } = location.state || {};
|
||||||
|
|
||||||
const { setModalView, otaState } = useUpdateStore();
|
const { setModalView, otaState } = useUpdateStore();
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
|
||||||
const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]);
|
|
||||||
const updateComponents = useMemo(() => searchParams.get("components") || "", [searchParams]);
|
|
||||||
const resetConfig = useMemo(() => searchParams.get("resetConfig") === "true", [searchParams]);
|
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
send("tryUpdate", {});
|
send("tryUpdate", {});
|
||||||
setModalView("updating");
|
setModalView("updating");
|
||||||
}, [send, setModalView]);
|
}, [send, setModalView]);
|
||||||
|
|
||||||
const onConfirmDowngrade = useCallback((system?: string, app?: string) => {
|
|
||||||
send("tryUpdateComponents", {
|
|
||||||
components: {
|
|
||||||
system, app,
|
|
||||||
components: updateComponents
|
|
||||||
},
|
|
||||||
includePreRelease: true,
|
|
||||||
checkOnly: false,
|
|
||||||
resetConfig: resetConfig,
|
|
||||||
});
|
|
||||||
setModalView("updating");
|
|
||||||
}, [send, setModalView, updateComponents, resetConfig]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (otaState.updating) {
|
if (otaState.updating) {
|
||||||
setModalView("updating");
|
setModalView("updating");
|
||||||
|
|
@ -55,56 +36,37 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
}
|
}
|
||||||
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
||||||
|
|
||||||
return <Dialog
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
onClose={() => navigate("..")}
|
|
||||||
onConfirmUpdate={onConfirmUpdate}
|
|
||||||
onConfirmDowngrade={onConfirmDowngrade}
|
|
||||||
downgrade={downgrade}
|
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
onClose,
|
onClose,
|
||||||
onConfirmUpdate,
|
onConfirmUpdate,
|
||||||
onConfirmDowngrade,
|
|
||||||
downgrade,
|
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
downgrade: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirmUpdate: () => void;
|
onConfirmUpdate: () => void;
|
||||||
onConfirmDowngrade: () => void;
|
|
||||||
}>) {
|
}>) {
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
||||||
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
|
||||||
const { modalView, setModalView, otaState } = useUpdateStore();
|
const { modalView, setModalView, otaState } = useUpdateStore();
|
||||||
const { send } = useJsonRpc();
|
|
||||||
|
|
||||||
const onFinishedLoading = useCallback(
|
const onFinishedLoading = useCallback(
|
||||||
(versionInfo: SystemVersionInfo) => {
|
(versionInfo: SystemVersionInfo) => {
|
||||||
const hasUpdate =
|
const hasUpdate =
|
||||||
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
|
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
|
||||||
const hasDowngrade =
|
|
||||||
versionInfo?.systemDowngradeAvailable || versionInfo?.appDowngradeAvailable;
|
|
||||||
|
|
||||||
setVersionInfo(versionInfo);
|
setVersionInfo(versionInfo);
|
||||||
|
|
||||||
if (hasDowngrade && downgrade) {
|
if (hasUpdate) {
|
||||||
setModalView("updateDowngradeAvailable");
|
|
||||||
} else if (hasUpdate) {
|
|
||||||
setModalView("updateAvailable");
|
setModalView("updateAvailable");
|
||||||
} else {
|
} else {
|
||||||
setModalView("upToDate");
|
setModalView("upToDate");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setModalView, downgrade],
|
[setModalView],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCancelDowngrade = useCallback(() => {
|
|
||||||
send("cancelDowngrade", {});
|
|
||||||
onClose();
|
|
||||||
}, [onClose, send]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto relative mx-auto text-left">
|
<div className="pointer-events-auto relative mx-auto text-left">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -127,13 +89,6 @@ export function Dialog({
|
||||||
versionInfo={versionInfo!}
|
versionInfo={versionInfo!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{modalView === "updateDowngradeAvailable" && (
|
|
||||||
<UpdateDowngradeAvailableState
|
|
||||||
onConfirmDowngrade={onConfirmDowngrade}
|
|
||||||
onCancelDowngrade={onCancelDowngrade}
|
|
||||||
versionInfo={versionInfo!}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "updating" && (
|
{modalView === "updating" && (
|
||||||
<UpdatingDeviceState
|
<UpdatingDeviceState
|
||||||
|
|
@ -445,52 +400,6 @@ function UpdateAvailableState({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateDowngradeAvailableState({
|
|
||||||
versionInfo,
|
|
||||||
onConfirmDowngrade,
|
|
||||||
onCancelDowngrade,
|
|
||||||
}: {
|
|
||||||
versionInfo: SystemVersionInfo;
|
|
||||||
onConfirmDowngrade: (system?: string, app?: string) => void;
|
|
||||||
onCancelDowngrade: () => void;
|
|
||||||
}) {
|
|
||||||
const confirmDowngrade = useCallback(() => {
|
|
||||||
onConfirmDowngrade(
|
|
||||||
versionInfo?.remote?.systemVersion || undefined,
|
|
||||||
versionInfo?.remote?.appVersion || undefined,
|
|
||||||
);
|
|
||||||
}, [versionInfo, 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">
|
|
||||||
{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={confirmDowngrade} />
|
|
||||||
<Button size="SM" theme="light" text={m.general_update_keep_current_button()} onClick={onCancelDowngrade} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue