mirror of https://github.com/jetkvm/kvm.git
Compare commits
10 Commits
d5e602bbff
...
0db1c542b5
| Author | SHA1 | Date |
|---|---|---|
|
|
0db1c542b5 | |
|
|
62b8dee170 | |
|
|
0ef128e6f2 | |
|
|
70cd19ddbc | |
|
|
72e2367011 | |
|
|
d7a56213ea | |
|
|
85c98ee998 | |
|
|
69f429d0a5 | |
|
|
0984ca7e40 | |
|
|
2fce23c5d6 |
9
go.mod
9
go.mod
|
|
@ -11,7 +11,7 @@ require (
|
|||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-co-op/gocron/v2 v2.16.6
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guregu/null/v6 v6.0.0
|
||||
|
|
@ -53,7 +53,6 @@ require (
|
|||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
|
|
@ -80,22 +79,16 @@ require (
|
|||
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -40,8 +40,8 @@ github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqr
|
|||
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
|
||||
github.com/go-co-op/gocron/v2 v2.16.6/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
|
|
@ -56,8 +56,6 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
|
|||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
|
|
@ -150,10 +148,6 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D
|
|||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
|
|
@ -189,22 +183,16 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
|||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -216,8 +204,6 @@ golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
|||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
27
main.go
27
main.go
|
|
@ -14,6 +14,7 @@ import (
|
|||
var appCtx context.Context
|
||||
|
||||
func Main() {
|
||||
logger.Log().Msg("JetKVM Starting Up")
|
||||
LoadConfig()
|
||||
|
||||
var cancel context.CancelFunc
|
||||
|
|
@ -78,16 +79,16 @@ func Main() {
|
|||
initDisplay()
|
||||
|
||||
go func() {
|
||||
// wait for 15 minutes before starting auto-update checks
|
||||
// this is to avoid interfering with initial setup processes
|
||||
// and to ensure the system is stable before checking for updates
|
||||
time.Sleep(15 * time.Minute)
|
||||
for {
|
||||
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
|
||||
if !config.AutoUpdateEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
||||
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
for {
|
||||
logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
|
||||
if !config.AutoUpdateEnabled {
|
||||
logger.Debug().Msg("auto-update disabled")
|
||||
time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +98,12 @@ func Main() {
|
|||
continue
|
||||
}
|
||||
|
||||
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
||||
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
includePreRelease := config.IncludePreRelease
|
||||
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||
if err != nil {
|
||||
|
|
@ -106,6 +113,7 @@ func Main() {
|
|||
time.Sleep(1 * time.Hour)
|
||||
}
|
||||
}()
|
||||
|
||||
//go RunFuseServer()
|
||||
go RunWebServer()
|
||||
|
||||
|
|
@ -122,7 +130,8 @@ func Main() {
|
|||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigs
|
||||
logger.Info().Msg("JetKVM Shutting Down")
|
||||
|
||||
logger.Log().Msg("JetKVM Shutting Down")
|
||||
//if fuseServer != nil {
|
||||
// err := setMassStorageImage(" ")
|
||||
// if err != nil {
|
||||
|
|
|
|||
45
ota.go
45
ota.go
|
|
@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
|||
if nr > 0 {
|
||||
nw, ew := file.Write(buf[0:nr])
|
||||
if nw < nr {
|
||||
return fmt.Errorf("short write: %d < %d", nw, nr)
|
||||
return fmt.Errorf("short file write: %d < %d", nw, nr)
|
||||
}
|
||||
written += int64(nw)
|
||||
if ew != nil {
|
||||
|
|
@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
|||
if nr > 0 {
|
||||
nw, ew := hash.Write(buf[0:nr])
|
||||
if nw < nr {
|
||||
return fmt.Errorf("short write: %d < %d", nw, nr)
|
||||
return fmt.Errorf("short hash write: %d < %d", nw, nr)
|
||||
}
|
||||
verified += int64(nw)
|
||||
if ew != nil {
|
||||
|
|
@ -260,11 +260,14 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
|||
}
|
||||
}
|
||||
|
||||
hashSum := hash.Sum(nil)
|
||||
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of")
|
||||
// close the file so we can rename below
|
||||
fileToHash.Close()
|
||||
|
||||
if hex.EncodeToString(hashSum) != expectedHash {
|
||||
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
||||
hashSum := hex.EncodeToString(hash.Sum(nil))
|
||||
scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of")
|
||||
|
||||
if hashSum != expectedHash {
|
||||
return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash)
|
||||
}
|
||||
|
||||
if err := os.Rename(unverifiedPath, path); err != nil {
|
||||
|
|
@ -296,6 +299,8 @@ type OTAState struct {
|
|||
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
||||
SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
||||
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
||||
RebootNeeded bool `json:"rebootNeeded,omitempty"`
|
||||
Rebooting bool `json:"rebooting,omitempty"`
|
||||
}
|
||||
|
||||
var otaState = OTAState{}
|
||||
|
|
@ -313,7 +318,7 @@ func triggerOTAStateUpdate() {
|
|||
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
||||
scopedLogger := otaLogger.With().
|
||||
Str("deviceId", deviceId).
|
||||
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
||||
Bool("includePreRelease", includePreRelease).
|
||||
Logger()
|
||||
|
||||
scopedLogger.Info().Msg("Trying to update...")
|
||||
|
|
@ -322,7 +327,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
}
|
||||
|
||||
otaState = OTAState{
|
||||
Updating: true,
|
||||
Updating: true,
|
||||
RebootNeeded: false,
|
||||
}
|
||||
triggerOTAStateUpdate()
|
||||
|
||||
|
|
@ -335,7 +341,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
if err != nil {
|
||||
otaState.Error = fmt.Sprintf("Error checking for updates: %v", err)
|
||||
scopedLogger.Error().Err(err).Msg("Error checking for updates")
|
||||
return fmt.Errorf("error checking for updates: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
|
@ -349,8 +355,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
appUpdateAvailable := updateStatus.AppUpdateAvailable
|
||||
systemUpdateAvailable := updateStatus.SystemUpdateAvailable
|
||||
|
||||
rebootNeeded := false
|
||||
|
||||
if appUpdateAvailable {
|
||||
scopedLogger.Info().
|
||||
Str("local", local.AppVersion).
|
||||
|
|
@ -361,7 +365,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
if err != nil {
|
||||
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
||||
scopedLogger.Error().Err(err).Msg("Error downloading app update")
|
||||
triggerOTAStateUpdate()
|
||||
return err
|
||||
}
|
||||
downloadFinished := time.Now()
|
||||
|
|
@ -378,18 +381,20 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
if err != nil {
|
||||
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
||||
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
|
||||
triggerOTAStateUpdate()
|
||||
return err
|
||||
}
|
||||
verifyFinished := time.Now()
|
||||
otaState.AppVerifiedAt = &verifyFinished
|
||||
otaState.AppVerificationProgress = 1
|
||||
triggerOTAStateUpdate()
|
||||
|
||||
otaState.AppUpdatedAt = &verifyFinished
|
||||
otaState.AppUpdateProgress = 1
|
||||
triggerOTAStateUpdate()
|
||||
|
||||
scopedLogger.Info().Msg("App update downloaded")
|
||||
rebootNeeded = true
|
||||
otaState.RebootNeeded = true
|
||||
triggerOTAStateUpdate()
|
||||
} else {
|
||||
scopedLogger.Info().Msg("App is up to date")
|
||||
}
|
||||
|
|
@ -404,7 +409,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
if err != nil {
|
||||
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
||||
scopedLogger.Error().Err(err).Msg("Error downloading system update")
|
||||
triggerOTAStateUpdate()
|
||||
return err
|
||||
}
|
||||
downloadFinished := time.Now()
|
||||
|
|
@ -421,7 +425,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
if err != nil {
|
||||
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
||||
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
|
||||
triggerOTAStateUpdate()
|
||||
return err
|
||||
}
|
||||
scopedLogger.Info().Msg("System update downloaded")
|
||||
|
|
@ -441,6 +444,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
|
||||
return fmt.Errorf("error starting rk_ota command: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -481,14 +485,19 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
|||
otaState.SystemUpdateProgress = 1
|
||||
otaState.SystemUpdatedAt = &verifyFinished
|
||||
triggerOTAStateUpdate()
|
||||
rebootNeeded = true
|
||||
|
||||
otaState.RebootNeeded = true
|
||||
triggerOTAStateUpdate()
|
||||
} else {
|
||||
scopedLogger.Info().Msg("System is up to date")
|
||||
}
|
||||
|
||||
if rebootNeeded {
|
||||
if otaState.RebootNeeded {
|
||||
scopedLogger.Info().Msg("System Rebooting in 10s")
|
||||
time.Sleep(10 * time.Second)
|
||||
otaState.Rebooting = true
|
||||
triggerOTAStateUpdate()
|
||||
|
||||
cmd := exec.Command("reboot")
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
|||
Button.displayName = "Button";
|
||||
|
||||
type LinkPropsType = Pick<LinkProps, "to"> &
|
||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
|
||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||
const classes = cx(
|
||||
"group outline-hidden",
|
||||
|
|
@ -231,7 +231,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<Link to={to} className={classes}>
|
||||
<Link to={to} reloadDocument={props.reloadDocument} className={classes}>
|
||||
<ButtonContent {...props} />
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -518,6 +518,7 @@ export type UpdateModalViews =
|
|||
| "upToDate"
|
||||
| "updateAvailable"
|
||||
| "updateCompleted"
|
||||
| "rebooting"
|
||||
| "error";
|
||||
|
||||
export interface OtaState {
|
||||
|
|
@ -549,19 +550,26 @@ export interface OtaState {
|
|||
|
||||
systemUpdateProgress: number;
|
||||
systemUpdatedAt: string | null;
|
||||
|
||||
rebootNeeded: boolean;
|
||||
rebooting: boolean;
|
||||
};
|
||||
|
||||
export interface UpdateState {
|
||||
isUpdatePending: boolean;
|
||||
setIsUpdatePending: (isPending: boolean) => void;
|
||||
|
||||
updateDialogHasBeenMinimized: boolean;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
|
||||
otaState: OtaState;
|
||||
setOtaState: (state: OtaState) => void;
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||
|
||||
modalView: UpdateModalViews
|
||||
setModalView: (view: UpdateModalViews) => void;
|
||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||
|
||||
updateErrorMessage: string | null;
|
||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||
}
|
||||
|
||||
export const useUpdateStore = create<UpdateState>(set => ({
|
||||
|
|
@ -587,13 +595,17 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
|||
appUpdatedAt: null,
|
||||
systemUpdateProgress: 0,
|
||||
systemUpdatedAt: null,
|
||||
rebootNeeded: false,
|
||||
rebooting: false,
|
||||
},
|
||||
|
||||
updateDialogHasBeenMinimized: false,
|
||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
||||
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
||||
|
||||
modalView: "loading",
|
||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||
|
||||
updateErrorMessage: null,
|
||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export async function checkDeviceAuth() {
|
|||
}
|
||||
|
||||
export async function checkAuth() {
|
||||
return import.meta.env.MODE === "device" ? checkDeviceAuth() : checkCloudAuth();
|
||||
return isOnDevice ? checkDeviceAuth() : checkCloudAuth();
|
||||
}
|
||||
|
||||
let router;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
}
|
||||
|
||||
getCloudState();
|
||||
// In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore
|
||||
// In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore
|
||||
if (!isOnDevice) navigate("/");
|
||||
return;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ import { Button } from "@components/Button";
|
|||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
navigate(".."); // back to the devices.$id.settings page
|
||||
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
|
||||
}, [navigate]);
|
||||
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
// This is where we send the RPC to the golang binary
|
||||
|
|
@ -16,7 +22,7 @@ export default function SettingsGeneralRebootRoute() {
|
|||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
|
|
@ -18,6 +19,11 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
const { setModalView, otaState } = useUpdateStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
navigate(".."); // back to the devices.$id.settings page
|
||||
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
|
||||
}, [navigate]);
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
send("tryUpdate", {});
|
||||
setModalView("updating");
|
||||
|
|
@ -33,16 +39,14 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
} else {
|
||||
setModalView("loading");
|
||||
}
|
||||
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
||||
}, [otaState.error, otaState.updating, setModalView, updateSuccess]);
|
||||
|
||||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function Dialog({
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
|
|
@ -224,7 +228,7 @@ function UpdatingDeviceState({
|
|||
100,
|
||||
);
|
||||
} else {
|
||||
// System: 10% download, 90% update
|
||||
// System: 10% download, 10% verification, 80% update
|
||||
return Math.min(
|
||||
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
|
||||
100,
|
||||
|
|
@ -239,14 +243,18 @@ function UpdatingDeviceState({
|
|||
|
||||
if (!otaState.metadataFetchedAt) {
|
||||
return "Fetching update information...";
|
||||
} else if (otaState.rebooting) {
|
||||
return "Rebooting...";
|
||||
} else if (!downloadFinishedAt) {
|
||||
return `Downloading ${type} update...`;
|
||||
} else if (!verfiedAt) {
|
||||
return `Verifying ${type} update...`;
|
||||
} else if (!updatedAt) {
|
||||
return `Installing ${type} update...`;
|
||||
} else if (otaState.rebootNeeded) {
|
||||
return "Reboot needed";
|
||||
} else {
|
||||
return `Awaiting reboot`;
|
||||
return "Awaiting reboot";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -278,12 +286,51 @@ function UpdatingDeviceState({
|
|||
<Card className="space-y-4 p-4">
|
||||
{areAllUpdatesComplete() ? (
|
||||
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Rebooting to complete the update...
|
||||
</span>
|
||||
</div>
|
||||
<CheckCircleIcon className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
{otaState.rebooting ? (
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Rebooting the device to complete the update...
|
||||
</span>
|
||||
<p className="flex-col text-black dark:text-white">
|
||||
This may take a few minutes. The device will automatically
|
||||
reconnect once it is back online.<br/>
|
||||
If it doesn{"'"}t reconnect automatically, you can manually
|
||||
reconnect by clicking here:
|
||||
<LinkButton
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Reconnect to KVM"
|
||||
LeadingIcon={MdConnectWithoutContact}
|
||||
textAlign="center"
|
||||
reloadDocument={true}
|
||||
to={".."}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
otaState.rebootNeeded && (
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Device reboot is pending...
|
||||
</span>
|
||||
<p className="flex-col text-black dark:text-white">
|
||||
The JetKVM is preparing to reboot. This may take a while.<br/>
|
||||
If it doesn{"'"}t automatically reboot after a few minutes, you
|
||||
can manually request a reboot by clicking here:
|
||||
<LinkButton
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Reboot the KVM"
|
||||
LeadingIcon={MdRestartAlt}
|
||||
textAlign="center"
|
||||
reloadDocument={true}
|
||||
to={"../reboot"}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Outlet,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
|
|
@ -15,7 +14,7 @@ import { FocusTrap } from "focus-trap-react";
|
|||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
import api from "@/api";
|
||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||
import { cx } from "@/cva.config";
|
||||
|
|
@ -48,7 +47,6 @@ import {
|
|||
} from "@/components/VideoOverlay";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||
import { DeviceStatus } from "@routes/welcome-local";
|
||||
import { useVersion } from "@/hooks/useVersion";
|
||||
|
||||
interface LocalLoaderResp {
|
||||
|
|
@ -70,20 +68,8 @@ export interface LocalDevice {
|
|||
}
|
||||
|
||||
const deviceLoader = async () => {
|
||||
const res = await api
|
||||
.GET(`${DEVICE_API}/device/status`)
|
||||
.then(res => res.json() as Promise<DeviceStatus>);
|
||||
|
||||
if (!res.isSetup) return redirect("/welcome");
|
||||
|
||||
const deviceRes = await api.GET(`${DEVICE_API}/device`);
|
||||
if (deviceRes.status === 401) return redirect("/login-local");
|
||||
if (deviceRes.ok) {
|
||||
const device = (await deviceRes.json()) as LocalDevice;
|
||||
return { authMode: device.authMode };
|
||||
}
|
||||
|
||||
throw new Error("Error fetching device");
|
||||
const device = await checkAuth();
|
||||
return { authMode: device.authMode } as LocalLoaderResp;
|
||||
};
|
||||
|
||||
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
||||
|
|
@ -106,11 +92,11 @@ const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> =>
|
|||
device: { id: string; name: string; user: { googleId: string } };
|
||||
};
|
||||
|
||||
return { user, iceConfig, deviceName: device.name || device.id };
|
||||
return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp;
|
||||
};
|
||||
|
||||
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
||||
return isOnDevice ? deviceLoader() : cloudLoader(params);
|
||||
};
|
||||
|
||||
export default function KvmIdRoute() {
|
||||
|
|
@ -146,6 +132,7 @@ export default function KvmIdRoute() {
|
|||
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
||||
|
||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||
|
||||
const cleanupAndStopReconnecting = useCallback(
|
||||
function cleanupAndStopReconnecting() {
|
||||
console.log("Closing peer connection");
|
||||
|
|
@ -182,11 +169,11 @@ export default function KvmIdRoute() {
|
|||
pc: RTCPeerConnection,
|
||||
remoteDescription: RTCSessionDescriptionInit,
|
||||
) {
|
||||
setLoadingMessage("Setting remote description");
|
||||
setLoadingMessage("Setting remote description type:" + remoteDescription.type);
|
||||
|
||||
try {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
||||
console.log("[setRemoteSessionDescription] Remote description set successfully");
|
||||
console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
|
||||
setLoadingMessage("Establishing secure connection...");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
|
@ -231,9 +218,15 @@ export default function KvmIdRoute() {
|
|||
const ignoreOffer = useRef(false);
|
||||
const isSettingRemoteAnswerPending = useRef(false);
|
||||
const makingOffer = useRef(false);
|
||||
const reconnectAttemptsRef = useRef(2000);
|
||||
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const reconnectInterval = (attempt: number) => {
|
||||
// Exponential backoff with a max of 10 seconds between attempts
|
||||
return Math.min(500 * 2 ** attempt, 10000);
|
||||
}
|
||||
|
||||
const { sendMessage, getWebSocket } = useWebSocket(
|
||||
isOnDevice
|
||||
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
|
||||
|
|
@ -241,17 +234,16 @@ export default function KvmIdRoute() {
|
|||
{
|
||||
heartbeat: true,
|
||||
retryOnError: true,
|
||||
reconnectAttempts: 15,
|
||||
reconnectInterval: 1000,
|
||||
onReconnectStop: () => {
|
||||
console.debug("Reconnect stopped");
|
||||
reconnectAttempts: reconnectAttemptsRef.current,
|
||||
reconnectInterval: reconnectInterval,
|
||||
onReconnectStop: (attempt: number) => {
|
||||
console.debug("Reconnect stopped after ", attempt, "attempts");
|
||||
cleanupAndStopReconnecting();
|
||||
},
|
||||
|
||||
shouldReconnect(event) {
|
||||
console.debug("[Websocket] shouldReconnect", event);
|
||||
// TODO: Why true?
|
||||
return true;
|
||||
return !connectionFailed; // we always want to try to reconnect unless we're explicitly stopped
|
||||
},
|
||||
|
||||
onClose(event) {
|
||||
|
|
@ -284,6 +276,7 @@ export default function KvmIdRoute() {
|
|||
*/
|
||||
|
||||
const parsedMessage = JSON.parse(message.data);
|
||||
|
||||
if (parsedMessage.type === "device-metadata") {
|
||||
const { deviceVersion } = parsedMessage.data;
|
||||
console.debug("[Websocket] Received device-metadata message");
|
||||
|
|
@ -300,10 +293,12 @@ export default function KvmIdRoute() {
|
|||
console.log("[Websocket] Device is using new signaling");
|
||||
isLegacySignalingEnabled.current = false;
|
||||
}
|
||||
|
||||
setupPeerConnection();
|
||||
}
|
||||
|
||||
if (!peerConnection) return;
|
||||
|
||||
if (parsedMessage.type === "answer") {
|
||||
console.debug("[Websocket] Received answer");
|
||||
const readyForOffer =
|
||||
|
|
@ -594,7 +589,9 @@ export default function KvmIdRoute() {
|
|||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
||||
bytesReceived: bytesReceivedDelta,
|
||||
bytesSent: bytesSentDelta,
|
||||
});
|
||||
}).catch(()=>{
|
||||
// we don't care about errors here, but we don't want unhandled promise rejections
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
const { setNetworkState} = useNetworkStateStore();
|
||||
|
|
|
|||
Loading…
Reference in New Issue