mirror of https://github.com/jetkvm/kvm.git
Merge branch 'dev' into feat/audio-support
This commit is contained in:
commit
516a953f41
|
|
@ -179,8 +179,9 @@ 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 {
|
if config.DisplayDimAfterSec != 0 && dimTicker != nil {
|
||||||
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.DisplayOffAfterSec != 0 {
|
if config.DisplayOffAfterSec != 0 && offTicker != nil {
|
||||||
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
}
|
}
|
||||||
backlightState = 0
|
backlightState = 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);
|
||||||
|
|
|
||||||
|
|
@ -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,10 +177,8 @@ 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 streamFactor, nil
|
return config.VideoQualityFactor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetStreamQualityFactor(factor float64) error {
|
func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
|
|
@ -190,7 +188,10 @@ func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
streamFactor = factor
|
config.VideoQualityFactor = factor
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
main.go
25
main.go
|
|
@ -14,6 +14,7 @@ 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
|
||||||
|
|
@ -80,16 +81,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 {
|
|
||||||
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
|
|
||||||
if !config.AutoUpdateEnabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
for {
|
||||||
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
|
||||||
time.Sleep(30 * time.Second)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +100,12 @@ 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 {
|
||||||
|
|
@ -108,6 +115,7 @@ func Main() {
|
||||||
time.Sleep(1 * time.Hour)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
//go RunFuseServer()
|
//go RunFuseServer()
|
||||||
go RunWebServer()
|
go RunWebServer()
|
||||||
|
|
||||||
|
|
@ -124,6 +132,7 @@ 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 write: %d < %d", nw, nr)
|
return fmt.Errorf("short file 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 write: %d < %d", nw, nr)
|
return fmt.Errorf("short hash write: %d < %d", nw, nr)
|
||||||
}
|
}
|
||||||
verified += int64(nw)
|
verified += int64(nw)
|
||||||
if ew != nil {
|
if ew != nil {
|
||||||
|
|
@ -260,11 +260,16 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hashSum := hash.Sum(nil)
|
// close the file so we can rename below
|
||||||
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of")
|
if err := fileToHash.Close(); err != nil {
|
||||||
|
return fmt.Errorf("error closing file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if hex.EncodeToString(hashSum) != expectedHash {
|
hashSum := hex.EncodeToString(hash.Sum(nil))
|
||||||
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
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 {
|
if err := os.Rename(unverifiedPath, path); err != nil {
|
||||||
|
|
@ -313,7 +318,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).
|
||||||
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
Bool("includePreRelease", includePreRelease).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("Trying to update...")
|
scopedLogger.Info().Msg("Trying to update...")
|
||||||
|
|
@ -362,8 +367,9 @@ 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 err
|
return fmt.Errorf("error downloading app update: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
otaState.AppDownloadFinishedAt = &downloadFinished
|
otaState.AppDownloadFinishedAt = &downloadFinished
|
||||||
otaState.AppDownloadProgress = 1
|
otaState.AppDownloadProgress = 1
|
||||||
|
|
@ -379,17 +385,21 @@ 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 err
|
return fmt.Errorf("error verifying app update: %w", 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")
|
||||||
}
|
}
|
||||||
|
|
@ -405,8 +415,9 @@ 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 err
|
return fmt.Errorf("error downloading system update: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
otaState.SystemDownloadFinishedAt = &downloadFinished
|
otaState.SystemDownloadFinishedAt = &downloadFinished
|
||||||
otaState.SystemDownloadProgress = 1
|
otaState.SystemDownloadProgress = 1
|
||||||
|
|
@ -422,8 +433,9 @@ 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 err
|
return fmt.Errorf("error verifying system update: %w", 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
|
||||||
|
|
@ -439,8 +451,10 @@ 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()
|
||||||
|
|
||||||
|
|
@ -475,13 +489,15 @@ 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
|
||||||
triggerOTAStateUpdate()
|
|
||||||
rebootNeeded = true
|
rebootNeeded = true
|
||||||
|
triggerOTAStateUpdate()
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("System is up to date")
|
scopedLogger.Info().Msg("System is up to date")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -125,11 +126,11 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Timeout == 0 {
|
if cfg.Timeout == 0 {
|
||||||
cfg.Timeout = defaultLinkUpTimeout
|
cfg.Timeout = defaultDHCPTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Retries == 0 {
|
if cfg.Retries == 0 {
|
||||||
cfg.Retries = 3
|
cfg.Retries = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
|
|
@ -153,9 +154,15 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetTimer(t *time.Timer, l *zerolog.Logger) {
|
func resetTimer(t *time.Timer, attempt int, l *zerolog.Logger) {
|
||||||
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later")
|
// Exponential backoff: 1s, 2s, 4s, 8s, max 8s
|
||||||
t.Reset(defaultTimerDuration)
|
backoffAttempt := attempt
|
||||||
|
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 {
|
||||||
|
|
@ -168,12 +175,14 @@ 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().Msg("requesting lease")
|
l.Info().Int("attempt", attempt).Msg("requesting lease")
|
||||||
|
|
||||||
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
if _, err := c.ensureInterfaceUp(ifname); err != nil {
|
||||||
l.Error().Err(err).Msg("failed to ensure interface up")
|
l.Error().Err(err).Int("attempt", attempt).Msg("failed to ensure interface up")
|
||||||
resetTimer(t, c.l)
|
resetTimer(t, attempt, c.l)
|
||||||
|
attempt++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,11 +197,14 @@ 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).Msg("failed to request lease")
|
l.Error().Err(err).Int("attempt", attempt).Msg("failed to request lease")
|
||||||
resetTimer(t, c.l)
|
resetTimer(t, attempt, 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,6 +26,31 @@ 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"
|
||||||
|
|
@ -113,6 +138,10 @@ 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"
|
||||||
|
|
@ -147,10 +176,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 "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||||
|
|
||||||
msg_info "▶ Running go tests"
|
msg_info "▶ Running go tests"
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
||||||
set -e
|
set -e
|
||||||
TMP_DIR=$(mktemp -d)
|
TMP_DIR=$(mktemp -d)
|
||||||
cd ${TMP_DIR}
|
cd ${TMP_DIR}
|
||||||
|
|
@ -193,10 +222,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 "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||||
|
|
||||||
# Reboot the device, the new app will be deployed by the startup process.
|
# Reboot the device, the new app will be deployed by the startup process.
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||||
else
|
else
|
||||||
msg_info "▶ Building development binary"
|
msg_info "▶ Building development binary"
|
||||||
do_make build_dev \
|
do_make build_dev \
|
||||||
|
|
@ -205,21 +234,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 "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||||
|
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||||
|
|
||||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
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 "${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}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "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 "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${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
|
||||||
|
|
@ -229,6 +258,17 @@ 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 };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean, reloadDocument?: 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} className={classes}>
|
<Link to={to} reloadDocument={props.reloadDocument} className={classes}>
|
||||||
<ButtonContent {...props} />
|
<ButtonContent {...props} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ 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 {
|
||||||
|
|
@ -481,8 +482,11 @@ 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,14 +589,18 @@ 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;
|
||||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
|
||||||
updateErrorMessage: string | null;
|
updateErrorMessage: string | null;
|
||||||
|
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateState>(set => ({
|
export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
|
|
@ -627,8 +631,10 @@ 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) return redirect("/welcome");
|
if (!res.isSetup) throw redirect("/welcome");
|
||||||
|
|
||||||
const deviceRes = await api.GET(`${DEVICE_API}/device`);
|
const deviceRes = await api.GET(`${DEVICE_API}/device`);
|
||||||
if (deviceRes.status === 401) return redirect("/login-local");
|
if (deviceRes.status === 401) throw 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 import.meta.env.MODE === "device" ? checkDeviceAuth() : checkCloudAuth();
|
return isOnDevice ? 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 { devices: [] };
|
return { user };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { devices: [] };
|
return { user };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 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("/");
|
if (!isOnDevice) navigate("/");
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,17 @@ 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={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ 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");
|
||||||
|
|
@ -36,9 +41,9 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
} else {
|
} else {
|
||||||
setModalView("loading");
|
setModalView("loading");
|
||||||
}
|
}
|
||||||
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
|
}, [otaState.error, otaState.updating, setModalView, updateSuccess]);
|
||||||
|
|
||||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@ export default function SettingsHardwareRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setBacklightSettings(settings);
|
setBacklightSettings(settings);
|
||||||
handleBacklightSettingsSave();
|
handleBacklightSettingsSave(settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBacklightSettingsSave = () => {
|
const handleBacklightSettingsSave = (backlightSettings: BacklightSettings) => {
|
||||||
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,7 +1,6 @@
|
||||||
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,
|
||||||
|
|
@ -16,7 +15,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, DEVICE_API, OPUS_STEREO_PARAMS } from "@/ui.config";
|
import { CLOUD_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 {
|
||||||
|
|
@ -51,9 +50,14 @@ 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;
|
||||||
|
|
@ -62,35 +66,20 @@ 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 res = await api
|
const device = await checkAuth();
|
||||||
.GET(`${DEVICE_API}/device/status`)
|
return { authMode: device.authMode } as LocalLoaderResp;
|
||||||
.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) {
|
||||||
|
|
@ -105,11 +94,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 };
|
return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
return isOnDevice ? deviceLoader() : cloudLoader(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function KvmIdRoute() {
|
export default function KvmIdRoute() {
|
||||||
|
|
@ -209,7 +198,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");
|
console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp);
|
||||||
setLoadingMessage(m.establishing_secure_connection());
|
setLoadingMessage(m.establishing_secure_connection());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
@ -254,9 +243,14 @@ 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 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(
|
const { sendMessage, getWebSocket } = useWebSocket(
|
||||||
isOnDevice
|
isOnDevice
|
||||||
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
|
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
|
||||||
|
|
@ -264,10 +258,10 @@ export default function KvmIdRoute() {
|
||||||
{
|
{
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
retryOnError: true,
|
retryOnError: true,
|
||||||
reconnectAttempts: 2000,
|
reconnectAttempts: reconnectAttemptsRef.current,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: reconnectInterval,
|
||||||
onReconnectStop: (numAttempts: number) => {
|
onReconnectStop: (numAttempts: number) => {
|
||||||
console.debug("Reconnect stopped", numAttempts);
|
console.debug("Reconnect stopped after ", numAttempts, "attempts");
|
||||||
cleanupAndStopReconnecting();
|
cleanupAndStopReconnecting();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -285,6 +279,7 @@ 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
|
||||||
|
|
@ -317,6 +312,7 @@ 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");
|
||||||
|
|
@ -333,10 +329,12 @@ 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: [] };
|
return { devices: [], user };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue