From 2b43ce6a3e9c92ccdee1368307c17402a51d0213 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Tue, 26 May 2026 20:19:43 +0800 Subject: [PATCH] Refresh client ownership developer docs --- docs/CHANGELOG.md | 3 ++ docs/CONTRIBUTING.md | 6 +++- docs/Development-Guide.md | 65 ++++++++++++++++++++++++--------------- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 49577d7..ddfa6eb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -83,6 +83,9 @@ - Refreshed contributor and development guidance so new commands are added through `command_catalog`, `exec_catalog`, and `i18n_text` instead of stale `ssh_server.c` / inline-`strcmp` instructions. +- Refreshed developer ownership guidance to match the current update-sequence + model: room broadcasts update shared state only, while each interactive + client renders and flushes its own SSH channel. - `exec_catalog` now owns SSH exec command matching as well as help metadata, reducing duplicate command knowledge in `src/exec.c`. - Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6632016..872d6d4 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -16,6 +16,9 @@ make release-check # release preflight make test # unit + integration tests make ci-test # local CI-equivalent checks make stress-test # concurrent-client stress test +make soak-test # idle/reconnect/control-plane soak +make slow-client-test # slow interactive-client backpressure +make user-lifecycle-test # two-user TUI lifecycle ``` ## Debug @@ -78,7 +81,8 @@ utf8.c → UTF-8 string handling ## Common Bugs to Avoid 1. Don't use `strtok()` on client data - use `strtok_r()` or copy first -2. Always increment ref_count before using client outside lock +2. Always use `client_addref()` / `client_release()` before using a client + outside `g_room->lock`; never modify `ref_count` directly 3. Check SSH API return values (can be SSH_ERROR, SSH_AGAIN, or negative) 4. UTF-8 chars are multi-byte - use utf8_* functions diff --git a/docs/Development-Guide.md b/docs/Development-Guide.md index b7c8b51..900531d 100644 --- a/docs/Development-Guide.md +++ b/docs/Development-Guide.md @@ -55,10 +55,13 @@ TNT uses a multi-threaded architecture with a main accept loop and per-client th ### Key Design Principles -1. **Fixed-size buffers** - No dynamic allocation in hot paths -2. **Reader-writer locks** - Multiple readers, single writer -3. **Reference counting** - Prevent use-after-free -4. **Ring buffer** - Fixed-size message history (last 100 messages) +1. **Fixed-size buffers** - Keep message, command, and UI buffers bounded +2. **Reader-writer locks** - Multiple readers, single writer for room state +3. **Per-client output ownership** - Each interactive session writes only to + its own SSH channel +4. **Reference counting** - Keep client objects alive across callbacks and + cross-thread lookups +5. **Ring buffer** - Fixed-size in-memory message history (last 100 messages) --- @@ -76,7 +79,7 @@ src/ ├── command_catalog.c - COMMAND-mode names, aliases, and help summaries ├── exec_catalog.c - SSH exec command matching and help metadata ├── exec.c - SSH exec command dispatch -├── chat_room.c - Chat room logic and message broadcasting +├── chat_room.c - Chat room state, message ring, and update sequence ├── message.c - Message persistence (RFC3339 format) ├── history_view.c - NORMAL-mode scroll window rules ├── tui.c - Terminal UI rendering (ANSI escape codes) @@ -119,12 +122,15 @@ typedef struct client { ssh_session session; ssh_channel channel; char username[MAX_USERNAME_LEN]; - int width, height; // Terminal dimensions + _Atomic int width, height; // Terminal dimensions client_mode_t mode; // INSERT/NORMAL/COMMAND int scroll_pos; - bool connected; + atomic_bool connected; + char *outbox; // Bounded queued interactive output + size_t outbox_len, outbox_pos; int ref_count; // Reference counting pthread_mutex_t ref_lock; + pthread_mutex_t io_lock; // Own SSH channel writes only } client_t; ``` @@ -134,6 +140,7 @@ typedef struct { pthread_rwlock_t lock; // Reader-writer lock struct client **clients; // Dynamic array int client_count; + uint64_t update_seq; // Bumped when message history changes message_t *messages; // Ring buffer int message_count; } chat_room_t; @@ -189,6 +196,9 @@ make anonymous-access-test # Verify default anonymous login behavior make connection-limit-test # Verify per-IP concurrency and rate limits make security-test # Run security feature checks make stress-test # Run configurable concurrent-client stress test +make soak-test # Run idle/reconnect/control-plane soak test +make slow-client-test # Run slow interactive-client backpressure test +make user-lifecycle-test # Run a two-user TUI lifecycle test make ci-test # Run the same checks as GitHub Actions # Individual tests @@ -197,6 +207,9 @@ cd tests ./test_security_features.sh # Security checks ./test_anonymous_access.sh # Anonymous access ./test_stress.sh # Concurrent connections +./test_soak.sh # Idle/reconnect soak +./test_slow_client.sh # Slow-client backpressure +./test_user_lifecycle.sh # Two-user TUI lifecycle ``` ### Test Coverage @@ -205,6 +218,10 @@ cd tests - **Security**: RSA keys, env vars, UTF-8 validation, buffer overflow protection - **Anonymous**: Passwordless access, any username - **Stress**: 10 concurrent clients for 30 seconds +- **Soak**: idle session, reconnect churn, health/stats/users/post/tail +- **Slow client**: unread interactive SSH client cannot block control paths +- **Lifecycle**: two-user TUI story covering help, history, search, private + messages, nickname, action messages, and persistence boundaries --- @@ -244,33 +261,29 @@ while ((!ctx->auth_success || ctx->channel == NULL || !ctx->channel_ready) && !t ### 2. Chat Room (chat_room.c) -**Thread-safe broadcasting:** +**Thread-safe message publication:** ```c void room_broadcast(chat_room_t *room, const message_t *msg) { pthread_rwlock_wrlock(&room->lock); - /* Copy client list with ref counting */ - client_t **clients_copy = calloc(...); - for (int i = 0; i < count; i++) { - clients_copy[i]->ref_count++; - } + room_add_message(room, msg); + room->update_seq++; - pthread_rwlock_unlock(&room->lock); // Release lock early - - /* Render outside lock (avoid deadlock) */ - for (int i = 0; i < count; i++) { - tui_render_screen(clients_copy[i]); - client_release(clients_copy[i]); - } + pthread_rwlock_unlock(&room->lock); } ``` **Why this works:** -- Copy client list while holding write lock -- Increment reference counts -- Release lock BEFORE rendering -- Render to all clients outside lock -- Decrement reference counts (may free clients) +- Broadcast updates shared room state only; it does not render or write to + any SSH channel. +- Each interactive session tracks `room_get_update_seq()` in its own + `input_run_session()` loop. +- When the sequence changes, the client renders and flushes its own output. +- This keeps slow SSH windows local to that client and prevents one recipient + from blocking a sender or the whole room. +- Cross-client lookups, such as mentions and private messages, must call + `client_addref()` before using a client pointer outside `g_room->lock`, then + `client_release()` when done. Do not increment `ref_count` directly. ### 3. Message Persistence (message.c) @@ -380,6 +393,8 @@ void utf8_remove_last_word(char *str) { ```sh tests/test_exec_mode.sh # exec command behavior tests/test_interactive_input.sh # COMMAND-mode/TUI behavior +tests/test_user_lifecycle.sh # end-to-end two-user TUI behavior +tests/test_slow_client.sh # slow SSH reader/backpressure behavior tests/unit/test_i18n.c # localized shared text tests/unit/test_command_catalog.c # interactive command metadata tests/unit/test_exec_catalog.c # exec command help metadata