diff --git a/TargetBridge-Receiver/TBReceiverC/Makefile b/TargetBridge-Receiver/TBReceiverC/Makefile index 26ca207..4eca060 100644 --- a/TargetBridge-Receiver/TBReceiverC/Makefile +++ b/TargetBridge-Receiver/TBReceiverC/Makefile @@ -34,7 +34,7 @@ ifeq ($(UNAME_S),Darwin) LDFLAGS += -framework ApplicationServices -framework AppKit -framework CoreFoundation -framework CoreGraphics -framework CoreText -framework VideoToolbox -framework CoreMedia -framework CoreVideo -framework IOSurface -framework CoreServices -framework CoreAudio -framework SystemConfiguration endif -C_SRC = src/main.c src/net.c src/decoder.c src/display.c src/input_queue.c src/receiver_profile.c src/tb_i18n.c +C_SRC = src/main.c src/net.c src/decoder.c src/display.c src/input_queue.c src/idle_watchdog.c src/receiver_profile.c src/tb_i18n.c OBJC_SRC = src/tb_gesture_bridge.m src/tb_display_tweaks.m OBJ = $(C_SRC:.c=.o) $(OBJC_SRC:.m=.o) BIN = tbreceiver @@ -51,7 +51,7 @@ $(BIN): $(OBJ) $(CC) $(CFLAGS) -fobjc-arc -c -o $@ $< clean: - rm -f $(OBJ) $(BIN) $(TEST_BIN) $(TEST_INPUT_QUEUE_BIN) $(TEST_RECEIVER_PROFILE_BIN) + rm -f $(OBJ) $(BIN) $(TEST_BIN) $(TEST_INPUT_QUEUE_BIN) $(TEST_IDLE_WATCHDOG_BIN) $(TEST_RECEIVER_PROFILE_BIN) # Unit tests for the packet parser. net.c is pure POSIX, so this needs no # ffmpeg/SDL/pkgconf — it runs anywhere, including CI, with no hardware. @@ -62,6 +62,7 @@ TEST_LDFLAGS += -framework CoreFoundation -framework SystemConfiguration endif TEST_BIN = test_net_parser TEST_INPUT_QUEUE_BIN = test_input_queue +TEST_IDLE_WATCHDOG_BIN = test_idle_watchdog TEST_RECEIVER_PROFILE_BIN = test_receiver_profile test_net_parser: tests/test_net_parser.c src/net.c src/net.h src/proto.h @@ -70,12 +71,16 @@ test_net_parser: tests/test_net_parser.c src/net.c src/net.h src/proto.h $(TEST_INPUT_QUEUE_BIN): tests/test_input_queue.c src/input_queue.c src/input_queue.h $(CC) $(TEST_CFLAGS) -Isrc tests/test_input_queue.c src/input_queue.c -o $@ +$(TEST_IDLE_WATCHDOG_BIN): tests/test_idle_watchdog.c src/idle_watchdog.c src/idle_watchdog.h + $(CC) $(TEST_CFLAGS) -Isrc tests/test_idle_watchdog.c src/idle_watchdog.c -o $@ + $(TEST_RECEIVER_PROFILE_BIN): tests/test_receiver_profile.c src/receiver_profile.c src/receiver_profile.h $(CC) $(TEST_CFLAGS) -Isrc tests/test_receiver_profile.c src/receiver_profile.c -o $@ -test: $(TEST_BIN) $(TEST_INPUT_QUEUE_BIN) $(TEST_RECEIVER_PROFILE_BIN) +test: $(TEST_BIN) $(TEST_INPUT_QUEUE_BIN) $(TEST_IDLE_WATCHDOG_BIN) $(TEST_RECEIVER_PROFILE_BIN) ./$(TEST_BIN) ./$(TEST_INPUT_QUEUE_BIN) + ./$(TEST_IDLE_WATCHDOG_BIN) ./$(TEST_RECEIVER_PROFILE_BIN) .PHONY: all clean test diff --git a/TargetBridge-Receiver/TBReceiverC/src/idle_watchdog.c b/TargetBridge-Receiver/TBReceiverC/src/idle_watchdog.c new file mode 100644 index 0000000..04c24d6 --- /dev/null +++ b/TargetBridge-Receiver/TBReceiverC/src/idle_watchdog.c @@ -0,0 +1,19 @@ +#include "idle_watchdog.h" + +enum tb_idle_watchdog_result tb_idle_watchdog_check(uint64_t now_ms, + uint64_t *last_activity_ms, + uint64_t timeout_ms, + uint64_t *elapsed_ms) { + if (!last_activity_ms || !elapsed_ms) return TB_IDLE_WATCHDOG_OK; + + if (now_ms < *last_activity_ms) { + *last_activity_ms = now_ms; + *elapsed_ms = 0; + return TB_IDLE_WATCHDOG_CLOCK_REWOUND; + } + + *elapsed_ms = now_ms - *last_activity_ms; + return *elapsed_ms >= timeout_ms + ? TB_IDLE_WATCHDOG_EXPIRED + : TB_IDLE_WATCHDOG_OK; +} diff --git a/TargetBridge-Receiver/TBReceiverC/src/idle_watchdog.h b/TargetBridge-Receiver/TBReceiverC/src/idle_watchdog.h new file mode 100644 index 0000000..3597316 --- /dev/null +++ b/TargetBridge-Receiver/TBReceiverC/src/idle_watchdog.h @@ -0,0 +1,20 @@ +#ifndef TB_IDLE_WATCHDOG_H +#define TB_IDLE_WATCHDOG_H + +#include + +enum tb_idle_watchdog_result { + TB_IDLE_WATCHDOG_OK = 0, + TB_IDLE_WATCHDOG_EXPIRED, + TB_IDLE_WATCHDOG_CLOCK_REWOUND +}; + +/* Compare timestamps without allowing an unsigned subtraction to turn a + * small backward jump into an immediate timeout. A backward jump rebases the + * last-activity timestamp so a genuinely stale connection can still expire. */ +enum tb_idle_watchdog_result tb_idle_watchdog_check(uint64_t now_ms, + uint64_t *last_activity_ms, + uint64_t timeout_ms, + uint64_t *elapsed_ms); + +#endif diff --git a/TargetBridge-Receiver/TBReceiverC/src/main.c b/TargetBridge-Receiver/TBReceiverC/src/main.c index f334413..ab3d8c0 100644 --- a/TargetBridge-Receiver/TBReceiverC/src/main.c +++ b/TargetBridge-Receiver/TBReceiverC/src/main.c @@ -16,6 +16,7 @@ #include "net.h" #include "decoder.h" #include "display.h" +#include "idle_watchdog.h" #include "receiver_profile.h" #include "proto.h" #include "tb_gesture_bridge.h" @@ -2054,19 +2055,33 @@ int main(int argc, char **argv) { close_client(&a); } else { socket_activity = drain_result; - if (drain_result > 0) a.last_recv_ms = t; + if (drain_result > 0) a.last_recv_ms = now_ms(); if (a.close_requested) { close_client(&a); - } else if (t - a.last_recv_ms >= TB_SENDER_IDLE_TIMEOUT_MS) { - /* The sender streams frames continuously and heartbeats - * every 2s. Total silence means it died without a FIN - * (crash, pulled cable, force sleep). Without this reap, - * the dead fd is held forever and — because the receiver - * is single-client — every future connect is locked out - * until the app is restarted. */ - fprintf(stderr, "[main] no data from sender for %llu ms; closing stale session\n", - (unsigned long long)(t - a.last_recv_ms)); - close_client(&a); + } else { + const uint64_t watchdog_now_ms = now_ms(); + uint64_t idle_ms = 0; + enum tb_idle_watchdog_result watchdog_result = + tb_idle_watchdog_check(watchdog_now_ms, + &a.last_recv_ms, + TB_SENDER_IDLE_TIMEOUT_MS, + &idle_ms); + if (watchdog_result == TB_IDLE_WATCHDOG_CLOCK_REWOUND) { + fprintf(stderr, + "[main] monotonic clock moved backwards; " + "rebasing sender idle watchdog\n"); + } else if (watchdog_result == TB_IDLE_WATCHDOG_EXPIRED) { + /* The sender streams frames continuously and heartbeats + * every 2s. Total silence means it died without a FIN + * (crash, pulled cable, force sleep). Without this reap, + * the dead fd is held forever and — because the receiver + * is single-client — every future connect is locked out + * until the app is restarted. */ + fprintf(stderr, + "[main] no data from sender for %llu ms; closing stale session\n", + (unsigned long long)idle_ms); + close_client(&a); + } } } } diff --git a/TargetBridge-Receiver/TBReceiverC/tests/test_idle_watchdog.c b/TargetBridge-Receiver/TBReceiverC/tests/test_idle_watchdog.c new file mode 100644 index 0000000..3defc41 --- /dev/null +++ b/TargetBridge-Receiver/TBReceiverC/tests/test_idle_watchdog.c @@ -0,0 +1,44 @@ +#include "idle_watchdog.h" + +#include +#include + +static int checks; +static int failures; + +#define CHECK(condition) do { \ + checks++; \ + if (!(condition)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + } \ +} while (0) + +int main(void) { + uint64_t last = 1000; + uint64_t elapsed = UINT64_MAX; + + CHECK(tb_idle_watchdog_check(10999, &last, 10000, &elapsed) == TB_IDLE_WATCHDOG_OK); + CHECK(elapsed == 9999); + CHECK(last == 1000); + + CHECK(tb_idle_watchdog_check(11000, &last, 10000, &elapsed) == TB_IDLE_WATCHDOG_EXPIRED); + CHECK(elapsed == 10000); + + /* Regression for issue #156: 52 ms backwards must not become + * UINT64_MAX - 51 and close an active session. */ + last = 5000; + elapsed = UINT64_MAX; + CHECK(tb_idle_watchdog_check(4948, &last, 10000, &elapsed) == + TB_IDLE_WATCHDOG_CLOCK_REWOUND); + CHECK(elapsed == 0); + CHECK(last == 4948); + + CHECK(tb_idle_watchdog_check(14947, &last, 10000, &elapsed) == TB_IDLE_WATCHDOG_OK); + CHECK(elapsed == 9999); + CHECK(tb_idle_watchdog_check(14948, &last, 10000, &elapsed) == TB_IDLE_WATCHDOG_EXPIRED); + CHECK(elapsed == 10000); + + printf("idle watchdog tests: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +}