mirror of https://github.com/jetkvm/kvm.git
Compare commits
No commits in common. "516a953f415a62d04e46e074af8ce063253bdfb5" and "eeacceb6676460308186f2b7092a4985758e5d01" have entirely different histories.
516a953f41
...
eeacceb667
|
|
@ -179,9 +179,8 @@ func getDefaultConfig() Config {
|
||||||
_ = confparser.SetDefaultsAndValidate(c)
|
_ = confparser.SetDefaultsAndValidate(c)
|
||||||
return c
|
return c
|
||||||
}(),
|
}(),
|
||||||
DefaultLogLevel: "INFO",
|
DefaultLogLevel: "INFO",
|
||||||
AudioOutputSource: "usb",
|
AudioOutputSource: "usb",
|
||||||
VideoQualityFactor: 1.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -305,11 +305,11 @@ func wakeDisplay(force bool, reason string) {
|
||||||
displayLogger.Warn().Err(err).Msg("failed to wake display")
|
displayLogger.Warn().Err(err).Msg("failed to wake display")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.DisplayDimAfterSec != 0 && dimTicker != nil {
|
if config.DisplayDimAfterSec != 0 {
|
||||||
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.DisplayOffAfterSec != 0 && offTicker != nil {
|
if config.DisplayOffAfterSec != 0 {
|
||||||
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
}
|
}
|
||||||
backlightState = 0
|
backlightState = 0
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ int jetkvm_ui_add_flag(const char *obj_name, const char *flag_name) {
|
||||||
if (obj == NULL) {
|
if (obj == NULL) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_obj_flag_t flag_val = str_to_lv_obj_flag(flag_name);
|
lv_obj_flag_t flag_val = str_to_lv_obj_flag(flag_name);
|
||||||
if (flag_val == 0)
|
if (flag_val == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -368,7 +368,7 @@ void jetkvm_video_stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
int jetkvm_video_set_quality_factor(float quality_factor) {
|
int jetkvm_video_set_quality_factor(float quality_factor) {
|
||||||
if (quality_factor <= 0 || quality_factor > 1) {
|
if (quality_factor < 0 || quality_factor > 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
video_set_quality_factor(quality_factor);
|
video_set_quality_factor(quality_factor);
|
||||||
|
|
@ -417,4 +417,4 @@ void jetkvm_crash() {
|
||||||
// let's call a function that will crash the program
|
// let's call a function that will crash the program
|
||||||
int* p = 0;
|
int* p = 0;
|
||||||
*p = 0;
|
*p = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +235,7 @@ int video_init(float factor)
|
||||||
{
|
{
|
||||||
detect_sleep_mode();
|
detect_sleep_mode();
|
||||||
|
|
||||||
if (factor <= 0 || factor > 1) {
|
if (factor < 0 || factor > 1) {
|
||||||
factor = 1.0f;
|
factor = 1.0f;
|
||||||
}
|
}
|
||||||
quality_factor = factor;
|
quality_factor = factor;
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ func NewNative(opts NativeOptions) *Native {
|
||||||
sleepModeSupported := isSleepModeSupported()
|
sleepModeSupported := isSleepModeSupported()
|
||||||
|
|
||||||
defaultQualityFactor := opts.DefaultQualityFactor
|
defaultQualityFactor := opts.DefaultQualityFactor
|
||||||
if defaultQualityFactor <= 0 || defaultQualityFactor > 1 {
|
if defaultQualityFactor < 0 || defaultQualityFactor > 1 {
|
||||||
defaultQualityFactor = 1.0
|
defaultQualityFactor = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,8 +177,10 @@ func rpcReboot(force bool) error {
|
||||||
return hwReboot(force, nil, 0)
|
return hwReboot(force, nil, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var streamFactor = 1.0
|
||||||
|
|
||||||
func rpcGetStreamQualityFactor() (float64, error) {
|
func rpcGetStreamQualityFactor() (float64, error) {
|
||||||
return config.VideoQualityFactor, nil
|
return streamFactor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetStreamQualityFactor(factor float64) error {
|
func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
|
|
@ -188,10 +190,7 @@ func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.VideoQualityFactor = factor
|
streamFactor = factor
|
||||||
if err := SaveConfig(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
23
main.go
23
main.go
|
|
@ -14,7 +14,6 @@ import (
|
||||||
var appCtx context.Context
|
var appCtx context.Context
|
||||||
|
|
||||||
func Main() {
|
func Main() {
|
||||||
logger.Log().Msg("JetKVM Starting Up")
|
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
|
|
@ -81,16 +80,16 @@ func Main() {
|
||||||
startVideoSleepModeTicker()
|
startVideoSleepModeTicker()
|
||||||
|
|
||||||
go func() {
|
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)
|
time.Sleep(15 * time.Minute)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
|
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
|
||||||
if !config.AutoUpdateEnabled {
|
if !config.AutoUpdateEnabled {
|
||||||
logger.Debug().Msg("auto-update disabled")
|
return
|
||||||
time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes
|
}
|
||||||
|
|
||||||
|
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
||||||
|
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,12 +99,6 @@ func Main() {
|
||||||
continue
|
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
|
includePreRelease := config.IncludePreRelease
|
||||||
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -115,7 +108,6 @@ func Main() {
|
||||||
time.Sleep(1 * time.Hour)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
//go RunFuseServer()
|
//go RunFuseServer()
|
||||||
go RunWebServer()
|
go RunWebServer()
|
||||||
|
|
||||||
|
|
@ -132,7 +124,6 @@ func Main() {
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigs
|
<-sigs
|
||||||
|
|
||||||
logger.Info().Msg("JetKVM Shutting Down")
|
logger.Info().Msg("JetKVM Shutting Down")
|
||||||
|
|
||||||
stopAudio()
|
stopAudio()
|
||||||
|
|
|
||||||
40
ota.go
40
ota.go
|
|
@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := file.Write(buf[0:nr])
|
nw, ew := file.Write(buf[0:nr])
|
||||||
if nw < nr {
|
if nw < nr {
|
||||||
return fmt.Errorf("short file write: %d < %d", nw, nr)
|
return fmt.Errorf("short write: %d < %d", nw, nr)
|
||||||
}
|
}
|
||||||
written += int64(nw)
|
written += int64(nw)
|
||||||
if ew != nil {
|
if ew != nil {
|
||||||
|
|
@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := hash.Write(buf[0:nr])
|
nw, ew := hash.Write(buf[0:nr])
|
||||||
if nw < nr {
|
if nw < nr {
|
||||||
return fmt.Errorf("short hash write: %d < %d", nw, nr)
|
return fmt.Errorf("short write: %d < %d", nw, nr)
|
||||||
}
|
}
|
||||||
verified += int64(nw)
|
verified += int64(nw)
|
||||||
if ew != nil {
|
if ew != nil {
|
||||||
|
|
@ -260,16 +260,11 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the file so we can rename below
|
hashSum := hash.Sum(nil)
|
||||||
if err := fileToHash.Close(); err != nil {
|
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of")
|
||||||
return fmt.Errorf("error closing file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hashSum := hex.EncodeToString(hash.Sum(nil))
|
if hex.EncodeToString(hashSum) != expectedHash {
|
||||||
scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of")
|
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
||||||
|
|
||||||
if hashSum != expectedHash {
|
|
||||||
return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Rename(unverifiedPath, path); err != nil {
|
if err := os.Rename(unverifiedPath, path); err != nil {
|
||||||
|
|
@ -318,7 +313,7 @@ func triggerOTAStateUpdate() {
|
||||||
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
||||||
scopedLogger := otaLogger.With().
|
scopedLogger := otaLogger.With().
|
||||||
Str("deviceId", deviceId).
|
Str("deviceId", deviceId).
|
||||||
Bool("includePreRelease", includePreRelease).
|
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("Trying to update...")
|
scopedLogger.Info().Msg("Trying to update...")
|
||||||
|
|
@ -367,9 +362,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error downloading app update")
|
scopedLogger.Error().Err(err).Msg("Error downloading app update")
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return fmt.Errorf("error downloading app update: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
otaState.AppDownloadFinishedAt = &downloadFinished
|
otaState.AppDownloadFinishedAt = &downloadFinished
|
||||||
otaState.AppDownloadProgress = 1
|
otaState.AppDownloadProgress = 1
|
||||||
|
|
@ -385,21 +379,17 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
|
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return fmt.Errorf("error verifying app update: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyFinished := time.Now()
|
verifyFinished := time.Now()
|
||||||
otaState.AppVerifiedAt = &verifyFinished
|
otaState.AppVerifiedAt = &verifyFinished
|
||||||
otaState.AppVerificationProgress = 1
|
otaState.AppVerificationProgress = 1
|
||||||
triggerOTAStateUpdate()
|
|
||||||
|
|
||||||
otaState.AppUpdatedAt = &verifyFinished
|
otaState.AppUpdatedAt = &verifyFinished
|
||||||
otaState.AppUpdateProgress = 1
|
otaState.AppUpdateProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("App update downloaded")
|
scopedLogger.Info().Msg("App update downloaded")
|
||||||
rebootNeeded = true
|
rebootNeeded = true
|
||||||
triggerOTAStateUpdate()
|
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("App is up to date")
|
scopedLogger.Info().Msg("App is up to date")
|
||||||
}
|
}
|
||||||
|
|
@ -415,9 +405,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error downloading system update")
|
scopedLogger.Error().Err(err).Msg("Error downloading system update")
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return fmt.Errorf("error downloading system update: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
otaState.SystemDownloadFinishedAt = &downloadFinished
|
otaState.SystemDownloadFinishedAt = &downloadFinished
|
||||||
otaState.SystemDownloadProgress = 1
|
otaState.SystemDownloadProgress = 1
|
||||||
|
|
@ -433,9 +422,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
|
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return fmt.Errorf("error verifying system update: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Info().Msg("System update downloaded")
|
scopedLogger.Info().Msg("System update downloaded")
|
||||||
verifyFinished := time.Now()
|
verifyFinished := time.Now()
|
||||||
otaState.SystemVerifiedAt = &verifyFinished
|
otaState.SystemVerifiedAt = &verifyFinished
|
||||||
|
|
@ -451,10 +439,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err)
|
otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
|
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
|
||||||
triggerOTAStateUpdate()
|
|
||||||
return fmt.Errorf("error starting rk_ota command: %w", err)
|
return fmt.Errorf("error starting rk_ota command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|
@ -489,15 +475,13 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
Str("output", output).
|
Str("output", output).
|
||||||
Int("exitCode", cmd.ProcessState.ExitCode()).
|
Int("exitCode", cmd.ProcessState.ExitCode()).
|
||||||
Msg("Error executing rk_ota command")
|
Msg("Error executing rk_ota command")
|
||||||
triggerOTAStateUpdate()
|
|
||||||
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
|
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Info().Str("output", output).Msg("rk_ota success")
|
scopedLogger.Info().Str("output", output).Msg("rk_ota success")
|
||||||
otaState.SystemUpdateProgress = 1
|
otaState.SystemUpdateProgress = 1
|
||||||
otaState.SystemUpdatedAt = &verifyFinished
|
otaState.SystemUpdatedAt = &verifyFinished
|
||||||
rebootNeeded = true
|
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
rebootNeeded = true
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("System is up to date")
|
scopedLogger.Info().Msg("System is up to date")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ type Client struct {
|
||||||
var (
|
var (
|
||||||
defaultTimerDuration = 1 * time.Second
|
defaultTimerDuration = 1 * time.Second
|
||||||
defaultLinkUpTimeout = 30 * time.Second
|
defaultLinkUpTimeout = 30 * time.Second
|
||||||
defaultDHCPTimeout = 5 * time.Second // DHCP request timeout (not link up timeout)
|
|
||||||
maxRenewalAttemptDuration = 2 * time.Hour
|
maxRenewalAttemptDuration = 2 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -126,11 +125,11 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Timeout == 0 {
|
if cfg.Timeout == 0 {
|
||||||
cfg.Timeout = defaultDHCPTimeout
|
cfg.Timeout = defaultLinkUpTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Retries == 0 {
|
if cfg.Retries == 0 {
|
||||||
cfg.Retries = 4
|
cfg.Retries = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
|
|
@ -154,15 +153,9 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetTimer(t *time.Timer, attempt int, l *zerolog.Logger) {
|
func resetTimer(t *time.Timer, l *zerolog.Logger) {
|
||||||
// Exponential backoff: 1s, 2s, 4s, 8s, max 8s
|
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later")
|
||||||
backoffAttempt := attempt
|
t.Reset(defaultTimerDuration)
|
||||||
if backoffAttempt > 3 {
|
|
||||||
backoffAttempt = 3
|
|
||||||
}
|
|
||||||
delay := time.Duration(1<<backoffAttempt) * time.Second
|
|
||||||
l.Debug().Dur("delay", delay).Int("attempt", attempt).Msg("will retry later")
|
|
||||||
t.Reset(delay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRenewalTime(lease *Lease) time.Duration {
|
func getRenewalTime(lease *Lease) time.Duration {
|
||||||
|
|
@ -175,14 +168,12 @@ func getRenewalTime(lease *Lease) time.Duration {
|
||||||
|
|
||||||
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
||||||
l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
|
l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
|
||||||
attempt := 0
|
|
||||||
for range t.C {
|
for range t.C {
|
||||||
l.Info().Int("attempt", attempt).Msg("requesting lease")
|
l.Info().Msg("requesting lease")
|
||||||
|
|
||||||
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
||||||
l.Error().Err(err).Int("attempt", attempt).Msg("failed to ensure interface up")
|
l.Error().Err(err).Msg("failed to ensure interface up")
|
||||||
resetTimer(t, attempt, c.l)
|
resetTimer(t, c.l)
|
||||||
attempt++
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,14 +188,11 @@ func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
|
||||||
lease, err = c.requestLease6(ifname)
|
lease, err = c.requestLease6(ifname)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error().Err(err).Int("attempt", attempt).Msg("failed to request lease")
|
l.Error().Err(err).Msg("failed to request lease")
|
||||||
resetTimer(t, attempt, c.l)
|
resetTimer(t, c.l)
|
||||||
attempt++
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successfully obtained lease, reset attempt counter
|
|
||||||
attempt = 0
|
|
||||||
c.handleLeaseChange(lease)
|
c.handleLeaseChange(lease)
|
||||||
|
|
||||||
nextRenewal := getRenewalTime(lease)
|
nextRenewal := getRenewalTime(lease)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -160,7 +131,7 @@ if [[ "$SKIP_UI_BUILD" = true && ! -f "static/index.html" ]]; then
|
||||||
SKIP_UI_BUILD=false
|
SKIP_UI_BUILD=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
|
if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
|
||||||
msg_info "▶ Building frontend"
|
msg_info "▶ Building frontend"
|
||||||
make frontend SKIP_UI_BUILD=0
|
make frontend SKIP_UI_BUILD=0
|
||||||
SKIP_UI_BUILD_RELEASE=1
|
SKIP_UI_BUILD_RELEASE=1
|
||||||
|
|
@ -173,13 +144,13 @@ fi
|
||||||
|
|
||||||
if [ "$RUN_GO_TESTS" = true ]; then
|
if [ "$RUN_GO_TESTS" = true ]; then
|
||||||
msg_info "▶ Building go tests"
|
msg_info "▶ Building go tests"
|
||||||
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}
|
||||||
|
|
@ -220,35 +191,35 @@ then
|
||||||
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
|
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
|
||||||
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
|
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
|
||||||
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 \
|
||||||
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
|
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
|
||||||
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
|
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
|
||||||
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}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
type LinkPropsType = Pick<LinkProps, "to"> &
|
type LinkPropsType = Pick<LinkProps, "to"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-hidden",
|
"group outline-hidden",
|
||||||
|
|
@ -230,7 +230,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Link to={to} reloadDocument={props.reloadDocument} className={classes}>
|
<Link to={to} className={classes}>
|
||||||
<ButtonContent {...props} />
|
<ButtonContent {...props} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { useRTCStore, PostRebootAction } from "@/hooks/stores";
|
||||||
import LogoBlue from "@/assets/logo-blue.svg";
|
import LogoBlue from "@/assets/logo-blue.svg";
|
||||||
import LogoWhite from "@/assets/logo-white.svg";
|
import LogoWhite from "@/assets/logo-white.svg";
|
||||||
import { isOnDevice } from "@/main";
|
import { isOnDevice } from "@/main";
|
||||||
import { sleep } from "@/utils";
|
|
||||||
|
|
||||||
|
|
||||||
interface OverlayContentProps {
|
interface OverlayContentProps {
|
||||||
|
|
@ -482,11 +481,8 @@ export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayPro
|
||||||
// - Protocol-relative URLs: resolved with current protocol
|
// - Protocol-relative URLs: resolved with current protocol
|
||||||
// - Fully qualified URLs: used as-is
|
// - Fully qualified URLs: used as-is
|
||||||
const targetUrl = new URL(postRebootAction.redirectTo, window.location.origin);
|
const targetUrl = new URL(postRebootAction.redirectTo, window.location.origin);
|
||||||
clearInterval(intervalId); // Stop polling before redirect
|
|
||||||
|
|
||||||
window.location.href = targetUrl.href;
|
window.location.href = targetUrl.href;
|
||||||
// Add 1s delay between setting location.href and calling reload() to prevent reload from interrupting the navigation.
|
|
||||||
await sleep(1000);
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -589,18 +589,14 @@ export interface OtaState {
|
||||||
export interface UpdateState {
|
export interface UpdateState {
|
||||||
isUpdatePending: boolean;
|
isUpdatePending: boolean;
|
||||||
setIsUpdatePending: (isPending: boolean) => void;
|
setIsUpdatePending: (isPending: boolean) => void;
|
||||||
|
|
||||||
updateDialogHasBeenMinimized: boolean;
|
updateDialogHasBeenMinimized: boolean;
|
||||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
|
||||||
|
|
||||||
otaState: OtaState;
|
otaState: OtaState;
|
||||||
setOtaState: (state: OtaState) => void;
|
setOtaState: (state: OtaState) => void;
|
||||||
|
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||||
modalView: UpdateModalViews
|
modalView: UpdateModalViews
|
||||||
setModalView: (view: UpdateModalViews) => void;
|
setModalView: (view: UpdateModalViews) => void;
|
||||||
|
|
||||||
updateErrorMessage: string | null;
|
|
||||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||||
|
updateErrorMessage: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateState>(set => ({
|
export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
|
|
@ -631,10 +627,8 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
updateDialogHasBeenMinimized: false,
|
updateDialogHasBeenMinimized: false,
|
||||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
||||||
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
||||||
|
|
||||||
modalView: "loading",
|
modalView: "loading",
|
||||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||||
|
|
||||||
updateErrorMessage: null,
|
updateErrorMessage: null,
|
||||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,10 @@ export async function checkDeviceAuth() {
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
.GET(`${DEVICE_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (!res.isSetup) throw redirect("/welcome");
|
if (!res.isSetup) return redirect("/welcome");
|
||||||
|
|
||||||
const deviceRes = await api.GET(`${DEVICE_API}/device`);
|
const deviceRes = await api.GET(`${DEVICE_API}/device`);
|
||||||
if (deviceRes.status === 401) throw redirect("/login-local");
|
if (deviceRes.status === 401) return redirect("/login-local");
|
||||||
if (deviceRes.ok) {
|
if (deviceRes.ok) {
|
||||||
const device = (await deviceRes.json()) as LocalDevice;
|
const device = (await deviceRes.json()) as LocalDevice;
|
||||||
return { authMode: device.authMode };
|
return { authMode: device.authMode };
|
||||||
|
|
@ -87,7 +87,7 @@ export async function checkDeviceAuth() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAuth() {
|
export async function checkAuth() {
|
||||||
return isOnDevice ? checkDeviceAuth() : checkCloudAuth();
|
return import.meta.env.MODE === "device" ? checkDeviceAuth() : checkCloudAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
let router;
|
let router;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
return { device, user };
|
return { device, user };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { user };
|
return { devices: [] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||||
return { device, user };
|
return { device, user };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { user };
|
return { devices: [] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCloudState();
|
getCloudState();
|
||||||
// In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore
|
// In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore
|
||||||
if (!isOnDevice) navigate("/");
|
if (!isOnDevice) navigate("/");
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,12 @@ import { m } from "@localizations/messages.js";
|
||||||
export default function SettingsGeneralRebootRoute() {
|
export default function SettingsGeneralRebootRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { send } = useJsonRpc();
|
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(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
send("reboot", { force: true});
|
send("reboot", { force: true});
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,6 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
const { setModalView, otaState } = useUpdateStore();
|
const { setModalView, otaState } = useUpdateStore();
|
||||||
const { send } = useJsonRpc();
|
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(() => {
|
const onConfirmUpdate = useCallback(() => {
|
||||||
send("tryUpdate", {});
|
send("tryUpdate", {});
|
||||||
setModalView("updating");
|
setModalView("updating");
|
||||||
|
|
@ -41,9 +36,9 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
} else {
|
} else {
|
||||||
setModalView("loading");
|
setModalView("loading");
|
||||||
}
|
}
|
||||||
}, [otaState.error, otaState.updating, setModalView, updateSuccess]);
|
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
||||||
|
|
||||||
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@ export default function SettingsHardwareRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setBacklightSettings(settings);
|
setBacklightSettings(settings);
|
||||||
handleBacklightSettingsSave(settings);
|
handleBacklightSettingsSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBacklightSettingsSave = (backlightSettings: BacklightSettings) => {
|
const handleBacklightSettingsSave = () => {
|
||||||
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
|
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
|
@ -81,7 +81,7 @@ export default function SettingsHardwareRoute() {
|
||||||
const duration = enabled ? 90 : -1;
|
const duration = enabled ? 90 : -1;
|
||||||
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
|
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(m.hardware_power_saving_failed_error({ error: resp.error.data || m.unknown_error() }));
|
notifications.error(m.hardware_power_saving_failed_error({ error: resp.error.data ||m.unknown_error() }));
|
||||||
setPowerSavingEnabled(!enabled); // Attempt to revert on error
|
setPowerSavingEnabled(!enabled); // Attempt to revert on error
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
|
redirect,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
useLocation,
|
useLocation,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
|
|
@ -15,7 +16,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||||
import useWebSocket from "react-use-websocket";
|
import useWebSocket from "react-use-websocket";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import { CLOUD_API, OPUS_STEREO_PARAMS } from "@/ui.config";
|
import { CLOUD_API, DEVICE_API, OPUS_STEREO_PARAMS } from "@/ui.config";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||||
import {
|
import {
|
||||||
|
|
@ -50,14 +51,9 @@ import {
|
||||||
RebootingOverlay,
|
RebootingOverlay,
|
||||||
} from "@components/VideoOverlay";
|
} from "@components/VideoOverlay";
|
||||||
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
||||||
|
import { DeviceStatus } from "@routes/welcome-local";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export type AuthMode = "password" | "noPassword" | null;
|
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
|
||||||
authMode: AuthMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CloudLoaderResp {
|
interface CloudLoaderResp {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
|
@ -66,20 +62,35 @@ interface CloudLoaderResp {
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthMode = "password" | "noPassword" | null;
|
||||||
export interface LocalDevice {
|
export interface LocalDevice {
|
||||||
authMode: AuthMode;
|
authMode: AuthMode;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceLoader = async () => {
|
const deviceLoader = async () => {
|
||||||
const device = await checkAuth();
|
const res = await api
|
||||||
return { authMode: device.authMode } as LocalLoaderResp;
|
.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 cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
|
const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
|
||||||
const iceConfig = await iceResp.json();
|
const iceConfig = await iceResp.json();
|
||||||
|
|
||||||
const deviceResp = await api.GET(`${CLOUD_API}/devices/${params.id}`);
|
const deviceResp = await api.GET(`${CLOUD_API}/devices/${params.id}`);
|
||||||
|
|
||||||
if (!deviceResp.ok) {
|
if (!deviceResp.ok) {
|
||||||
|
|
@ -94,11 +105,11 @@ const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> =>
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
device: { id: string; name: string; user: { googleId: string } };
|
||||||
};
|
};
|
||||||
|
|
||||||
return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp;
|
return { user, iceConfig, deviceName: device.name || device.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return isOnDevice ? deviceLoader() : cloudLoader(params);
|
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function KvmIdRoute() {
|
export default function KvmIdRoute() {
|
||||||
|
|
@ -198,7 +209,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
||||||
console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
|
console.log("[setRemoteSessionDescription] Remote description set successfully");
|
||||||
setLoadingMessage(m.establishing_secure_connection());
|
setLoadingMessage(m.establishing_secure_connection());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -243,13 +254,8 @@ export default function KvmIdRoute() {
|
||||||
const ignoreOffer = useRef(false);
|
const ignoreOffer = useRef(false);
|
||||||
const isSettingRemoteAnswerPending = useRef(false);
|
const isSettingRemoteAnswerPending = useRef(false);
|
||||||
const makingOffer = useRef(false);
|
const makingOffer = useRef(false);
|
||||||
const reconnectAttemptsRef = useRef(2000);
|
|
||||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
|
|
||||||
const reconnectInterval = (attempt: number) => {
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
// Exponential backoff with a max of 10 seconds between attempts
|
|
||||||
return Math.min(500 * 2 ** attempt, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sendMessage, getWebSocket } = useWebSocket(
|
const { sendMessage, getWebSocket } = useWebSocket(
|
||||||
isOnDevice
|
isOnDevice
|
||||||
|
|
@ -258,10 +264,10 @@ export default function KvmIdRoute() {
|
||||||
{
|
{
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
retryOnError: true,
|
retryOnError: true,
|
||||||
reconnectAttempts: reconnectAttemptsRef.current,
|
reconnectAttempts: 2000,
|
||||||
reconnectInterval: reconnectInterval,
|
reconnectInterval: 1000,
|
||||||
onReconnectStop: (numAttempts: number) => {
|
onReconnectStop: (numAttempts: number) => {
|
||||||
console.debug("Reconnect stopped after ", numAttempts, "attempts");
|
console.debug("Reconnect stopped", numAttempts);
|
||||||
cleanupAndStopReconnecting();
|
cleanupAndStopReconnecting();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -279,7 +285,6 @@ export default function KvmIdRoute() {
|
||||||
console.error("[Websocket] onError", event);
|
console.error("[Websocket] onError", event);
|
||||||
// We don't want to close everything down, we wait for the reconnect to stop instead
|
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||||
},
|
},
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
console.debug("[Websocket] onOpen");
|
console.debug("[Websocket] onOpen");
|
||||||
// We want to clear the reboot state when the websocket connection is opened
|
// We want to clear the reboot state when the websocket connection is opened
|
||||||
|
|
@ -312,7 +317,6 @@ export default function KvmIdRoute() {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const parsedMessage = JSON.parse(message.data);
|
const parsedMessage = JSON.parse(message.data);
|
||||||
|
|
||||||
if (parsedMessage.type === "device-metadata") {
|
if (parsedMessage.type === "device-metadata") {
|
||||||
const { deviceVersion } = parsedMessage.data;
|
const { deviceVersion } = parsedMessage.data;
|
||||||
console.debug("[Websocket] Received device-metadata message");
|
console.debug("[Websocket] Received device-metadata message");
|
||||||
|
|
@ -329,12 +333,10 @@ export default function KvmIdRoute() {
|
||||||
console.log("[Websocket] Device is using new signaling");
|
console.log("[Websocket] Device is using new signaling");
|
||||||
isLegacySignalingEnabled.current = false;
|
isLegacySignalingEnabled.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPeerConnection();
|
setupPeerConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!peerConnection) return;
|
if (!peerConnection) return;
|
||||||
|
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
console.debug("[Websocket] Received answer");
|
console.debug("[Websocket] Received answer");
|
||||||
const readyForOffer =
|
const readyForOffer =
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ interface LoaderData {
|
||||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
const loader: LoaderFunction = async () => {
|
const loader: LoaderFunction = async ()=> {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -30,7 +30,7 @@ const loader: LoaderFunction = async () => {
|
||||||
return { devices, user };
|
return { devices, user };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return { devices: [], user };
|
return { devices: [] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue