Compare commits

..

1 Commits

Author SHA1 Message Date
Alex 91635901f2
Merge 08b0dd0c37 into 74e64f69a7 2025-10-17 13:34:34 +00:00
17 changed files with 103 additions and 302 deletions

View File

@ -2,8 +2,8 @@ BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
BUILDDATE := $(shell date -u +%FT%T%z) BUILDDATE := $(shell date -u +%FT%T%z)
BUILDTS := $(shell date -u +%s) BUILDTS := $(shell date -u +%s)
REVISION := $(shell git rev-parse HEAD) REVISION := $(shell git rev-parse HEAD)
VERSION_DEV ?= 0.4.9-dev$(shell date +%Y%m%d%H%M) VERSION_DEV := 0.4.9-dev$(shell date +%Y%m%d%H%M)
VERSION ?= 0.4.8 VERSION := 0.4.8
PROMETHEUS_TAG := github.com/prometheus/common/version PROMETHEUS_TAG := github.com/prometheus/common/version
KVM_PKG_NAME := github.com/jetkvm/kvm KVM_PKG_NAME := github.com/jetkvm/kvm

View File

@ -117,7 +117,6 @@ type Config struct {
DefaultLogLevel string `json:"default_log_level"` DefaultLogLevel string `json:"default_log_level"`
SessionSettings *SessionSettings `json:"session_settings"` SessionSettings *SessionSettings `json:"session_settings"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"` VideoSleepAfterSec int `json:"video_sleep_after_sec"`
VideoQualityFactor float64 `json:"video_quality_factor"`
} }
func (c *Config) GetDisplayRotation() uint16 { func (c *Config) GetDisplayRotation() uint16 {

View File

@ -70,7 +70,7 @@ func updateDisplay() {
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected") nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected")
_, _ = nativeInstance.UIObjClearState("hdmi_status_label", "LV_STATE_CHECKED") _, _ = nativeInstance.UIObjClearState("hdmi_status_label", "LV_STATE_CHECKED")
} }
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", getActiveSessions())) nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
if networkManager != nil && networkManager.IsUp() { if networkManager != nil && networkManager.IsUp() {
nativeInstance.UISetVar("main_screen", "home_screen") nativeInstance.UISetVar("main_screen", "home_screen")

View File

@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
return (char *)videoc_log_status(); return (char *)videoc_log_status();
} }
int jetkvm_video_init(float factor) { int jetkvm_video_init() {
return video_init(factor); return video_init();
} }
void jetkvm_video_shutdown() { void jetkvm_video_shutdown() {

View File

@ -52,7 +52,7 @@ const char *jetkvm_ui_get_lvgl_version();
const char *jetkvm_ui_event_code_to_name(int code); const char *jetkvm_ui_event_code_to_name(int code);
int jetkvm_video_init(float quality_factor); int jetkvm_video_init();
void jetkvm_video_shutdown(); void jetkvm_video_shutdown();
void jetkvm_video_start(); void jetkvm_video_start();
void jetkvm_video_stop(); void jetkvm_video_stop();

View File

@ -29,7 +29,6 @@
#define VIDEO_DEV "/dev/video0" #define VIDEO_DEV "/dev/video0"
#define SUB_DEV "/dev/v4l-subdev2" #define SUB_DEV "/dev/v4l-subdev2"
#define SLEEP_MODE_FILE "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
#define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1)) #define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
#define RK_ALIGN_2(x) RK_ALIGN(x, 2) #define RK_ALIGN_2(x) RK_ALIGN(x, 2)
@ -40,7 +39,6 @@ int sub_dev_fd = -1;
#define VENC_CHANNEL 0 #define VENC_CHANNEL 0
MB_POOL memPool = MB_INVALID_POOLID; MB_POOL memPool = MB_INVALID_POOLID;
bool sleep_mode_available = false;
bool should_exit = false; bool should_exit = false;
float quality_factor = 1.0f; float quality_factor = 1.0f;
@ -53,45 +51,6 @@ RK_U64 get_us()
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */ return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
} }
static void ensure_sleep_mode_disabled()
{
if (!sleep_mode_available)
{
return;
}
int fd = open(SLEEP_MODE_FILE, O_RDWR);
if (fd < 0)
{
log_error("Failed to open sleep mode file: %s", strerror(errno));
return;
}
lseek(fd, 0, SEEK_SET);
char buffer[1];
read(fd, buffer, 1);
if (buffer[0] == '0') {
close(fd);
return;
}
log_warn("HDMI sleep mode is not disabled, disabling it");
lseek(fd, 0, SEEK_SET);
write(fd, "0", 1);
close(fd);
usleep(1000); // give some time to the system to disable the sleep mode
return;
}
static void detect_sleep_mode()
{
if (access(SLEEP_MODE_FILE, F_OK) != 0) {
sleep_mode_available = false;
return;
}
sleep_mode_available = true;
ensure_sleep_mode_disabled();
}
double calculate_bitrate(float bitrate_factor, int width, int height) double calculate_bitrate(float bitrate_factor, int width, int height)
{ {
const int32_t base_bitrate_high = 2000; const int32_t base_bitrate_high = 2000;
@ -231,15 +190,8 @@ static int32_t buf_init()
pthread_t *format_thread = NULL; pthread_t *format_thread = NULL;
int video_init(float factor) int video_init()
{ {
detect_sleep_mode();
if (factor < 0 || factor > 1) {
factor = 1.0f;
}
quality_factor = factor;
if (RK_MPI_SYS_Init() != RK_SUCCESS) if (RK_MPI_SYS_Init() != RK_SUCCESS)
{ {
log_error("RK_MPI_SYS_Init failed"); log_error("RK_MPI_SYS_Init failed");
@ -349,29 +301,11 @@ static void *venc_read_stream(void *arg)
} }
uint32_t detected_width, detected_height; uint32_t detected_width, detected_height;
bool detected_signal = false, streaming_flag = false, streaming_stopped = true; bool detected_signal = false, streaming_flag = false;
pthread_t *streaming_thread = NULL; pthread_t *streaming_thread = NULL;
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
bool get_streaming_flag()
{
log_info("getting streaming flag");
pthread_mutex_lock(&streaming_mutex);
bool flag = streaming_flag;
pthread_mutex_unlock(&streaming_mutex);
return flag;
}
void set_streaming_flag(bool flag)
{
log_info("setting streaming flag to %d", flag);
pthread_mutex_lock(&streaming_mutex);
streaming_flag = flag;
pthread_mutex_unlock(&streaming_mutex);
}
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename) void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
{ {
FILE *file = fopen(filename, "wb"); FILE *file = fopen(filename, "wb");
@ -385,8 +319,6 @@ void *run_video_stream(void *arg)
log_info("running video stream"); log_info("running video stream");
streaming_stopped = false;
while (streaming_flag) while (streaming_flag)
{ {
if (detected_signal == false) if (detected_signal == false)
@ -469,7 +401,7 @@ void *run_video_stream(void *arg)
{ {
log_error("get mb blk failed!"); log_error("get mb blk failed!");
close(video_dev_fd); close(video_dev_fd);
return (void *)errno; return ;
} }
log_info("Got memory block for buffer %d", i); log_info("Got memory block for buffer %d", i);
@ -606,18 +538,6 @@ void *run_video_stream(void *arg)
log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno)); log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
} }
// Explicitly free V4L2 buffer queue
struct v4l2_requestbuffers req_free;
memset(&req_free, 0, sizeof(req_free));
req_free.count = 0;
req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
req_free.memory = V4L2_MEMORY_DMABUF;
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free) < 0)
{
log_error("Failed to free V4L2 buffers: %s", strerror(errno));
}
venc_stop(); venc_stop();
for (int i = 0; i < input_buffer_count; i++) for (int i = 0; i < input_buffer_count; i++)
@ -633,9 +553,6 @@ void *run_video_stream(void *arg)
} }
log_info("video stream thread exiting"); log_info("video stream thread exiting");
streaming_stopped = true;
return NULL; return NULL;
} }
@ -665,75 +582,56 @@ void video_shutdown()
log_info("Destroyed streaming mutex"); log_info("Destroyed streaming mutex");
} }
void video_start_streaming() void video_start_streaming()
{ {
log_info("starting video streaming"); pthread_mutex_lock(&streaming_mutex);
if (streaming_thread != NULL) if (streaming_thread != NULL)
{ {
if (streaming_stopped == true) {
log_error("video streaming already stopped but streaming_thread is not NULL");
assert(streaming_stopped == true);
}
log_warn("video streaming already started"); log_warn("video streaming already started");
return; goto cleanup;
} }
pthread_t *new_thread = malloc(sizeof(pthread_t)); pthread_t *new_thread = malloc(sizeof(pthread_t));
if (new_thread == NULL) if (new_thread == NULL)
{ {
log_error("Failed to allocate memory for streaming thread"); log_error("Failed to allocate memory for streaming thread");
return; goto cleanup;
} }
set_streaming_flag(true); streaming_flag = true;
int result = pthread_create(new_thread, NULL, run_video_stream, NULL); int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
if (result != 0) if (result != 0)
{ {
log_error("Failed to create streaming thread: %s", strerror(result)); log_error("Failed to create streaming thread: %s", strerror(result));
set_streaming_flag(false); streaming_flag = false;
free(new_thread); free(new_thread);
return; goto cleanup;
} }
// Only set streaming_thread after successful creation // Only set streaming_thread after successful creation, and before unlocking the mutex
streaming_thread = new_thread; streaming_thread = new_thread;
cleanup:
pthread_mutex_unlock(&streaming_mutex);
return;
} }
void video_stop_streaming() void video_stop_streaming()
{ {
if (streaming_thread == NULL) { pthread_mutex_lock(&streaming_mutex);
log_info("video streaming already stopped"); if (streaming_thread != NULL)
return;
}
log_info("stopping video streaming");
set_streaming_flag(false);
log_info("waiting for video streaming thread to exit");
int attempts = 0;
while (!streaming_stopped && attempts < 30) {
usleep(100000); // 100ms
attempts++;
}
if (!streaming_stopped) {
log_error("video streaming thread did not exit after 30s");
}
pthread_join(*streaming_thread, NULL);
free(streaming_thread);
streaming_thread = NULL;
log_info("video streaming stopped");
}
void video_restart_streaming()
{
if (get_streaming_flag() == true)
{ {
log_info("restarting video streaming"); streaming_flag = false;
video_stop_streaming(); log_info("stopping video streaming");
// wait 100ms for the thread to exit
usleep(1000000);
log_info("waiting for video streaming thread to exit");
pthread_join(*streaming_thread, NULL);
free(streaming_thread);
streaming_thread = NULL;
log_info("video streaming stopped");
} }
video_start_streaming(); pthread_mutex_unlock(&streaming_mutex);
} }
void *run_detect_format(void *arg) void *run_detect_format(void *arg)
@ -752,8 +650,6 @@ void *run_detect_format(void *arg)
while (!should_exit) while (!should_exit)
{ {
ensure_sleep_mode_disabled();
memset(&dv_timings, 0, sizeof(dv_timings)); memset(&dv_timings, 0, sizeof(dv_timings));
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0) if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
{ {
@ -793,17 +689,21 @@ void *run_detect_format(void *arg)
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync + (dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
dv_timings.bt.hbackporch)); dv_timings.bt.hbackporch));
log_info("Frames per second: %.2f fps", frames_per_second); log_info("Frames per second: %.2f fps", frames_per_second);
bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || !detected_signal;
detected_width = dv_timings.bt.width; detected_width = dv_timings.bt.width;
detected_height = dv_timings.bt.height; detected_height = dv_timings.bt.height;
detected_signal = true; detected_signal = true;
video_report_format(true, NULL, detected_width, detected_height, frames_per_second); video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
pthread_mutex_lock(&streaming_mutex);
if (should_restart) { if (streaming_flag == true)
log_info("restarting video streaming due to format change"); {
video_restart_streaming(); pthread_mutex_unlock(&streaming_mutex);
log_info("restarting on going video streaming");
video_stop_streaming();
video_start_streaming();
}
else
{
pthread_mutex_unlock(&streaming_mutex);
} }
} }
@ -831,7 +731,19 @@ void video_set_quality_factor(float factor)
quality_factor = factor; quality_factor = factor;
// TODO: update venc bitrate without stopping streaming // TODO: update venc bitrate without stopping streaming
video_restart_streaming();
pthread_mutex_lock(&streaming_mutex);
if (streaming_flag == true)
{
pthread_mutex_unlock(&streaming_mutex);
log_info("restarting on going video streaming due to quality factor change");
video_stop_streaming();
video_start_streaming();
}
else
{
pthread_mutex_unlock(&streaming_mutex);
}
} }
float video_get_quality_factor() { float video_get_quality_factor() {

View File

@ -6,7 +6,7 @@
* *
* @return int 0 on success, -1 on failure * @return int 0 on success, -1 on failure
*/ */
int video_init(float quality_factor); int video_init();
/** /**
* @brief Shutdown the video subsystem * @brief Shutdown the video subsystem

View File

@ -129,13 +129,11 @@ func uiTick() {
C.jetkvm_ui_tick() C.jetkvm_ui_tick()
} }
func videoInit(factor float64) error { func videoInit() error {
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
factorC := C.float(factor) ret := C.jetkvm_video_init()
ret := C.jetkvm_video_init(factorC)
if ret != 0 { if ret != 0 {
return fmt.Errorf("failed to initialize video: %d", ret) return fmt.Errorf("failed to initialize video: %d", ret)
} }

View File

@ -15,7 +15,6 @@ type Native struct {
systemVersion *semver.Version systemVersion *semver.Version
appVersion *semver.Version appVersion *semver.Version
displayRotation uint16 displayRotation uint16
defaultQualityFactor float64
onVideoStateChange func(state VideoState) onVideoStateChange func(state VideoState)
onVideoFrameReceived func(frame []byte, duration time.Duration) onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string) onIndevEvent func(event string)
@ -23,14 +22,12 @@ type Native struct {
sleepModeSupported bool sleepModeSupported bool
videoLock sync.Mutex videoLock sync.Mutex
screenLock sync.Mutex screenLock sync.Mutex
extraLock sync.Mutex
} }
type NativeOptions struct { type NativeOptions struct {
SystemVersion *semver.Version SystemVersion *semver.Version
AppVersion *semver.Version AppVersion *semver.Version
DisplayRotation uint16 DisplayRotation uint16
DefaultQualityFactor float64
OnVideoStateChange func(state VideoState) OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration) OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string) OnIndevEvent func(event string)
@ -68,11 +65,6 @@ func NewNative(opts NativeOptions) *Native {
sleepModeSupported := isSleepModeSupported() sleepModeSupported := isSleepModeSupported()
defaultQualityFactor := opts.DefaultQualityFactor
if defaultQualityFactor < 0 || defaultQualityFactor > 1 {
defaultQualityFactor = 1.0
}
return &Native{ return &Native{
ready: make(chan struct{}), ready: make(chan struct{}),
l: nativeLogger, l: nativeLogger,
@ -80,7 +72,6 @@ func NewNative(opts NativeOptions) *Native {
systemVersion: opts.SystemVersion, systemVersion: opts.SystemVersion,
appVersion: opts.AppVersion, appVersion: opts.AppVersion,
displayRotation: opts.DisplayRotation, displayRotation: opts.DisplayRotation,
defaultQualityFactor: defaultQualityFactor,
onVideoStateChange: onVideoStateChange, onVideoStateChange: onVideoStateChange,
onVideoFrameReceived: onVideoFrameReceived, onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent, onIndevEvent: onIndevEvent,
@ -106,7 +97,7 @@ func (n *Native) Start() {
n.initUI() n.initUI()
go n.tickUI() go n.tickUI()
if err := videoInit(n.defaultQualityFactor); err != nil { if err := videoInit(); err != nil {
n.l.Error().Err(err).Msg("failed to initialize video") n.l.Error().Err(err).Msg("failed to initialize video")
} }

View File

@ -1,18 +1,11 @@
package native package native
import ( import (
"fmt"
"os" "os"
"time"
) )
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode" const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
// DefaultEDID is the default EDID for the video stream.
const DefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
var extraLockTimeout = 5 * time.Second
// VideoState is the state of the video stream. // VideoState is the state of the video stream.
type VideoState struct { type VideoState struct {
Ready bool `json:"ready"` Ready bool `json:"ready"`
@ -73,32 +66,12 @@ func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported return n.sleepModeSupported
} }
// useExtraLock uses the extra lock to execute a function.
// if the lock is currently held by another goroutine, returns an error.
//
// it's used to ensure that only one change is made to the video stream at a time.
// as the change usually requires to restart video streaming
// TODO: check video streaming status instead of using a hardcoded timeout
func (n *Native) useExtraLock(fn func() error) error {
if !n.extraLock.TryLock() {
return fmt.Errorf("the previous change hasn't been completed yet")
}
err := fn()
if err == nil {
time.Sleep(extraLockTimeout)
}
n.extraLock.Unlock()
return err
}
// VideoSetQualityFactor sets the quality factor for the video stream. // VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error { func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
return n.useExtraLock(func() error { return videoSetStreamQualityFactor(factor)
return videoSetStreamQualityFactor(factor)
})
} }
// VideoGetQualityFactor gets the quality factor for the video stream. // VideoGetQualityFactor gets the quality factor for the video stream.
@ -114,13 +87,7 @@ func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
if edid == "" { return videoSetEDID(edid)
edid = DefaultEDID
}
return n.useExtraLock(func() error {
return videoSetEDID(edid)
})
} }
// VideoGetEDID gets the EDID for the video stream. // VideoGetEDID gets the EDID for the video stream.

View File

@ -366,6 +366,7 @@ func rpcGetEDID() (string, error) {
func rpcSetEDID(edid string) error { func rpcSetEDID(edid string) error {
if edid == "" { if edid == "" {
logger.Info().Msg("Restoring EDID to default") logger.Info().Msg("Restoring EDID to default")
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
} else { } else {
logger.Info().Str("edid", edid).Msg("Setting EDID") logger.Info().Str("edid", edid).Msg("Setting EDID")
} }

View File

@ -17,10 +17,9 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) { func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{ nativeInstance = native.NewNative(native.NativeOptions{
SystemVersion: systemVersion, SystemVersion: systemVersion,
AppVersion: appVersion, AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(), DisplayRotation: config.GetDisplayRotation(),
DefaultQualityFactor: config.VideoQualityFactor,
OnVideoStateChange: func(state native.VideoState) { OnVideoStateChange: func(state native.VideoState) {
lastVideoState = state lastVideoState = state
triggerVideoStateUpdate() triggerVideoStateUpdate()
@ -71,13 +70,7 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
}) })
}, },
}) })
nativeInstance.Start() nativeInstance.Start()
go func() {
if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil {
nativeLogger.Warn().Err(err).Msg("error setting EDID")
}
}()
if os.Getenv("JETKVM_CRASH_TESTING") == "1" { if os.Getenv("JETKVM_CRASH_TESTING") == "1" {
nativeInstance.DoNotUseThisIsForCrashTestingOnly() nativeInstance.DoNotUseThisIsForCrashTestingOnly()

View File

@ -25,18 +25,6 @@ func (sm *SessionManager) attemptEmergencyPromotion(ctx emergencyPromotionContex
sm.emergencyWindowMutex.Lock() sm.emergencyWindowMutex.Lock()
defer sm.emergencyWindowMutex.Unlock() defer sm.emergencyWindowMutex.Unlock()
// CRITICAL: Bypass all rate limits if no primary exists to prevent deadlock
// System availability takes priority over DoS protection
noPrimaryExists := (sm.primarySessionID == "")
if noPrimaryExists {
sm.logger.Info().
Str("triggerSessionID", ctx.triggerSessionID).
Str("triggerReason", ctx.triggerReason).
Msg("Bypassing emergency promotion rate limits - no primary exists")
promotedSessionID := sm.findMostTrustedSessionForEmergency()
return promotedSessionID, true, false
}
const slidingWindowDuration = 60 * time.Second const slidingWindowDuration = 60 * time.Second
const maxEmergencyPromotionsPerMinute = 3 const maxEmergencyPromotionsPerMinute = 3
@ -199,21 +187,19 @@ func (sm *SessionManager) promoteAfterGraceExpiration(expiredSessionID string, n
// handlePendingSessionTimeout removes timed-out pending sessions (DoS protection) // handlePendingSessionTimeout removes timed-out pending sessions (DoS protection)
// Returns true if any pending session was removed // Returns true if any pending session was removed
func (sm *SessionManager) handlePendingSessionTimeout(now time.Time) bool { func (sm *SessionManager) handlePendingSessionTimeout(now time.Time) bool {
toDelete := make([]string, 0) needsCleanup := false
for id, session := range sm.sessions { for id, session := range sm.sessions {
if session.Mode == SessionModePending && if session.Mode == SessionModePending &&
now.Sub(session.CreatedAt) > defaultPendingSessionTimeout { now.Sub(session.CreatedAt) > defaultPendingSessionTimeout {
websocketLogger.Debug(). websocketLogger.Info().
Str("sessionId", id). Str("sessionId", id).
Dur("age", now.Sub(session.CreatedAt)). Dur("age", now.Sub(session.CreatedAt)).
Msg("Removing timed-out pending session") Msg("Removing timed-out pending session")
toDelete = append(toDelete, id) delete(sm.sessions, id)
needsCleanup = true
} }
} }
for _, id := range toDelete { return needsCleanup
delete(sm.sessions, id)
}
return len(toDelete) > 0
} }
// handleObserverSessionCleanup removes inactive observer sessions with closed RPC channels // handleObserverSessionCleanup removes inactive observer sessions with closed RPC channels
@ -224,23 +210,21 @@ func (sm *SessionManager) handleObserverSessionCleanup(now time.Time) bool {
observerTimeout = time.Duration(currentSessionSettings.ObserverTimeout) * time.Second observerTimeout = time.Duration(currentSessionSettings.ObserverTimeout) * time.Second
} }
toDelete := make([]string, 0) needsCleanup := false
for id, session := range sm.sessions { for id, session := range sm.sessions {
if session.Mode == SessionModeObserver { if session.Mode == SessionModeObserver {
if session.RPCChannel == nil && now.Sub(session.LastActive) > observerTimeout { if session.RPCChannel == nil && now.Sub(session.LastActive) > observerTimeout {
sm.logger.Debug(). sm.logger.Info().
Str("sessionId", id). Str("sessionId", id).
Dur("inactiveFor", now.Sub(session.LastActive)). Dur("inactiveFor", now.Sub(session.LastActive)).
Dur("observerTimeout", observerTimeout). Dur("observerTimeout", observerTimeout).
Msg("Removing inactive observer session with closed RPC channel") Msg("Removing inactive observer session with closed RPC channel")
toDelete = append(toDelete, id) delete(sm.sessions, id)
needsCleanup = true
} }
} }
} }
for _, id := range toDelete { return needsCleanup
delete(sm.sessions, id)
}
return len(toDelete) > 0
} }
// handlePrimarySessionTimeout checks and handles primary session timeout // handlePrimarySessionTimeout checks and handles primary session timeout

View File

@ -250,10 +250,6 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
// Ensure session has auto-generated nickname if needed // Ensure session has auto-generated nickname if needed
sm.ensureNickname(session) sm.ensureNickname(session)
if !nicknameReserved && session.Nickname != "" {
sm.nicknameIndex[session.Nickname] = session
}
sm.sessions[session.ID] = session sm.sessions[session.ID] = session
// If this was the primary, try to restore primary status // If this was the primary, try to restore primary status
@ -1204,8 +1200,8 @@ func (sm *SessionManager) transferPrimaryRole(fromSessionID, toSessionID, transf
// Promote target session // Promote target session
toSession.Mode = SessionModePrimary toSession.Mode = SessionModePrimary
toSession.hidRPCAvailable = false toSession.hidRPCAvailable = false
// Reset LastActive for all emergency promotions to prevent immediate re-timeout // Reset LastActive only for emergency promotions to prevent immediate re-timeout
if strings.HasPrefix(transferType, "emergency_") { if transferType == "emergency_timeout_promotion" || transferType == "emergency_promotion_deadlock_prevention" {
toSession.LastActive = time.Now() toSession.LastActive = time.Now()
} }
sm.primarySessionID = toSessionID sm.primarySessionID = toSessionID

View File

@ -1,15 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { BacklightSettings, useSettingsStore } from "@/hooks/stores"; import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { usePermissions } from "@/hooks/usePermissions"; import { usePermissions } from "@/hooks/usePermissions";
import { Permission } from "@/types/permissions"; import { Permission } from "@/types/permissions";
import { Checkbox } from "@components/Checkbox";
import notifications from "../notifications"; import notifications from "../notifications";
import { UsbInfoSetting } from "../components/UsbInfoSetting"; import { UsbInfoSetting } from "../components/UsbInfoSetting";
@ -20,7 +18,6 @@ export default function SettingsHardwareRoute() {
const settings = useSettingsStore(); const settings = useSettingsStore();
const { setDisplayRotation } = useSettingsStore(); const { setDisplayRotation } = useSettingsStore();
const { hasPermission, isLoading, permissions } = usePermissions(); const { hasPermission, isLoading, permissions } = usePermissions();
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
const handleDisplayRotationChange = (rotation: string) => { const handleDisplayRotationChange = (rotation: string) => {
setDisplayRotation(rotation); setDisplayRotation(rotation);
@ -64,21 +61,7 @@ export default function SettingsHardwareRoute() {
}); });
}; };
const handlePowerSavingChange = (enabled: boolean) => { // Check permissions before fetching settings data
setPowerSavingEnabled(enabled);
const duration = enabled ? 90 : -1;
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
`Failed to set power saving mode: ${resp.error.data || "Unknown error"}`,
);
setPowerSavingEnabled(!enabled); // Revert on error
return;
}
notifications.success(`Power saving mode ${enabled ? "enabled" : "disabled"}`);
});
};
useEffect(() => { useEffect(() => {
// Only fetch settings if user has permission // Only fetch settings if user has permission
if (!isLoading && permissions[Permission.SETTINGS_READ] === true) { if (!isLoading && permissions[Permission.SETTINGS_READ] === true) {
@ -94,17 +77,6 @@ export default function SettingsHardwareRoute() {
} }
}, [send, setBacklightSettings, isLoading, permissions]); }, [send, setBacklightSettings, isLoading, permissions]);
useEffect(() => {
send("getVideoSleepMode", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error("Failed to get power saving mode:", resp.error);
return;
}
const result = resp.result as { enabled: boolean; duration: number };
setPowerSavingEnabled(result.duration >= 0);
});
}, [send]);
// Return early if permissions are loading // Return early if permissions are loading
if (isLoading) { if (isLoading) {
return ( return (
@ -220,26 +192,6 @@ export default function SettingsHardwareRoute() {
</p> </p>
</div> </div>
<FeatureFlag minAppVersion="0.4.9">
<div className="space-y-4">
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<SettingsSectionHeader
title="Power Saving"
description="Reduce power consumption when not in use"
/>
<SettingsItem
badge="Experimental"
title="HDMI Sleep Mode"
description="Turn off capture after 90 seconds of inactivity"
>
<Checkbox
checked={powerSavingEnabled}
onChange={(e) => handlePowerSavingChange(e.target.checked)}
/>
</SettingsItem>
</div>
</FeatureFlag>
<FeatureFlag minAppVersion="0.3.8"> <FeatureFlag minAppVersion="0.3.8">
<UsbDeviceSetting /> <UsbDeviceSetting />
</FeatureFlag> </FeatureFlag>

View File

@ -898,11 +898,10 @@ export default function KvmIdRoute() {
useEffect(() => { useEffect(() => {
if (appVersion) return; if (appVersion) return;
if (rpcDataChannel?.readyState !== "open") return;
getLocalVersion(); getLocalVersion();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [appVersion, rpcDataChannel?.readyState]); }, [appVersion]);
const ConnectionStatusElement = useMemo(() => { const ConnectionStatusElement = useMemo(() => {
const isOtherSession = location.pathname.includes("other-session"); const isOtherSession = location.pathname.includes("other-session");

View File

@ -7,7 +7,6 @@ import (
"net" "net"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/coder/websocket" "github.com/coder/websocket"
@ -67,14 +66,24 @@ type Session struct {
keysDownStateQueue chan usbgadget.KeysDownState keysDownStateQueue chan usbgadget.KeysDownState
} }
var actionSessions atomic.Int32 var (
actionSessions int = 0
activeSessionsMutex = &sync.Mutex{}
)
func incrActiveSessions() int32 { func incrActiveSessions() int {
return actionSessions.Add(1) activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions++
return actionSessions
} }
func getActiveSessions() int32 { func getActiveSessions() int {
return actionSessions.Load() activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
return actionSessions
} }
// CheckRPCRateLimit checks if the session has exceeded RPC rate limits (DoS protection) // CheckRPCRateLimit checks if the session has exceeded RPC rate limits (DoS protection)
@ -485,9 +494,9 @@ func newSession(config SessionConfig) (*Session, error) {
if isConnected { if isConnected {
isConnected = false isConnected = false
newCount := actionSessions.Add(-1) actionSessions--
onActiveSessionsChanged() onActiveSessionsChanged()
if newCount == 0 { if actionSessions == 0 {
onLastSessionDisconnected() onLastSessionDisconnected()
} }
} }