Refresh client ownership developer docs

This commit is contained in:
m1ngsama 2026-05-26 20:19:43 +08:00
parent cbaf02c769
commit 2b43ce6a3e
3 changed files with 48 additions and 26 deletions

View file

@ -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

View file

@ -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

View file

@ -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