mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 09:14:38 +08:00
Compare commits
No commits in common. "d4b260c160311f7b6e892ab33db7689daa390df4" and "f3e2762f30fdc205df896e00a9d4ee05b30f43de" have entirely different histories.
d4b260c160
...
f3e2762f30
85 changed files with 577 additions and 3754 deletions
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
|
|
@ -35,9 +35,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Verify release tag matches source version
|
|
||||||
run: scripts/check_release_ref.sh "${GITHUB_REF_NAME}"
|
|
||||||
|
|
||||||
- name: Install dependencies (Ubuntu)
|
- name: Install dependencies (Ubuntu)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -100,9 +97,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Verify release tag matches source version
|
|
||||||
run: scripts/check_release_ref.sh "${GITHUB_REF_NAME}"
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -128,24 +122,12 @@ jobs:
|
||||||
body: |
|
body: |
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install the libssh runtime before running TNT:
|
|
||||||
```bash
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt install libssh-4
|
|
||||||
|
|
||||||
# Arch
|
|
||||||
sudo pacman -S libssh
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
brew install libssh
|
|
||||||
```
|
|
||||||
|
|
||||||
Download the binary for your platform:
|
Download the binary for your platform:
|
||||||
|
|
||||||
**Linux AMD64:**
|
**Linux AMD64:**
|
||||||
```bash
|
```bash
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-linux-amd64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-linux-amd64
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-linux-amd64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-linux-amd64
|
||||||
chmod +x tnt-linux-amd64
|
chmod +x tnt-linux-amd64
|
||||||
chmod +x tntctl-linux-amd64
|
chmod +x tntctl-linux-amd64
|
||||||
sudo mv tnt-linux-amd64 /usr/local/bin/tnt
|
sudo mv tnt-linux-amd64 /usr/local/bin/tnt
|
||||||
|
|
@ -154,8 +136,8 @@ jobs:
|
||||||
|
|
||||||
**Linux ARM64:**
|
**Linux ARM64:**
|
||||||
```bash
|
```bash
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-linux-arm64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-linux-arm64
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-linux-arm64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-linux-arm64
|
||||||
chmod +x tnt-linux-arm64
|
chmod +x tnt-linux-arm64
|
||||||
chmod +x tntctl-linux-arm64
|
chmod +x tntctl-linux-arm64
|
||||||
sudo mv tnt-linux-arm64 /usr/local/bin/tnt
|
sudo mv tnt-linux-arm64 /usr/local/bin/tnt
|
||||||
|
|
@ -164,8 +146,8 @@ jobs:
|
||||||
|
|
||||||
**macOS Intel:**
|
**macOS Intel:**
|
||||||
```bash
|
```bash
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-darwin-amd64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-darwin-amd64
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-darwin-amd64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-darwin-amd64
|
||||||
chmod +x tnt-darwin-amd64
|
chmod +x tnt-darwin-amd64
|
||||||
chmod +x tntctl-darwin-amd64
|
chmod +x tntctl-darwin-amd64
|
||||||
sudo mv tnt-darwin-amd64 /usr/local/bin/tnt
|
sudo mv tnt-darwin-amd64 /usr/local/bin/tnt
|
||||||
|
|
@ -174,8 +156,8 @@ jobs:
|
||||||
|
|
||||||
**macOS Apple Silicon:**
|
**macOS Apple Silicon:**
|
||||||
```bash
|
```bash
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-darwin-arm64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tnt-darwin-arm64
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-darwin-arm64
|
wget https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/tntctl-darwin-arm64
|
||||||
chmod +x tnt-darwin-arm64
|
chmod +x tnt-darwin-arm64
|
||||||
chmod +x tntctl-darwin-arm64
|
chmod +x tntctl-darwin-arm64
|
||||||
sudo mv tnt-darwin-arm64 /usr/local/bin/tnt
|
sudo mv tnt-darwin-arm64 /usr/local/bin/tnt
|
||||||
|
|
@ -184,15 +166,7 @@ jobs:
|
||||||
|
|
||||||
**Verify checksums:**
|
**Verify checksums:**
|
||||||
```bash
|
```bash
|
||||||
curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.txt
|
sha256sum -c checksums.txt
|
||||||
|
|
||||||
# Linux
|
|
||||||
sha256sum -c checksums.txt --ignore-missing
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
for f in tnt-* tntctl-*; do
|
|
||||||
grep " $f$" checksums.txt | shasum -a 256 -c -
|
|
||||||
done
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -24,5 +24,4 @@ tests/unit/test_help_text
|
||||||
tests/unit/test_manual_text
|
tests/unit/test_manual_text
|
||||||
tests/unit/test_support_text
|
tests/unit/test_support_text
|
||||||
tests/unit/test_cli_text
|
tests/unit/test_cli_text
|
||||||
tests/unit/test_tntctl_text
|
|
||||||
tests/unit/test_ratelimit
|
tests/unit/test_ratelimit
|
||||||
|
|
|
||||||
29
Makefile
29
Makefile
|
|
@ -4,7 +4,6 @@
|
||||||
CC = gcc
|
CC = gcc
|
||||||
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_XOPEN_SOURCE=700
|
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_XOPEN_SOURCE=700
|
||||||
LDFLAGS = -pthread -lssh
|
LDFLAGS = -pthread -lssh
|
||||||
CTL_LDFLAGS =
|
|
||||||
INCLUDES = -Iinclude
|
INCLUDES = -Iinclude
|
||||||
DEPFLAGS = -MMD -MP
|
DEPFLAGS = -MMD -MP
|
||||||
|
|
||||||
|
|
@ -21,12 +20,12 @@ SRC_DIR = src
|
||||||
INC_DIR = include
|
INC_DIR = include
|
||||||
OBJ_DIR = obj
|
OBJ_DIR = obj
|
||||||
|
|
||||||
SOURCES = $(filter-out $(SRC_DIR)/tntctl.c $(SRC_DIR)/tntctl_text.c,$(wildcard $(SRC_DIR)/*.c))
|
SOURCES = $(filter-out $(SRC_DIR)/tntctl.c,$(wildcard $(SRC_DIR)/*.c))
|
||||||
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
|
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
|
||||||
DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d)
|
DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d)
|
||||||
TARGET = tnt
|
TARGET = tnt
|
||||||
CTL_TARGET = tntctl
|
CTL_TARGET = tntctl
|
||||||
CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/tntctl_text.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o $(OBJ_DIR)/config_defaults.o $(OBJ_DIR)/i18n.o
|
CTL_OBJECTS = $(OBJ_DIR)/tntctl.o
|
||||||
TARGETS = $(TARGET) $(CTL_TARGET)
|
TARGETS = $(TARGET) $(CTL_TARGET)
|
||||||
|
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
|
|
@ -35,7 +34,7 @@ MANDIR ?= $(PREFIX)/share/man
|
||||||
SYSTEMD_UNIT_DIR ?= $(PREFIX)/lib/systemd/system
|
SYSTEMD_UNIT_DIR ?= $(PREFIX)/lib/systemd/system
|
||||||
CI_TEST_PORT ?= $(if $(PORT),$(PORT),2222)
|
CI_TEST_PORT ?= $(if $(PORT),$(PORT),2222)
|
||||||
|
|
||||||
.PHONY: all clean install install-systemd uninstall uninstall-systemd debug release release-check release-check-strict package-publish-check debian-source-package asan valgrind check test test-advisory ci-test unit-test script-test integration-test anonymous-access-test connection-limit-test security-test stress-test soak-test slow-client-test user-lifecycle-test info
|
.PHONY: all clean install install-systemd uninstall uninstall-systemd debug release release-check release-check-strict asan valgrind check test test-advisory ci-test unit-test integration-test anonymous-access-test connection-limit-test security-test stress-test soak-test user-lifecycle-test info
|
||||||
|
|
||||||
all: $(TARGETS)
|
all: $(TARGETS)
|
||||||
|
|
||||||
|
|
@ -44,7 +43,7 @@ $(TARGET): $(OBJECTS)
|
||||||
@echo "Build complete: $(TARGET)"
|
@echo "Build complete: $(TARGET)"
|
||||||
|
|
||||||
$(CTL_TARGET): $(CTL_OBJECTS)
|
$(CTL_TARGET): $(CTL_OBJECTS)
|
||||||
$(CC) $(CTL_OBJECTS) -o $@ $(CTL_LDFLAGS)
|
$(CC) $(CTL_OBJECTS) -o $@
|
||||||
@echo "Build complete: $(CTL_TARGET)"
|
@echo "Build complete: $(CTL_TARGET)"
|
||||||
|
|
||||||
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
|
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
|
||||||
|
|
@ -95,15 +94,8 @@ release-check:
|
||||||
release-check-strict:
|
release-check-strict:
|
||||||
./scripts/release_check.sh --strict
|
./scripts/release_check.sh --strict
|
||||||
|
|
||||||
package-publish-check:
|
|
||||||
./scripts/package_publish_check.sh
|
|
||||||
|
|
||||||
debian-source-package:
|
|
||||||
./scripts/package_debian_source.sh $${OUT_DIR:-dist/debian-source}
|
|
||||||
|
|
||||||
asan: CFLAGS += -g -fsanitize=address -fno-omit-frame-pointer
|
asan: CFLAGS += -g -fsanitize=address -fno-omit-frame-pointer
|
||||||
asan: LDFLAGS += -fsanitize=address
|
asan: LDFLAGS += -fsanitize=address
|
||||||
asan: CTL_LDFLAGS += -fsanitize=address
|
|
||||||
asan: clean $(TARGETS)
|
asan: clean $(TARGETS)
|
||||||
@echo "AddressSanitizer build complete. Run with: ASAN_OPTIONS=detect_leaks=1 ./tnt"
|
@echo "AddressSanitizer build complete. Run with: ASAN_OPTIONS=detect_leaks=1 ./tnt"
|
||||||
|
|
||||||
|
|
@ -116,7 +108,7 @@ check:
|
||||||
@command -v clang-tidy >/dev/null 2>&1 && clang-tidy src/*.c -- -Iinclude $(INCLUDES) || echo "clang-tidy not installed"
|
@command -v clang-tidy >/dev/null 2>&1 && clang-tidy src/*.c -- -Iinclude $(INCLUDES) || echo "clang-tidy not installed"
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
test: all unit-test script-test integration-test
|
test: all unit-test integration-test
|
||||||
|
|
||||||
test-advisory: all unit-test
|
test-advisory: all unit-test
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
|
|
@ -128,13 +120,6 @@ unit-test:
|
||||||
@echo "Running unit tests..."
|
@echo "Running unit tests..."
|
||||||
@$(MAKE) -C tests/unit run
|
@$(MAKE) -C tests/unit run
|
||||||
|
|
||||||
script-test: all
|
|
||||||
@echo "Running script tests..."
|
|
||||||
@cd tests && ./test_cli_options.sh
|
|
||||||
@cd tests && ./test_docs_help_surface.sh
|
|
||||||
@cd tests && ./test_logrotate.sh
|
|
||||||
@cd tests && ./test_message_log_tool.sh
|
|
||||||
|
|
||||||
integration-test: all
|
integration-test: all
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
@cd tests && PORT=$${PORT:-2222} ./test_basic.sh
|
@cd tests && PORT=$${PORT:-2222} ./test_basic.sh
|
||||||
|
|
@ -163,10 +148,6 @@ soak-test: all
|
||||||
@echo "Running soak tests..."
|
@echo "Running soak tests..."
|
||||||
@cd tests && PORT=$${PORT:-2222} ./test_soak.sh $${DURATION:-8} $${RECONNECTS:-5}
|
@cd tests && PORT=$${PORT:-2222} ./test_soak.sh $${DURATION:-8} $${RECONNECTS:-5}
|
||||||
|
|
||||||
slow-client-test: all
|
|
||||||
@echo "Running slow-client tests..."
|
|
||||||
@cd tests && PORT=$${PORT:-2222} ./test_slow_client.sh $${DURATION:-8} $${BURST_CHARS:-1600}
|
|
||||||
|
|
||||||
user-lifecycle-test: all
|
user-lifecycle-test: all
|
||||||
@echo "Running user lifecycle tests..."
|
@echo "Running user lifecycle tests..."
|
||||||
@cd tests && PORT=$${PORT:-2222} ./test_user_lifecycle.sh
|
@cd tests && PORT=$${PORT:-2222} ./test_user_lifecycle.sh
|
||||||
|
|
|
||||||
56
README.md
56
README.md
|
|
@ -48,12 +48,9 @@ PORT=3333 tnt # via env var
|
||||||
### Connecting
|
### Connecting
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh -p 2222 localhost
|
ssh -p 2222 chat.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
For a deployed server, replace `localhost` with your public host, for example
|
|
||||||
`chat.example.com`.
|
|
||||||
|
|
||||||
**Anonymous access by default**: Users can connect with ANY username/password (or empty password). No SSH keys required. Perfect for public chat servers.
|
**Anonymous access by default**: Users can connect with ANY username/password (or empty password). No SSH keys required. Perfect for public chat servers.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
@ -98,7 +95,7 @@ Ctrl+C - Exit chat
|
||||||
:w <user> <text> - Short alias for :msg
|
:w <user> <text> - Short alias for :msg
|
||||||
:inbox - Show private messages
|
:inbox - Show private messages
|
||||||
:last [N] - Show last N messages from history (max 50, default 10)
|
:last [N] - Show last N messages from history (max 50, default 10)
|
||||||
:search <keyword> - Search message history (shows last 15 matches)
|
:search <keyword> - Search full message history (case-insensitive)
|
||||||
:mute-joins - Toggle join/leave system notifications
|
:mute-joins - Toggle join/leave system notifications
|
||||||
:lang <en|zh> - Switch UI language for this session
|
:lang <en|zh> - Switch UI language for this session
|
||||||
:help - Show concise manual
|
:help - Show concise manual
|
||||||
|
|
@ -108,10 +105,6 @@ Up/Down - Browse command history
|
||||||
ESC - Return to NORMAL mode
|
ESC - Return to NORMAL mode
|
||||||
```
|
```
|
||||||
|
|
||||||
Command output pages use `j/k`, `Ctrl+D/U`, and `g/G` for paging. `:inbox`
|
|
||||||
is live: press `r` to refresh it manually, and it refreshes when a new private
|
|
||||||
message arrives while the inbox is open.
|
|
||||||
|
|
||||||
**Special messages (INSERT mode)**
|
**Special messages (INSERT mode)**
|
||||||
```
|
```
|
||||||
/me <action> - Send action (e.g. /me waves)
|
/me <action> - Send action (e.g. /me waves)
|
||||||
|
|
@ -200,7 +193,6 @@ ssh -p 2222 chat.example.com health
|
||||||
ssh -p 2222 chat.example.com stats --json
|
ssh -p 2222 chat.example.com stats --json
|
||||||
ssh -p 2222 chat.example.com users
|
ssh -p 2222 chat.example.com users
|
||||||
ssh -p 2222 chat.example.com "tail -n 20"
|
ssh -p 2222 chat.example.com "tail -n 20"
|
||||||
ssh -p 2222 chat.example.com "dump -n 100"
|
|
||||||
ssh -p 2222 operator@chat.example.com post "service notice"
|
ssh -p 2222 operator@chat.example.com post "service notice"
|
||||||
ssh -p 2222 chat.example.com post "/me deploys v2.0"
|
ssh -p 2222 chat.example.com post "/me deploys v2.0"
|
||||||
```
|
```
|
||||||
|
|
@ -216,34 +208,9 @@ around the same SSH exec interface:
|
||||||
```sh
|
```sh
|
||||||
tntctl chat.example.com health
|
tntctl chat.example.com health
|
||||||
tntctl -p 2222 chat.example.com stats --json
|
tntctl -p 2222 chat.example.com stats --json
|
||||||
tntctl -p 2222 chat.example.com dump -n 100
|
|
||||||
tntctl -l operator chat.example.com post "service notice"
|
tntctl -l operator chat.example.com post "service notice"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Log Maintenance
|
|
||||||
|
|
||||||
Persisted public history is stored as `messages.log` in the TNT state
|
|
||||||
directory. For manual maintenance, archive and compact it with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
scripts/logrotate.sh /var/lib/tnt/messages.log 100 10000
|
|
||||||
```
|
|
||||||
|
|
||||||
The script archives the full log, keeps the last `KEEP_LINES` records in the
|
|
||||||
active file, compresses the archive when `gzip` is available, and can be
|
|
||||||
previewed with `--dry-run`.
|
|
||||||
|
|
||||||
Installed binaries also include offline checks for the v1 log format:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tnt --log-check /var/lib/tnt/messages.log
|
|
||||||
tnt --log-recover /var/lib/tnt/messages.log > messages.recovered.log
|
|
||||||
```
|
|
||||||
|
|
||||||
`--log-check` prints record counts and exits non-zero when invalid records are
|
|
||||||
found. `--log-recover` writes valid records to stdout and reports skipped
|
|
||||||
records to stderr; it never edits the source log in place.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
@ -267,7 +234,6 @@ make connection-limit-test # verify per-IP concurrency and rate limits
|
||||||
make security-test # run security feature checks
|
make security-test # run security feature checks
|
||||||
make stress-test # run configurable concurrent-client stress test
|
make stress-test # run configurable concurrent-client stress test
|
||||||
make soak-test # run idle/reconnect/control-plane soak 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 user-lifecycle-test # run a two-user TUI lifecycle test
|
||||||
make ci-test # run the same checks as GitHub Actions
|
make ci-test # run the same checks as GitHub Actions
|
||||||
|
|
||||||
|
|
@ -279,7 +245,6 @@ cd tests
|
||||||
./test_connection_limits.sh # per-IP concurrency and rate limits
|
./test_connection_limits.sh # per-IP concurrency and rate limits
|
||||||
./test_stress.sh # stress test
|
./test_stress.sh # stress test
|
||||||
./test_soak.sh # soak test
|
./test_soak.sh # soak test
|
||||||
./test_slow_client.sh # slow-client backpressure
|
|
||||||
./test_user_lifecycle.sh # two-user TUI lifecycle
|
./test_user_lifecycle.sh # two-user TUI lifecycle
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -288,8 +253,6 @@ cd tests
|
||||||
- Anonymous access: 2 tests
|
- Anonymous access: 2 tests
|
||||||
- Security features: 12 tests
|
- Security features: 12 tests
|
||||||
- Stress test: configurable concurrent clients (`CLIENTS=20 DURATION=60 make stress-test`)
|
- Stress test: configurable concurrent clients (`CLIENTS=20 DURATION=60 make stress-test`)
|
||||||
- Slow-client test: an unread interactive SSH client cannot block health,
|
|
||||||
stats, post, tail, or server survival checks
|
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
|
|
@ -324,7 +287,6 @@ TNT/
|
||||||
│ ├── exec_catalog.c # SSH exec command matching, usage, and argument shape
|
│ ├── exec_catalog.c # SSH exec command matching, usage, and argument shape
|
||||||
│ ├── exec.c # SSH exec command dispatch
|
│ ├── exec.c # SSH exec command dispatch
|
||||||
│ ├── tntctl.c # local wrapper around the SSH exec interface
|
│ ├── tntctl.c # local wrapper around the SSH exec interface
|
||||||
│ ├── tntctl_text.c # tntctl help and option text
|
|
||||||
│ ├── ssh_server.c # SSH server implementation
|
│ ├── ssh_server.c # SSH server implementation
|
||||||
│ ├── bootstrap.c # SSH authentication and session bootstrap
|
│ ├── bootstrap.c # SSH authentication and session bootstrap
|
||||||
│ ├── chat_room.c # chat room logic
|
│ ├── chat_room.c # chat room logic
|
||||||
|
|
@ -395,17 +357,10 @@ Before preparing a release locally:
|
||||||
make release-check
|
make release-check
|
||||||
```
|
```
|
||||||
|
|
||||||
Longer local preflight can opt into runtime soak and slow-client coverage:
|
Before publishing package recipes, replace placeholder checksums and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
|
make release-check-strict
|
||||||
```
|
|
||||||
|
|
||||||
Before publishing package recipes, download the final GitHub source archive,
|
|
||||||
replace placeholder checksums, and run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
@ -417,9 +372,6 @@ motd.txt - Message of the Day (optional, shown to users on connect)
|
||||||
tnt.service - systemd service unit
|
tnt.service - systemd service unit
|
||||||
```
|
```
|
||||||
|
|
||||||
The persisted chat-history format is documented in
|
|
||||||
[docs/MESSAGE_LOG.md](docs/MESSAGE_LOG.md).
|
|
||||||
|
|
||||||
### MOTD (Message of the Day)
|
### MOTD (Message of the Day)
|
||||||
|
|
||||||
Place a `motd.txt` file in the state directory to show a welcome message to every user on connect. Users see the MOTD before entering the chat and press any key to continue.
|
Place a `motd.txt` file in the state directory to show a welcome message to every user on connect. Users see the MOTD before entering the chat and press any key to continue.
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,8 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Added a release tag/version guard used by the GitHub release workflow, so a
|
|
||||||
`vX.Y.Z` tag must match `TNT_VERSION` before release assets are built.
|
|
||||||
- Added `make package-publish-check` for verifying Arch/Homebrew source
|
|
||||||
checksums against the final GitHub source archive after a tag exists.
|
|
||||||
- Added a `config_defaults` module and unit coverage for runtime default
|
|
||||||
values, env keys, and accepted numeric ranges.
|
|
||||||
- Added a dedicated `tntctl_text` module with unit coverage for local
|
|
||||||
`tntctl` help and validation diagnostics.
|
|
||||||
- Documented the stable SSH exec interface contract, including exit statuses
|
- Documented the stable SSH exec interface contract, including exit statuses
|
||||||
and JSON field shapes for package tests, scripts, and future `tntctl` work.
|
and JSON field shapes for package tests, scripts, and future `tntctl` work.
|
||||||
- Documented `messages.log` v1 as the stable TNT 1.x persisted history format,
|
|
||||||
including parser, sanitization, and partial-record recovery rules.
|
|
||||||
- Added `dump [N]` / `dump -n N` to the SSH exec interface and `tntctl` for
|
|
||||||
exporting valid persisted `messages.log` v1 records.
|
|
||||||
- Added regression-tested manual log archive and compaction coverage for
|
|
||||||
`scripts/logrotate.sh`.
|
|
||||||
- Added offline `tnt --log-check` and `tnt --log-recover` modes for auditing
|
|
||||||
and recovering valid `messages.log` v1 records without editing the source
|
|
||||||
log in place.
|
|
||||||
- Added a public security policy, supported-version guidance, and GitHub issue
|
- Added a public security policy, supported-version guidance, and GitHub issue
|
||||||
templates for bug reports and feature requests.
|
templates for bug reports and feature requests.
|
||||||
- Added `tntctl`, a thin local wrapper around the documented SSH exec
|
- Added `tntctl`, a thin local wrapper around the documented SSH exec
|
||||||
|
|
@ -34,31 +17,8 @@
|
||||||
the main onboarding, chat, help, history, search, private-message, nickname,
|
the main onboarding, chat, help, history, search, private-message, nickname,
|
||||||
action-message, and exit paths.
|
action-message, and exit paths.
|
||||||
- Added a VHS tape draft for recording the core TNT terminal-chat experience.
|
- Added a VHS tape draft for recording the core TNT terminal-chat experience.
|
||||||
- Added live `:inbox` refresh behavior: `r` refreshes the inbox manually, and
|
|
||||||
an open inbox refreshes when a new private message arrives.
|
|
||||||
- Added `/` in NORMAL mode as a fast history-search entrypoint backed by the
|
|
||||||
existing `:search` command.
|
|
||||||
- Added `make slow-client-test`, an opt-in regression for an unread
|
|
||||||
interactive SSH client under backpressure while health, stats, post, tail,
|
|
||||||
and server survival stay responsive.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- INSERT-mode chrome now only advertises message sending and `Esc` to NORMAL;
|
|
||||||
`? keys` appears only in NORMAL mode, matching where help keys work.
|
|
||||||
- Dismissing MOTD now returns first-time users to INSERT mode, and `Ctrl+C`
|
|
||||||
closes the full key reference before it disconnects from NORMAL mode.
|
|
||||||
- COMMAND mode now accepts an optional leading `:` in typed commands, matching
|
|
||||||
the way commands are written in the manual.
|
|
||||||
- `:search` output and docs now state that the command shows the last 15
|
|
||||||
matches, avoiding the impression that the pager is a complete result set.
|
|
||||||
- Release checks now separate tag/source-archive readiness from package-manager
|
|
||||||
checksum publishing, avoiding self-referential checksum requirements before
|
|
||||||
the final GitHub source archive exists.
|
|
||||||
- `tntctl --help` now gets its exec command list from `exec_catalog`, reducing
|
|
||||||
duplicate command metadata between the local wrapper and SSH exec mode.
|
|
||||||
- Updated `tnt(1)` to document the current TUI search and pager keys, and
|
|
||||||
added script coverage to keep active help surfaces free of removed support
|
|
||||||
commands.
|
|
||||||
- `make install-systemd` now rewrites the installed unit's `ExecStart` to match
|
- `make install-systemd` now rewrites the installed unit's `ExecStart` to match
|
||||||
the selected `PREFIX`/`BINDIR`, so package builds that install to `/usr`
|
the selected `PREFIX`/`BINDIR`, so package builds that install to `/usr`
|
||||||
produce a unit pointing at `/usr/bin/tnt`.
|
produce a unit pointing at `/usr/bin/tnt`.
|
||||||
|
|
@ -70,9 +30,6 @@
|
||||||
- The release guide now documents SemVer expectations, manual release review,
|
- The release guide now documents SemVer expectations, manual release review,
|
||||||
smoke testing, and rollback steps.
|
smoke testing, and rollback steps.
|
||||||
- Package installs now include `tntctl` and its man page alongside `tnt`.
|
- Package installs now include `tntctl` and its man page alongside `tnt`.
|
||||||
- The binary naming policy is now explicit: `tnt` remains the stable 1.x
|
|
||||||
server process name, and any future `tntd` split requires a major-version
|
|
||||||
compatibility plan.
|
|
||||||
- SSH exec commands longer than the command buffer are now rejected with a
|
- SSH exec commands longer than the command buffer are now rejected with a
|
||||||
usage error instead of being truncated and executed.
|
usage error instead of being truncated and executed.
|
||||||
- SSH exec `post` now persists the message before broadcasting or returning
|
- SSH exec `post` now persists the message before broadcasting or returning
|
||||||
|
|
@ -83,28 +40,6 @@
|
||||||
- Interactive client writes now pass through a bounded per-client outbox and
|
- Interactive client writes now pass through a bounded per-client outbox and
|
||||||
flush against the remote SSH window from that client's session loop. Exec
|
flush against the remote SSH window from that client's session loop. Exec
|
||||||
sessions still write synchronously to preserve script output ordering.
|
sessions still write synchronously to preserve script output ordering.
|
||||||
- Session callback refs are now owned and released through `client.c`, so
|
|
||||||
bootstrap and interactive cleanup no longer need to manually mirror the
|
|
||||||
main-ref / callback-ref release sequence.
|
|
||||||
- Message-log replay and search now share one strict record parser and skip
|
|
||||||
malformed, invalid UTF-8, extra-separator, oversized, or unterminated
|
|
||||||
records instead of accepting partial replay data.
|
|
||||||
- `scripts/logrotate.sh` now has validated arguments, stable exit statuses,
|
|
||||||
dry-run support, archive retention, gzip-aware archives, and a regression
|
|
||||||
test in the normal test suite.
|
|
||||||
- `messages.log` v1 record parsing and formatting now live in a dedicated
|
|
||||||
`message_log` module instead of being embedded in `message.c`.
|
|
||||||
- Offline message-log recovery shares the same `message_log` parser used by
|
|
||||||
replay, search, and `dump`, so recovery behavior follows the documented v1
|
|
||||||
contract.
|
|
||||||
- The two-user lifecycle test now covers opening `:inbox` before a private
|
|
||||||
message arrives, matching the way users often leave an inbox page open.
|
|
||||||
- Help and command-output pagers now accept arrow keys, PgUp/PgDn, Home/End,
|
|
||||||
and Space/`b` in addition to the existing Vim-style keys.
|
|
||||||
- Pre-login username entry now handles Ctrl+C/Ctrl+D cancel, Ctrl+U clear
|
|
||||||
line, and Ctrl+W delete-word before the user joins the room.
|
|
||||||
- Long COMMAND-mode input is now left-truncated with a visible marker in the
|
|
||||||
status line instead of wrapping and damaging the TUI.
|
|
||||||
- Private-message inbox access now uses its own mutex instead of sharing the
|
- Private-message inbox access now uses its own mutex instead of sharing the
|
||||||
SSH channel write lock, reducing unrelated contention on slow clients.
|
SSH channel write lock, reducing unrelated contention on slow clients.
|
||||||
- Client writes now check the SSH channel's remote window before writing and
|
- Client writes now check the SSH channel's remote window before writing and
|
||||||
|
|
@ -112,8 +47,6 @@
|
||||||
direct slow-reader blocking path.
|
direct slow-reader blocking path.
|
||||||
- `make release-check` can now run the soak test with `RUN_SOAK=1`, keeping
|
- `make release-check` can now run the soak test with `RUN_SOAK=1`, keeping
|
||||||
longer runtime checks opt-in for local release validation.
|
longer runtime checks opt-in for local release validation.
|
||||||
- `make release-check` can also run the slow-client backpressure test with
|
|
||||||
`RUN_SLOW_CLIENT=1`.
|
|
||||||
- Room capacity and mention notification bookkeeping now follow
|
- Room capacity and mention notification bookkeeping now follow
|
||||||
`TNT_MAX_CONNECTIONS` instead of a hidden fixed 64-client array limit.
|
`TNT_MAX_CONNECTIONS` instead of a hidden fixed 64-client array limit.
|
||||||
- Updated the roadmap to reflect completed `tntctl`, stable exec contract, and
|
- Updated the roadmap to reflect completed `tntctl`, stable exec contract, and
|
||||||
|
|
@ -124,27 +57,6 @@
|
||||||
source release.
|
source release.
|
||||||
- Release documentation now creates the local tag before strict release checks,
|
- Release documentation now creates the local tag before strict release checks,
|
||||||
matching the strict gate's tag-at-HEAD requirement.
|
matching the strict gate's tag-at-HEAD requirement.
|
||||||
- Startup option parsing now reports missing values for `--bind`, `-p`,
|
|
||||||
`--idle-timeout`, and related flags with the localized
|
|
||||||
"option requires argument" diagnostic instead of treating the option as
|
|
||||||
unknown.
|
|
||||||
- `tntctl` now reuses the SSH exec command matcher for local command
|
|
||||||
validation, so `tntctl host --help` reaches the server-side exec help alias
|
|
||||||
instead of being rejected locally.
|
|
||||||
- `tntctl` local help and local validation errors now follow `TNT_LANG` and
|
|
||||||
locale selection, matching the server CLI's i18n behavior.
|
|
||||||
- Arch and Debian packaging drafts now create the `tnt` system user used by
|
|
||||||
the packaged systemd unit, and release preflight checks that metadata.
|
|
||||||
- The Homebrew formula draft now defines a `brew services` entry that runs the
|
|
||||||
installed `tnt` binary with state under `var/tnt`.
|
|
||||||
- Added `scripts/package_debian_source.sh` and `make debian-source-package`
|
|
||||||
to assemble Debian/Ubuntu source-package trees from the current project
|
|
||||||
without publishing or uploading anything.
|
|
||||||
- Release preflight now smoke-tests the staged installed `tnt` binary's
|
|
||||||
`--log-check` and `--log-recover` modes, catching package artifact drift.
|
|
||||||
- The i18n helper now supports language-keyed string initializers through
|
|
||||||
`I18N_STRING_MAP`, so future languages can be added incrementally without
|
|
||||||
changing every existing two-language string initializer.
|
|
||||||
- Split UI-language parsing from localized text lookup: `src/i18n.c` now owns
|
- Split UI-language parsing from localized text lookup: `src/i18n.c` now owns
|
||||||
locale/code parsing, while `src/i18n_text.c` owns the table-driven text
|
locale/code parsing, while `src/i18n_text.c` owns the table-driven text
|
||||||
catalog with coverage checks for every message ID.
|
catalog with coverage checks for every message ID.
|
||||||
|
|
@ -159,15 +71,10 @@
|
||||||
- Refreshed contributor and development guidance so new commands are added
|
- Refreshed contributor and development guidance so new commands are added
|
||||||
through `command_catalog`, `exec_catalog`, and `i18n_text` instead of stale
|
through `command_catalog`, `exec_catalog`, and `i18n_text` instead of stale
|
||||||
`ssh_server.c` / inline-`strcmp` instructions.
|
`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,
|
- `exec_catalog` now owns SSH exec command matching as well as help metadata,
|
||||||
reducing duplicate command knowledge in `src/exec.c`.
|
reducing duplicate command knowledge in `src/exec.c`.
|
||||||
- Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so
|
- Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so
|
||||||
public documentation does not imply a specific production host.
|
public documentation does not imply a specific production host.
|
||||||
- First-run connection examples now use `localhost`, keeping
|
|
||||||
`chat.example.com` for deployed public-host examples.
|
|
||||||
- Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so
|
- Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so
|
||||||
`src/exec.c` no longer duplicates `--json` and required-message validation.
|
`src/exec.c` no longer duplicates `--json` and required-message validation.
|
||||||
- Moved interactive command usage text and first-pass argument-shape checks
|
- Moved interactive command usage text and first-pass argument-shape checks
|
||||||
|
|
|
||||||
24
docs/CICD.md
24
docs/CICD.md
|
|
@ -35,17 +35,15 @@ Release policy:
|
||||||
- packaging/arch/PKGBUILD
|
- packaging/arch/PKGBUILD
|
||||||
- packaging/homebrew/tnt-chat.rb
|
- packaging/homebrew/tnt-chat.rb
|
||||||
- packaging/debian/debian/changelog
|
- packaging/debian/debian/changelog
|
||||||
- maintainer metadata, when preparing public package recipes
|
- package checksums and maintainer metadata, when preparing public package
|
||||||
|
recipes
|
||||||
|
|
||||||
2. Run the local preflight:
|
2. Run the local preflight:
|
||||||
make release-check
|
make release-check
|
||||||
|
|
||||||
For a longer local runtime gate before publishing or production rollout:
|
|
||||||
RUN_SOAK=1 RUN_SLOW_CLIENT=1 make release-check
|
|
||||||
|
|
||||||
3. Commit the release changes and create a local tag. Do not push the tag
|
3. Commit the release changes and create a local tag. Do not push the tag
|
||||||
until strict checks pass:
|
until strict checks pass:
|
||||||
git tag vX.Y.Z
|
git tag v1.0.1
|
||||||
|
|
||||||
4. Run strict release checks:
|
4. Run strict release checks:
|
||||||
make release-check-strict
|
make release-check-strict
|
||||||
|
|
@ -55,7 +53,7 @@ Release policy:
|
||||||
untracked and would be missing from GitHub's source archive.
|
untracked and would be missing from GitHub's source archive.
|
||||||
|
|
||||||
5. Push the tag:
|
5. Push the tag:
|
||||||
git push origin vX.Y.Z
|
git push origin v1.0.1
|
||||||
|
|
||||||
6. GitHub Actions automatically:
|
6. GitHub Actions automatically:
|
||||||
- Builds `tnt` and `tntctl` binaries (Linux/macOS, AMD64/ARM64)
|
- Builds `tnt` and `tntctl` binaries (Linux/macOS, AMD64/ARM64)
|
||||||
|
|
@ -76,9 +74,7 @@ RELEASE REVIEW CHECKLIST
|
||||||
Before publishing a draft release:
|
Before publishing a draft release:
|
||||||
- Confirm `git tag` points at the intended commit.
|
- Confirm `git tag` points at the intended commit.
|
||||||
- Download every release asset from GitHub, not from the local workspace.
|
- Download every release asset from GitHub, not from the local workspace.
|
||||||
- Verify downloaded assets against `checksums.txt` (`sha256sum -c
|
- Verify `checksums.txt` with `sha256sum -c checksums.txt`.
|
||||||
checksums.txt --ignore-missing` on Linux, or `shasum -a 256 -c` for each
|
|
||||||
downloaded asset on macOS).
|
|
||||||
- Run downloaded `tnt --version` and `tntctl --version`.
|
- Run downloaded `tnt --version` and `tntctl --version`.
|
||||||
- Start a temporary server and check:
|
- Start a temporary server and check:
|
||||||
ssh -p 2222 server health
|
ssh -p 2222 server health
|
||||||
|
|
@ -88,10 +84,8 @@ Before publishing a draft release:
|
||||||
ssh -p 2222 server "tail -n 1"
|
ssh -p 2222 server "tail -n 1"
|
||||||
- Check runtime dynamic links (`ldd` on Linux, `otool -L` on macOS) and make
|
- Check runtime dynamic links (`ldd` on Linux, `otool -L` on macOS) and make
|
||||||
sure `libssh` is documented for the target install path.
|
sure `libssh` is documented for the target install path.
|
||||||
- Confirm `make release-check-strict` passed before pushing the tag.
|
- Confirm `make release-check-strict` passed after package checksums were
|
||||||
- For package-manager recipes, download the final GitHub source archive,
|
replaced.
|
||||||
replace Arch/Homebrew source checksums, then run:
|
|
||||||
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
|
|
||||||
|
|
||||||
|
|
||||||
ROLLBACK
|
ROLLBACK
|
||||||
|
|
@ -161,8 +155,8 @@ make && make asan && make release-check
|
||||||
./tnt
|
./tnt
|
||||||
|
|
||||||
# Create release
|
# Create release
|
||||||
git tag vX.Y.Z
|
git tag v1.0.1
|
||||||
git push origin vX.Y.Z
|
git push origin v1.0.1
|
||||||
# Wait 5 minutes for builds
|
# Wait 5 minutes for builds
|
||||||
|
|
||||||
# Deploy to production manually after validation
|
# Deploy to production manually after validation
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@ make release-check # release preflight
|
||||||
make test # unit + integration tests
|
make test # unit + integration tests
|
||||||
make ci-test # local CI-equivalent checks
|
make ci-test # local CI-equivalent checks
|
||||||
make stress-test # concurrent-client stress test
|
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
|
## Debug
|
||||||
|
|
@ -40,7 +37,6 @@ make check
|
||||||
```
|
```
|
||||||
main.c → entry point, signal handling
|
main.c → entry point, signal handling
|
||||||
cli_text.c → startup CLI text
|
cli_text.c → startup CLI text
|
||||||
tntctl_text.c → tntctl local help and diagnostics
|
|
||||||
command_catalog.c → COMMAND-mode command metadata, usage, and argument shape
|
command_catalog.c → COMMAND-mode command metadata, usage, and argument shape
|
||||||
commands.c → COMMAND-mode command dispatch
|
commands.c → COMMAND-mode command dispatch
|
||||||
exec_catalog.c → SSH exec command matching, usage, and argument shape
|
exec_catalog.c → SSH exec command matching, usage, and argument shape
|
||||||
|
|
@ -82,8 +78,7 @@ utf8.c → UTF-8 string handling
|
||||||
## Common Bugs to Avoid
|
## Common Bugs to Avoid
|
||||||
|
|
||||||
1. Don't use `strtok()` on client data - use `strtok_r()` or copy first
|
1. Don't use `strtok()` on client data - use `strtok_r()` or copy first
|
||||||
2. Always use `client_addref()` / `client_release()` before using a client
|
2. Always increment ref_count before using client outside lock
|
||||||
outside `g_room->lock`; never modify `ref_count` directly
|
|
||||||
3. Check SSH API return values (can be SSH_ERROR, SSH_AGAIN, or negative)
|
3. Check SSH API return values (can be SSH_ERROR, SSH_AGAIN, or negative)
|
||||||
4. UTF-8 chars are multi-byte - use utf8_* functions
|
4. UTF-8 chars are multi-byte - use utf8_* functions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
|
||||||
|
|
||||||
Specific version:
|
Specific version:
|
||||||
```bash
|
```bash
|
||||||
VERSION=vX.Y.Z curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
|
VERSION=v1.0.1 curl -sSL https://raw.githubusercontent.com/m1ngsama/TNT/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual Install
|
## Manual Install
|
||||||
|
|
@ -18,12 +18,12 @@ Download binary for your platform from [releases](https://github.com/m1ngsama/TN
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux AMD64
|
# Linux AMD64
|
||||||
curl -LO https://github.com/m1ngsama/TNT/releases/latest/download/tnt-linux-amd64
|
wget https://github.com/m1ngsama/TNT/releases/latest/download/tnt-linux-amd64
|
||||||
chmod +x tnt-linux-amd64
|
chmod +x tnt-linux-amd64
|
||||||
sudo mv tnt-linux-amd64 /usr/local/bin/tnt
|
sudo mv tnt-linux-amd64 /usr/local/bin/tnt
|
||||||
|
|
||||||
# macOS ARM64 (Apple Silicon)
|
# macOS ARM64 (Apple Silicon)
|
||||||
curl -LO https://github.com/m1ngsama/TNT/releases/latest/download/tnt-darwin-arm64
|
wget https://github.com/m1ngsama/TNT/releases/latest/download/tnt-darwin-arm64
|
||||||
chmod +x tnt-darwin-arm64
|
chmod +x tnt-darwin-arm64
|
||||||
sudo mv tnt-darwin-arm64 /usr/local/bin/tnt
|
sudo mv tnt-darwin-arm64 /usr/local/bin/tnt
|
||||||
```
|
```
|
||||||
|
|
@ -107,34 +107,6 @@ sudo rm /var/lib/tnt/motd.txt
|
||||||
|
|
||||||
No restart required — TNT reads the file on each new connection.
|
No restart required — TNT reads the file on each new connection.
|
||||||
|
|
||||||
## Manual Log Maintenance
|
|
||||||
|
|
||||||
TNT stores public chat history in `messages.log` under the state directory.
|
|
||||||
Use the maintenance script from a source checkout when the service is stopped
|
|
||||||
or during a quiet maintenance window:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl stop tnt
|
|
||||||
sudo scripts/logrotate.sh /var/lib/tnt/messages.log 100 10000
|
|
||||||
sudo systemctl start tnt
|
|
||||||
```
|
|
||||||
|
|
||||||
The arguments are `LOG_FILE MAX_SIZE_MB KEEP_LINES`. The script archives the
|
|
||||||
full log, compacts the active log to the last `KEEP_LINES` records, compresses
|
|
||||||
the archive when `gzip` is available, and keeps the newest five archives by
|
|
||||||
default. Use `--dry-run` to preview actions, or `--keep-archives N` to change
|
|
||||||
archive retention.
|
|
||||||
|
|
||||||
Before replacing a suspicious log, inspect and recover it offline:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tnt --log-check /var/lib/tnt/messages.log
|
|
||||||
tnt --log-recover /var/lib/tnt/messages.log > /var/lib/tnt/messages.recovered.log
|
|
||||||
```
|
|
||||||
|
|
||||||
`--log-recover` writes valid records to stdout and reports skipped records to
|
|
||||||
stderr. Review the recovered file before replacing the active log.
|
|
||||||
|
|
||||||
## Firewall
|
## Firewall
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,10 @@ TNT uses a multi-threaded architecture with a main accept loop and per-client th
|
||||||
|
|
||||||
### Key Design Principles
|
### Key Design Principles
|
||||||
|
|
||||||
1. **Fixed-size buffers** - Keep message, command, and UI buffers bounded
|
1. **Fixed-size buffers** - No dynamic allocation in hot paths
|
||||||
2. **Reader-writer locks** - Multiple readers, single writer for room state
|
2. **Reader-writer locks** - Multiple readers, single writer
|
||||||
3. **Per-client output ownership** - Each interactive session writes only to
|
3. **Reference counting** - Prevent use-after-free
|
||||||
its own SSH channel
|
4. **Ring buffer** - Fixed-size message history (last 100 messages)
|
||||||
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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -72,7 +69,6 @@ TNT uses a multi-threaded architecture with a main accept loop and per-client th
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── main.c - CLI entry point and startup option parsing
|
├── main.c - CLI entry point and startup option parsing
|
||||||
├── cli_text.c - Server CLI help and option diagnostics
|
|
||||||
├── ssh_server.c - SSH listener setup and connection accept loop
|
├── ssh_server.c - SSH listener setup and connection accept loop
|
||||||
├── bootstrap.c - SSH authentication/session bootstrap
|
├── bootstrap.c - SSH authentication/session bootstrap
|
||||||
├── input.c - Interactive session loop and key handling
|
├── input.c - Interactive session loop and key handling
|
||||||
|
|
@ -80,12 +76,8 @@ src/
|
||||||
├── command_catalog.c - COMMAND-mode names, aliases, and help summaries
|
├── command_catalog.c - COMMAND-mode names, aliases, and help summaries
|
||||||
├── exec_catalog.c - SSH exec command matching and help metadata
|
├── exec_catalog.c - SSH exec command matching and help metadata
|
||||||
├── exec.c - SSH exec command dispatch
|
├── exec.c - SSH exec command dispatch
|
||||||
├── tntctl.c - Local wrapper around the SSH exec interface
|
├── chat_room.c - Chat room logic and message broadcasting
|
||||||
├── tntctl_text.c - tntctl local help and diagnostics
|
|
||||||
├── chat_room.c - Chat room state, message ring, and update sequence
|
|
||||||
├── message.c - Message persistence (RFC3339 format)
|
├── message.c - Message persistence (RFC3339 format)
|
||||||
├── message_log.c - messages.log v1 parsing and formatting
|
|
||||||
├── message_log_tool.c - Offline messages.log check/recover CLI
|
|
||||||
├── history_view.c - NORMAL-mode scroll window rules
|
├── history_view.c - NORMAL-mode scroll window rules
|
||||||
├── tui.c - Terminal UI rendering (ANSI escape codes)
|
├── tui.c - Terminal UI rendering (ANSI escape codes)
|
||||||
├── tui_status.c - Mode/status/input-line rendering
|
├── tui_status.c - Mode/status/input-line rendering
|
||||||
|
|
@ -108,20 +100,13 @@ include/
|
||||||
├── bootstrap.h - SSH session bootstrap interface
|
├── bootstrap.h - SSH session bootstrap interface
|
||||||
├── chat_room.h - Chat room interface
|
├── chat_room.h - Chat room interface
|
||||||
├── message.h - Message structure and persistence
|
├── message.h - Message structure and persistence
|
||||||
├── message_log.h - messages.log v1 parser/formatter interface
|
|
||||||
├── message_log_tool.h - Offline log check/recover interface
|
|
||||||
├── command_catalog.h - COMMAND-mode command metadata interface
|
├── command_catalog.h - COMMAND-mode command metadata interface
|
||||||
├── exec_catalog.h - SSH exec command metadata interface
|
|
||||||
├── cli_text.h - Server CLI text interface
|
|
||||||
├── tntctl_text.h - tntctl text interface
|
|
||||||
├── history_view.h - Scroll-state helpers
|
├── history_view.h - Scroll-state helpers
|
||||||
├── tui.h - TUI rendering functions
|
├── tui.h - TUI rendering functions
|
||||||
├── tui_status.h - TUI status/input-line rendering interface
|
|
||||||
├── i18n.h - Language and shared text IDs
|
├── i18n.h - Language and shared text IDs
|
||||||
├── help_text.h - Key reference text interface
|
├── help_text.h - Key reference text interface
|
||||||
├── manual.h - Concise manual panel interface
|
├── manual.h - Concise manual panel interface
|
||||||
├── manual_text.h - Concise manual text interface
|
├── manual_text.h - Concise manual text interface
|
||||||
├── system_message.h - Localized system message builders
|
|
||||||
├── ratelimit.h - Connection limit interface
|
├── ratelimit.h - Connection limit interface
|
||||||
└── utf8.h - UTF-8 utilities
|
└── utf8.h - UTF-8 utilities
|
||||||
```
|
```
|
||||||
|
|
@ -134,16 +119,12 @@ typedef struct client {
|
||||||
ssh_session session;
|
ssh_session session;
|
||||||
ssh_channel channel;
|
ssh_channel channel;
|
||||||
char username[MAX_USERNAME_LEN];
|
char username[MAX_USERNAME_LEN];
|
||||||
_Atomic int width, height; // Terminal dimensions
|
int width, height; // Terminal dimensions
|
||||||
client_mode_t mode; // INSERT/NORMAL/COMMAND
|
client_mode_t mode; // INSERT/NORMAL/COMMAND
|
||||||
int scroll_pos;
|
int scroll_pos;
|
||||||
atomic_bool connected;
|
bool connected;
|
||||||
char *outbox; // Bounded queued interactive output
|
|
||||||
size_t outbox_len, outbox_pos;
|
|
||||||
int ref_count; // Reference counting
|
int ref_count; // Reference counting
|
||||||
pthread_mutex_t ref_lock;
|
pthread_mutex_t ref_lock;
|
||||||
pthread_mutex_t io_lock; // Own SSH channel writes only
|
|
||||||
bool channel_callback_ref; // Ref held while callbacks are installed
|
|
||||||
} client_t;
|
} client_t;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -153,7 +134,6 @@ typedef struct {
|
||||||
pthread_rwlock_t lock; // Reader-writer lock
|
pthread_rwlock_t lock; // Reader-writer lock
|
||||||
struct client **clients; // Dynamic array
|
struct client **clients; // Dynamic array
|
||||||
int client_count;
|
int client_count;
|
||||||
uint64_t update_seq; // Bumped when message history changes
|
|
||||||
message_t *messages; // Ring buffer
|
message_t *messages; // Ring buffer
|
||||||
int message_count;
|
int message_count;
|
||||||
} chat_room_t;
|
} chat_room_t;
|
||||||
|
|
@ -209,9 +189,6 @@ make anonymous-access-test # Verify default anonymous login behavior
|
||||||
make connection-limit-test # Verify per-IP concurrency and rate limits
|
make connection-limit-test # Verify per-IP concurrency and rate limits
|
||||||
make security-test # Run security feature checks
|
make security-test # Run security feature checks
|
||||||
make stress-test # Run configurable concurrent-client stress test
|
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
|
make ci-test # Run the same checks as GitHub Actions
|
||||||
|
|
||||||
# Individual tests
|
# Individual tests
|
||||||
|
|
@ -220,9 +197,6 @@ cd tests
|
||||||
./test_security_features.sh # Security checks
|
./test_security_features.sh # Security checks
|
||||||
./test_anonymous_access.sh # Anonymous access
|
./test_anonymous_access.sh # Anonymous access
|
||||||
./test_stress.sh # Concurrent connections
|
./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
|
### Test Coverage
|
||||||
|
|
@ -231,10 +205,6 @@ cd tests
|
||||||
- **Security**: RSA keys, env vars, UTF-8 validation, buffer overflow protection
|
- **Security**: RSA keys, env vars, UTF-8 validation, buffer overflow protection
|
||||||
- **Anonymous**: Passwordless access, any username
|
- **Anonymous**: Passwordless access, any username
|
||||||
- **Stress**: 10 concurrent clients for 30 seconds
|
- **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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -274,48 +244,41 @@ while ((!ctx->auth_success || ctx->channel == NULL || !ctx->channel_ready) && !t
|
||||||
|
|
||||||
### 2. Chat Room (chat_room.c)
|
### 2. Chat Room (chat_room.c)
|
||||||
|
|
||||||
**Thread-safe message publication:**
|
**Thread-safe broadcasting:**
|
||||||
```c
|
```c
|
||||||
void room_broadcast(chat_room_t *room, const message_t *msg) {
|
void room_broadcast(chat_room_t *room, const message_t *msg) {
|
||||||
pthread_rwlock_wrlock(&room->lock);
|
pthread_rwlock_wrlock(&room->lock);
|
||||||
|
|
||||||
room_add_message(room, msg);
|
/* Copy client list with ref counting */
|
||||||
room->update_seq++;
|
client_t **clients_copy = calloc(...);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
clients_copy[i]->ref_count++;
|
||||||
|
}
|
||||||
|
|
||||||
pthread_rwlock_unlock(&room->lock);
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Why this works:**
|
**Why this works:**
|
||||||
- Broadcast updates shared room state only; it does not render or write to
|
- Copy client list while holding write lock
|
||||||
any SSH channel.
|
- Increment reference counts
|
||||||
- Each interactive session tracks `room_get_update_seq()` in its own
|
- Release lock BEFORE rendering
|
||||||
`input_run_session()` loop.
|
- Render to all clients outside lock
|
||||||
- When the sequence changes, the client renders and flushes its own output.
|
- Decrement reference counts (may free clients)
|
||||||
- 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.
|
|
||||||
- Session callback lifetime is owned by `client.c`: `client_install_channel_callbacks()`
|
|
||||||
takes the callback ref, and `client_release_session()` removes callbacks and
|
|
||||||
releases both the callback ref and the session main ref.
|
|
||||||
|
|
||||||
### 3. Message Persistence (message.c)
|
### 3. Message Persistence (message.c)
|
||||||
|
|
||||||
See [MESSAGE_LOG.md](MESSAGE_LOG.md) for the stable TNT 1.x on-disk record
|
|
||||||
contract.
|
|
||||||
|
|
||||||
**Log format:**
|
**Log format:**
|
||||||
```
|
```
|
||||||
2024-01-13T10:30:45Z|username|message content
|
2024-01-13T10:30:45Z|username|message content
|
||||||
```
|
```
|
||||||
|
|
||||||
Log replay and search use the same strict parser. A record is accepted only
|
|
||||||
when it has exactly three fields, a strict UTC RFC3339 timestamp, valid UTF-8
|
|
||||||
username/content, bounded field lengths, and a trailing newline. Unterminated
|
|
||||||
last lines are treated as partial writes and skipped.
|
|
||||||
|
|
||||||
**Optimized loading** (backward scan):
|
**Optimized loading** (backward scan):
|
||||||
```c
|
```c
|
||||||
/* Scan backwards from file end */
|
/* Scan backwards from file end */
|
||||||
|
|
@ -417,13 +380,9 @@ void utf8_remove_last_word(char *str) {
|
||||||
```sh
|
```sh
|
||||||
tests/test_exec_mode.sh # exec command behavior
|
tests/test_exec_mode.sh # exec command behavior
|
||||||
tests/test_interactive_input.sh # COMMAND-mode/TUI 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_i18n.c # localized shared text
|
||||||
tests/unit/test_command_catalog.c # interactive command metadata
|
tests/unit/test_command_catalog.c # interactive command metadata
|
||||||
tests/unit/test_exec_catalog.c # exec command help metadata
|
tests/unit/test_exec_catalog.c # exec command help metadata
|
||||||
tests/unit/test_tntctl_text.c # tntctl local help/diagnostic text
|
|
||||||
tests/test_docs_help_surface.sh # active help/manual drift checks
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding a New Keybinding
|
### Adding a New Keybinding
|
||||||
|
|
@ -490,10 +449,6 @@ keys.
|
||||||
fragments.
|
fragments.
|
||||||
- Keep placeholders visible and stable, for example `%s`, `%d`,
|
- Keep placeholders visible and stable, for example `%s`, `%d`,
|
||||||
`<user>`, and `<message>`.
|
`<user>`, and `<message>`.
|
||||||
- Use `I18N_STRING(en, zh)` for ordinary two-language entries. Use
|
|
||||||
`I18N_STRING_MAP(I18N_EN(...), I18N_ZH(...))` when an entry needs
|
|
||||||
language-keyed initialization so future languages can be added without
|
|
||||||
changing every existing initializer.
|
|
||||||
- Every new user-facing string needs tests for at least English fallback
|
- Every new user-facing string needs tests for at least English fallback
|
||||||
and Chinese output while this project has two UI languages.
|
and Chinese output while this project has two UI languages.
|
||||||
|
|
||||||
|
|
@ -502,8 +457,7 @@ keys.
|
||||||
The current `src/i18n_text.c` implementation is a small-project translation
|
The current `src/i18n_text.c` implementation is a small-project translation
|
||||||
table implemented in C, not a full gettext catalog. It is acceptable for two
|
table implemented in C, not a full gettext catalog. It is acceptable for two
|
||||||
languages because message lookup is already split from language parsing in
|
languages because message lookup is already split from language parsing in
|
||||||
`src/i18n.c`, and localized strings can now be initialized by language key.
|
`src/i18n.c`, but adding more languages should move toward catalog-like
|
||||||
Adding many more languages should still move toward external catalog-like
|
|
||||||
storage instead of adding ad hoc branches for every locale.
|
storage instead of adding ad hoc branches for every locale.
|
||||||
|
|
||||||
Relevant conventions:
|
Relevant conventions:
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,9 @@ tnt -p 2222 -d /var/lib/tnt
|
||||||
## Connect
|
## Connect
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh -p 2222 localhost
|
ssh -p 2222 chat.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
For a deployed server, replace `localhost` with your public host.
|
|
||||||
|
|
||||||
Default access rules:
|
Default access rules:
|
||||||
|
|
||||||
- Any SSH username is accepted.
|
- Any SSH username is accepted.
|
||||||
|
|
@ -66,10 +64,7 @@ Esc enter NORMAL mode
|
||||||
i return to INSERT mode
|
i return to INSERT mode
|
||||||
: enter COMMAND mode
|
: enter COMMAND mode
|
||||||
? open the full key reference
|
? open the full key reference
|
||||||
/ search message history
|
|
||||||
G or End jump to latest messages
|
G or End jump to latest messages
|
||||||
Up/Down recall sent messages in INSERT mode
|
|
||||||
Tab complete @mention in INSERT mode
|
|
||||||
Ctrl+C disconnect from NORMAL mode
|
Ctrl+C disconnect from NORMAL mode
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -201,11 +196,9 @@ tnt
|
||||||
### 连接
|
### 连接
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh -p 2222 localhost
|
ssh -p 2222 chat.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
部署到公网后,将 `localhost` 替换为你的域名。
|
|
||||||
|
|
||||||
默认情况下,任意 SSH 用户名和空密码都可以连接。进入后 TNT 会询问显示名称。
|
默认情况下,任意 SSH 用户名和空密码都可以连接。进入后 TNT 会询问显示名称。
|
||||||
|
|
||||||
### 常用操作
|
### 常用操作
|
||||||
|
|
@ -216,10 +209,7 @@ Esc 进入 NORMAL 模式
|
||||||
i 回到 INSERT 模式
|
i 回到 INSERT 模式
|
||||||
: 输入命令
|
: 输入命令
|
||||||
? 查看完整按键参考
|
? 查看完整按键参考
|
||||||
/ 搜索消息历史
|
|
||||||
G 或 End 回到最新消息
|
G 或 End 回到最新消息
|
||||||
Up/Down 在 INSERT 模式调出已发送消息
|
|
||||||
Tab 在 INSERT 模式补全 @mention
|
|
||||||
:help 查看简明手册
|
:help 查看简明手册
|
||||||
:lang en|zh 切换界面语言
|
:lang en|zh 切换界面语言
|
||||||
:q 断开连接
|
:q 断开连接
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,25 @@
|
||||||
This document defines the public surfaces that scripts, package tests, and
|
This document defines the public surfaces that scripts, package tests, and
|
||||||
operators may rely on.
|
operators may rely on.
|
||||||
|
|
||||||
For 1.x, the public binary names are stable:
|
TNT is still evolving toward a split `tntd` / `tntctl` model. The stable
|
||||||
|
control surface is the SSH exec interface exposed by the `tnt` daemon.
|
||||||
- `tnt` is the server process and daemon entrypoint.
|
`tntctl` is a thin wrapper around that same interface.
|
||||||
- `tntctl` is a thin local wrapper around the SSH exec interface.
|
|
||||||
|
|
||||||
TNT will not introduce a separate `tntd` binary during 1.x. If the project
|
|
||||||
ever splits the server into `tntd`, that change must ship with a major-version
|
|
||||||
compatibility plan, package migration notes, and a transition period for the
|
|
||||||
`tnt` command.
|
|
||||||
|
|
||||||
## Stability Scope
|
## Stability Scope
|
||||||
|
|
||||||
Stable:
|
Stable:
|
||||||
|
|
||||||
- public binary names for 1.x: `tnt` and `tntctl`
|
|
||||||
- documented command-line flags in `tnt(1)`
|
- documented command-line flags in `tnt(1)`
|
||||||
- documented environment variables in `tnt(1)`
|
- documented environment variables in `tnt(1)`
|
||||||
- SSH exec command names and argument shapes listed below
|
- SSH exec command names and argument shapes listed below
|
||||||
- SSH exec exit statuses
|
- SSH exec exit statuses
|
||||||
- JSON field names and value types for documented `--json` commands
|
- JSON field names and value types for documented `--json` commands
|
||||||
- `messages.log` v1 record format documented in
|
|
||||||
[MESSAGE_LOG.md](MESSAGE_LOG.md)
|
|
||||||
|
|
||||||
Not yet stable:
|
Not yet stable:
|
||||||
|
|
||||||
- exact human-readable diagnostic wording
|
- exact human-readable diagnostic wording
|
||||||
- interactive TUI layout
|
- interactive TUI layout
|
||||||
- future storage migration tooling
|
- on-disk message log format
|
||||||
- internal module names and helper functions
|
- internal module names and helper functions
|
||||||
|
|
||||||
## Exit Status
|
## Exit Status
|
||||||
|
|
@ -56,7 +47,6 @@ ssh -p 2222 chat.example.com health
|
||||||
ssh -p 2222 chat.example.com stats --json
|
ssh -p 2222 chat.example.com stats --json
|
||||||
ssh -p 2222 chat.example.com users --json
|
ssh -p 2222 chat.example.com users --json
|
||||||
ssh -p 2222 chat.example.com "tail -n 20"
|
ssh -p 2222 chat.example.com "tail -n 20"
|
||||||
ssh -p 2222 chat.example.com "dump -n 100"
|
|
||||||
ssh -p 2222 operator@chat.example.com post "service notice"
|
ssh -p 2222 operator@chat.example.com post "service notice"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -65,7 +55,6 @@ The same commands can be run through `tntctl`:
|
||||||
```sh
|
```sh
|
||||||
tntctl chat.example.com health
|
tntctl chat.example.com health
|
||||||
tntctl -p 2222 chat.example.com stats --json
|
tntctl -p 2222 chat.example.com stats --json
|
||||||
tntctl -p 2222 chat.example.com dump -n 100
|
|
||||||
tntctl -l operator chat.example.com post "service notice"
|
tntctl -l operator chat.example.com post "service notice"
|
||||||
tntctl --host-key-checking accept-new chat.example.com users
|
tntctl --host-key-checking accept-new chat.example.com users
|
||||||
```
|
```
|
||||||
|
|
@ -130,22 +119,6 @@ Prints recent in-memory messages as tab-separated lines:
|
||||||
The current upper bound is `MAX_MESSAGES`. This command reads the live
|
The current upper bound is `MAX_MESSAGES`. This command reads the live
|
||||||
in-memory room buffer, not the full persisted log.
|
in-memory room buffer, not the full persisted log.
|
||||||
|
|
||||||
### `dump [N]` / `dump -n N`
|
|
||||||
|
|
||||||
Exports valid persisted `messages.log` v1 records in chronological order:
|
|
||||||
|
|
||||||
```text
|
|
||||||
2026-05-25T12:00:00Z|alice|hello
|
|
||||||
```
|
|
||||||
|
|
||||||
Without `N`, `dump` exports all valid persisted records. With `N`, it exports
|
|
||||||
the last `N` valid persisted records. Malformed, invalid UTF-8, oversized, or
|
|
||||||
truncated records are skipped by the same strict parser used for replay and
|
|
||||||
search.
|
|
||||||
|
|
||||||
This command reads the on-disk log, not the live in-memory room buffer. A
|
|
||||||
missing log produces empty output and exit status `0`.
|
|
||||||
|
|
||||||
### `post MESSAGE`
|
### `post MESSAGE`
|
||||||
|
|
||||||
Posts a message as the SSH login name and prints:
|
Posts a message as the SSH login name and prints:
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
# Message Log
|
|
||||||
|
|
||||||
This document defines the persisted chat-history format used by TNT 1.x.
|
|
||||||
|
|
||||||
## Format: `messages.log` v1
|
|
||||||
|
|
||||||
Each record is one UTF-8 line:
|
|
||||||
|
|
||||||
```text
|
|
||||||
RFC3339_UTC|username|content\n
|
|
||||||
```
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```text
|
|
||||||
2026-05-27T12:34:56Z|alice|hello
|
|
||||||
```
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Timestamp is strict UTC RFC3339: `YYYY-MM-DDTHH:MM:SSZ`.
|
|
||||||
- The separator is literal `|`.
|
|
||||||
- A valid record has exactly three fields and exactly two separators.
|
|
||||||
- `username` and `content` must be non-empty valid UTF-8.
|
|
||||||
- `username` must fit `MAX_USERNAME_LEN`; `content` must fit
|
|
||||||
`MAX_MESSAGE_LEN`.
|
|
||||||
- Every complete record ends with `\n`.
|
|
||||||
|
|
||||||
The file has no header. The version is defined by this record contract so
|
|
||||||
existing append-only logs remain readable.
|
|
||||||
|
|
||||||
## Write Behavior
|
|
||||||
|
|
||||||
`message_save()` sanitizes fields before appending:
|
|
||||||
|
|
||||||
- `|`, `\n`, and `\r` in usernames become `_`.
|
|
||||||
- `|`, `\n`, and `\r` in content become spaces.
|
|
||||||
- Timestamps are written in UTC.
|
|
||||||
|
|
||||||
Private messages are not written to `messages.log`.
|
|
||||||
|
|
||||||
## Replay And Search
|
|
||||||
|
|
||||||
Replay and search use the same strict parser. TNT skips records that are:
|
|
||||||
|
|
||||||
- malformed or missing fields
|
|
||||||
- invalid UTF-8
|
|
||||||
- too long
|
|
||||||
- outside the accepted timestamp window
|
|
||||||
- terminated without a trailing newline
|
|
||||||
- written with extra separators
|
|
||||||
|
|
||||||
Skipping a bad record is intentional recovery behavior. A truncated final
|
|
||||||
line is treated as a partial append and ignored rather than replayed.
|
|
||||||
|
|
||||||
## Export
|
|
||||||
|
|
||||||
`dump [N]` and `dump -n N` export valid persisted records through the SSH exec
|
|
||||||
interface and `tntctl`. The output format is exactly the v1 record format
|
|
||||||
above. Without `N`, `dump` exports all valid records; with `N`, it exports the
|
|
||||||
last `N` valid records.
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
`scripts/logrotate.sh` is the manual archive and compaction tool for
|
|
||||||
`messages.log`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
scripts/logrotate.sh [--dry-run] [--keep-archives N] LOG_FILE MAX_SIZE_MB KEEP_LINES
|
|
||||||
```
|
|
||||||
|
|
||||||
When the log exceeds `MAX_SIZE_MB`, the script archives the full file, compacts
|
|
||||||
the active file to the last `KEEP_LINES` records, compresses the archive when
|
|
||||||
`gzip` is available, and removes older archives beyond the retention limit.
|
|
||||||
Run it while TNT is stopped or during a quiet maintenance window if strict log
|
|
||||||
consistency matters.
|
|
||||||
|
|
||||||
## Recovery
|
|
||||||
|
|
||||||
Installed `tnt` binaries provide offline log checking and recovery:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
tnt --log-check LOG_FILE
|
|
||||||
tnt --log-recover LOG_FILE > recovered.messages.log
|
|
||||||
```
|
|
||||||
|
|
||||||
`--log-check` prints a summary:
|
|
||||||
|
|
||||||
```text
|
|
||||||
path /var/lib/tnt/messages.log
|
|
||||||
records_seen 120
|
|
||||||
valid_records 119
|
|
||||||
invalid_records 1
|
|
||||||
first_invalid_line 120
|
|
||||||
```
|
|
||||||
|
|
||||||
It exits `0` when every record is valid and `1` when invalid records are found
|
|
||||||
or the log cannot be read. `--log-recover` writes only valid v1 records to
|
|
||||||
stdout, prints the same summary to stderr, and also exits `1` if records were
|
|
||||||
skipped. It never modifies the source log.
|
|
||||||
|
|
||||||
## Compatibility
|
|
||||||
|
|
||||||
The v1 record format is stable for TNT 1.x. Future incompatible storage
|
|
||||||
changes must document downgrade behavior in release notes and provide an
|
|
||||||
operator-visible migration or export path.
|
|
||||||
|
|
@ -15,9 +15,6 @@ TEST
|
||||||
make connection-limit-test per-IP concurrency/rate-limit checks
|
make connection-limit-test per-IP concurrency/rate-limit checks
|
||||||
make security-test security feature checks
|
make security-test security feature checks
|
||||||
make stress-test concurrent-client stress test
|
make stress-test concurrent-client stress test
|
||||||
make soak-test idle/reconnect/control-plane soak test
|
|
||||||
make slow-client-test slow interactive-client backpressure test
|
|
||||||
make user-lifecycle-test two-user TUI lifecycle test
|
|
||||||
make ci-test same checks as GitHub Actions
|
make ci-test same checks as GitHub Actions
|
||||||
|
|
||||||
DEBUG
|
DEBUG
|
||||||
|
|
@ -46,27 +43,9 @@ INSERT MODE
|
||||||
limit 1023 bytes/message; over-limit input rings bell
|
limit 1023 bytes/message; over-limit input rings bell
|
||||||
normal opens/follows latest; k/PgUp older, j/PgDn newer
|
normal opens/follows latest; k/PgUp older, j/PgDn newer
|
||||||
|
|
||||||
EXEC COMMANDS
|
|
||||||
health print service health
|
|
||||||
stats [--json] print room statistics
|
|
||||||
users [--json] list online users
|
|
||||||
tail [N] / tail -n N recent in-memory room messages
|
|
||||||
dump [N] / dump -n N persisted messages.log v1 records
|
|
||||||
post <message> post as the SSH login name
|
|
||||||
|
|
||||||
MAINTENANCE
|
|
||||||
scripts/logrotate.sh LOG_FILE MAX_SIZE_MB KEEP_LINES
|
|
||||||
archive and compact messages.log
|
|
||||||
scripts/logrotate.sh --dry-run ...
|
|
||||||
preview log maintenance actions
|
|
||||||
tnt --log-check LOG_FILE audit messages.log v1 records
|
|
||||||
tnt --log-recover LOG_FILE > OUT
|
|
||||||
write valid records to stdout
|
|
||||||
|
|
||||||
STRUCTURE
|
STRUCTURE
|
||||||
src/main.c entry, signals
|
src/main.c entry, signals
|
||||||
src/cli_text.c startup CLI text
|
src/cli_text.c startup CLI text
|
||||||
src/tntctl_text.c tntctl local help and diagnostics
|
|
||||||
src/command_catalog.c command metadata, usage, argument shape
|
src/command_catalog.c command metadata, usage, argument shape
|
||||||
src/ssh_server.c SSH listener and server setup
|
src/ssh_server.c SSH listener and server setup
|
||||||
src/bootstrap.c SSH auth/session bootstrap
|
src/bootstrap.c SSH auth/session bootstrap
|
||||||
|
|
@ -75,8 +54,6 @@ STRUCTURE
|
||||||
src/exec_catalog.c SSH exec command matching, usage, argument shape
|
src/exec_catalog.c SSH exec command matching, usage, argument shape
|
||||||
src/exec.c SSH exec command dispatch
|
src/exec.c SSH exec command dispatch
|
||||||
src/message.c persistence, search
|
src/message.c persistence, search
|
||||||
src/message_log.c messages.log v1 parsing and formatting
|
|
||||||
src/message_log_tool.c offline messages.log check/recover CLI
|
|
||||||
src/history_view.c message viewport / scroll state
|
src/history_view.c message viewport / scroll state
|
||||||
src/help_text.c full-screen key reference text
|
src/help_text.c full-screen key reference text
|
||||||
src/manual.c concise manual panel rendering
|
src/manual.c concise manual panel rendering
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,11 @@ Goal: make TNT predictable for operators, scripts, and package maintainers.
|
||||||
- `stats`
|
- `stats`
|
||||||
- `users`
|
- `users`
|
||||||
- `tail`
|
- `tail`
|
||||||
- `dump`
|
|
||||||
- `post`
|
- `post`
|
||||||
- ✅ support text and JSON output modes where machine use is likely
|
- ✅ support text and JSON output modes where machine use is likely
|
||||||
- ✅ normalize command parsing, help text, and error reporting
|
- ✅ normalize command parsing, help text, and error reporting
|
||||||
- ✅ keep `tnt` as the 1.x server binary; reserve any future `tntd` split for a
|
- decide whether the server binary should remain `tnt` or split later into a
|
||||||
major-version compatibility plan
|
separate `tntd` daemon name
|
||||||
- ✅ add `--bind`, `--port`, `--state-dir`, `--public-host`,
|
- ✅ add `--bind`, `--port`, `--state-dir`, `--public-host`,
|
||||||
`--max-connections`, and related long options consistently
|
`--max-connections`, and related long options consistently
|
||||||
- ✅ add man pages for `tnt` and `tntctl`
|
- ✅ add man pages for `tnt` and `tntctl`
|
||||||
|
|
@ -39,58 +38,52 @@ Goal: make TNT predictable for operators, scripts, and package maintainers.
|
||||||
|
|
||||||
Goal: make long-running operation boring and reliable.
|
Goal: make long-running operation boring and reliable.
|
||||||
|
|
||||||
- ✅ move session callback ownership into `client.c` and release sessions
|
- move client state to a clearer ownership model with one release path
|
||||||
through one `client_release_session()` path
|
|
||||||
- ✅ remove cross-client SSH channel writes from mention and private-message
|
- ✅ remove cross-client SSH channel writes from mention and private-message
|
||||||
notifications
|
notifications
|
||||||
- continue replacing ad hoc cross-thread UI mutation with per-client event
|
- continue replacing ad hoc cross-thread UI mutation with per-client event
|
||||||
delivery where new features need cross-client notifications
|
delivery
|
||||||
- ✅ add bounded outbound queues so closed SSH windows cannot immediately stall
|
- ✅ add bounded outbound queues so closed SSH windows cannot immediately stall
|
||||||
interactive output writes
|
interactive output writes
|
||||||
- separate accept, session bootstrap, interactive I/O, and persistence concerns more cleanly
|
- separate accept, session bootstrap, interactive I/O, and persistence concerns more cleanly
|
||||||
- ✅ make room/client capacity fully runtime-configurable with no hidden
|
- make room/client capacity fully runtime-configurable with no hidden compile-time ceiling
|
||||||
compile-time ceiling
|
- document hard guarantees and soft limits
|
||||||
- ✅ document hard guarantees and soft limits
|
|
||||||
|
|
||||||
## Stage 3: Data and Persistence
|
## Stage 3: Data and Persistence
|
||||||
|
|
||||||
Goal: make stored history durable, inspectable, and recoverable.
|
Goal: make stored history durable, inspectable, and recoverable.
|
||||||
|
|
||||||
- ✅ formalize the message log v1 format
|
- formalize the message log format and version it
|
||||||
- ✅ keep persisted timestamps in UTC throughout write and replay
|
- keep timestamps in a timezone-safe format throughout write and replay
|
||||||
- ✅ validate persisted UTF-8 and record structure before replay/search
|
- validate persisted UTF-8 and record structure before replay
|
||||||
- ✅ provide an inspection/export command for persisted records
|
- add log rotation and compaction tooling
|
||||||
- ✅ add log rotation and compaction tooling
|
- provide an offline inspection/export command
|
||||||
- ✅ define broader recovery tooling for truncated or partially corrupted logs
|
- define recovery behavior for truncated or partially corrupted logs
|
||||||
|
|
||||||
## Stage 4: Interactive UX
|
## Stage 4: Interactive UX
|
||||||
|
|
||||||
Goal: keep the interface efficient for terminal users without sacrificing simplicity.
|
Goal: keep the interface efficient for terminal users without sacrificing simplicity.
|
||||||
|
|
||||||
- ✅ keep the current modal editing model precise and documented
|
- keep the current modal editing model, but make its behavior precise and documented
|
||||||
- ✅ support resize, command history, pager navigation, and predictable paste
|
- support resize, cursor movement, command history, and predictable paste behavior
|
||||||
behavior
|
|
||||||
- add in-line cursor movement/editing only if it can stay simple and testable
|
|
||||||
- add useful chat commands with clear semantics:
|
- add useful chat commands with clear semantics:
|
||||||
- ✅ `:nick` / `:name` — nickname change with broadcast
|
- ✅ `:nick` / `:name` — nickname change with broadcast
|
||||||
- ✅ `/me` — action messages
|
- ✅ `/me` — action messages
|
||||||
- ✅ `:last N` — show last N messages from disk history
|
- ✅ `:last N` — show last N messages from disk history
|
||||||
- ✅ `:search <keyword>` — case-insensitive full-text search
|
- ✅ `:search <keyword>` — case-insensitive full-text search
|
||||||
- ✅ `:mute-joins` — per-client join/leave notification toggle
|
- ✅ `:mute-joins` — per-client join/leave notification toggle
|
||||||
- ✅ improve discoverability of NORMAL and COMMAND mode actions
|
- improve discoverability of NORMAL and COMMAND mode actions
|
||||||
- ✅ make status lines and help output concise enough for small terminals
|
- make status lines and help output concise enough for small terminals
|
||||||
|
|
||||||
## Stage 5: Operations and Security
|
## Stage 5: Operations and Security
|
||||||
|
|
||||||
Goal: make public deployment manageable.
|
Goal: make public deployment manageable.
|
||||||
|
|
||||||
- ✅ provide clear distinction between concurrent session limits and
|
- provide clear distinction between concurrent session limits and connection-rate limits
|
||||||
connection-rate limits
|
|
||||||
- add admin-only controls for read-only mode, mute, and ban
|
- add admin-only controls for read-only mode, mute, and ban
|
||||||
- ✅ expose a minimal health and stats surface suitable for monitoring
|
- ✅ expose a minimal health and stats surface suitable for monitoring
|
||||||
- support systemd-friendly readiness and watchdog behavior
|
- support systemd-friendly readiness and watchdog behavior
|
||||||
- ✅ document recommended production defaults for public, private, and
|
- document recommended production defaults for public, private, and localhost-only deployments
|
||||||
localhost-only deployments
|
|
||||||
- tighten CI around authentication, limits, and restart behavior
|
- tighten CI around authentication, limits, and restart behavior
|
||||||
|
|
||||||
## Stage 6: Release Quality
|
## Stage 6: Release Quality
|
||||||
|
|
@ -101,11 +94,8 @@ Goal: make regressions harder to introduce.
|
||||||
- add sanitizer jobs and targeted fuzzing for UTF-8, log parsing, and command parsing
|
- add sanitizer jobs and targeted fuzzing for UTF-8, log parsing, and command parsing
|
||||||
- ✅ add a configurable soak test for idle sessions, reconnects, and control
|
- ✅ add a configurable soak test for idle sessions, reconnects, and control
|
||||||
interface availability
|
interface availability
|
||||||
- ✅ add deeper slow-client coverage with a deliberately backpressured SSH
|
- add deeper slow-client soak coverage with a deliberately backpressured SSH
|
||||||
client
|
client
|
||||||
- ✅ verify staged package installs, systemd unit paths, packaging metadata,
|
|
||||||
Debian source assembly, Homebrew service metadata, and installed log
|
|
||||||
maintenance modes in release preflight
|
|
||||||
- keep deployment and test docs aligned with actual runtime behavior
|
- keep deployment and test docs aligned with actual runtime behavior
|
||||||
- require every user-visible interface change to update docs and tests in the same change set
|
- require every user-visible interface change to update docs and tests in the same change set
|
||||||
|
|
||||||
|
|
@ -113,9 +103,10 @@ Goal: make regressions harder to introduce.
|
||||||
|
|
||||||
These are the next changes that should happen before new feature work expands the surface area.
|
These are the next changes that should happen before new feature work expands the surface area.
|
||||||
|
|
||||||
1. Replace remaining source-archive checksum placeholders only after the final
|
1. Decide the daemon naming path: keep `tnt` as the server binary for 1.x, or
|
||||||
GitHub source archive exists, then run `make package-publish-check`.
|
introduce `tntd` later with a compatibility plan.
|
||||||
2. Create or move the `vX.Y.Z` tag only when the release commit is final, then
|
2. Finish untangling client-state ownership into a clearer release path.
|
||||||
run `make release-check-strict` before pushing it.
|
3. Add deeper slow-client soak coverage with a deliberately backpressured SSH
|
||||||
3. Decide whether admin-only moderation controls belong in 1.0.x or should
|
client.
|
||||||
wait for a later minor release.
|
4. Replace remaining release placeholders with real maintainer metadata and
|
||||||
|
source-archive checksums when cutting a public package release.
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,10 @@ The product path should stay short:
|
||||||
4. User lands in INSERT mode at the live tail and can type immediately.
|
4. User lands in INSERT mode at the live tail and can type immediately.
|
||||||
5. User presses Esc to browse history with Vim-style movement.
|
5. User presses Esc to browse history with Vim-style movement.
|
||||||
6. User uses `:help` for the concise manual or `?` for the full key reference.
|
6. User uses `:help` for the concise manual or `?` for the full key reference.
|
||||||
7. User searches from NORMAL with `/term`, or uses commands when needed:
|
7. User uses commands only when needed: `:users`, `:msg`, `:inbox`, `:last`,
|
||||||
`:users`, `:msg`, `:inbox`, `:last`, `:search`, `:nick`, `:mute-joins`,
|
`:search`, `:nick`, `:mute-joins`, and `:q`.
|
||||||
and `:q`.
|
|
||||||
8. Scripts and operators use `tntctl` or SSH exec commands for `health`,
|
8. Scripts and operators use `tntctl` or SSH exec commands for `health`,
|
||||||
`stats`, `users`, `tail`, `dump`, and `post`.
|
`stats`, `users`, `tail`, and `post`.
|
||||||
|
|
||||||
## TUI Experience Notes
|
## TUI Experience Notes
|
||||||
|
|
||||||
|
|
@ -24,19 +23,12 @@ The product path should stay short:
|
||||||
- INSERT mode is the default because most users arrive to send a message.
|
- INSERT mode is the default because most users arrive to send a message.
|
||||||
- NORMAL mode opens at the latest messages, not the oldest history. Users can
|
- NORMAL mode opens at the latest messages, not the oldest history. Users can
|
||||||
move upward for older context and use `G` or End to return to live chat.
|
move upward for older context and use `G` or End to return to live chat.
|
||||||
- NORMAL mode accepts `/` as the fast path for history search, matching a
|
|
||||||
common terminal-reader habit while reusing the existing `:search` command.
|
|
||||||
- INSERT mode keeps a small per-session sent-message history on Up/Down and
|
|
||||||
completes trailing `@mention` prefixes with Tab.
|
|
||||||
- `:help` is a compact manual, while `?` is a full key reference. Do not add
|
- `:help` is a compact manual, while `?` is a full key reference. Do not add
|
||||||
parallel support commands for the same task.
|
parallel support commands for the same task.
|
||||||
- Command syntax stays ASCII even in localized UI text. Translations explain;
|
- Command syntax stays ASCII even in localized UI text. Translations explain;
|
||||||
they do not change the command language.
|
they do not change the command language.
|
||||||
- Private messages are visible only in the recipient inbox and are not written
|
- Private messages are visible only in the recipient inbox and are not written
|
||||||
to `messages.log`.
|
to `messages.log`.
|
||||||
- `:inbox` is live enough for normal chat use: it can be refreshed with `r`
|
|
||||||
and refreshes automatically when a new private message arrives while the
|
|
||||||
inbox is open.
|
|
||||||
- Long command output uses a small pager so `:last` and `:search` are readable
|
- Long command output uses a small pager so `:last` and `:search` are readable
|
||||||
on small terminals.
|
on small terminals.
|
||||||
|
|
||||||
|
|
@ -49,8 +41,7 @@ The product path should stay short:
|
||||||
`:last` and `:search`
|
`:last` and `:search`
|
||||||
- first user toggles `:mute-joins`, sends `:msg`, changes nickname, sends
|
- first user toggles `:mute-joins`, sends `:msg`, changes nickname, sends
|
||||||
`/me`, and exits
|
`/me`, and exits
|
||||||
- second user opens `:inbox` before the private message arrives and sees it
|
- second user reads `:inbox`
|
||||||
auto-refresh after delivery
|
|
||||||
- exec `tail` sees public messages
|
- exec `tail` sees public messages
|
||||||
- `messages.log` contains public history and excludes private-message content
|
- `messages.log` contains public history and excludes private-message content
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
const char *program_name, ui_lang_t lang);
|
const char *program_name, ui_lang_t lang);
|
||||||
const char *cli_text_invalid_port_format(ui_lang_t lang);
|
const char *cli_text_invalid_port_format(ui_lang_t lang);
|
||||||
const char *cli_text_invalid_value_format(ui_lang_t lang);
|
const char *cli_text_invalid_value_format(ui_lang_t lang);
|
||||||
const char *cli_text_option_requires_arg_format(ui_lang_t lang);
|
|
||||||
const char *cli_text_unknown_option_format(ui_lang_t lang);
|
const char *cli_text_unknown_option_format(ui_lang_t lang);
|
||||||
const char *cli_text_short_usage_format(ui_lang_t lang);
|
const char *cli_text_short_usage_format(ui_lang_t lang);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,20 @@ int client_printf(client_t *client, const char *fmt, ...);
|
||||||
/* Reference counting for safe cross-thread cleanup.
|
/* Reference counting for safe cross-thread cleanup.
|
||||||
*
|
*
|
||||||
* Lifecycle: bootstrap_run() creates the client_t with ref_count = 1
|
* Lifecycle: bootstrap_run() creates the client_t with ref_count = 1
|
||||||
* (the "main" ref). client_install_channel_callbacks() takes a second
|
* (the "main" ref), then adds a second ref before installing the channel
|
||||||
* ref owned by client.c while channel callbacks are installed, so the
|
* callbacks (the "callback" ref) so the client outlives any in-flight
|
||||||
* client outlives in-flight eof / close / window-change callbacks.
|
* eof / close / window-change callback invocation. The interactive
|
||||||
* input_run_session() ends ownership with client_release_session(). */
|
* session releases both refs in its cleanup path; the final release
|
||||||
|
* frees the SSH session, channel, callback struct, and the client_t. */
|
||||||
void client_addref(client_t *client);
|
void client_addref(client_t *client);
|
||||||
void client_release(client_t *client);
|
void client_release(client_t *client);
|
||||||
void client_release_session(client_t *client);
|
|
||||||
|
|
||||||
/* Install the post-bootstrap channel callbacks (window-change, eof, close).
|
/* Install the post-bootstrap channel callbacks (window-change, eof, close)
|
||||||
* On success this function takes the callback reference described above.
|
* that target this client_t. Caller MUST have already added one
|
||||||
* On failure no callback reference remains and the caller still owns only
|
* client_addref() to keep the client alive across in-flight callback
|
||||||
* its original main reference. */
|
* invocations; the matching client_release() happens during cleanup in
|
||||||
|
* input_run_session(). Returns 0 on success, -1 on failure (in which
|
||||||
|
* case the caller still owns both refs and must release them). */
|
||||||
int client_install_channel_callbacks(client_t *client);
|
int client_install_channel_callbacks(client_t *client);
|
||||||
|
|
||||||
#endif /* CLIENT_H */
|
#endif /* CLIENT_H */
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,4 @@
|
||||||
* path; callers must not hold client->io_lock before dispatching. */
|
* path; callers must not hold client->io_lock before dispatching. */
|
||||||
void commands_dispatch(client_t *client);
|
void commands_dispatch(client_t *client);
|
||||||
|
|
||||||
/* Rebuild the currently visible command output when it is backed by live
|
|
||||||
* client state, such as :inbox. Returns true if output changed and the caller
|
|
||||||
* should render it again. */
|
|
||||||
bool commands_refresh_active_output(client_t *client);
|
|
||||||
|
|
||||||
#endif /* COMMANDS_H */
|
#endif /* COMMANDS_H */
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
|
||||||
#include "config_defaults.h"
|
|
||||||
|
|
||||||
/* Project Metadata */
|
/* Project Metadata */
|
||||||
#define TNT_VERSION "1.0.1"
|
#define TNT_VERSION "1.0.1"
|
||||||
|
|
||||||
|
|
@ -25,6 +23,7 @@
|
||||||
#define TNT_EXIT_CONFIG 78
|
#define TNT_EXIT_CONFIG 78
|
||||||
|
|
||||||
/* Configuration constants */
|
/* Configuration constants */
|
||||||
|
#define DEFAULT_PORT 2222
|
||||||
#define MAX_MESSAGES 100
|
#define MAX_MESSAGES 100
|
||||||
#define MAX_USERNAME_LEN 64
|
#define MAX_USERNAME_LEN 64
|
||||||
#define MAX_MESSAGE_LEN 1024
|
#define MAX_MESSAGE_LEN 1024
|
||||||
|
|
@ -32,17 +31,13 @@
|
||||||
#define MAX_COMMAND_OUTPUT_LEN 8192
|
#define MAX_COMMAND_OUTPUT_LEN 8192
|
||||||
#define CLIENT_OUTBOX_CAPACITY (128 * 1024)
|
#define CLIENT_OUTBOX_CAPACITY (128 * 1024)
|
||||||
#define CLIENT_OUTBOX_FLUSH_BUDGET 32768
|
#define CLIENT_OUTBOX_FLUSH_BUDGET 32768
|
||||||
|
#define DEFAULT_MAX_CLIENTS 64
|
||||||
|
#define MAX_CONFIGURED_CLIENTS 1024
|
||||||
#define LOG_FILE "messages.log"
|
#define LOG_FILE "messages.log"
|
||||||
#define MAX_LOG_SIZE (10 * 1024 * 1024) /* 10 MiB */
|
#define MAX_LOG_SIZE (10 * 1024 * 1024) /* 10 MiB */
|
||||||
#define HOST_KEY_FILE "host_key"
|
#define HOST_KEY_FILE "host_key"
|
||||||
#define TNT_DEFAULT_STATE_DIR "."
|
#define TNT_DEFAULT_STATE_DIR "."
|
||||||
|
#define DEFAULT_IDLE_TIMEOUT 1800 /* 30 minutes */
|
||||||
/* Backward-compatible names for older modules while config_defaults owns the
|
|
||||||
* actual runtime defaults and accepted ranges. */
|
|
||||||
#define DEFAULT_PORT TNT_DEFAULT_PORT
|
|
||||||
#define DEFAULT_MAX_CLIENTS TNT_DEFAULT_MAX_CONNECTIONS
|
|
||||||
#define MAX_CONFIGURED_CLIENTS TNT_MAX_CONFIGURED_CLIENTS
|
|
||||||
#define DEFAULT_IDLE_TIMEOUT TNT_DEFAULT_IDLE_TIMEOUT
|
|
||||||
|
|
||||||
/* ANSI color codes */
|
/* ANSI color codes */
|
||||||
#define ANSI_RESET "\033[0m"
|
#define ANSI_RESET "\033[0m"
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
#ifndef CONFIG_DEFAULTS_H
|
|
||||||
#define CONFIG_DEFAULTS_H
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#define TNT_STRINGIFY_VALUE(value) #value
|
|
||||||
#define TNT_STRINGIFY(value) TNT_STRINGIFY_VALUE(value)
|
|
||||||
|
|
||||||
#define TNT_DEFAULT_PORT 2222
|
|
||||||
#define TNT_DEFAULT_PORT_TEXT TNT_STRINGIFY(TNT_DEFAULT_PORT)
|
|
||||||
#define TNT_DEFAULT_MAX_CONNECTIONS 64
|
|
||||||
#define TNT_DEFAULT_MAX_CONN_PER_IP 5
|
|
||||||
#define TNT_DEFAULT_MAX_CONN_RATE_PER_IP 10
|
|
||||||
#define TNT_DEFAULT_RATE_LIMIT_ENABLED 1
|
|
||||||
#define TNT_DEFAULT_IDLE_TIMEOUT 1800
|
|
||||||
|
|
||||||
#define TNT_MIN_PORT 1
|
|
||||||
#define TNT_MAX_PORT 65535
|
|
||||||
#define TNT_MIN_CONFIGURED_CLIENTS 1
|
|
||||||
#define TNT_MAX_CONFIGURED_CLIENTS 1024
|
|
||||||
#define TNT_MIN_RATE_LIMIT_ENABLED 0
|
|
||||||
#define TNT_MAX_RATE_LIMIT_ENABLED 1
|
|
||||||
#define TNT_MIN_IDLE_TIMEOUT 0
|
|
||||||
#define TNT_MAX_IDLE_TIMEOUT 86400
|
|
||||||
#define TNT_MIN_SSH_LOG_LEVEL 0
|
|
||||||
#define TNT_MAX_SSH_LOG_LEVEL 4
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
const char *env_name;
|
|
||||||
int fallback;
|
|
||||||
int min_value;
|
|
||||||
int max_value;
|
|
||||||
} tnt_int_config_spec_t;
|
|
||||||
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_PORT;
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_MAX_CONNECTIONS;
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_MAX_CONN_PER_IP;
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_MAX_CONN_RATE_PER_IP;
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_RATE_LIMIT;
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_IDLE_TIMEOUT;
|
|
||||||
extern const tnt_int_config_spec_t TNT_CONFIG_SSH_LOG_LEVEL;
|
|
||||||
|
|
||||||
int tnt_config_env_int(const tnt_int_config_spec_t *spec);
|
|
||||||
bool tnt_config_parse_int(const char *value, const tnt_int_config_spec_t *spec,
|
|
||||||
int *out);
|
|
||||||
|
|
||||||
#endif /* CONFIG_DEFAULTS_H */
|
|
||||||
|
|
@ -9,10 +9,8 @@ typedef enum {
|
||||||
TNT_EXEC_COMMAND_USERS,
|
TNT_EXEC_COMMAND_USERS,
|
||||||
TNT_EXEC_COMMAND_STATS,
|
TNT_EXEC_COMMAND_STATS,
|
||||||
TNT_EXEC_COMMAND_TAIL,
|
TNT_EXEC_COMMAND_TAIL,
|
||||||
TNT_EXEC_COMMAND_DUMP,
|
|
||||||
TNT_EXEC_COMMAND_POST,
|
TNT_EXEC_COMMAND_POST,
|
||||||
TNT_EXEC_COMMAND_EXIT,
|
TNT_EXEC_COMMAND_EXIT
|
||||||
TNT_EXEC_COMMAND_COUNT
|
|
||||||
} tnt_exec_command_id_t;
|
} tnt_exec_command_id_t;
|
||||||
|
|
||||||
bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id,
|
bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id,
|
||||||
|
|
@ -20,8 +18,6 @@ bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id,
|
||||||
bool exec_catalog_args_valid(tnt_exec_command_id_t id, const char *args);
|
bool exec_catalog_args_valid(tnt_exec_command_id_t id, const char *args);
|
||||||
void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos,
|
void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
ui_lang_t lang);
|
ui_lang_t lang);
|
||||||
void exec_catalog_append_command_list(char *buffer, size_t buf_size,
|
|
||||||
size_t *pos);
|
|
||||||
void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
||||||
tnt_exec_command_id_t id, ui_lang_t lang);
|
tnt_exec_command_id_t id, ui_lang_t lang);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,8 @@ typedef struct {
|
||||||
const char *text[UI_LANG_COUNT];
|
const char *text[UI_LANG_COUNT];
|
||||||
} i18n_string_t;
|
} i18n_string_t;
|
||||||
|
|
||||||
#define I18N_LANG_TEXT(lang, value) [lang] = (value)
|
|
||||||
#define I18N_EN(value) I18N_LANG_TEXT(UI_LANG_EN, value)
|
|
||||||
#define I18N_ZH(value) I18N_LANG_TEXT(UI_LANG_ZH, value)
|
|
||||||
#define I18N_STRING_MAP(...) {{ __VA_ARGS__ }}
|
|
||||||
#define I18N_STRING(en_text, zh_text) \
|
#define I18N_STRING(en_text, zh_text) \
|
||||||
I18N_STRING_MAP(I18N_EN(en_text), I18N_ZH(zh_text))
|
{{ [UI_LANG_EN] = (en_text), [UI_LANG_ZH] = (zh_text) }}
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
I18N_USERNAME_PROMPT,
|
I18N_USERNAME_PROMPT,
|
||||||
|
|
@ -29,7 +25,6 @@ typedef enum {
|
||||||
I18N_HELP_STATUS_FORMAT,
|
I18N_HELP_STATUS_FORMAT,
|
||||||
I18N_COMMAND_OUTPUT_TITLE,
|
I18N_COMMAND_OUTPUT_TITLE,
|
||||||
I18N_COMMAND_OUTPUT_STATUS_FORMAT,
|
I18N_COMMAND_OUTPUT_STATUS_FORMAT,
|
||||||
I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT,
|
|
||||||
I18N_MOTD_TITLE,
|
I18N_MOTD_TITLE,
|
||||||
I18N_MOTD_CONTINUE_HINT,
|
I18N_MOTD_CONTINUE_HINT,
|
||||||
I18N_TITLE_ONLINE_FORMAT,
|
I18N_TITLE_ONLINE_FORMAT,
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,4 @@ void message_format(const message_t *msg, char *buffer, size_t buf_size, int wid
|
||||||
* Returns the last max_results matches in chronological order; caller must free *results. */
|
* Returns the last max_results matches in chronological order; caller must free *results. */
|
||||||
int message_search(const char *query, message_t **results, int max_results);
|
int message_search(const char *query, message_t **results, int max_results);
|
||||||
|
|
||||||
/* Export valid persisted log records in messages.log v1 format. max_records
|
|
||||||
* 0 exports all valid records; positive values export the last max_records
|
|
||||||
* valid records. Caller must free *output. */
|
|
||||||
int message_dump_text(char **output, size_t *output_len, int max_records);
|
|
||||||
|
|
||||||
#endif /* MESSAGE_H */
|
#endif /* MESSAGE_H */
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
#ifndef MESSAGE_LOG_H
|
|
||||||
#define MESSAGE_LOG_H
|
|
||||||
|
|
||||||
#include "message.h"
|
|
||||||
|
|
||||||
#define MESSAGE_LOG_MAX_LINE 2048
|
|
||||||
|
|
||||||
void message_log_format_timestamp_utc(time_t ts, char *buffer,
|
|
||||||
size_t buf_size);
|
|
||||||
|
|
||||||
/* Parse one complete messages.log v1 record. `now` is used to reject records
|
|
||||||
* outside TNT's accepted replay window. */
|
|
||||||
bool message_log_parse_record(const char *line, message_t *out, time_t now);
|
|
||||||
|
|
||||||
/* Format one messages.log v1 record. record_len receives the number of bytes
|
|
||||||
* that would be written, excluding the trailing NUL. Passing NULL/0 for the
|
|
||||||
* output buffer is allowed when only the length is needed. */
|
|
||||||
int message_log_format_record(const message_t *msg, char *buffer,
|
|
||||||
size_t buf_size, size_t *record_len);
|
|
||||||
|
|
||||||
#endif /* MESSAGE_LOG_H */
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
#ifndef MESSAGE_LOG_TOOL_H
|
|
||||||
#define MESSAGE_LOG_TOOL_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
int message_log_tool_check(const char *path);
|
|
||||||
int message_log_tool_recover(const char *path);
|
|
||||||
|
|
||||||
#endif /* MESSAGE_LOG_TOOL_H */
|
|
||||||
|
|
@ -17,12 +17,6 @@ typedef struct {
|
||||||
char content[MAX_MESSAGE_LEN];
|
char content[MAX_MESSAGE_LEN];
|
||||||
} whisper_t;
|
} whisper_t;
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TNT_COMMAND_OUTPUT_NONE,
|
|
||||||
TNT_COMMAND_OUTPUT_GENERIC,
|
|
||||||
TNT_COMMAND_OUTPUT_INBOX
|
|
||||||
} tnt_command_output_kind_t;
|
|
||||||
|
|
||||||
/* Client connection structure */
|
/* Client connection structure */
|
||||||
typedef struct client {
|
typedef struct client {
|
||||||
ssh_session session; /* SSH session */
|
ssh_session session; /* SSH session */
|
||||||
|
|
@ -48,7 +42,6 @@ typedef struct client {
|
||||||
int insert_history_pos;
|
int insert_history_pos;
|
||||||
char command_output[MAX_COMMAND_OUTPUT_LEN];
|
char command_output[MAX_COMMAND_OUTPUT_LEN];
|
||||||
int command_output_scroll;
|
int command_output_scroll;
|
||||||
tnt_command_output_kind_t command_output_kind;
|
|
||||||
bool show_motd; /* command_output holds MOTD text */
|
bool show_motd; /* command_output holds MOTD text */
|
||||||
char exec_command[MAX_EXEC_COMMAND_LEN];
|
char exec_command[MAX_EXEC_COMMAND_LEN];
|
||||||
bool exec_command_too_long;
|
bool exec_command_too_long;
|
||||||
|
|
@ -74,7 +67,6 @@ typedef struct client {
|
||||||
pthread_mutex_t ref_lock; /* Lock for ref_count */
|
pthread_mutex_t ref_lock; /* Lock for ref_count */
|
||||||
pthread_mutex_t io_lock; /* Serialize SSH channel writes */
|
pthread_mutex_t io_lock; /* Serialize SSH channel writes */
|
||||||
pthread_mutex_t whisper_lock; /* Serialize whisper inbox access */
|
pthread_mutex_t whisper_lock; /* Serialize whisper inbox access */
|
||||||
bool channel_callback_ref; /* client.c owns one ref while callbacks are installed */
|
|
||||||
struct ssh_channel_callbacks_struct *channel_cb;
|
struct ssh_channel_callbacks_struct *channel_cb;
|
||||||
} client_t;
|
} client_t;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#ifndef TNTCTL_TEXT_H
|
|
||||||
#define TNTCTL_TEXT_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TNTCTL_TEXT_INVALID_PORT,
|
|
||||||
TNTCTL_TEXT_INVALID_LOGIN,
|
|
||||||
TNTCTL_TEXT_INVALID_HOST_KEY_MODE,
|
|
||||||
TNTCTL_TEXT_INVALID_KNOWN_HOSTS,
|
|
||||||
TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT,
|
|
||||||
TNTCTL_TEXT_MISSING_HOST,
|
|
||||||
TNTCTL_TEXT_INVALID_HOST,
|
|
||||||
TNTCTL_TEXT_LOGIN_HOST_CONFLICT,
|
|
||||||
TNTCTL_TEXT_UNKNOWN_COMMAND,
|
|
||||||
TNTCTL_TEXT_INVALID_REMOTE_COMMAND,
|
|
||||||
TNTCTL_TEXT_DESTINATION_TOO_LONG,
|
|
||||||
TNTCTL_TEXT_INVALID_DESTINATION,
|
|
||||||
TNTCTL_TEXT_OUT_OF_MEMORY,
|
|
||||||
TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG,
|
|
||||||
TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG,
|
|
||||||
TNTCTL_TEXT_COUNT
|
|
||||||
} tntctl_text_id_t;
|
|
||||||
|
|
||||||
void tntctl_text_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
|
||||||
ui_lang_t lang);
|
|
||||||
const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id);
|
|
||||||
|
|
||||||
#endif /* TNTCTL_TEXT_H */
|
|
||||||
29
install.sh
29
install.sh
|
|
@ -27,34 +27,6 @@ sha256_of() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
warn_missing_libssh() {
|
|
||||||
case "$OS" in
|
|
||||||
linux)
|
|
||||||
if command -v ldconfig >/dev/null 2>&1 &&
|
|
||||||
ldconfig -p 2>/dev/null | grep -q 'libssh\.so'; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
for path in /usr/lib/libssh.so* /usr/lib64/libssh.so* \
|
|
||||||
/lib/libssh.so* /lib64/libssh.so*; do
|
|
||||||
[ -e "$path" ] && return
|
|
||||||
done
|
|
||||||
echo "WARNING: TNT requires the libssh runtime library."
|
|
||||||
echo "Install it first, for example:"
|
|
||||||
echo " Ubuntu/Debian: sudo apt install libssh-4"
|
|
||||||
echo " Arch: sudo pacman -S libssh"
|
|
||||||
;;
|
|
||||||
darwin)
|
|
||||||
if [ -e /opt/homebrew/opt/libssh/lib/libssh.dylib ] ||
|
|
||||||
[ -e /usr/local/opt/libssh/lib/libssh.dylib ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "WARNING: TNT requires the libssh runtime library."
|
|
||||||
echo "Install it first:"
|
|
||||||
echo " brew install libssh"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
need_cmd curl
|
need_cmd curl
|
||||||
need_cmd awk
|
need_cmd awk
|
||||||
|
|
||||||
|
|
@ -81,7 +53,6 @@ echo "OS: $OS"
|
||||||
echo "Arch: $ARCH"
|
echo "Arch: $ARCH"
|
||||||
echo "Version: $VERSION"
|
echo "Version: $VERSION"
|
||||||
echo ""
|
echo ""
|
||||||
warn_missing_libssh
|
|
||||||
|
|
||||||
# Get latest version if not specified
|
# Get latest version if not specified
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Package installs include both `tnt` and `tntctl`. `tnt` is the server process;
|
||||||
|
|
||||||
1. Confirm `TNT_VERSION` in `include/common.h` and the manpage version match.
|
1. Confirm `TNT_VERSION` in `include/common.h` and the manpage version match.
|
||||||
Also update package versions in Arch, Homebrew, and Debian drafts.
|
Also update package versions in Arch, Homebrew, and Debian drafts.
|
||||||
2. Create a GitHub release tag such as `vX.Y.Z`.
|
2. Create a GitHub release tag such as `v1.0.1`.
|
||||||
3. Build and upload release tarballs or rely on GitHub source archives.
|
3. Build and upload release tarballs or rely on GitHub source archives.
|
||||||
4. Replace placeholder checksums in package drafts.
|
4. Replace placeholder checksums in package drafts.
|
||||||
5. Verify package contents in an isolated directory:
|
5. Verify package contents in an isolated directory:
|
||||||
|
|
@ -26,23 +26,13 @@ Package installs include both `tnt` and `tntctl`. `tnt` is the server process;
|
||||||
make release-check
|
make release-check
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Assemble a Debian/PPA source tree when preparing Ubuntu packaging:
|
6. Before submitting package recipes, replace checksum placeholders and run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make debian-source-package
|
make release-check-strict
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `scripts/package_debian_source.sh --build` on a Debian/Ubuntu system
|
7. Submit packages manually:
|
||||||
with `dpkg-buildpackage` installed to build the unsigned source package.
|
|
||||||
|
|
||||||
7. Before submitting package recipes, download the final GitHub source archive,
|
|
||||||
replace checksum placeholders, and run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
|
|
||||||
```
|
|
||||||
|
|
||||||
8. Submit packages manually:
|
|
||||||
- Arch: upload `PKGBUILD` and generated `.SRCINFO` to AUR.
|
- Arch: upload `PKGBUILD` and generated `.SRCINFO` to AUR.
|
||||||
- Homebrew: open a PR to the project tap, or later Homebrew core if eligible.
|
- Homebrew: open a PR to the project tap, or later Homebrew core if eligible.
|
||||||
- Ubuntu: build Debian source packages and upload to a Launchpad PPA.
|
- Ubuntu: build Debian source packages and upload to a Launchpad PPA.
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ pkgbase = tnt-chat
|
||||||
makedepends = make
|
makedepends = make
|
||||||
depends = libssh
|
depends = libssh
|
||||||
source = tnt-chat-1.0.1.tar.gz::https://github.com/m1ngsama/TNT/archive/refs/tags/v1.0.1.tar.gz
|
source = tnt-chat-1.0.1.tar.gz::https://github.com/m1ngsama/TNT/archive/refs/tags/v1.0.1.tar.gz
|
||||||
source = tnt-chat.sysusers
|
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
sha256sums = 8a1f7dfbdc9f1305c4ed50d80e89f91333ffdf937890c497f93e41abaf76e3ed
|
|
||||||
|
|
||||||
pkgname = tnt-chat
|
pkgname = tnt-chat
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Maintainer: M1ng <contact@m1ng.space>
|
# Maintainer: M1ng <REPLACE_WITH_EMAIL>
|
||||||
|
|
||||||
pkgname=tnt-chat
|
pkgname=tnt-chat
|
||||||
pkgver=1.0.1
|
pkgver=1.0.1
|
||||||
|
|
@ -9,10 +9,8 @@ url='https://github.com/m1ngsama/TNT'
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('libssh')
|
depends=('libssh')
|
||||||
makedepends=('gcc' 'make')
|
makedepends=('gcc' 'make')
|
||||||
source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz"
|
source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz")
|
||||||
"${pkgname}.sysusers")
|
sha256sums=('SKIP')
|
||||||
sha256sums=('SKIP'
|
|
||||||
'8a1f7dfbdc9f1305c4ed50d80e89f91333ffdf937890c497f93e41abaf76e3ed')
|
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "TNT-${pkgver}"
|
cd "TNT-${pkgver}"
|
||||||
|
|
@ -23,7 +21,5 @@ package() {
|
||||||
cd "TNT-${pkgver}"
|
cd "TNT-${pkgver}"
|
||||||
make DESTDIR="${pkgdir}" PREFIX=/usr install
|
make DESTDIR="${pkgdir}" PREFIX=/usr install
|
||||||
make DESTDIR="${pkgdir}" PREFIX=/usr install-systemd
|
make DESTDIR="${pkgdir}" PREFIX=/usr install-systemd
|
||||||
install -Dm644 "${srcdir}/${pkgname}.sysusers" \
|
|
||||||
"${pkgdir}/usr/lib/sysusers.d/${pkgname}.conf"
|
|
||||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,11 @@ After editing `PKGBUILD`, regenerate `.SRCINFO`:
|
||||||
makepkg --printsrcinfo > .SRCINFO
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
```
|
```
|
||||||
|
|
||||||
Before AUR submission, replace `sha256sums=('SKIP')` with the real GitHub
|
Before AUR submission, replace `sha256sums=('SKIP')` with the real release
|
||||||
source archive checksum, regenerate `.SRCINFO`, then run the package publish
|
archive checksum, then run the project-level strict check:
|
||||||
check:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
|
make release-check-strict
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual AUR submission
|
## Manual AUR submission
|
||||||
|
|
@ -41,7 +40,7 @@ git clone ssh://aur@aur.archlinux.org/tnt-chat.git aur-tnt-chat
|
||||||
cp PKGBUILD .SRCINFO aur-tnt-chat/
|
cp PKGBUILD .SRCINFO aur-tnt-chat/
|
||||||
cd aur-tnt-chat
|
cd aur-tnt-chat
|
||||||
git add PKGBUILD .SRCINFO
|
git add PKGBUILD .SRCINFO
|
||||||
git commit -m "Update to X.Y.Z"
|
git commit -m "Update to 1.0.1"
|
||||||
git push
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
u tnt - "TNT chat server" /var/lib/tnt -
|
|
||||||
|
|
@ -6,17 +6,18 @@ the project has a stable release cadence.
|
||||||
|
|
||||||
## Draft metadata
|
## Draft metadata
|
||||||
|
|
||||||
The `debian/` directory in this folder is a packaging draft. To assemble it
|
The `debian/` directory in this folder is a packaging draft. To test it against
|
||||||
against a clean source tree:
|
an upstream release tree, copy it to the root of a clean source checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make debian-source-package
|
cp -a packaging/debian/debian ./debian
|
||||||
|
dpkg-buildpackage -us -uc
|
||||||
```
|
```
|
||||||
|
|
||||||
For PPA uploads, build a source package on Debian/Ubuntu:
|
For PPA uploads, build a signed source package instead:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scripts/package_debian_source.sh --build
|
debuild -S
|
||||||
```
|
```
|
||||||
|
|
||||||
## Recommended path
|
## Recommended path
|
||||||
|
|
@ -46,5 +47,3 @@ scripts/package_debian_source.sh --build
|
||||||
- Installed commands: `/usr/bin/tnt`, `/usr/bin/tntctl`
|
- Installed commands: `/usr/bin/tnt`, `/usr/bin/tntctl`
|
||||||
- Runtime dependency: `libssh`
|
- Runtime dependency: `libssh`
|
||||||
- Optional systemd unit: `/usr/lib/systemd/system/tnt.service`
|
- Optional systemd unit: `/usr/lib/systemd/system/tnt.service`
|
||||||
- System user: package maintainer scripts create `tnt:tnt`; the systemd unit
|
|
||||||
owns `/var/lib/tnt` through `StateDirectory=tnt`
|
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ tnt-chat (1.0.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Initial package draft.
|
* Initial package draft.
|
||||||
|
|
||||||
-- M1ng <contact@m1ng.space> Thu, 21 May 2026 00:00:00 +0800
|
-- M1ng <REPLACE_WITH_EMAIL> Thu, 21 May 2026 00:00:00 +0800
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
Source: tnt-chat
|
Source: tnt-chat
|
||||||
Section: net
|
Section: net
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: M1ng <contact@m1ng.space>
|
Maintainer: M1ng <REPLACE_WITH_EMAIL>
|
||||||
Build-Depends:
|
Build-Depends:
|
||||||
debhelper-compat (= 13),
|
debhelper-compat (= 13),
|
||||||
libssh-dev,
|
libssh-dev,
|
||||||
|
|
@ -15,8 +15,7 @@ Package: tnt-chat
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends:
|
Depends:
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
${shlibs:Depends},
|
${shlibs:Depends}
|
||||||
adduser
|
|
||||||
Description: SSH-native terminal chat server
|
Description: SSH-native terminal chat server
|
||||||
TNT is a minimalist terminal chat server accessed over SSH. It provides a
|
TNT is a minimalist terminal chat server accessed over SSH. It provides a
|
||||||
Vim-style terminal interface, anonymous access by default, persistent message
|
Vim-style terminal interface, anonymous access by default, persistent message
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ "$1" = "configure" ] && ! getent passwd tnt >/dev/null; then
|
|
||||||
adduser --system --group --home /var/lib/tnt --no-create-home --gecos "TNT chat server" tnt
|
|
||||||
fi
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
@ -6,7 +6,6 @@ project tap first, not Homebrew core:
|
||||||
```sh
|
```sh
|
||||||
brew tap m1ngsama/tnt
|
brew tap m1ngsama/tnt
|
||||||
brew install tnt-chat
|
brew install tnt-chat
|
||||||
brew services start tnt-chat
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Homebrew core should wait until TNT has stable releases and broader usage.
|
Homebrew core should wait until TNT has stable releases and broader usage.
|
||||||
|
|
@ -19,7 +18,6 @@ From a tap repository:
|
||||||
brew audit --strict --online tnt-chat
|
brew audit --strict --online tnt-chat
|
||||||
brew install --build-from-source ./Formula/tnt-chat.rb
|
brew install --build-from-source ./Formula/tnt-chat.rb
|
||||||
brew test tnt-chat
|
brew test tnt-chat
|
||||||
brew services run tnt-chat
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For local syntax-only validation from this repository:
|
For local syntax-only validation from this repository:
|
||||||
|
|
@ -30,20 +28,20 @@ ruby -c packaging/homebrew/tnt-chat.rb
|
||||||
|
|
||||||
## Updating the formula
|
## Updating the formula
|
||||||
|
|
||||||
1. Publish a GitHub release tag such as `vX.Y.Z`.
|
1. Publish a GitHub release tag such as `v1.0.1`.
|
||||||
2. Download or hash the release source archive:
|
2. Download or hash the release source archive:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -L -o dist/tnt-chat-vX.Y.Z.tar.gz \
|
curl -L -o tnt-chat-1.0.1.tar.gz \
|
||||||
https://github.com/m1ngsama/TNT/archive/refs/tags/vX.Y.Z.tar.gz
|
https://github.com/m1ngsama/TNT/archive/refs/tags/v1.0.1.tar.gz
|
||||||
shasum -a 256 dist/tnt-chat-vX.Y.Z.tar.gz
|
shasum -a 256 tnt-chat-1.0.1.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Replace `REPLACE_WITH_RELEASE_TARBALL_SHA256` in `tnt-chat.rb`.
|
3. Replace `REPLACE_WITH_RELEASE_TARBALL_SHA256` in `tnt-chat.rb`.
|
||||||
4. Run:
|
4. Run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
SOURCE_TARBALL=dist/tnt-chat-vX.Y.Z.tar.gz make package-publish-check
|
make release-check-strict
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Copy the formula into the tap repository and open a normal review PR.
|
5. Copy the formula into the tap repository and open a normal review PR.
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,6 @@ class TntChat < Formula
|
||||||
bin.install "#{buildpath}/stage#{prefix}/bin/tntctl"
|
bin.install "#{buildpath}/stage#{prefix}/bin/tntctl"
|
||||||
man1.install "#{buildpath}/stage#{prefix}/share/man/man1/tnt.1"
|
man1.install "#{buildpath}/stage#{prefix}/share/man/man1/tnt.1"
|
||||||
man1.install "#{buildpath}/stage#{prefix}/share/man/man1/tntctl.1"
|
man1.install "#{buildpath}/stage#{prefix}/share/man/man1/tntctl.1"
|
||||||
|
|
||||||
(var/"tnt").mkpath
|
|
||||||
(var/"log").mkpath
|
|
||||||
end
|
|
||||||
|
|
||||||
service do
|
|
||||||
run [opt_bin/"tnt", "-d", var/"tnt"]
|
|
||||||
keep_alive true
|
|
||||||
working_dir var/"tnt"
|
|
||||||
log_path var/"log/tnt.log"
|
|
||||||
error_log_path var/"log/tnt.log"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test do
|
test do
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Verify that a release tag matches TNT_VERSION.
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "release-ref-check: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ref=${1:-${GITHUB_REF_NAME:-}}
|
|
||||||
[ -n "$ref" ] || fail "missing release ref; pass vX.Y.Z or set GITHUB_REF_NAME"
|
|
||||||
|
|
||||||
case "$ref" in
|
|
||||||
refs/tags/*) tag=${ref#refs/tags/} ;;
|
|
||||||
*) tag=$ref ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' ||
|
|
||||||
fail "release ref must be vMAJOR.MINOR.PATCH, got $tag"
|
|
||||||
|
|
||||||
version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
|
|
||||||
[ -n "$version" ] || fail "could not read TNT_VERSION from include/common.h"
|
|
||||||
|
|
||||||
[ "$tag" = "v$version" ] ||
|
|
||||||
fail "release tag $tag does not match TNT_VERSION $version"
|
|
||||||
|
|
||||||
echo "release ref matches TNT_VERSION: $tag"
|
|
||||||
|
|
@ -1,174 +1,44 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# Compact and archive a TNT messages.log file.
|
# TNT Log Rotation Script
|
||||||
#
|
# Keeps chat history manageable and prevents disk space issues
|
||||||
# This is an operator-run maintenance tool. For strict consistency, stop TNT
|
|
||||||
# or run it during a quiet maintenance window before compacting the active log.
|
|
||||||
|
|
||||||
set -eu
|
LOG_FILE="${1:-/var/lib/tnt/messages.log}"
|
||||||
|
MAX_SIZE_MB="${2:-100}"
|
||||||
|
KEEP_LINES="${3:-10000}"
|
||||||
|
|
||||||
DRY_RUN=0
|
# Check if log file exists
|
||||||
KEEP_ARCHIVES=5
|
if [ ! -f "$LOG_FILE" ]; then
|
||||||
|
echo "Log file $LOG_FILE does not exist"
|
||||||
usage() {
|
|
||||||
cat <<'USAGE'
|
|
||||||
Usage: scripts/logrotate.sh [--dry-run] [--keep-archives N] [LOG_FILE [MAX_SIZE_MB [KEEP_LINES]]]
|
|
||||||
|
|
||||||
Defaults:
|
|
||||||
LOG_FILE /var/lib/tnt/messages.log
|
|
||||||
MAX_SIZE_MB 100
|
|
||||||
KEEP_LINES 10000
|
|
||||||
|
|
||||||
Exit status:
|
|
||||||
0 success, including missing log file
|
|
||||||
1 runtime error
|
|
||||||
64 invalid arguments
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
fail_usage() {
|
|
||||||
echo "logrotate: $*" >&2
|
|
||||||
usage >&2
|
|
||||||
exit 64
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "logrotate: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
is_uint() {
|
|
||||||
case "${1:-}" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_positive_uint() {
|
|
||||||
is_uint "$1" && [ "$1" -gt 0 ]
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--keep-archives)
|
|
||||||
[ "$#" -ge 2 ] || fail_usage "missing value for --keep-archives"
|
|
||||||
is_uint "$2" || fail_usage "invalid archive count: $2"
|
|
||||||
KEEP_ARCHIVES=$2
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--)
|
|
||||||
shift
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
-*)
|
|
||||||
fail_usage "unknown option: $1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[ "$#" -le 3 ] || fail_usage "too many arguments"
|
|
||||||
|
|
||||||
LOG_FILE=${1:-/var/lib/tnt/messages.log}
|
|
||||||
MAX_SIZE_MB=${2:-100}
|
|
||||||
KEEP_LINES=${3:-10000}
|
|
||||||
|
|
||||||
case "$LOG_FILE" in
|
|
||||||
''|-*)
|
|
||||||
fail_usage "invalid log path"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
is_uint "$MAX_SIZE_MB" || fail_usage "invalid max size: $MAX_SIZE_MB"
|
|
||||||
is_positive_uint "$KEEP_LINES" || fail_usage "invalid keep lines: $KEEP_LINES"
|
|
||||||
|
|
||||||
if [ ! -e "$LOG_FILE" ]; then
|
|
||||||
echo "logrotate: $LOG_FILE does not exist"
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
[ -f "$LOG_FILE" ] || fail "$LOG_FILE is not a regular file"
|
|
||||||
|
|
||||||
MAX_BYTES=$((MAX_SIZE_MB * 1024 * 1024))
|
# Get file size in MB
|
||||||
FILE_SIZE=$(wc -c < "$LOG_FILE" | tr -d ' ')
|
FILE_SIZE=$(du -m "$LOG_FILE" | cut -f1)
|
||||||
[ -n "$FILE_SIZE" ] || fail "could not read log size"
|
|
||||||
|
|
||||||
compact_log() {
|
# Rotate if file is too large
|
||||||
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
|
if [ "$FILE_SIZE" -gt "$MAX_SIZE_MB" ]; then
|
||||||
backup="${LOG_FILE}.${timestamp}"
|
echo "Log file size: ${FILE_SIZE}MB, rotating..."
|
||||||
suffix=1
|
|
||||||
|
|
||||||
while [ -e "$backup" ] || [ -e "${backup}.gz" ]; do
|
# Create backup
|
||||||
backup="${LOG_FILE}.${timestamp}.${suffix}"
|
BACKUP="${LOG_FILE}.$(date +%Y%m%d_%H%M%S)"
|
||||||
suffix=$((suffix + 1))
|
cp "$LOG_FILE" "$BACKUP"
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
# Keep only last N lines
|
||||||
echo "logrotate: would archive $LOG_FILE to $backup"
|
tail -n "$KEEP_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp"
|
||||||
echo "logrotate: would keep last $KEEP_LINES lines"
|
mv "${LOG_FILE}.tmp" "$LOG_FILE"
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp="${LOG_FILE}.tmp.$$"
|
# Compress old backup
|
||||||
rm -f "$tmp"
|
gzip "$BACKUP"
|
||||||
cp -p "$LOG_FILE" "$backup" || fail "failed to create archive"
|
|
||||||
if ! tail -n "$KEEP_LINES" "$LOG_FILE" > "$tmp"; then
|
|
||||||
rm -f "$tmp"
|
|
||||||
fail "failed to compact log"
|
|
||||||
fi
|
|
||||||
if ! cat "$tmp" > "$LOG_FILE"; then
|
|
||||||
rm -f "$tmp"
|
|
||||||
fail "failed to replace log"
|
|
||||||
fi
|
|
||||||
rm -f "$tmp"
|
|
||||||
|
|
||||||
if command -v gzip >/dev/null 2>&1; then
|
echo "Log rotated. Backup: ${BACKUP}.gz"
|
||||||
gzip -f "$backup" || fail "failed to compress archive"
|
echo "Kept last $KEEP_LINES lines"
|
||||||
backup="${backup}.gz"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "logrotate: archived $backup"
|
|
||||||
echo "logrotate: kept last $KEEP_LINES lines"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_archives() {
|
|
||||||
[ "$KEEP_ARCHIVES" -ge 0 ] || return 0
|
|
||||||
|
|
||||||
archives=$(
|
|
||||||
ls -1t "$LOG_FILE".*.gz "$LOG_FILE".[0-9]* 2>/dev/null || true
|
|
||||||
)
|
|
||||||
[ -n "$archives" ] || return 0
|
|
||||||
|
|
||||||
printf '%s\n' "$archives" |
|
|
||||||
awk '!seen[$0]++' |
|
|
||||||
awk -v keep="$KEEP_ARCHIVES" 'NR > keep' |
|
|
||||||
while IFS= read -r old; do
|
|
||||||
[ -n "$old" ] || continue
|
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
|
||||||
echo "logrotate: would remove $old"
|
|
||||||
else
|
|
||||||
rm -f "$old"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$FILE_SIZE" -gt "$MAX_BYTES" ]; then
|
|
||||||
echo "logrotate: size ${FILE_SIZE} bytes exceeds ${MAX_BYTES} bytes"
|
|
||||||
compact_log
|
|
||||||
else
|
else
|
||||||
echo "logrotate: size ${FILE_SIZE} bytes is within ${MAX_BYTES} bytes"
|
echo "Log file size: ${FILE_SIZE}MB (under ${MAX_SIZE_MB}MB limit)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cleanup_archives
|
# Clean up old compressed logs (keep last 5)
|
||||||
echo "logrotate: complete"
|
LOG_DIR=$(dirname "$LOG_FILE")
|
||||||
|
cd "$LOG_DIR" || exit
|
||||||
|
ls -t messages.log.*.gz 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null
|
||||||
|
|
||||||
|
echo "Log rotation complete"
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Assemble a Debian/Ubuntu source-package tree. This script never uploads.
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'USAGE'
|
|
||||||
Usage: scripts/package_debian_source.sh [--build] [OUT_DIR]
|
|
||||||
|
|
||||||
Create OUT_DIR/tnt-chat-$TNT_VERSION from tracked source files and copy the
|
|
||||||
draft Debian metadata to OUT_DIR/tnt-chat-$TNT_VERSION/debian.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--build run dpkg-buildpackage -S -us -uc after assembly
|
|
||||||
|
|
||||||
Default OUT_DIR: dist/debian-source
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "package-debian-source: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
BUILD=0
|
|
||||||
OUT_DIR=${TNT_DEBIAN_SOURCE_OUT:-dist/debian-source}
|
|
||||||
OUT_SET=0
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--build)
|
|
||||||
BUILD=1
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
-*)
|
|
||||||
fail "unknown option: $1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
[ "$OUT_SET" -eq 0 ] || fail "multiple output directories"
|
|
||||||
OUT_DIR=$1
|
|
||||||
OUT_SET=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
VERSION=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
|
|
||||||
[ -n "$VERSION" ] || fail "could not read TNT_VERSION"
|
|
||||||
|
|
||||||
SOURCE_NAME="tnt-chat-$VERSION"
|
|
||||||
SOURCE_ROOT="$OUT_DIR/$SOURCE_NAME"
|
|
||||||
|
|
||||||
[ ! -e "$SOURCE_ROOT" ] || fail "$SOURCE_ROOT already exists"
|
|
||||||
mkdir -p "$OUT_DIR"
|
|
||||||
mkdir -p "$SOURCE_ROOT"
|
|
||||||
|
|
||||||
git ls-files -z | cpio -0 -pdm "$SOURCE_ROOT" >/dev/null 2>&1
|
|
||||||
cp -R "$ROOT/packaging/debian/debian" "$SOURCE_ROOT/debian"
|
|
||||||
|
|
||||||
[ -f "$SOURCE_ROOT/debian/control" ] || fail "missing debian/control"
|
|
||||||
[ -x "$SOURCE_ROOT/debian/rules" ] || fail "missing executable debian/rules"
|
|
||||||
[ -x "$SOURCE_ROOT/debian/postinst" ] || fail "missing executable debian/postinst"
|
|
||||||
|
|
||||||
echo "Debian source tree assembled: $SOURCE_ROOT"
|
|
||||||
|
|
||||||
if [ "$BUILD" -eq 1 ]; then
|
|
||||||
command -v dpkg-buildpackage >/dev/null 2>&1 ||
|
|
||||||
fail "dpkg-buildpackage not found"
|
|
||||||
(
|
|
||||||
cd "$SOURCE_ROOT"
|
|
||||||
dpkg-buildpackage -S -us -uc
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Verify package-manager recipes against a final release source archive.
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "package-publish-check: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
sha256_of() {
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then
|
|
||||||
sha256sum "$1" | awk '{print $1}'
|
|
||||||
elif command -v shasum >/dev/null 2>&1; then
|
|
||||||
shasum -a 256 "$1" | awk '{print $1}'
|
|
||||||
else
|
|
||||||
fail "sha256sum or shasum is required"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
|
|
||||||
[ -n "$version" ] || fail "could not read TNT_VERSION from include/common.h"
|
|
||||||
|
|
||||||
source_tarball=${SOURCE_TARBALL:-${RELEASE_SOURCE_TARBALL:-}}
|
|
||||||
[ -n "$source_tarball" ] ||
|
|
||||||
fail "set SOURCE_TARBALL to the final GitHub source archive"
|
|
||||||
[ -f "$source_tarball" ] ||
|
|
||||||
fail "SOURCE_TARBALL does not exist: $source_tarball"
|
|
||||||
|
|
||||||
! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null ||
|
|
||||||
fail "replace maintainer email placeholders before package publishing"
|
|
||||||
|
|
||||||
arch_sha=$(sed -n "s/^[[:space:]]*sha256sums=('\([^']*\)'.*/\1/p" \
|
|
||||||
packaging/arch/PKGBUILD | head -n 1)
|
|
||||||
srcinfo_sha=$(sed -n 's/^[[:space:]]*sha256sums = \([^[:space:]]*\).*/\1/p' \
|
|
||||||
packaging/arch/.SRCINFO | head -n 1)
|
|
||||||
brew_sha=$(sed -n 's/^[[:space:]]*sha256 "\([^"]*\)".*/\1/p' \
|
|
||||||
packaging/homebrew/tnt-chat.rb | head -n 1)
|
|
||||||
|
|
||||||
[ -n "$arch_sha" ] || fail "could not read PKGBUILD source checksum"
|
|
||||||
[ -n "$srcinfo_sha" ] || fail "could not read .SRCINFO source checksum"
|
|
||||||
[ -n "$brew_sha" ] || fail "could not read Homebrew source checksum"
|
|
||||||
[ "$arch_sha" != "SKIP" ] || fail "replace PKGBUILD sha256sums before publishing"
|
|
||||||
[ "$srcinfo_sha" != "SKIP" ] || fail "replace .SRCINFO sha256sums before publishing"
|
|
||||||
[ "$brew_sha" != "REPLACE_WITH_RELEASE_TARBALL_SHA256" ] ||
|
|
||||||
fail "replace Homebrew sha256 before publishing"
|
|
||||||
|
|
||||||
expected_sha=$(sha256_of "$source_tarball")
|
|
||||||
[ "$arch_sha" = "$expected_sha" ] ||
|
|
||||||
fail "PKGBUILD source checksum does not match SOURCE_TARBALL"
|
|
||||||
[ "$srcinfo_sha" = "$expected_sha" ] ||
|
|
||||||
fail ".SRCINFO source checksum does not match SOURCE_TARBALL"
|
|
||||||
[ "$brew_sha" = "$expected_sha" ] ||
|
|
||||||
fail "Homebrew source checksum does not match SOURCE_TARBALL"
|
|
||||||
|
|
||||||
grep -q "^pkgver=$version$" packaging/arch/PKGBUILD ||
|
|
||||||
fail "PKGBUILD pkgver does not match $version"
|
|
||||||
grep -q "pkgver = $version" packaging/arch/.SRCINFO ||
|
|
||||||
fail ".SRCINFO pkgver does not match $version"
|
|
||||||
grep -q "v${version}.tar.gz" packaging/homebrew/tnt-chat.rb ||
|
|
||||||
fail "Homebrew URL does not match v$version"
|
|
||||||
grep -q "^tnt-chat (${version}-1)" packaging/debian/debian/changelog ||
|
|
||||||
fail "Debian changelog version does not match $version"
|
|
||||||
|
|
||||||
echo "package recipes match SOURCE_TARBALL for $version: $expected_sha"
|
|
||||||
|
|
@ -13,7 +13,6 @@ Default checks:
|
||||||
- version metadata alignment
|
- version metadata alignment
|
||||||
- clean build
|
- clean build
|
||||||
- unit tests
|
- unit tests
|
||||||
- script tests
|
|
||||||
- staged install layout with PREFIX=/usr and DESTDIR
|
- staged install layout with PREFIX=/usr and DESTDIR
|
||||||
- installer shell syntax
|
- installer shell syntax
|
||||||
- Debian packaging metadata
|
- Debian packaging metadata
|
||||||
|
|
@ -22,13 +21,11 @@ Default checks:
|
||||||
Environment:
|
Environment:
|
||||||
RUN_INTEGRATION=1 also run full make test
|
RUN_INTEGRATION=1 also run full make test
|
||||||
RUN_SOAK=1 also run the configurable soak test
|
RUN_SOAK=1 also run the configurable soak test
|
||||||
RUN_SLOW_CLIENT=1 also run the slow-client backpressure test
|
|
||||||
PORT=12720 base port for integration tests
|
PORT=12720 base port for integration tests
|
||||||
|
|
||||||
Strict checks additionally require a clean tree, a vX.Y.Z tag at HEAD, a
|
Strict checks additionally require a clean tree, a vX.Y.Z tag at HEAD, a
|
||||||
matching changelog release section, non-placeholder maintainer metadata, and a
|
matching changelog release section, real package checksums, and non-placeholder
|
||||||
build from the tagged source archive. Run `make package-publish-check` after
|
maintainer metadata, then build from the tagged source archive.
|
||||||
the final GitHub source archive exists to verify package checksums.
|
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,9 +101,6 @@ step "running unit tests"
|
||||||
make -C tests/unit clean
|
make -C tests/unit clean
|
||||||
make -C tests/unit run
|
make -C tests/unit run
|
||||||
|
|
||||||
step "running script tests"
|
|
||||||
make script-test
|
|
||||||
|
|
||||||
step "checking client I/O ownership boundaries"
|
step "checking client I/O ownership boundaries"
|
||||||
! grep -R "client_send(target" src include >/dev/null ||
|
! grep -R "client_send(target" src include >/dev/null ||
|
||||||
fail "cross-client target writes must be queued through client_queue_bell"
|
fail "cross-client target writes must be queued through client_queue_bell"
|
||||||
|
|
@ -114,10 +108,6 @@ step "checking client I/O ownership boundaries"
|
||||||
fail "cross-client target-array writes must be queued through client_queue_bell"
|
fail "cross-client target-array writes must be queued through client_queue_bell"
|
||||||
! grep -n "pthread_mutex_lock(&.*->io_lock)" src/commands.c >/dev/null ||
|
! grep -n "pthread_mutex_lock(&.*->io_lock)" src/commands.c >/dev/null ||
|
||||||
fail "commands.c must not use SSH io_lock for in-memory command state"
|
fail "commands.c must not use SSH io_lock for in-memory command state"
|
||||||
! grep -n "client_addref(client)" src/bootstrap.c >/dev/null ||
|
|
||||||
fail "bootstrap.c must let client_install_channel_callbacks own callback refs"
|
|
||||||
grep -q "client_release_session(client)" src/input.c ||
|
|
||||||
fail "input.c must release session ownership through client_release_session"
|
|
||||||
if grep -R "ssh_channel_write" src include | grep -v "^src/client.c:" >/dev/null; then
|
if grep -R "ssh_channel_write" src include | grep -v "^src/client.c:" >/dev/null; then
|
||||||
fail "raw SSH channel writes must stay inside src/client.c"
|
fail "raw SSH channel writes must stay inside src/client.c"
|
||||||
fi
|
fi
|
||||||
|
|
@ -133,13 +123,6 @@ if [ "${RUN_SOAK:-0}" = "1" ]; then
|
||||||
DURATION="${SOAK_DURATION:-8}" RECONNECTS="${SOAK_RECONNECTS:-5}"
|
DURATION="${SOAK_DURATION:-8}" RECONNECTS="${SOAK_RECONNECTS:-5}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${RUN_SLOW_CLIENT:-0}" = "1" ]; then
|
|
||||||
step "running slow-client test"
|
|
||||||
make slow-client-test PORT="$((${PORT:-12720} + 40))" \
|
|
||||||
DURATION="${SLOW_CLIENT_DURATION:-8}" \
|
|
||||||
BURST_CHARS="${SLOW_CLIENT_BURST_CHARS:-1600}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/tnt-release-check.XXXXXX")
|
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/tnt-release-check.XXXXXX")
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$tmpdir"
|
rm -rf "$tmpdir"
|
||||||
|
|
@ -159,80 +142,14 @@ make DESTDIR="$tmpdir" PREFIX=/usr install-systemd
|
||||||
grep -q "^ExecStart=/usr/bin/tnt$" "$tmpdir/usr/lib/systemd/system/tnt.service" ||
|
grep -q "^ExecStart=/usr/bin/tnt$" "$tmpdir/usr/lib/systemd/system/tnt.service" ||
|
||||||
fail "systemd unit ExecStart does not match PREFIX=/usr install path"
|
fail "systemd unit ExecStart does not match PREFIX=/usr install path"
|
||||||
|
|
||||||
step "checking installed log maintenance modes"
|
|
||||||
log_smoke="$tmpdir/messages.log"
|
|
||||||
recovered_log="$tmpdir/recovered.messages.log"
|
|
||||||
recover_report="$tmpdir/recovered.report"
|
|
||||||
smoke_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
||||||
cat > "$log_smoke" <<EOF
|
|
||||||
$smoke_ts|alice|one
|
|
||||||
$smoke_ts|mallory|extra|pipe
|
|
||||||
$smoke_ts|bob|two
|
|
||||||
EOF
|
|
||||||
if "$tmpdir/usr/bin/tnt" --log-check "$log_smoke" >"$tmpdir/log-check.out" 2>&1; then
|
|
||||||
fail "installed tnt --log-check should report invalid records"
|
|
||||||
fi
|
|
||||||
grep -q '^valid_records 2$' "$tmpdir/log-check.out" ||
|
|
||||||
fail "installed tnt --log-check did not report valid records"
|
|
||||||
grep -q '^invalid_records 1$' "$tmpdir/log-check.out" ||
|
|
||||||
fail "installed tnt --log-check did not report invalid records"
|
|
||||||
if "$tmpdir/usr/bin/tnt" --log-recover "$log_smoke" \
|
|
||||||
>"$recovered_log" 2>"$recover_report"; then
|
|
||||||
fail "installed tnt --log-recover should report invalid records"
|
|
||||||
fi
|
|
||||||
grep -q "$smoke_ts|alice|one" "$recovered_log" ||
|
|
||||||
fail "installed tnt --log-recover missed alice record"
|
|
||||||
grep -q "$smoke_ts|bob|two" "$recovered_log" ||
|
|
||||||
fail "installed tnt --log-recover missed bob record"
|
|
||||||
! grep -q 'mallory' "$recovered_log" ||
|
|
||||||
fail "installed tnt --log-recover preserved invalid record"
|
|
||||||
grep -q '^invalid_records 1$' "$recover_report" ||
|
|
||||||
fail "installed tnt --log-recover did not report invalid records"
|
|
||||||
|
|
||||||
step "checking installer syntax"
|
step "checking installer syntax"
|
||||||
sh -n install.sh
|
sh -n install.sh
|
||||||
sh -n scripts/check_release_ref.sh
|
|
||||||
sh -n scripts/package_publish_check.sh
|
|
||||||
scripts/check_release_ref.sh "v$version"
|
|
||||||
bad_ref=v0.0.0
|
|
||||||
[ "$version" != "0.0.0" ] || bad_ref=v9.9.9
|
|
||||||
if scripts/check_release_ref.sh "$bad_ref" >/dev/null 2>&1; then
|
|
||||||
fail "release ref check accepted a mismatched tag"
|
|
||||||
fi
|
|
||||||
|
|
||||||
step "checking Debian packaging metadata"
|
step "checking Debian packaging metadata"
|
||||||
[ -x packaging/debian/debian/rules ] ||
|
[ -x packaging/debian/debian/rules ] ||
|
||||||
fail "packaging/debian/debian/rules must be executable"
|
fail "packaging/debian/debian/rules must be executable"
|
||||||
[ -x packaging/debian/debian/postinst ] ||
|
|
||||||
fail "packaging/debian/debian/postinst must be executable"
|
|
||||||
grep -q "^3.0 (quilt)$" packaging/debian/debian/source/format ||
|
grep -q "^3.0 (quilt)$" packaging/debian/debian/source/format ||
|
||||||
fail "unsupported Debian source format"
|
fail "unsupported Debian source format"
|
||||||
grep -q "adduser .* tnt" packaging/debian/debian/postinst ||
|
|
||||||
fail "Debian postinst must create the tnt system user"
|
|
||||||
grep -q " adduser" packaging/debian/debian/control ||
|
|
||||||
fail "Debian package must depend on adduser for postinst user creation"
|
|
||||||
|
|
||||||
step "checking Debian source assembly"
|
|
||||||
sh -n scripts/package_debian_source.sh
|
|
||||||
scripts/package_debian_source.sh "$tmpdir/debian-source"
|
|
||||||
[ -f "$tmpdir/debian-source/tnt-chat-$version/debian/control" ] ||
|
|
||||||
fail "assembled Debian source tree is missing debian/control"
|
|
||||||
[ -x "$tmpdir/debian-source/tnt-chat-$version/debian/rules" ] ||
|
|
||||||
fail "assembled Debian source tree is missing executable debian/rules"
|
|
||||||
[ -x "$tmpdir/debian-source/tnt-chat-$version/debian/postinst" ] ||
|
|
||||||
fail "assembled Debian source tree is missing executable debian/postinst"
|
|
||||||
|
|
||||||
step "checking packaged system user metadata"
|
|
||||||
grep -q '^u tnt ' packaging/arch/tnt-chat.sysusers ||
|
|
||||||
fail "Arch sysusers file must create the tnt system user"
|
|
||||||
grep -q 'usr/lib/sysusers.d' packaging/arch/PKGBUILD ||
|
|
||||||
fail "PKGBUILD must install the sysusers.d file"
|
|
||||||
|
|
||||||
step "checking Homebrew service metadata"
|
|
||||||
grep -q "service do" packaging/homebrew/tnt-chat.rb ||
|
|
||||||
fail "Homebrew formula must define a brew services entry"
|
|
||||||
grep -q 'opt_bin/"tnt"' packaging/homebrew/tnt-chat.rb ||
|
|
||||||
fail "Homebrew service must run the installed tnt binary"
|
|
||||||
|
|
||||||
step "checking packaging syntax"
|
step "checking packaging syntax"
|
||||||
if command -v bash >/dev/null 2>&1; then
|
if command -v bash >/dev/null 2>&1; then
|
||||||
|
|
@ -257,6 +174,12 @@ if [ "$STRICT" -eq 1 ]; then
|
||||||
fail "local tag v$version does not point at HEAD"
|
fail "local tag v$version does not point at HEAD"
|
||||||
grep -q "^## $version " docs/CHANGELOG.md ||
|
grep -q "^## $version " docs/CHANGELOG.md ||
|
||||||
fail "docs/CHANGELOG.md does not contain a release section for $version"
|
fail "docs/CHANGELOG.md does not contain a release section for $version"
|
||||||
|
! grep -q "sha256sums=('SKIP')" packaging/arch/PKGBUILD ||
|
||||||
|
fail "replace PKGBUILD sha256sums before strict release"
|
||||||
|
! grep -q "sha256sums = SKIP" packaging/arch/.SRCINFO ||
|
||||||
|
fail "replace .SRCINFO sha256sums before strict release"
|
||||||
|
! grep -q "REPLACE_WITH_RELEASE_TARBALL_SHA256" packaging/homebrew/tnt-chat.rb ||
|
||||||
|
fail "replace Homebrew sha256 before strict release"
|
||||||
! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null ||
|
! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null ||
|
||||||
fail "replace maintainer email placeholders before strict release"
|
fail "replace maintainer email placeholders before strict release"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -476,12 +476,17 @@ void *bootstrap_run(void *arg) {
|
||||||
}
|
}
|
||||||
client->exec_command_too_long = ctx->exec_command_too_long;
|
client->exec_command_too_long = ctx->exec_command_too_long;
|
||||||
|
|
||||||
|
/* Add a ref for the channel callbacks (eof/close/window_change) so the
|
||||||
|
* client_t outlives any in-flight callback invocation. */
|
||||||
|
client_addref(client);
|
||||||
|
|
||||||
if (client_install_channel_callbacks(client) < 0) {
|
if (client_install_channel_callbacks(client) < 0) {
|
||||||
/* Nullify session/channel ownership so client_release won't
|
/* Nullify session/channel ownership so client_release won't
|
||||||
* double-free what cleanup_failed_session is about to free. */
|
* double-free what cleanup_failed_session is about to free. */
|
||||||
client->session = NULL;
|
client->session = NULL;
|
||||||
client->channel = NULL;
|
client->channel = NULL;
|
||||||
client_release(client);
|
client_release(client); /* drop the callback ref (2 → 1) */
|
||||||
|
client_release(client); /* drop the main ref (1 → 0, frees client) */
|
||||||
cleanup_failed_session(session, ctx);
|
cleanup_failed_session(session, ctx);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
#include "chat_room.h"
|
#include "chat_room.h"
|
||||||
#include "config_defaults.h"
|
|
||||||
|
|
||||||
/* Global chat room instance */
|
/* Global chat room instance */
|
||||||
chat_room_t *g_room = NULL;
|
chat_room_t *g_room = NULL;
|
||||||
|
|
||||||
static int room_capacity_from_env(void) {
|
static int room_capacity_from_env(void) {
|
||||||
return tnt_config_env_int(&TNT_CONFIG_MAX_CONNECTIONS);
|
return env_int("TNT_MAX_CONNECTIONS", DEFAULT_MAX_CLIENTS, 1,
|
||||||
|
MAX_CONFIGURED_CLIENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize chat room */
|
/* Initialize chat room */
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
#include "cli_text.h"
|
#include "cli_text.h"
|
||||||
|
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
|
|
||||||
void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
|
|
@ -13,14 +12,12 @@ void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" -d, --state-dir DIR Store host key and logs in DIR\n"
|
" -d, --state-dir DIR Store host key and logs in DIR\n"
|
||||||
" --bind ADDR Bind to ADDR (default: 0.0.0.0)\n"
|
" --bind ADDR Bind to ADDR (default: 0.0.0.0)\n"
|
||||||
" --public-host HOST Show HOST in startup connection hints\n"
|
" --public-host HOST Show HOST in startup connection hints\n"
|
||||||
" --max-connections N Global connection limit (default: %d)\n"
|
" --max-connections N Global connection limit (default: 64)\n"
|
||||||
" --max-conn-per-ip N Per-IP concurrent session limit\n"
|
" --max-conn-per-ip N Per-IP concurrent session limit\n"
|
||||||
" --max-conn-rate-per-ip N Per-IP connection-rate limit\n"
|
" --max-conn-rate-per-ip N Per-IP connection-rate limit\n"
|
||||||
" --rate-limit 0|1 Disable/enable rate-based blocking\n"
|
" --rate-limit 0|1 Disable/enable rate-based blocking\n"
|
||||||
" --idle-timeout SECONDS Idle disconnect timeout\n"
|
" --idle-timeout SECONDS Idle disconnect timeout\n"
|
||||||
" --ssh-log-level LEVEL libssh log level 0..4\n"
|
" --ssh-log-level LEVEL libssh log level 0..4\n"
|
||||||
" --log-check FILE Check messages.log v1 records\n"
|
|
||||||
" --log-recover FILE Write valid records to stdout\n"
|
|
||||||
" -V, --version Show version\n"
|
" -V, --version Show version\n"
|
||||||
" -h, --help Show this help\n"
|
" -h, --help Show this help\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
@ -29,9 +26,9 @@ void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" TNT_STATE_DIR State directory\n"
|
" TNT_STATE_DIR State directory\n"
|
||||||
" TNT_ACCESS_TOKEN Require this password for SSH auth\n"
|
" TNT_ACCESS_TOKEN Require this password for SSH auth\n"
|
||||||
" TNT_LANG UI language: en or zh (default: locale)\n"
|
" TNT_LANG UI language: en or zh (default: locale)\n"
|
||||||
" TNT_MAX_CONNECTIONS Global connection limit (default: %d)\n"
|
" TNT_MAX_CONNECTIONS Global connection limit (default: 64)\n"
|
||||||
" TNT_RATE_LIMIT Set to 0 to disable rate limiting\n"
|
" TNT_RATE_LIMIT Set to 0 to disable rate limiting\n"
|
||||||
" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: %d)\n",
|
" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: 1800)\n",
|
||||||
"tnt %s - 匿名 SSH 聊天服务器\n\n"
|
"tnt %s - 匿名 SSH 聊天服务器\n\n"
|
||||||
"用法: %s [options]\n\n"
|
"用法: %s [options]\n\n"
|
||||||
"选项:\n"
|
"选项:\n"
|
||||||
|
|
@ -39,14 +36,12 @@ void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" -d, --state-dir DIR 将主机密钥和日志存放在 DIR\n"
|
" -d, --state-dir DIR 将主机密钥和日志存放在 DIR\n"
|
||||||
" --bind ADDR 绑定到 ADDR (默认: 0.0.0.0)\n"
|
" --bind ADDR 绑定到 ADDR (默认: 0.0.0.0)\n"
|
||||||
" --public-host HOST 在启动提示中显示 HOST\n"
|
" --public-host HOST 在启动提示中显示 HOST\n"
|
||||||
" --max-connections N 全局连接数限制 (默认: %d)\n"
|
" --max-connections N 全局连接数限制 (默认: 64)\n"
|
||||||
" --max-conn-per-ip N 单 IP 并发会话限制\n"
|
" --max-conn-per-ip N 单 IP 并发会话限制\n"
|
||||||
" --max-conn-rate-per-ip N 单 IP 连接速率限制\n"
|
" --max-conn-rate-per-ip N 单 IP 连接速率限制\n"
|
||||||
" --rate-limit 0|1 禁用/启用速率封禁\n"
|
" --rate-limit 0|1 禁用/启用速率封禁\n"
|
||||||
" --idle-timeout SECONDS 空闲断开时间\n"
|
" --idle-timeout SECONDS 空闲断开时间\n"
|
||||||
" --ssh-log-level LEVEL libssh 日志级别 0..4\n"
|
" --ssh-log-level LEVEL libssh 日志级别 0..4\n"
|
||||||
" --log-check FILE 检查 messages.log v1 记录\n"
|
|
||||||
" --log-recover FILE 将有效记录写入 stdout\n"
|
|
||||||
" -V, --version 显示版本\n"
|
" -V, --version 显示版本\n"
|
||||||
" -h, --help 显示此帮助\n"
|
" -h, --help 显示此帮助\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
@ -55,19 +50,16 @@ void cli_text_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" TNT_STATE_DIR 状态目录\n"
|
" TNT_STATE_DIR 状态目录\n"
|
||||||
" TNT_ACCESS_TOKEN 要求 SSH 认证使用此密码\n"
|
" TNT_ACCESS_TOKEN 要求 SSH 认证使用此密码\n"
|
||||||
" TNT_LANG UI 语言: en 或 zh (默认跟随 locale)\n"
|
" TNT_LANG UI 语言: en 或 zh (默认跟随 locale)\n"
|
||||||
" TNT_MAX_CONNECTIONS 全局连接数限制 (默认: %d)\n"
|
" TNT_MAX_CONNECTIONS 全局连接数限制 (默认: 64)\n"
|
||||||
" TNT_RATE_LIMIT 设为 0 可禁用速率限制\n"
|
" TNT_RATE_LIMIT 设为 0 可禁用速率限制\n"
|
||||||
" TNT_IDLE_TIMEOUT 空闲断开时间,单位秒 (默认: %d)\n"
|
" TNT_IDLE_TIMEOUT 空闲断开时间,单位秒 (默认: 1800)\n"
|
||||||
);
|
);
|
||||||
const char *program = (program_name && program_name[0] != '\0')
|
const char *program = (program_name && program_name[0] != '\0')
|
||||||
? program_name
|
? program_name
|
||||||
: "tnt";
|
: "tnt";
|
||||||
|
|
||||||
buffer_appendf(buffer, buf_size, pos, i18n_string(help_format, lang),
|
buffer_appendf(buffer, buf_size, pos, i18n_string(help_format, lang),
|
||||||
TNT_VERSION, program, TNT_DEFAULT_PORT,
|
TNT_VERSION, program, DEFAULT_PORT);
|
||||||
TNT_DEFAULT_MAX_CONNECTIONS,
|
|
||||||
TNT_DEFAULT_MAX_CONNECTIONS,
|
|
||||||
TNT_DEFAULT_IDLE_TIMEOUT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *cli_text_invalid_port_format(ui_lang_t lang) {
|
const char *cli_text_invalid_port_format(ui_lang_t lang) {
|
||||||
|
|
@ -82,13 +74,6 @@ const char *cli_text_invalid_value_format(ui_lang_t lang) {
|
||||||
return i18n_string(text, lang);
|
return i18n_string(text, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *cli_text_option_requires_arg_format(ui_lang_t lang) {
|
|
||||||
static const i18n_string_t text =
|
|
||||||
I18N_STRING("Option requires argument: %s\n",
|
|
||||||
"选项需要参数: %s\n");
|
|
||||||
return i18n_string(text, lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *cli_text_unknown_option_format(ui_lang_t lang) {
|
const char *cli_text_unknown_option_format(ui_lang_t lang) {
|
||||||
static const i18n_string_t text =
|
static const i18n_string_t text =
|
||||||
I18N_STRING("Unknown option: %s\n", "未知选项: %s\n");
|
I18N_STRING("Unknown option: %s\n", "未知选项: %s\n");
|
||||||
|
|
|
||||||
26
src/client.c
26
src/client.c
|
|
@ -244,25 +244,6 @@ void client_release(client_t *client) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void client_release_session(client_t *client) {
|
|
||||||
if (!client) return;
|
|
||||||
|
|
||||||
if (client->channel && client->channel_cb) {
|
|
||||||
ssh_remove_channel_callbacks(client->channel, client->channel_cb);
|
|
||||||
}
|
|
||||||
if (client->channel_cb) {
|
|
||||||
free(client->channel_cb);
|
|
||||||
client->channel_cb = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client->channel_callback_ref) {
|
|
||||||
client->channel_callback_ref = false;
|
|
||||||
client_release(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
client_release(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Send formatted string to client */
|
/* Send formatted string to client */
|
||||||
int client_printf(client_t *client, const char *fmt, ...) {
|
int client_printf(client_t *client, const char *fmt, ...) {
|
||||||
char buffer[2048];
|
char buffer[2048];
|
||||||
|
|
@ -334,13 +315,8 @@ int client_install_channel_callbacks(client_t *client) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
client_addref(client);
|
|
||||||
client->channel_callback_ref = true;
|
|
||||||
|
|
||||||
client->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct));
|
client->channel_cb = calloc(1, sizeof(struct ssh_channel_callbacks_struct));
|
||||||
if (!client->channel_cb) {
|
if (!client->channel_cb) {
|
||||||
client->channel_callback_ref = false;
|
|
||||||
client_release(client);
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,8 +330,6 @@ int client_install_channel_callbacks(client_t *client) {
|
||||||
if (ssh_set_channel_callbacks(client->channel, client->channel_cb) != SSH_OK) {
|
if (ssh_set_channel_callbacks(client->channel, client->channel_cb) != SSH_OK) {
|
||||||
free(client->channel_cb);
|
free(client->channel_cb);
|
||||||
client->channel_cb = NULL;
|
client->channel_cb = NULL;
|
||||||
client->channel_callback_ref = false;
|
|
||||||
client_release(client);
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,60 +52,12 @@ static void append_command_usage(char *output, size_t buf_size, size_t *pos,
|
||||||
command_catalog_append_usage(output, buf_size, pos, id, lang);
|
command_catalog_append_usage(output, buf_size, pos, id, lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void append_inbox_output(client_t *client, char *output,
|
|
||||||
size_t buf_size, size_t *pos) {
|
|
||||||
whisper_t snapshot[WHISPER_INBOX_SIZE];
|
|
||||||
int snap_count;
|
|
||||||
|
|
||||||
pthread_mutex_lock(&client->whisper_lock);
|
|
||||||
snap_count = client->whisper_inbox_count;
|
|
||||||
memcpy(snapshot, client->whisper_inbox,
|
|
||||||
snap_count * sizeof(whisper_t));
|
|
||||||
client->unread_whispers = 0;
|
|
||||||
pthread_mutex_unlock(&client->whisper_lock);
|
|
||||||
|
|
||||||
buffer_appendf(output, buf_size, pos,
|
|
||||||
"\033[1;36m%s\033[0m \033[2;37m· %d\033[0m\n",
|
|
||||||
i18n_text(client->ui_lang, I18N_INBOX_TITLE),
|
|
||||||
snap_count);
|
|
||||||
if (snap_count == 0) {
|
|
||||||
buffer_appendf(output, buf_size, pos,
|
|
||||||
" \033[2;37m%s\033[0m\n",
|
|
||||||
i18n_text(client->ui_lang, I18N_INBOX_EMPTY));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < snap_count; i++) {
|
|
||||||
char ts[20];
|
|
||||||
struct tm tmi;
|
|
||||||
localtime_r(&snapshot[i].timestamp, &tmi);
|
|
||||||
strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi);
|
|
||||||
buffer_appendf(output, buf_size, pos,
|
|
||||||
" \033[90m%s\033[0m \033[35m%s\033[0m: %s\n",
|
|
||||||
ts, snapshot[i].from, snapshot[i].content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool commands_refresh_active_output(client_t *client) {
|
|
||||||
char output[MAX_COMMAND_OUTPUT_LEN] = {0};
|
|
||||||
size_t pos = 0;
|
|
||||||
|
|
||||||
if (!client || client->command_output_kind != TNT_COMMAND_OUTPUT_INBOX) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
append_inbox_output(client, output, sizeof(output), &pos);
|
|
||||||
snprintf(client->command_output, sizeof(client->command_output), "%s",
|
|
||||||
output);
|
|
||||||
client->command_output_scroll = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void commands_dispatch(client_t *client) {
|
void commands_dispatch(client_t *client) {
|
||||||
char cmd_buf[256];
|
char cmd_buf[256];
|
||||||
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
||||||
cmd_buf[sizeof(cmd_buf) - 1] = '\0';
|
cmd_buf[sizeof(cmd_buf) - 1] = '\0';
|
||||||
char *cmd = cmd_buf;
|
char *cmd = cmd_buf;
|
||||||
char output[MAX_COMMAND_OUTPUT_LEN] = {0};
|
char output[MAX_COMMAND_OUTPUT_LEN] = {0};
|
||||||
tnt_command_output_kind_t output_kind = TNT_COMMAND_OUTPUT_GENERIC;
|
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
|
||||||
/* Trim whitespace */
|
/* Trim whitespace */
|
||||||
|
|
@ -118,10 +70,6 @@ void commands_dispatch(client_t *client) {
|
||||||
end--;
|
end--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cmd[0] == ':') {
|
|
||||||
cmd++;
|
|
||||||
while (*cmd == ' ') cmd++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Save to command history */
|
/* Save to command history */
|
||||||
if (cmd[0] != '\0') {
|
if (cmd[0] != '\0') {
|
||||||
|
|
@ -271,9 +219,9 @@ void commands_dispatch(client_t *client) {
|
||||||
snprintf(target->whisper_inbox[slot].content,
|
snprintf(target->whisper_inbox[slot].content,
|
||||||
sizeof(target->whisper_inbox[slot].content),
|
sizeof(target->whisper_inbox[slot].content),
|
||||||
"%s", rest);
|
"%s", rest);
|
||||||
target->unread_whispers++;
|
|
||||||
pthread_mutex_unlock(&target->whisper_lock);
|
pthread_mutex_unlock(&target->whisper_lock);
|
||||||
|
|
||||||
|
target->unread_whispers++;
|
||||||
/* Audible nudge — the title bar ✉ counter (UX-11 style)
|
/* Audible nudge — the title bar ✉ counter (UX-11 style)
|
||||||
* carries the persistent signal. */
|
* carries the persistent signal. */
|
||||||
client_queue_bell(target);
|
client_queue_bell(target);
|
||||||
|
|
@ -294,8 +242,35 @@ void commands_dispatch(client_t *client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (command_id == TNT_COMMAND_INBOX) {
|
} else if (command_id == TNT_COMMAND_INBOX) {
|
||||||
output_kind = TNT_COMMAND_OUTPUT_INBOX;
|
/* Snapshot the inbox under whisper_lock so a concurrent sender doesn't
|
||||||
append_inbox_output(client, output, sizeof(output), &pos);
|
* tear what we're rendering. Counter reset happens after copy. */
|
||||||
|
whisper_t snapshot[WHISPER_INBOX_SIZE];
|
||||||
|
int snap_count;
|
||||||
|
pthread_mutex_lock(&client->whisper_lock);
|
||||||
|
snap_count = client->whisper_inbox_count;
|
||||||
|
memcpy(snapshot, client->whisper_inbox,
|
||||||
|
snap_count * sizeof(whisper_t));
|
||||||
|
pthread_mutex_unlock(&client->whisper_lock);
|
||||||
|
client->unread_whispers = 0;
|
||||||
|
|
||||||
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
"\033[1;36m%s\033[0m \033[2;37m· %d\033[0m\n",
|
||||||
|
i18n_text(client->ui_lang, I18N_INBOX_TITLE),
|
||||||
|
snap_count);
|
||||||
|
if (snap_count == 0) {
|
||||||
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
" \033[2;37m%s\033[0m\n",
|
||||||
|
i18n_text(client->ui_lang, I18N_INBOX_EMPTY));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < snap_count; i++) {
|
||||||
|
char ts[20];
|
||||||
|
struct tm tmi;
|
||||||
|
localtime_r(&snapshot[i].timestamp, &tmi);
|
||||||
|
strftime(ts, sizeof(ts), "%m-%d %H:%M", &tmi);
|
||||||
|
buffer_appendf(output, sizeof(output), &pos,
|
||||||
|
" \033[90m%s\033[0m \033[35m%s\033[0m: %s\n",
|
||||||
|
ts, snapshot[i].from, snapshot[i].content);
|
||||||
|
}
|
||||||
|
|
||||||
} else if (command_id == TNT_COMMAND_NICK) {
|
} else if (command_id == TNT_COMMAND_NICK) {
|
||||||
const char *new_name = arg;
|
const char *new_name = arg;
|
||||||
|
|
@ -439,7 +414,6 @@ void commands_dispatch(client_t *client) {
|
||||||
cmd_done:
|
cmd_done:
|
||||||
snprintf(client->command_output, sizeof(client->command_output), "%s", output);
|
snprintf(client->command_output, sizeof(client->command_output), "%s", output);
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
client->command_output_kind = output_kind;
|
|
||||||
client->command_input[0] = '\0';
|
client->command_input[0] = '\0';
|
||||||
tui_render_command_output(client);
|
tui_render_command_output(client);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_PORT = {
|
|
||||||
"PORT",
|
|
||||||
TNT_DEFAULT_PORT,
|
|
||||||
TNT_MIN_PORT,
|
|
||||||
TNT_MAX_PORT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_MAX_CONNECTIONS = {
|
|
||||||
"TNT_MAX_CONNECTIONS",
|
|
||||||
TNT_DEFAULT_MAX_CONNECTIONS,
|
|
||||||
TNT_MIN_CONFIGURED_CLIENTS,
|
|
||||||
TNT_MAX_CONFIGURED_CLIENTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_MAX_CONN_PER_IP = {
|
|
||||||
"TNT_MAX_CONN_PER_IP",
|
|
||||||
TNT_DEFAULT_MAX_CONN_PER_IP,
|
|
||||||
TNT_MIN_CONFIGURED_CLIENTS,
|
|
||||||
TNT_MAX_CONFIGURED_CLIENTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_MAX_CONN_RATE_PER_IP = {
|
|
||||||
"TNT_MAX_CONN_RATE_PER_IP",
|
|
||||||
TNT_DEFAULT_MAX_CONN_RATE_PER_IP,
|
|
||||||
TNT_MIN_CONFIGURED_CLIENTS,
|
|
||||||
TNT_MAX_CONFIGURED_CLIENTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_RATE_LIMIT = {
|
|
||||||
"TNT_RATE_LIMIT",
|
|
||||||
TNT_DEFAULT_RATE_LIMIT_ENABLED,
|
|
||||||
TNT_MIN_RATE_LIMIT_ENABLED,
|
|
||||||
TNT_MAX_RATE_LIMIT_ENABLED,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_IDLE_TIMEOUT = {
|
|
||||||
"TNT_IDLE_TIMEOUT",
|
|
||||||
TNT_DEFAULT_IDLE_TIMEOUT,
|
|
||||||
TNT_MIN_IDLE_TIMEOUT,
|
|
||||||
TNT_MAX_IDLE_TIMEOUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tnt_int_config_spec_t TNT_CONFIG_SSH_LOG_LEVEL = {
|
|
||||||
"TNT_SSH_LOG_LEVEL",
|
|
||||||
0,
|
|
||||||
TNT_MIN_SSH_LOG_LEVEL,
|
|
||||||
TNT_MAX_SSH_LOG_LEVEL,
|
|
||||||
};
|
|
||||||
|
|
||||||
int tnt_config_env_int(const tnt_int_config_spec_t *spec) {
|
|
||||||
if (!spec) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return env_int(spec->env_name, spec->fallback, spec->min_value,
|
|
||||||
spec->max_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool tnt_config_parse_int(const char *value, const tnt_int_config_spec_t *spec,
|
|
||||||
int *out) {
|
|
||||||
char *end = NULL;
|
|
||||||
long val;
|
|
||||||
|
|
||||||
if (!value || value[0] == '\0' || !spec || !out) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
val = strtol(value, &end, 10);
|
|
||||||
if (!end || *end != '\0' || val < spec->min_value ||
|
|
||||||
val > spec->max_value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out = (int)val;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
64
src/exec.c
64
src/exec.c
|
|
@ -291,45 +291,6 @@ static int parse_tail_count(const char *args, int *count) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int parse_dump_count(const char *args, int *count) {
|
|
||||||
char *end = NULL;
|
|
||||||
long value;
|
|
||||||
|
|
||||||
if (!count) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*count = 0;
|
|
||||||
if (!args || args[0] == '\0') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strncmp(args, "-n", 2) == 0) {
|
|
||||||
args += 2;
|
|
||||||
while (*args && isspace((unsigned char)*args)) {
|
|
||||||
args++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value = strtol(args, &end, 10);
|
|
||||||
if (end == args) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
while (*end) {
|
|
||||||
if (!isspace((unsigned char)*end)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
end++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 1 || value > 10000) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*count = (int)value;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int exec_command_tail(client_t *client, const char *args) {
|
static int exec_command_tail(client_t *client, const char *args) {
|
||||||
int requested = 20;
|
int requested = 20;
|
||||||
int total_messages;
|
int total_messages;
|
||||||
|
|
@ -386,27 +347,6 @@ static int exec_command_tail(client_t *client, const char *args) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int exec_command_dump(client_t *client, const char *args) {
|
|
||||||
int requested = 0;
|
|
||||||
char *output = NULL;
|
|
||||||
size_t output_len = 0;
|
|
||||||
int rc;
|
|
||||||
|
|
||||||
if (parse_dump_count(args, &requested) < 0) {
|
|
||||||
return exec_command_usage(client, TNT_EXEC_COMMAND_DUMP);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message_dump_text(&output, &output_len, requested) < 0) {
|
|
||||||
client_printf(client, "dump: failed to read message log\n");
|
|
||||||
return TNT_EXIT_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = client_send(client, output, output_len) == 0 ? TNT_EXIT_OK
|
|
||||||
: TNT_EXIT_ERROR;
|
|
||||||
free(output);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int exec_command_post(client_t *client, const char *args) {
|
static int exec_command_post(client_t *client, const char *args) {
|
||||||
char content[MAX_MESSAGE_LEN];
|
char content[MAX_MESSAGE_LEN];
|
||||||
char username[MAX_USERNAME_LEN];
|
char username[MAX_USERNAME_LEN];
|
||||||
|
|
@ -511,14 +451,10 @@ int exec_dispatch(client_t *client) {
|
||||||
return exec_command_stats(client, args != NULL);
|
return exec_command_stats(client, args != NULL);
|
||||||
case TNT_EXEC_COMMAND_TAIL:
|
case TNT_EXEC_COMMAND_TAIL:
|
||||||
return exec_command_tail(client, args);
|
return exec_command_tail(client, args);
|
||||||
case TNT_EXEC_COMMAND_DUMP:
|
|
||||||
return exec_command_dump(client, args);
|
|
||||||
case TNT_EXEC_COMMAND_POST:
|
case TNT_EXEC_COMMAND_POST:
|
||||||
return exec_command_post(client, args);
|
return exec_command_post(client, args);
|
||||||
case TNT_EXEC_COMMAND_EXIT:
|
case TNT_EXEC_COMMAND_EXIT:
|
||||||
return TNT_EXIT_OK;
|
return TNT_EXIT_OK;
|
||||||
case TNT_EXEC_COMMAND_COUNT:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,6 @@ static const exec_catalog_entry_t entries[] = {
|
||||||
"tail -n N", "tail [N] | tail -n N",
|
"tail -n N", "tail [N] | tail -n N",
|
||||||
I18N_STRING("Print recent messages", "输出最近消息"),
|
I18N_STRING("Print recent messages", "输出最近消息"),
|
||||||
false, false, false},
|
false, false, false},
|
||||||
{TNT_EXEC_COMMAND_DUMP, "dump", NULL,
|
|
||||||
"dump [N]", "dump [N] | dump -n N",
|
|
||||||
I18N_STRING("Export persisted messages", "导出持久化消息"),
|
|
||||||
false, false, false},
|
|
||||||
{TNT_EXEC_COMMAND_DUMP, "dump", NULL,
|
|
||||||
"dump -n N", "dump [N] | dump -n N",
|
|
||||||
I18N_STRING("Export persisted messages", "导出持久化消息"),
|
|
||||||
false, false, false},
|
|
||||||
{TNT_EXEC_COMMAND_POST, "post", NULL,
|
{TNT_EXEC_COMMAND_POST, "post", NULL,
|
||||||
"post MESSAGE", "post MESSAGE",
|
"post MESSAGE", "post MESSAGE",
|
||||||
I18N_STRING("Post a message non-interactively", "非交互发送消息"),
|
I18N_STRING("Post a message non-interactively", "非交互发送消息"),
|
||||||
|
|
@ -155,26 +147,6 @@ void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void exec_catalog_append_command_list(char *buffer, size_t buf_size,
|
|
||||||
size_t *pos) {
|
|
||||||
bool seen[TNT_EXEC_COMMAND_COUNT] = {0};
|
|
||||||
size_t count = 0;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
|
||||||
tnt_exec_command_id_t id = entries[i].id;
|
|
||||||
|
|
||||||
if (id < 0 || id >= TNT_EXEC_COMMAND_COUNT || seen[id]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (count > 0) {
|
|
||||||
buffer_appendf(buffer, buf_size, pos, ", ");
|
|
||||||
}
|
|
||||||
buffer_appendf(buffer, buf_size, pos, "%s", entries[i].name);
|
|
||||||
seen[id] = true;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
||||||
tnt_exec_command_id_t id, ui_lang_t lang) {
|
tnt_exec_command_id_t id, ui_lang_t lang) {
|
||||||
const exec_catalog_entry_t *entry = entry_for_id(id);
|
const exec_catalog_entry_t *entry = entry_for_id(id);
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" Backspace - Delete character\n"
|
" Backspace - Delete character\n"
|
||||||
" Ctrl+W - Delete last word\n"
|
" Ctrl+W - Delete last word\n"
|
||||||
" Ctrl+U - Delete line\n"
|
" Ctrl+U - Delete line\n"
|
||||||
" Up/Down - Recall sent messages\n"
|
|
||||||
" Tab - Complete @mention\n"
|
|
||||||
" Ctrl+C - Enter NORMAL mode\n"
|
" Ctrl+C - Enter NORMAL mode\n"
|
||||||
"\n"
|
"\n"
|
||||||
"NORMAL MODE KEYS:\n"
|
"NORMAL MODE KEYS:\n"
|
||||||
|
|
@ -28,7 +26,6 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" Follows latest until you scroll up\n"
|
" Follows latest until you scroll up\n"
|
||||||
" i - Return to INSERT mode\n"
|
" i - Return to INSERT mode\n"
|
||||||
" : - Enter COMMAND mode\n"
|
" : - Enter COMMAND mode\n"
|
||||||
" / - Search message history\n"
|
|
||||||
" j/k - Scroll down/up one line\n"
|
" j/k - Scroll down/up one line\n"
|
||||||
" Ctrl+D/U - Scroll half page down/up\n"
|
" Ctrl+D/U - Scroll half page down/up\n"
|
||||||
" Ctrl+F/B - Scroll full page down/up\n"
|
" Ctrl+F/B - Scroll full page down/up\n"
|
||||||
|
|
@ -52,8 +49,6 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" Backspace - 删除字符\n"
|
" Backspace - 删除字符\n"
|
||||||
" Ctrl+W - 删除上个单词\n"
|
" Ctrl+W - 删除上个单词\n"
|
||||||
" Ctrl+U - 删除整行\n"
|
" Ctrl+U - 删除整行\n"
|
||||||
" Up/Down - 调出已发送消息\n"
|
|
||||||
" Tab - 补全 @mention\n"
|
|
||||||
" Ctrl+C - 进入 NORMAL 模式\n"
|
" Ctrl+C - 进入 NORMAL 模式\n"
|
||||||
"\n"
|
"\n"
|
||||||
"NORMAL 模式按键:\n"
|
"NORMAL 模式按键:\n"
|
||||||
|
|
@ -61,7 +56,6 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
" 未向上翻阅时自动跟随最新消息\n"
|
" 未向上翻阅时自动跟随最新消息\n"
|
||||||
" i - 返回 INSERT 模式\n"
|
" i - 返回 INSERT 模式\n"
|
||||||
" : - 进入 COMMAND 模式\n"
|
" : - 进入 COMMAND 模式\n"
|
||||||
" / - 搜索消息历史\n"
|
|
||||||
" j/k - 向下/上滚动一行\n"
|
" j/k - 向下/上滚动一行\n"
|
||||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||||
|
|
@ -77,14 +71,10 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
"\n"
|
"\n"
|
||||||
"COMMAND OUTPUT KEYS:\n"
|
"COMMAND OUTPUT KEYS:\n"
|
||||||
" q, ESC - Close output\n"
|
" q, ESC - Close output\n"
|
||||||
" j/k, arrows - Scroll down/up\n"
|
" j/k - Scroll down/up\n"
|
||||||
" Ctrl+D/U - Scroll half page down/up\n"
|
" Ctrl+D/U - Scroll half page down/up\n"
|
||||||
" Ctrl+F/B - Scroll full page down/up\n"
|
" Ctrl+F/B - Scroll full page down/up\n"
|
||||||
" Space/b - Scroll full page down/up\n"
|
|
||||||
" PgDn/PgUp - Scroll full page down/up\n"
|
|
||||||
" End/Home - Jump to bottom/top\n"
|
|
||||||
" g/G - Jump to top/bottom\n"
|
" g/G - Jump to top/bottom\n"
|
||||||
" r - Refresh live output (:inbox)\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"SPECIAL MESSAGES:\n"
|
"SPECIAL MESSAGES:\n"
|
||||||
" /me <action> - Send action (e.g. /me waves)\n"
|
" /me <action> - Send action (e.g. /me waves)\n"
|
||||||
|
|
@ -92,25 +82,18 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
"\n"
|
"\n"
|
||||||
"HELP SCREEN KEYS:\n"
|
"HELP SCREEN KEYS:\n"
|
||||||
" q, ESC - Close help\n"
|
" q, ESC - Close help\n"
|
||||||
" j/k, arrows - Scroll down/up\n"
|
" j/k - Scroll down/up\n"
|
||||||
" Ctrl+D/U - Scroll half page down/up\n"
|
" Ctrl+D/U - Scroll half page down/up\n"
|
||||||
" Ctrl+F/B - Scroll full page down/up\n"
|
" Ctrl+F/B - Scroll full page down/up\n"
|
||||||
" Space/b - Scroll full page down/up\n"
|
|
||||||
" PgDn/PgUp - Scroll full page down/up\n"
|
|
||||||
" End/Home - Jump to bottom/top\n"
|
|
||||||
" g/G - Jump to top/bottom\n"
|
" g/G - Jump to top/bottom\n"
|
||||||
" l - Cycle UI language\n",
|
" l - Cycle UI language\n",
|
||||||
"\n"
|
"\n"
|
||||||
"命令输出按键:\n"
|
"命令输出按键:\n"
|
||||||
" q, ESC - 关闭输出\n"
|
" q, ESC - 关闭输出\n"
|
||||||
" j/k, arrows - 向下/上滚动\n"
|
" j/k - 向下/上滚动\n"
|
||||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||||
" Space/b - 向下/上滚动整页\n"
|
|
||||||
" PgDn/PgUp - 向下/上滚动整页\n"
|
|
||||||
" End/Home - 跳到底部/顶部\n"
|
|
||||||
" g/G - 跳到顶部/底部\n"
|
" g/G - 跳到顶部/底部\n"
|
||||||
" r - 刷新动态输出 (:inbox)\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"特殊消息:\n"
|
"特殊消息:\n"
|
||||||
" /me <action> - 发送动作 (如 /me waves)\n"
|
" /me <action> - 发送动作 (如 /me waves)\n"
|
||||||
|
|
@ -118,12 +101,9 @@ void help_text_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
"\n"
|
"\n"
|
||||||
"帮助界面按键:\n"
|
"帮助界面按键:\n"
|
||||||
" q, ESC - 关闭帮助\n"
|
" q, ESC - 关闭帮助\n"
|
||||||
" j/k, arrows - 向下/上滚动\n"
|
" j/k - 向下/上滚动\n"
|
||||||
" Ctrl+D/U - 向下/上滚动半页\n"
|
" Ctrl+D/U - 向下/上滚动半页\n"
|
||||||
" Ctrl+F/B - 向下/上滚动整页\n"
|
" Ctrl+F/B - 向下/上滚动整页\n"
|
||||||
" Space/b - 向下/上滚动整页\n"
|
|
||||||
" PgDn/PgUp - 向下/上滚动整页\n"
|
|
||||||
" End/Home - 跳到底部/顶部\n"
|
|
||||||
" g/G - 跳到顶部/底部\n"
|
" g/G - 跳到顶部/底部\n"
|
||||||
" l - 切换界面语言\n"
|
" l - 切换界面语言\n"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"TNT %s - SSH 匿名聊天室\r\n\r\n"
|
"TNT %s - SSH 匿名聊天室\r\n\r\n"
|
||||||
),
|
),
|
||||||
[I18N_INSERT_HINT_WIDE] = I18N_STRING(
|
[I18N_INSERT_HINT_WIDE] = I18N_STRING(
|
||||||
"Enter send · Esc NORMAL",
|
"Enter send · Esc browse · :help",
|
||||||
"Enter 发送 · Esc NORMAL"
|
"Enter 发送 · Esc 浏览 · :help"
|
||||||
),
|
),
|
||||||
[I18N_INSERT_HINT_NARROW] = I18N_STRING(
|
[I18N_INSERT_HINT_NARROW] = I18N_STRING(
|
||||||
"Enter · Esc",
|
"Enter · Esc · :help",
|
||||||
"Enter · Esc"
|
"Enter · Esc · :help"
|
||||||
),
|
),
|
||||||
[I18N_NORMAL_LATEST] = I18N_STRING(
|
[I18N_NORMAL_LATEST] = I18N_STRING(
|
||||||
"G latest",
|
"G latest",
|
||||||
|
|
@ -57,10 +57,6 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom q:close",
|
"-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom q:close",
|
||||||
"-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 q:关闭"
|
"-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 q:关闭"
|
||||||
),
|
),
|
||||||
[I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT] = I18N_STRING(
|
|
||||||
"-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom r:refresh q:close",
|
|
||||||
"-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 r:刷新 q:关闭"
|
|
||||||
),
|
|
||||||
[I18N_MOTD_TITLE] = I18N_STRING(
|
[I18N_MOTD_TITLE] = I18N_STRING(
|
||||||
" NOTICE ",
|
" NOTICE ",
|
||||||
" 公告 "
|
" 公告 "
|
||||||
|
|
@ -142,8 +138,8 @@ static const i18n_string_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"--- 最近 %d 条消息 ---\n"
|
"--- 最近 %d 条消息 ---\n"
|
||||||
),
|
),
|
||||||
[I18N_SEARCH_HEADER_FORMAT] = I18N_STRING(
|
[I18N_SEARCH_HEADER_FORMAT] = I18N_STRING(
|
||||||
"--- Search: \"%s\" (showing last %d match(es)) ---\n",
|
"--- Search: \"%s\" (%d match(es)) ---\n",
|
||||||
"--- 搜索: \"%s\" (显示最近 %d 条匹配) ---\n"
|
"--- 搜索: \"%s\" (%d 条匹配) ---\n"
|
||||||
),
|
),
|
||||||
[I18N_MUTE_JOINS_FORMAT] = I18N_STRING(
|
[I18N_MUTE_JOINS_FORMAT] = I18N_STRING(
|
||||||
"Join/leave notifications: %s\n",
|
"Join/leave notifications: %s\n",
|
||||||
|
|
|
||||||
262
src/input.c
262
src/input.c
|
|
@ -2,7 +2,6 @@
|
||||||
#include "chat_room.h"
|
#include "chat_room.h"
|
||||||
#include "client.h"
|
#include "client.h"
|
||||||
#include "commands.h"
|
#include "commands.h"
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "exec.h"
|
#include "exec.h"
|
||||||
#include "history_view.h"
|
#include "history_view.h"
|
||||||
|
|
@ -21,11 +20,11 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
static int g_idle_timeout = TNT_DEFAULT_IDLE_TIMEOUT;
|
static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT;
|
||||||
static ui_lang_t g_default_ui_lang = UI_LANG_EN;
|
static ui_lang_t g_default_ui_lang = UI_LANG_EN;
|
||||||
|
|
||||||
void input_init(void) {
|
void input_init(void) {
|
||||||
g_idle_timeout = tnt_config_env_int(&TNT_CONFIG_IDLE_TIMEOUT);
|
g_idle_timeout = env_int("TNT_IDLE_TIMEOUT", DEFAULT_IDLE_TIMEOUT, 0, 86400);
|
||||||
g_default_ui_lang = i18n_default_ui_lang();
|
g_default_ui_lang = i18n_default_ui_lang();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +32,10 @@ static int read_username(client_t *client) {
|
||||||
char username[MAX_USERNAME_LEN] = {0};
|
char username[MAX_USERNAME_LEN] = {0};
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
char buf[4];
|
char buf[4];
|
||||||
const char *prompt = i18n_text(client->ui_lang, I18N_USERNAME_PROMPT);
|
|
||||||
|
|
||||||
tui_render_welcome(client);
|
tui_render_welcome(client);
|
||||||
client_printf(client, "%s", prompt);
|
client_printf(client, "%s", i18n_text(client->ui_lang,
|
||||||
|
I18N_USERNAME_PROMPT));
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
int n = ssh_channel_read_timeout(client->channel, buf, 1, 0, 60000); /* 60 sec timeout */
|
int n = ssh_channel_read_timeout(client->channel, buf, 1, 0, 60000); /* 60 sec timeout */
|
||||||
|
|
@ -55,18 +54,6 @@ static int read_username(client_t *client) {
|
||||||
|
|
||||||
if (b == '\r' || b == '\n') {
|
if (b == '\r' || b == '\n') {
|
||||||
break;
|
break;
|
||||||
} else if (b == 3 || b == 4) { /* Ctrl+C / Ctrl+D */
|
|
||||||
return -1;
|
|
||||||
} else if (b == 21) { /* Ctrl+U: clear line */
|
|
||||||
username[0] = '\0';
|
|
||||||
pos = 0;
|
|
||||||
client_printf(client, "\r\033[K%s", prompt);
|
|
||||||
} else if (b == 23) { /* Ctrl+W: delete word */
|
|
||||||
if (username[0] != '\0') {
|
|
||||||
utf8_remove_last_word(username);
|
|
||||||
pos = (int)strlen(username);
|
|
||||||
client_printf(client, "\r\033[K%s%s", prompt, username);
|
|
||||||
}
|
|
||||||
} else if (b == 127 || b == 8) { /* Backspace */
|
} else if (b == 127 || b == 8) { /* Backspace */
|
||||||
if (pos > 0) {
|
if (pos > 0) {
|
||||||
/* Compute width of the last character before removing it */
|
/* Compute width of the last character before removing it */
|
||||||
|
|
@ -234,134 +221,20 @@ static void dismiss_command_output(client_t *client) {
|
||||||
was_motd = client->show_motd;
|
was_motd = client->show_motd;
|
||||||
client->command_output[0] = '\0';
|
client->command_output[0] = '\0';
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
client->command_output_kind = TNT_COMMAND_OUTPUT_NONE;
|
|
||||||
client->show_motd = false;
|
client->show_motd = false;
|
||||||
if (was_motd) {
|
|
||||||
client->mode = MODE_INSERT;
|
|
||||||
client->follow_tail = true;
|
|
||||||
client->unread_mentions = 0;
|
|
||||||
normal_scroll_to_latest(client);
|
|
||||||
} else {
|
|
||||||
client->mode = MODE_NORMAL;
|
client->mode = MODE_NORMAL;
|
||||||
|
if (was_motd) {
|
||||||
|
normal_scroll_to_latest(client);
|
||||||
}
|
}
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
PAGER_ACTION_NONE,
|
|
||||||
PAGER_ACTION_SCROLL,
|
|
||||||
PAGER_ACTION_CLOSE,
|
|
||||||
PAGER_ACTION_REFRESH
|
|
||||||
} pager_action_t;
|
|
||||||
|
|
||||||
static int pager_page_height(client_t *client) {
|
|
||||||
int page = client->height - 2;
|
|
||||||
if (page < 1) page = 1;
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void pager_scroll_by(int *scroll_pos, int delta) {
|
|
||||||
*scroll_pos += delta;
|
|
||||||
if (*scroll_pos < 0) {
|
|
||||||
*scroll_pos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static pager_action_t pager_apply_key(client_t *client, unsigned char key,
|
|
||||||
int *scroll_pos, bool allow_refresh) {
|
|
||||||
int page = pager_page_height(client);
|
|
||||||
int half = page / 2;
|
|
||||||
if (half < 1) half = 1;
|
|
||||||
|
|
||||||
if (key == 'q') {
|
|
||||||
return PAGER_ACTION_CLOSE;
|
|
||||||
} else if (key == 'j') {
|
|
||||||
pager_scroll_by(scroll_pos, 1);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 'k') {
|
|
||||||
pager_scroll_by(scroll_pos, -1);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 4) { /* Ctrl+D: half page down */
|
|
||||||
pager_scroll_by(scroll_pos, half);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 21) { /* Ctrl+U: half page up */
|
|
||||||
pager_scroll_by(scroll_pos, -half);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 6 || key == ' ') { /* Ctrl+F / Space: page down */
|
|
||||||
pager_scroll_by(scroll_pos, page);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 2 || key == 'b') { /* Ctrl+B / b: page up */
|
|
||||||
pager_scroll_by(scroll_pos, -page);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 'g') {
|
|
||||||
*scroll_pos = 0;
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (key == 'G') {
|
|
||||||
*scroll_pos = 999;
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if ((key == 'r' || key == 'R') && allow_refresh) {
|
|
||||||
return PAGER_ACTION_REFRESH;
|
|
||||||
} else if (key == 27) {
|
|
||||||
char seq[3];
|
|
||||||
int n = ssh_channel_read_timeout(client->channel, seq, 1, 0, 50);
|
|
||||||
if (n != 1) {
|
|
||||||
return PAGER_ACTION_CLOSE;
|
|
||||||
}
|
|
||||||
if (seq[0] != '[') {
|
|
||||||
return PAGER_ACTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
n = ssh_channel_read_timeout(client->channel, &seq[1], 1, 0, 50);
|
|
||||||
if (n != 1) {
|
|
||||||
return PAGER_ACTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seq[1] == 'A') { /* Up arrow */
|
|
||||||
pager_scroll_by(scroll_pos, -1);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] == 'B') { /* Down arrow */
|
|
||||||
pager_scroll_by(scroll_pos, 1);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] == 'H') { /* Home */
|
|
||||||
*scroll_pos = 0;
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] == 'F') { /* End */
|
|
||||||
*scroll_pos = 999;
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] >= '1' && seq[1] <= '6') {
|
|
||||||
n = ssh_channel_read_timeout(client->channel, &seq[2], 1, 0, 50);
|
|
||||||
if (n == 1 && seq[2] == '~') {
|
|
||||||
if (seq[1] == '5') { /* PageUp */
|
|
||||||
pager_scroll_by(scroll_pos, -page);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] == '6') { /* PageDown */
|
|
||||||
pager_scroll_by(scroll_pos, page);
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] == '1') { /* Home */
|
|
||||||
*scroll_pos = 0;
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
} else if (seq[1] == '4') { /* End */
|
|
||||||
*scroll_pos = 999;
|
|
||||||
return PAGER_ACTION_SCROLL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PAGER_ACTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle a single key press. Returns true if the key was fully consumed
|
/* Handle a single key press. Returns true if the key was fully consumed
|
||||||
* (no further character buffering needed). */
|
* (no further character buffering needed). */
|
||||||
static bool handle_key(client_t *client, unsigned char key, char *input) {
|
static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
/* Handle Ctrl+C (Exit or switch to NORMAL) */
|
/* Handle Ctrl+C (Exit or switch to NORMAL) */
|
||||||
if (key == 3) {
|
if (key == 3) {
|
||||||
client_mode_t previous_mode = client->mode;
|
client_mode_t previous_mode = client->mode;
|
||||||
if (client->show_help) {
|
|
||||||
client->show_help = false;
|
|
||||||
tui_render_screen(client);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (client->command_output[0] != '\0') {
|
if (client->command_output[0] != '\0') {
|
||||||
dismiss_command_output(client);
|
dismiss_command_output(client);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -383,20 +256,44 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
|
|
||||||
/* Handle help screen */
|
/* Handle help screen */
|
||||||
if (client->show_help) {
|
if (client->show_help) {
|
||||||
pager_action_t action;
|
/* Page size: roughly the visible help body region. */
|
||||||
|
int page = client->height - 2;
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
int half = page / 2;
|
||||||
|
if (half < 1) half = 1;
|
||||||
|
|
||||||
if (key == 'l' || key == 'L') {
|
if (key == 'q' || key == 27) {
|
||||||
|
client->show_help = false;
|
||||||
|
tui_render_screen(client);
|
||||||
|
} else if (key == 'l' || key == 'L') {
|
||||||
client->ui_lang = i18n_next_ui_lang(client->ui_lang);
|
client->ui_lang = i18n_next_ui_lang(client->ui_lang);
|
||||||
client->help_scroll_pos = 0;
|
client->help_scroll_pos = 0;
|
||||||
tui_render_help(client);
|
tui_render_help(client);
|
||||||
return true;
|
} else if (key == 'j') {
|
||||||
}
|
client->help_scroll_pos++;
|
||||||
|
tui_render_help(client);
|
||||||
action = pager_apply_key(client, key, &client->help_scroll_pos, false);
|
} else if (key == 'k' && client->help_scroll_pos > 0) {
|
||||||
if (action == PAGER_ACTION_CLOSE) {
|
client->help_scroll_pos--;
|
||||||
client->show_help = false;
|
tui_render_help(client);
|
||||||
tui_render_screen(client);
|
} else if (key == 4) { /* Ctrl+D: half page down */
|
||||||
} else if (action == PAGER_ACTION_SCROLL) {
|
client->help_scroll_pos += half;
|
||||||
|
tui_render_help(client);
|
||||||
|
} else if (key == 21) { /* Ctrl+U: half page up */
|
||||||
|
client->help_scroll_pos -= half;
|
||||||
|
if (client->help_scroll_pos < 0) client->help_scroll_pos = 0;
|
||||||
|
tui_render_help(client);
|
||||||
|
} else if (key == 6) { /* Ctrl+F: full page down */
|
||||||
|
client->help_scroll_pos += page;
|
||||||
|
tui_render_help(client);
|
||||||
|
} else if (key == 2) { /* Ctrl+B: full page up */
|
||||||
|
client->help_scroll_pos -= page;
|
||||||
|
if (client->help_scroll_pos < 0) client->help_scroll_pos = 0;
|
||||||
|
tui_render_help(client);
|
||||||
|
} else if (key == 'g') {
|
||||||
|
client->help_scroll_pos = 0;
|
||||||
|
tui_render_help(client);
|
||||||
|
} else if (key == 'G') {
|
||||||
|
client->help_scroll_pos = 999; /* Large number */
|
||||||
tui_render_help(client);
|
tui_render_help(client);
|
||||||
}
|
}
|
||||||
return true; /* Key consumed */
|
return true; /* Key consumed */
|
||||||
|
|
@ -405,23 +302,53 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
/* Handle command output / MOTD display. MOTD remains a simple notice;
|
/* Handle command output / MOTD display. MOTD remains a simple notice;
|
||||||
* command output behaves like a small pager so long results can be read. */
|
* command output behaves like a small pager so long results can be read. */
|
||||||
if (client->command_output[0] != '\0') {
|
if (client->command_output[0] != '\0') {
|
||||||
pager_action_t action;
|
int page = client->height - 2;
|
||||||
|
int half;
|
||||||
|
|
||||||
if (client->show_motd) {
|
if (client->show_motd) {
|
||||||
dismiss_command_output(client);
|
dismiss_command_output(client);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
action = pager_apply_key(client, key, &client->command_output_scroll,
|
if (page < 1) page = 1;
|
||||||
true);
|
half = page / 2;
|
||||||
if (action == PAGER_ACTION_CLOSE) {
|
if (half < 1) half = 1;
|
||||||
|
|
||||||
|
if (key == 'q' || key == 27) {
|
||||||
dismiss_command_output(client);
|
dismiss_command_output(client);
|
||||||
} else if (action == PAGER_ACTION_SCROLL) {
|
} else if (key == 'j') {
|
||||||
tui_render_command_output(client);
|
client->command_output_scroll++;
|
||||||
} else if (action == PAGER_ACTION_REFRESH) {
|
|
||||||
if (commands_refresh_active_output(client)) {
|
|
||||||
tui_render_command_output(client);
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 'k') {
|
||||||
|
client->command_output_scroll--;
|
||||||
|
if (client->command_output_scroll < 0) {
|
||||||
|
client->command_output_scroll = 0;
|
||||||
}
|
}
|
||||||
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 4) { /* Ctrl+D: half page down */
|
||||||
|
client->command_output_scroll += half;
|
||||||
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 21) { /* Ctrl+U: half page up */
|
||||||
|
client->command_output_scroll -= half;
|
||||||
|
if (client->command_output_scroll < 0) {
|
||||||
|
client->command_output_scroll = 0;
|
||||||
|
}
|
||||||
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 6) { /* Ctrl+F: full page down */
|
||||||
|
client->command_output_scroll += page;
|
||||||
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 2) { /* Ctrl+B: full page up */
|
||||||
|
client->command_output_scroll -= page;
|
||||||
|
if (client->command_output_scroll < 0) {
|
||||||
|
client->command_output_scroll = 0;
|
||||||
|
}
|
||||||
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 'g') {
|
||||||
|
client->command_output_scroll = 0;
|
||||||
|
tui_render_command_output(client);
|
||||||
|
} else if (key == 'G') {
|
||||||
|
client->command_output_scroll = 999;
|
||||||
|
tui_render_command_output(client);
|
||||||
}
|
}
|
||||||
return true; /* Key consumed */
|
return true; /* Key consumed */
|
||||||
}
|
}
|
||||||
|
|
@ -640,12 +567,6 @@ static bool handle_key(client_t *client, unsigned char key, char *input) {
|
||||||
client->command_input[0] = '\0';
|
client->command_input[0] = '\0';
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
return true;
|
return true;
|
||||||
} else if (key == '/') {
|
|
||||||
client->mode = MODE_COMMAND;
|
|
||||||
snprintf(client->command_input, sizeof(client->command_input),
|
|
||||||
"search ");
|
|
||||||
tui_render_screen(client);
|
|
||||||
return true;
|
|
||||||
} else if (key == 'j') {
|
} else if (key == 'j') {
|
||||||
normal_scroll_by(client, 1);
|
normal_scroll_by(client, 1);
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
|
|
@ -814,7 +735,6 @@ void input_run_session(client_t *client) {
|
||||||
client->command_history_count = 0;
|
client->command_history_count = 0;
|
||||||
client->command_history_pos = 0;
|
client->command_history_pos = 0;
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
client->command_output_kind = TNT_COMMAND_OUTPUT_NONE;
|
|
||||||
client->connect_time = time(NULL);
|
client->connect_time = time(NULL);
|
||||||
client->last_active = time(NULL);
|
client->last_active = time(NULL);
|
||||||
|
|
||||||
|
|
@ -868,7 +788,6 @@ void input_run_session(client_t *client) {
|
||||||
sizeof(client->command_output),
|
sizeof(client->command_output),
|
||||||
"%s", motd_buf);
|
"%s", motd_buf);
|
||||||
client->command_output_scroll = 0;
|
client->command_output_scroll = 0;
|
||||||
client->command_output_kind = TNT_COMMAND_OUTPUT_NONE;
|
|
||||||
client->show_motd = true;
|
client->show_motd = true;
|
||||||
tui_render_motd(client);
|
tui_render_motd(client);
|
||||||
seen_update_seq = room_get_update_seq(g_room);
|
seen_update_seq = room_get_update_seq(g_room);
|
||||||
|
|
@ -917,13 +836,6 @@ main_loop:
|
||||||
room_updated = true;
|
room_updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client->command_output_kind == TNT_COMMAND_OUTPUT_INBOX &&
|
|
||||||
client->command_output[0] != '\0' &&
|
|
||||||
client->unread_whispers > 0) {
|
|
||||||
commands_refresh_active_output(client);
|
|
||||||
client->redraw_pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client->redraw_pending ||
|
if (client->redraw_pending ||
|
||||||
(room_updated && !client->show_help &&
|
(room_updated && !client->show_help &&
|
||||||
client->command_output[0] == '\0')) {
|
client->command_output[0] == '\0')) {
|
||||||
|
|
@ -1028,8 +940,6 @@ main_loop:
|
||||||
client->command_input[len] = b;
|
client->command_input[len] = b;
|
||||||
client->command_input[len + 1] = '\0';
|
client->command_input[len + 1] = '\0';
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
} else {
|
|
||||||
client_send(client, "\a", 1);
|
|
||||||
}
|
}
|
||||||
} else if (b >= 128) { /* UTF-8 multi-byte */
|
} else if (b >= 128) { /* UTF-8 multi-byte */
|
||||||
int char_len = utf8_byte_length(b);
|
int char_len = utf8_byte_length(b);
|
||||||
|
|
@ -1042,12 +952,10 @@ main_loop:
|
||||||
}
|
}
|
||||||
if (!utf8_is_valid_sequence(buf, char_len)) continue;
|
if (!utf8_is_valid_sequence(buf, char_len)) continue;
|
||||||
size_t len = strlen(client->command_input);
|
size_t len = strlen(client->command_input);
|
||||||
if (len + (size_t)char_len <= sizeof(client->command_input) - 1) {
|
if (len + (size_t)char_len < sizeof(client->command_input) - 1) {
|
||||||
memcpy(client->command_input + len, buf, char_len);
|
memcpy(client->command_input + len, buf, char_len);
|
||||||
client->command_input[len + char_len] = '\0';
|
client->command_input[len + char_len] = '\0';
|
||||||
tui_render_screen(client);
|
tui_render_screen(client);
|
||||||
} else {
|
|
||||||
client_send(client, "\a", 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1074,7 +982,17 @@ cleanup:
|
||||||
|
|
||||||
ratelimit_release_ip(client->client_ip);
|
ratelimit_release_ip(client->client_ip);
|
||||||
|
|
||||||
client_release_session(client);
|
/* Remove channel callbacks before releasing refs to prevent use-after-free
|
||||||
|
* if a callback fires between the two releases. */
|
||||||
|
if (client->channel && client->channel_cb) {
|
||||||
|
ssh_remove_channel_callbacks(client->channel, client->channel_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Release the callback reference (paired with addref before client_install_channel_callbacks) */
|
||||||
|
client_release(client);
|
||||||
|
|
||||||
|
/* Release the main reference - client will be freed when all refs are gone */
|
||||||
|
client_release(client);
|
||||||
|
|
||||||
/* Decrement connection count */
|
/* Decrement connection count */
|
||||||
ratelimit_decrement_total();
|
ratelimit_decrement_total();
|
||||||
|
|
|
||||||
166
src/main.c
166
src/main.c
|
|
@ -1,10 +1,8 @@
|
||||||
#include "chat_room.h"
|
#include "chat_room.h"
|
||||||
#include "cli_text.h"
|
#include "cli_text.h"
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
#include "message.h"
|
#include "message.h"
|
||||||
#include "message_log_tool.h"
|
|
||||||
#include "ssh_server.h"
|
#include "ssh_server.h"
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
@ -20,6 +18,24 @@ static void signal_handler(int sig) {
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool parse_int_arg(const char *value, int min_val, int max_val,
|
||||||
|
int *out) {
|
||||||
|
char *end = NULL;
|
||||||
|
long val;
|
||||||
|
|
||||||
|
if (!value || value[0] == '\0' || !out) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val = strtol(value, &end, 10);
|
||||||
|
if (!end || *end != '\0' || val < min_val || val > max_val) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = (int)val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool is_config_token(const char *value) {
|
static bool is_config_token(const char *value) {
|
||||||
const unsigned char *p = (const unsigned char *)value;
|
const unsigned char *p = (const unsigned char *)value;
|
||||||
|
|
||||||
|
|
@ -43,64 +59,59 @@ static int set_env_option(const char *name, const char *value) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int set_numeric_env_option(const tnt_int_config_spec_t *spec,
|
static int set_numeric_env_option(const char *env_name, const char *opt_name,
|
||||||
const char *opt_name, const char *value,
|
const char *value, int min_val,
|
||||||
ui_lang_t lang) {
|
int max_val, ui_lang_t lang) {
|
||||||
int parsed;
|
int parsed;
|
||||||
|
|
||||||
if (!tnt_config_parse_int(value, spec, &parsed)) {
|
if (!parse_int_arg(value, min_val, max_val, &parsed)) {
|
||||||
fprintf(stderr, cli_text_invalid_value_format(lang), opt_name, value);
|
fprintf(stderr, cli_text_invalid_value_format(lang), opt_name, value);
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
if (set_env_option(spec->env_name, value) != 0) {
|
if (set_env_option(env_name, value) != 0) {
|
||||||
return TNT_EXIT_ERROR;
|
return TNT_EXIT_ERROR;
|
||||||
}
|
}
|
||||||
return TNT_EXIT_OK;
|
return TNT_EXIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool require_option_arg(int argc, char **argv, int index,
|
|
||||||
ui_lang_t lang) {
|
|
||||||
if (index + 1 >= argc || argv[index + 1][0] == '\0') {
|
|
||||||
fprintf(stderr, cli_text_option_requires_arg_format(lang),
|
|
||||||
argv[index]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
int port = tnt_config_env_int(&TNT_CONFIG_PORT);
|
int port = DEFAULT_PORT;
|
||||||
ui_lang_t lang = i18n_default_ui_lang();
|
ui_lang_t lang = i18n_default_ui_lang();
|
||||||
const char *log_check_path = NULL;
|
|
||||||
const char *log_recover_path = NULL;
|
/* Environment provides defaults; command-line flags override it. */
|
||||||
|
const char *port_env = getenv("PORT");
|
||||||
|
if (port_env && port_env[0] != '\0') {
|
||||||
|
char *end;
|
||||||
|
long val = strtol(port_env, &end, 10);
|
||||||
|
if (*end == '\0' && val > 0 && val <= 65535) {
|
||||||
|
port = (int)val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Parse command line arguments */
|
/* Parse command line arguments */
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
|
if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) &&
|
||||||
|
i + 1 < argc) {
|
||||||
int val;
|
int val;
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
if (!parse_int_arg(argv[i + 1], 1, 65535, &val)) {
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
if (!tnt_config_parse_int(argv[i + 1], &TNT_CONFIG_PORT, &val)) {
|
|
||||||
fprintf(stderr, cli_text_invalid_port_format(lang),
|
fprintf(stderr, cli_text_invalid_port_format(lang),
|
||||||
argv[i + 1]);
|
argv[i + 1]);
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
port = val;
|
port = val;
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "-d") == 0 ||
|
} else if ((strcmp(argv[i], "-d") == 0 ||
|
||||||
strcmp(argv[i], "--state-dir") == 0) {
|
strcmp(argv[i], "--state-dir") == 0) && i + 1 < argc) {
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
if (argv[i + 1][0] == '\0') {
|
||||||
|
fprintf(stderr, cli_text_invalid_value_format(lang),
|
||||||
|
argv[i], argv[i + 1]);
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
if (set_env_option("TNT_STATE_DIR", argv[i + 1]) != 0) {
|
if (set_env_option("TNT_STATE_DIR", argv[i + 1]) != 0) {
|
||||||
return TNT_EXIT_ERROR;
|
return TNT_EXIT_ERROR;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--bind") == 0) {
|
} else if (strcmp(argv[i], "--bind") == 0 && i + 1 < argc) {
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
if (!is_config_token(argv[i + 1])) {
|
if (!is_config_token(argv[i + 1])) {
|
||||||
fprintf(stderr, cli_text_invalid_value_format(lang),
|
fprintf(stderr, cli_text_invalid_value_format(lang),
|
||||||
argv[i], argv[i + 1]);
|
argv[i], argv[i + 1]);
|
||||||
|
|
@ -110,10 +121,7 @@ int main(int argc, char **argv) {
|
||||||
return TNT_EXIT_ERROR;
|
return TNT_EXIT_ERROR;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--public-host") == 0) {
|
} else if (strcmp(argv[i], "--public-host") == 0 && i + 1 < argc) {
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
if (!is_config_token(argv[i + 1])) {
|
if (!is_config_token(argv[i + 1])) {
|
||||||
fprintf(stderr, cli_text_invalid_value_format(lang),
|
fprintf(stderr, cli_text_invalid_value_format(lang),
|
||||||
argv[i], argv[i + 1]);
|
argv[i], argv[i + 1]);
|
||||||
|
|
@ -123,80 +131,54 @@ int main(int argc, char **argv) {
|
||||||
return TNT_EXIT_ERROR;
|
return TNT_EXIT_ERROR;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--max-connections") == 0) {
|
} else if (strcmp(argv[i], "--max-connections") == 0 &&
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
i + 1 < argc) {
|
||||||
return TNT_EXIT_USAGE;
|
int rc = set_numeric_env_option("TNT_MAX_CONNECTIONS", argv[i],
|
||||||
}
|
argv[i + 1], 1,
|
||||||
int rc = set_numeric_env_option(&TNT_CONFIG_MAX_CONNECTIONS,
|
MAX_CONFIGURED_CLIENTS, lang);
|
||||||
argv[i], argv[i + 1], lang);
|
|
||||||
if (rc != TNT_EXIT_OK) {
|
if (rc != TNT_EXIT_OK) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--max-conn-per-ip") == 0) {
|
} else if (strcmp(argv[i], "--max-conn-per-ip") == 0 &&
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
i + 1 < argc) {
|
||||||
return TNT_EXIT_USAGE;
|
int rc = set_numeric_env_option("TNT_MAX_CONN_PER_IP", argv[i],
|
||||||
}
|
argv[i + 1], 1,
|
||||||
int rc = set_numeric_env_option(&TNT_CONFIG_MAX_CONN_PER_IP,
|
MAX_CONFIGURED_CLIENTS, lang);
|
||||||
argv[i], argv[i + 1], lang);
|
|
||||||
if (rc != TNT_EXIT_OK) {
|
if (rc != TNT_EXIT_OK) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--max-conn-rate-per-ip") == 0) {
|
} else if (strcmp(argv[i], "--max-conn-rate-per-ip") == 0 &&
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
i + 1 < argc) {
|
||||||
return TNT_EXIT_USAGE;
|
int rc = set_numeric_env_option("TNT_MAX_CONN_RATE_PER_IP",
|
||||||
}
|
argv[i], argv[i + 1], 1,
|
||||||
int rc = set_numeric_env_option(&TNT_CONFIG_MAX_CONN_RATE_PER_IP,
|
MAX_CONFIGURED_CLIENTS, lang);
|
||||||
argv[i], argv[i + 1], lang);
|
|
||||||
if (rc != TNT_EXIT_OK) {
|
if (rc != TNT_EXIT_OK) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--rate-limit") == 0) {
|
} else if (strcmp(argv[i], "--rate-limit") == 0 && i + 1 < argc) {
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
int rc = set_numeric_env_option("TNT_RATE_LIMIT", argv[i],
|
||||||
return TNT_EXIT_USAGE;
|
argv[i + 1], 0, 1, lang);
|
||||||
}
|
|
||||||
int rc = set_numeric_env_option(&TNT_CONFIG_RATE_LIMIT, argv[i],
|
|
||||||
argv[i + 1], lang);
|
|
||||||
if (rc != TNT_EXIT_OK) {
|
if (rc != TNT_EXIT_OK) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--idle-timeout") == 0) {
|
} else if (strcmp(argv[i], "--idle-timeout") == 0 && i + 1 < argc) {
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
int rc = set_numeric_env_option("TNT_IDLE_TIMEOUT", argv[i],
|
||||||
return TNT_EXIT_USAGE;
|
argv[i + 1], 0, 86400, lang);
|
||||||
}
|
|
||||||
int rc = set_numeric_env_option(&TNT_CONFIG_IDLE_TIMEOUT, argv[i],
|
|
||||||
argv[i + 1], lang);
|
|
||||||
if (rc != TNT_EXIT_OK) {
|
if (rc != TNT_EXIT_OK) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--ssh-log-level") == 0) {
|
} else if (strcmp(argv[i], "--ssh-log-level") == 0 && i + 1 < argc) {
|
||||||
if (!require_option_arg(argc, argv, i, lang)) {
|
int rc = set_numeric_env_option("TNT_SSH_LOG_LEVEL", argv[i],
|
||||||
return TNT_EXIT_USAGE;
|
argv[i + 1], 0, 4, lang);
|
||||||
}
|
|
||||||
int rc = set_numeric_env_option(&TNT_CONFIG_SSH_LOG_LEVEL,
|
|
||||||
argv[i], argv[i + 1], lang);
|
|
||||||
if (rc != TNT_EXIT_OK) {
|
if (rc != TNT_EXIT_OK) {
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (strcmp(argv[i], "--log-check") == 0) {
|
|
||||||
if (i + 1 >= argc || argv[i + 1][0] == '\0') {
|
|
||||||
fprintf(stderr, cli_text_option_requires_arg_format(lang),
|
|
||||||
argv[i]);
|
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
log_check_path = argv[++i];
|
|
||||||
} else if (strcmp(argv[i], "--log-recover") == 0) {
|
|
||||||
if (i + 1 >= argc || argv[i + 1][0] == '\0') {
|
|
||||||
fprintf(stderr, cli_text_option_requires_arg_format(lang),
|
|
||||||
argv[i]);
|
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
log_recover_path = argv[++i];
|
|
||||||
} else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
|
} else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
|
||||||
printf("tnt %s\n", TNT_VERSION);
|
printf("tnt %s\n", TNT_VERSION);
|
||||||
return TNT_EXIT_OK;
|
return TNT_EXIT_OK;
|
||||||
|
|
@ -214,18 +196,6 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log_check_path && log_recover_path) {
|
|
||||||
fprintf(stderr, cli_text_invalid_value_format(lang),
|
|
||||||
"--log-check", "--log-recover");
|
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
if (log_check_path) {
|
|
||||||
return message_log_tool_check(log_check_path);
|
|
||||||
}
|
|
||||||
if (log_recover_path) {
|
|
||||||
return message_log_tool_recover(log_recover_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Setup signal handlers */
|
/* Setup signal handlers */
|
||||||
signal(SIGINT, signal_handler);
|
signal(SIGINT, signal_handler);
|
||||||
signal(SIGTERM, signal_handler);
|
signal(SIGTERM, signal_handler);
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ void manual_text_append_interactive(char *buffer, size_t buf_size,
|
||||||
" TNT - SSH terminal chat room\n"
|
" TNT - SSH terminal chat room\n"
|
||||||
"\n"
|
"\n"
|
||||||
"\033[1;37mUse\033[0m\n"
|
"\033[1;37mUse\033[0m\n"
|
||||||
" Type, Enter sends; Up/Down recalls; Tab completes @mentions\n"
|
" Type a message and press Enter; Esc browses; G latest; i types\n"
|
||||||
" Esc browses; / searches; G latest; i types; : commands; ? keys\n"
|
" : runs commands; ? opens the full key reference\n"
|
||||||
"\n"
|
"\n"
|
||||||
"\033[1;37mCommands\033[0m\n",
|
"\033[1;37mCommands\033[0m\n",
|
||||||
"\033[1;36mTNT(1) 帮助\033[0m\n"
|
"\033[1;36mTNT(1) 帮助\033[0m\n"
|
||||||
|
|
@ -22,8 +22,8 @@ void manual_text_append_interactive(char *buffer, size_t buf_size,
|
||||||
" TNT - SSH 终端聊天室\n"
|
" TNT - SSH 终端聊天室\n"
|
||||||
"\n"
|
"\n"
|
||||||
"\033[1;37m使用\033[0m\n"
|
"\033[1;37m使用\033[0m\n"
|
||||||
" 输入并 Enter 发送;Up/Down 调出消息;Tab 补全 @mention\n"
|
" 输入消息并 Enter 发送;Esc 浏览历史;G 最新;i 输入\n"
|
||||||
" Esc 浏览;/ 搜索;G 最新;i 输入;: 命令;? 按键\n"
|
" : 运行命令;? 打开完整按键参考\n"
|
||||||
"\n"
|
"\n"
|
||||||
"\033[1;37m命令\033[0m\n"
|
"\033[1;37m命令\033[0m\n"
|
||||||
);
|
);
|
||||||
|
|
|
||||||
287
src/message.c
287
src/message.c
|
|
@ -1,63 +1,29 @@
|
||||||
#ifndef _DEFAULT_SOURCE
|
#ifndef _DEFAULT_SOURCE
|
||||||
#define _DEFAULT_SOURCE /* for strcasestr() on glibc */
|
#define _DEFAULT_SOURCE /* for timegm() on glibc */
|
||||||
#endif
|
#endif
|
||||||
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
||||||
#define _DARWIN_C_SOURCE /* for strcasestr() on macOS */
|
#define _DARWIN_C_SOURCE /* for timegm() on macOS */
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "message.h"
|
#include "message.h"
|
||||||
#include "message_log.h"
|
|
||||||
#include "utf8.h"
|
#include "utf8.h"
|
||||||
#include <errno.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
||||||
static pthread_mutex_t g_message_file_lock = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_message_file_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
static void discard_line_remainder(FILE *fp) {
|
static time_t parse_rfc3339_utc(const char *timestamp_str) {
|
||||||
int c;
|
struct tm tm = {0};
|
||||||
|
|
||||||
while ((c = fgetc(fp)) != '\n' && c != EOF) {
|
if (!timestamp_str) {
|
||||||
}
|
return (time_t)-1;
|
||||||
}
|
|
||||||
|
|
||||||
static int append_dump_record(char **output, size_t *capacity,
|
|
||||||
size_t *len, const message_t *msg) {
|
|
||||||
size_t needed;
|
|
||||||
size_t available;
|
|
||||||
|
|
||||||
if (!output || !capacity || !len || !msg) {
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message_log_format_record(msg, NULL, 0, &needed) < 0) {
|
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||||||
return -1;
|
if (!result || *result != '\0') {
|
||||||
|
return (time_t)-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
available = *capacity > *len ? *capacity - *len : 0;
|
return timegm(&tm);
|
||||||
if (needed + 1 > available) {
|
|
||||||
size_t new_capacity = *capacity ? *capacity : 1024;
|
|
||||||
while (needed + 1 > new_capacity - *len) {
|
|
||||||
if (new_capacity > SIZE_MAX / 2) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
new_capacity *= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *grown = realloc(*output, new_capacity);
|
|
||||||
if (!grown) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*output = grown;
|
|
||||||
*capacity = new_capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message_log_format_record(msg, *output + *len, *capacity - *len,
|
|
||||||
NULL) < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*len += needed;
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize message subsystem */
|
/* Initialize message subsystem */
|
||||||
|
|
@ -152,25 +118,67 @@ int message_load(message_t **messages, int max_messages) {
|
||||||
fseek(fp, 0, SEEK_SET);
|
fseek(fp, 0, SEEK_SET);
|
||||||
|
|
||||||
read_messages:;
|
read_messages:;
|
||||||
char line[MESSAGE_LOG_MAX_LINE];
|
char line[2048];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
time_t now = time(NULL);
|
|
||||||
|
|
||||||
/* Now read forward */
|
/* Now read forward */
|
||||||
while (fgets(line, sizeof(line), fp) && count < max_messages) {
|
while (fgets(line, sizeof(line), fp) && count < max_messages) {
|
||||||
/* Check for oversized lines */
|
/* Check for oversized lines */
|
||||||
size_t line_len = strlen(line);
|
size_t line_len = strlen(line);
|
||||||
if (line_len >= sizeof(line) - 1 && line[line_len - 1] != '\n') {
|
if (line_len >= sizeof(line) - 1) {
|
||||||
discard_line_remainder(fp);
|
/* Skip remainder of line */
|
||||||
|
int c;
|
||||||
|
while ((c = fgetc(fp)) != '\n' && c != EOF);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
message_t parsed;
|
/* Format: RFC3339_timestamp|username|content */
|
||||||
if (!message_log_parse_record(line, &parsed, now)) {
|
char line_copy[2048];
|
||||||
|
strncpy(line_copy, line, sizeof(line_copy) - 1);
|
||||||
|
line_copy[sizeof(line_copy) - 1] = '\0';
|
||||||
|
|
||||||
|
char *timestamp_str = strtok(line_copy, "|");
|
||||||
|
char *username = strtok(NULL, "|");
|
||||||
|
char *content = strtok(NULL, "\n");
|
||||||
|
|
||||||
|
/* Validate all fields exist and are non-empty */
|
||||||
|
if (!timestamp_str || !username || !content) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (username[0] == '\0') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg_array[count++] = parsed;
|
/* Validate field lengths */
|
||||||
|
if (strlen(username) >= MAX_USERNAME_LEN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strlen(content) >= MAX_MESSAGE_LEN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse strict UTC RFC3339 timestamp */
|
||||||
|
time_t msg_time = parse_rfc3339_utc(timestamp_str);
|
||||||
|
if (msg_time == (time_t)-1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validate timestamp is reasonable (not in far future or past) */
|
||||||
|
time_t now = time(NULL);
|
||||||
|
if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_array[count].timestamp = msg_time;
|
||||||
|
strncpy(msg_array[count].username, username, MAX_USERNAME_LEN - 1);
|
||||||
|
msg_array[count].username[MAX_USERNAME_LEN - 1] = '\0';
|
||||||
|
strncpy(msg_array[count].content, content, MAX_MESSAGE_LEN - 1);
|
||||||
|
msg_array[count].content[MAX_MESSAGE_LEN - 1] = '\0';
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
|
@ -182,9 +190,6 @@ read_messages:;
|
||||||
/* Save a message to log file */
|
/* Save a message to log file */
|
||||||
int message_save(const message_t *msg) {
|
int message_save(const message_t *msg) {
|
||||||
char log_path[PATH_MAX];
|
char log_path[PATH_MAX];
|
||||||
message_t safe_msg;
|
|
||||||
char record[MAX_USERNAME_LEN + MAX_MESSAGE_LEN + 48];
|
|
||||||
size_t record_len = 0;
|
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
|
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
|
||||||
|
|
@ -199,29 +204,36 @@ int message_save(const message_t *msg) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sanitize username and content to prevent log injection */
|
/* Format timestamp as RFC3339 */
|
||||||
safe_msg.timestamp = msg->timestamp;
|
char timestamp[64];
|
||||||
strncpy(safe_msg.username, msg->username, sizeof(safe_msg.username) - 1);
|
struct tm tm_info;
|
||||||
safe_msg.username[sizeof(safe_msg.username) - 1] = '\0';
|
gmtime_r(&msg->timestamp, &tm_info);
|
||||||
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
||||||
|
|
||||||
strncpy(safe_msg.content, msg->content, sizeof(safe_msg.content) - 1);
|
/* Sanitize username and content to prevent log injection */
|
||||||
safe_msg.content[sizeof(safe_msg.content) - 1] = '\0';
|
char safe_username[MAX_USERNAME_LEN];
|
||||||
|
char safe_content[MAX_MESSAGE_LEN];
|
||||||
|
|
||||||
|
strncpy(safe_username, msg->username, sizeof(safe_username) - 1);
|
||||||
|
safe_username[sizeof(safe_username) - 1] = '\0';
|
||||||
|
|
||||||
|
strncpy(safe_content, msg->content, sizeof(safe_content) - 1);
|
||||||
|
safe_content[sizeof(safe_content) - 1] = '\0';
|
||||||
|
|
||||||
/* Replace pipe characters and newlines to prevent log format corruption */
|
/* Replace pipe characters and newlines to prevent log format corruption */
|
||||||
for (char *p = safe_msg.username; *p; p++) {
|
for (char *p = safe_username; *p; p++) {
|
||||||
if (*p == '|' || *p == '\n' || *p == '\r') {
|
if (*p == '|' || *p == '\n' || *p == '\r') {
|
||||||
*p = '_';
|
*p = '_';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (char *p = safe_msg.content; *p; p++) {
|
for (char *p = safe_content; *p; p++) {
|
||||||
if (*p == '|' || *p == '\n' || *p == '\r') {
|
if (*p == '|' || *p == '\n' || *p == '\r') {
|
||||||
*p = ' ';
|
*p = ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message_log_format_record(&safe_msg, record, sizeof(record),
|
/* Write to file: timestamp|username|content */
|
||||||
&record_len) < 0 ||
|
if (fprintf(fp, "%s|%s|%s\n", timestamp, safe_username, safe_content) < 0 ||
|
||||||
fwrite(record, 1, record_len, fp) != record_len ||
|
|
||||||
fflush(fp) != 0) {
|
fflush(fp) != 0) {
|
||||||
rc = -1;
|
rc = -1;
|
||||||
}
|
}
|
||||||
|
|
@ -262,21 +274,40 @@ int message_search(const char *query, message_t **results, int max_results) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
char line[MESSAGE_LOG_MAX_LINE];
|
char line[2048];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
time_t now = time(NULL);
|
|
||||||
|
|
||||||
while (fgets(line, sizeof(line), fp)) {
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
size_t line_len = strlen(line);
|
size_t line_len = strlen(line);
|
||||||
if (line_len >= sizeof(line) - 1 && line[line_len - 1] != '\n') {
|
if (line_len >= sizeof(line) - 1) {
|
||||||
discard_line_remainder(fp);
|
int c;
|
||||||
|
while ((c = fgetc(fp)) != '\n' && c != EOF);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char line_copy[2048];
|
||||||
|
strncpy(line_copy, line, sizeof(line_copy) - 1);
|
||||||
|
line_copy[sizeof(line_copy) - 1] = '\0';
|
||||||
|
|
||||||
|
char *timestamp_str = strtok(line_copy, "|");
|
||||||
|
char *username = strtok(NULL, "|");
|
||||||
|
char *content = strtok(NULL, "\n");
|
||||||
|
|
||||||
|
if (!timestamp_str || !username || !content || username[0] == '\0') continue;
|
||||||
|
if (strlen(username) >= MAX_USERNAME_LEN || strlen(content) >= MAX_MESSAGE_LEN) continue;
|
||||||
|
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) continue;
|
||||||
|
|
||||||
|
if (strcasestr(username, query) == NULL && strcasestr(content, query) == NULL) continue;
|
||||||
|
|
||||||
|
time_t msg_time = parse_rfc3339_utc(timestamp_str);
|
||||||
|
if (msg_time == (time_t)-1) continue;
|
||||||
|
|
||||||
message_t m;
|
message_t m;
|
||||||
if (!message_log_parse_record(line, &m, now)) continue;
|
m.timestamp = msg_time;
|
||||||
if (strcasestr(m.username, query) == NULL &&
|
strncpy(m.username, username, MAX_USERNAME_LEN - 1);
|
||||||
strcasestr(m.content, query) == NULL) continue;
|
m.username[MAX_USERNAME_LEN - 1] = '\0';
|
||||||
|
strncpy(m.content, content, MAX_MESSAGE_LEN - 1);
|
||||||
|
m.content[MAX_MESSAGE_LEN - 1] = '\0';
|
||||||
|
|
||||||
if (count < max_results) {
|
if (count < max_results) {
|
||||||
res[count++] = m;
|
res[count++] = m;
|
||||||
|
|
@ -293,118 +324,6 @@ int message_search(const char *query, message_t **results, int max_results) {
|
||||||
return (count < max_results) ? count : max_results;
|
return (count < max_results) ? count : max_results;
|
||||||
}
|
}
|
||||||
|
|
||||||
int message_dump_text(char **output, size_t *output_len, int max_records) {
|
|
||||||
char log_path[PATH_MAX];
|
|
||||||
char *buf = NULL;
|
|
||||||
size_t capacity = 0;
|
|
||||||
size_t len = 0;
|
|
||||||
message_t *ring = NULL;
|
|
||||||
int seen = 0;
|
|
||||||
int rc = 0;
|
|
||||||
|
|
||||||
if (!output || !output_len || max_records < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*output = calloc(1, 1);
|
|
||||||
if (!*output) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*output_len = 0;
|
|
||||||
|
|
||||||
if (tnt_state_path(log_path, sizeof(log_path), LOG_FILE) < 0) {
|
|
||||||
free(*output);
|
|
||||||
*output = NULL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max_records > 0) {
|
|
||||||
ring = calloc((size_t)max_records, sizeof(*ring));
|
|
||||||
if (!ring) {
|
|
||||||
free(*output);
|
|
||||||
*output = NULL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pthread_mutex_lock(&g_message_file_lock);
|
|
||||||
FILE *fp = fopen(log_path, "r");
|
|
||||||
if (!fp) {
|
|
||||||
int saved_errno = errno;
|
|
||||||
pthread_mutex_unlock(&g_message_file_lock);
|
|
||||||
free(ring);
|
|
||||||
if (saved_errno != ENOENT) {
|
|
||||||
free(*output);
|
|
||||||
*output = NULL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
char line[MESSAGE_LOG_MAX_LINE];
|
|
||||||
time_t now = time(NULL);
|
|
||||||
while (fgets(line, sizeof(line), fp)) {
|
|
||||||
size_t line_len = strlen(line);
|
|
||||||
if (line_len >= sizeof(line) - 1 && line[line_len - 1] != '\n') {
|
|
||||||
discard_line_remainder(fp);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
message_t parsed;
|
|
||||||
if (!message_log_parse_record(line, &parsed, now)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max_records > 0) {
|
|
||||||
ring[seen % max_records] = parsed;
|
|
||||||
seen++;
|
|
||||||
} else if (append_dump_record(output, &capacity, output_len,
|
|
||||||
&parsed) < 0) {
|
|
||||||
rc = -1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
pthread_mutex_unlock(&g_message_file_lock);
|
|
||||||
|
|
||||||
if (rc == 0 && max_records > 0 && seen > 0) {
|
|
||||||
int count = seen < max_records ? seen : max_records;
|
|
||||||
int start = seen < max_records ? 0 : seen % max_records;
|
|
||||||
|
|
||||||
free(*output);
|
|
||||||
*output = NULL;
|
|
||||||
*output_len = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
message_t *msg = &ring[(start + i) % max_records];
|
|
||||||
if (append_dump_record(&buf, &capacity, &len, msg) < 0) {
|
|
||||||
rc = -1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rc == 0) {
|
|
||||||
*output = buf ? buf : calloc(1, 1);
|
|
||||||
*output_len = len;
|
|
||||||
if (!*output) {
|
|
||||||
rc = -1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
free(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(ring);
|
|
||||||
if (rc < 0) {
|
|
||||||
free(*output);
|
|
||||||
*output = NULL;
|
|
||||||
*output_len = 0;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Format a message for display */
|
/* Format a message for display */
|
||||||
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
||||||
struct tm tm_info;
|
struct tm tm_info;
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
#ifndef _DEFAULT_SOURCE
|
|
||||||
#define _DEFAULT_SOURCE /* for timegm() on glibc */
|
|
||||||
#endif
|
|
||||||
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
|
||||||
#define _DARWIN_C_SOURCE /* for timegm() on macOS */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "message_log.h"
|
|
||||||
#include "utf8.h"
|
|
||||||
|
|
||||||
static time_t parse_rfc3339_utc(const char *timestamp_str) {
|
|
||||||
struct tm tm = {0};
|
|
||||||
|
|
||||||
if (!timestamp_str) {
|
|
||||||
return (time_t)-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%SZ", &tm);
|
|
||||||
if (!result || *result != '\0') {
|
|
||||||
return (time_t)-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return timegm(&tm);
|
|
||||||
}
|
|
||||||
|
|
||||||
void message_log_format_timestamp_utc(time_t ts, char *buffer,
|
|
||||||
size_t buf_size) {
|
|
||||||
struct tm tm_info;
|
|
||||||
|
|
||||||
if (!buffer || buf_size == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gmtime_r(&ts, &tm_info);
|
|
||||||
strftime(buffer, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool message_log_parse_record(const char *line, message_t *out, time_t now) {
|
|
||||||
char line_copy[MESSAGE_LOG_MAX_LINE];
|
|
||||||
char *first_sep;
|
|
||||||
char *second_sep;
|
|
||||||
char *timestamp_str;
|
|
||||||
char *username;
|
|
||||||
char *content;
|
|
||||||
time_t msg_time;
|
|
||||||
size_t line_len;
|
|
||||||
|
|
||||||
if (!line || !out) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
line_len = strlen(line);
|
|
||||||
if (line_len == 0 || line[line_len - 1] != '\n') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (line_len >= sizeof(line_copy)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(line_copy, line, line_len + 1);
|
|
||||||
line_copy[line_len - 1] = '\0';
|
|
||||||
|
|
||||||
first_sep = strchr(line_copy, '|');
|
|
||||||
if (!first_sep) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
second_sep = strchr(first_sep + 1, '|');
|
|
||||||
if (!second_sep || strchr(second_sep + 1, '|')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*first_sep = '\0';
|
|
||||||
*second_sep = '\0';
|
|
||||||
timestamp_str = line_copy;
|
|
||||||
username = first_sep + 1;
|
|
||||||
content = second_sep + 1;
|
|
||||||
|
|
||||||
if (timestamp_str[0] == '\0' || username[0] == '\0' ||
|
|
||||||
content[0] == '\0') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (strlen(username) >= MAX_USERNAME_LEN ||
|
|
||||||
strlen(content) >= MAX_MESSAGE_LEN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!utf8_is_valid_string(username) || !utf8_is_valid_string(content)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
msg_time = parse_rfc3339_utc(timestamp_str);
|
|
||||||
if (msg_time == (time_t)-1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (msg_time > now + 86400 || msg_time < now - 31536000 * 10) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
out->timestamp = msg_time;
|
|
||||||
strncpy(out->username, username, MAX_USERNAME_LEN - 1);
|
|
||||||
out->username[MAX_USERNAME_LEN - 1] = '\0';
|
|
||||||
strncpy(out->content, content, MAX_MESSAGE_LEN - 1);
|
|
||||||
out->content[MAX_MESSAGE_LEN - 1] = '\0';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int message_log_format_record(const message_t *msg, char *buffer,
|
|
||||||
size_t buf_size, size_t *record_len) {
|
|
||||||
char timestamp[64];
|
|
||||||
int needed;
|
|
||||||
|
|
||||||
if (!msg) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message_log_format_timestamp_utc(msg->timestamp, timestamp,
|
|
||||||
sizeof(timestamp));
|
|
||||||
needed = snprintf(buffer, buf_size, "%s|%s|%s\n", timestamp,
|
|
||||||
msg->username, msg->content);
|
|
||||||
if (needed < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (record_len) {
|
|
||||||
*record_len = (size_t)needed;
|
|
||||||
}
|
|
||||||
if (!buffer || buf_size == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return (size_t)needed < buf_size ? 0 : -1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
#include "message_log_tool.h"
|
|
||||||
|
|
||||||
#include "message_log.h"
|
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
long records_seen;
|
|
||||||
long valid_records;
|
|
||||||
long invalid_records;
|
|
||||||
long first_invalid_line;
|
|
||||||
} message_log_report_t;
|
|
||||||
|
|
||||||
static void discard_line_remainder(FILE *fp) {
|
|
||||||
int c;
|
|
||||||
|
|
||||||
while ((c = fgetc(fp)) != '\n' && c != EOF) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int print_recovered_record(const message_t *msg) {
|
|
||||||
char record[MAX_USERNAME_LEN + MAX_MESSAGE_LEN + 48];
|
|
||||||
size_t record_len = 0;
|
|
||||||
|
|
||||||
if (message_log_format_record(msg, record, sizeof(record),
|
|
||||||
&record_len) < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return fwrite(record, 1, record_len, stdout) == record_len ? 0 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void print_report(FILE *stream, const char *path,
|
|
||||||
const message_log_report_t *report) {
|
|
||||||
fprintf(stream,
|
|
||||||
"path %s\n"
|
|
||||||
"records_seen %ld\n"
|
|
||||||
"valid_records %ld\n"
|
|
||||||
"invalid_records %ld\n"
|
|
||||||
"first_invalid_line %ld\n",
|
|
||||||
path,
|
|
||||||
report->records_seen,
|
|
||||||
report->valid_records,
|
|
||||||
report->invalid_records,
|
|
||||||
report->first_invalid_line);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int scan_log(const char *path, bool recover) {
|
|
||||||
FILE *fp;
|
|
||||||
char line[MESSAGE_LOG_MAX_LINE];
|
|
||||||
long line_no = 0;
|
|
||||||
time_t now = time(NULL);
|
|
||||||
message_log_report_t report = {0};
|
|
||||||
|
|
||||||
if (!path || path[0] == '\0') {
|
|
||||||
fprintf(stderr, "log: invalid path\n");
|
|
||||||
return TNT_EXIT_USAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
fp = fopen(path, "r");
|
|
||||||
if (!fp) {
|
|
||||||
fprintf(stderr, "log: %s: %s\n", path, strerror(errno));
|
|
||||||
return TNT_EXIT_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (fgets(line, sizeof(line), fp)) {
|
|
||||||
size_t line_len = strlen(line);
|
|
||||||
message_t parsed;
|
|
||||||
bool valid = false;
|
|
||||||
|
|
||||||
line_no++;
|
|
||||||
report.records_seen++;
|
|
||||||
|
|
||||||
if (line_len >= sizeof(line) - 1 && line[line_len - 1] != '\n') {
|
|
||||||
discard_line_remainder(fp);
|
|
||||||
} else {
|
|
||||||
valid = message_log_parse_record(line, &parsed, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
report.valid_records++;
|
|
||||||
if (recover && print_recovered_record(&parsed) < 0) {
|
|
||||||
fclose(fp);
|
|
||||||
fprintf(stderr, "log: failed to write recovered output\n");
|
|
||||||
return TNT_EXIT_ERROR;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
report.invalid_records++;
|
|
||||||
if (report.first_invalid_line == 0) {
|
|
||||||
report.first_invalid_line = line_no;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ferror(fp)) {
|
|
||||||
fclose(fp);
|
|
||||||
fprintf(stderr, "log: failed to read %s\n", path);
|
|
||||||
return TNT_EXIT_ERROR;
|
|
||||||
}
|
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
print_report(recover ? stderr : stdout, path, &report);
|
|
||||||
return report.invalid_records == 0 ? TNT_EXIT_OK : TNT_EXIT_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
int message_log_tool_check(const char *path) {
|
|
||||||
return scan_log(path, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
int message_log_tool_recover(const char *path) {
|
|
||||||
return scan_log(path, true);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
#include "ratelimit.h"
|
#include "ratelimit.h"
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
|
@ -28,20 +27,16 @@ static pthread_mutex_t g_rate_limit_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static int g_total_connections = 0;
|
static int g_total_connections = 0;
|
||||||
static pthread_mutex_t g_conn_count_lock = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_conn_count_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
static int g_max_connections = TNT_DEFAULT_MAX_CONNECTIONS;
|
static int g_max_connections = 64;
|
||||||
static int g_max_conn_per_ip = TNT_DEFAULT_MAX_CONN_PER_IP;
|
static int g_max_conn_per_ip = 5;
|
||||||
static int g_max_conn_rate_per_ip = TNT_DEFAULT_MAX_CONN_RATE_PER_IP;
|
static int g_max_conn_rate_per_ip = 10;
|
||||||
static int g_rate_limit_enabled = TNT_DEFAULT_RATE_LIMIT_ENABLED;
|
static int g_rate_limit_enabled = 1;
|
||||||
|
|
||||||
void ratelimit_init(void) {
|
void ratelimit_init(void) {
|
||||||
g_max_connections =
|
g_max_connections = env_int("TNT_MAX_CONNECTIONS", 64, 1, 1024);
|
||||||
tnt_config_env_int(&TNT_CONFIG_MAX_CONNECTIONS);
|
g_max_conn_per_ip = env_int("TNT_MAX_CONN_PER_IP", 5, 1, 1024);
|
||||||
g_max_conn_per_ip =
|
g_max_conn_rate_per_ip = env_int("TNT_MAX_CONN_RATE_PER_IP", 10, 1, 1024);
|
||||||
tnt_config_env_int(&TNT_CONFIG_MAX_CONN_PER_IP);
|
g_rate_limit_enabled = env_int("TNT_RATE_LIMIT", 1, 0, 1);
|
||||||
g_max_conn_rate_per_ip =
|
|
||||||
tnt_config_env_int(&TNT_CONFIG_MAX_CONN_RATE_PER_IP);
|
|
||||||
g_rate_limit_enabled =
|
|
||||||
tnt_config_env_int(&TNT_CONFIG_RATE_LIMIT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Caller MUST hold g_rate_limit_lock. */
|
/* Caller MUST hold g_rate_limit_lock. */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#include "ssh_server.h"
|
#include "ssh_server.h"
|
||||||
#include "bootstrap.h"
|
#include "bootstrap.h"
|
||||||
#include "commands.h"
|
#include "commands.h"
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "exec.h"
|
#include "exec.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
#include "ratelimit.h"
|
#include "ratelimit.h"
|
||||||
|
|
@ -24,7 +23,7 @@
|
||||||
|
|
||||||
/* Global SSH bind instance */
|
/* Global SSH bind instance */
|
||||||
static ssh_bind g_sshbind = NULL;
|
static ssh_bind g_sshbind = NULL;
|
||||||
static int g_listen_port = TNT_DEFAULT_PORT;
|
static int g_listen_port = DEFAULT_PORT;
|
||||||
|
|
||||||
static time_t g_server_start_time = 0;
|
static time_t g_server_start_time = 0;
|
||||||
|
|
||||||
|
|
|
||||||
88
src/tntctl.c
88
src/tntctl.c
|
|
@ -1,8 +1,4 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "exec_catalog.h"
|
|
||||||
#include "i18n.h"
|
|
||||||
#include "tntctl_text.h"
|
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
@ -10,24 +6,21 @@
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
static void print_usage(FILE *stream, ui_lang_t lang) {
|
static void print_usage(FILE *stream) {
|
||||||
char output[2048];
|
fprintf(stream,
|
||||||
size_t pos = 0;
|
"Usage: tntctl [options] host command [args...]\n"
|
||||||
|
"\n"
|
||||||
output[0] = '\0';
|
"Options:\n"
|
||||||
tntctl_text_append_usage(output, sizeof(output), &pos, lang);
|
" -p, --port PORT SSH port (default: 2222)\n"
|
||||||
fputs(output, stream);
|
" -l, --login USER SSH login name for exec identity\n"
|
||||||
}
|
" --host-key-checking MODE\n"
|
||||||
|
" OpenSSH host-key mode: yes, accept-new, no\n"
|
||||||
static void print_error(ui_lang_t lang, tntctl_text_id_t id) {
|
" --known-hosts FILE OpenSSH known_hosts file\n"
|
||||||
fprintf(stderr, "tntctl: %s\n", tntctl_text(lang, id));
|
" -V, --version Print version and exit\n"
|
||||||
}
|
" -h, --help Print this help and exit\n"
|
||||||
|
"\n"
|
||||||
static void print_error_format(ui_lang_t lang, tntctl_text_id_t id,
|
"Commands mirror the TNT SSH exec interface: health, stats, users,\n"
|
||||||
const char *value) {
|
"tail, post, help, and exit.\n");
|
||||||
fprintf(stderr, "tntctl: ");
|
|
||||||
fprintf(stderr, tntctl_text(lang, id), value);
|
|
||||||
fputc('\n', stderr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_valid_port(const char *value) {
|
static bool is_valid_port(const char *value) {
|
||||||
|
|
@ -80,7 +73,14 @@ static bool is_host_key_checking_mode(const char *value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_known_exec_command(const char *command) {
|
static bool is_known_exec_command(const char *command) {
|
||||||
return exec_catalog_match(command, NULL, NULL);
|
return command &&
|
||||||
|
(strcmp(command, "health") == 0 ||
|
||||||
|
strcmp(command, "stats") == 0 ||
|
||||||
|
strcmp(command, "users") == 0 ||
|
||||||
|
strcmp(command, "tail") == 0 ||
|
||||||
|
strcmp(command, "post") == 0 ||
|
||||||
|
strcmp(command, "help") == 0 ||
|
||||||
|
strcmp(command, "exit") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int build_remote_command(char *buffer, size_t buf_size, int argc,
|
static int build_remote_command(char *buffer, size_t buf_size, int argc,
|
||||||
|
|
@ -146,7 +146,7 @@ static int run_ssh(char **ssh_argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
const char *port = TNT_DEFAULT_PORT_TEXT;
|
const char *port = "2222";
|
||||||
const char *login = NULL;
|
const char *login = NULL;
|
||||||
const char *host_key_checking = NULL;
|
const char *host_key_checking = NULL;
|
||||||
const char *known_hosts = NULL;
|
const char *known_hosts = NULL;
|
||||||
|
|
@ -159,7 +159,6 @@ int main(int argc, char **argv) {
|
||||||
char **ssh_argv = NULL;
|
char **ssh_argv = NULL;
|
||||||
int ssh_argc = 0;
|
int ssh_argc = 0;
|
||||||
int rc;
|
int rc;
|
||||||
ui_lang_t lang = i18n_default_ui_lang();
|
|
||||||
|
|
||||||
for (i = 1; i < argc; i++) {
|
for (i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "--") == 0) {
|
if (strcmp(argv[i], "--") == 0) {
|
||||||
|
|
@ -167,7 +166,7 @@ int main(int argc, char **argv) {
|
||||||
break;
|
break;
|
||||||
} else if (strcmp(argv[i], "-h") == 0 ||
|
} else if (strcmp(argv[i], "-h") == 0 ||
|
||||||
strcmp(argv[i], "--help") == 0) {
|
strcmp(argv[i], "--help") == 0) {
|
||||||
print_usage(stdout, lang);
|
print_usage(stdout);
|
||||||
return TNT_EXIT_OK;
|
return TNT_EXIT_OK;
|
||||||
} else if (strcmp(argv[i], "-V") == 0 ||
|
} else if (strcmp(argv[i], "-V") == 0 ||
|
||||||
strcmp(argv[i], "--version") == 0) {
|
strcmp(argv[i], "--version") == 0) {
|
||||||
|
|
@ -176,7 +175,7 @@ int main(int argc, char **argv) {
|
||||||
} else if (strcmp(argv[i], "-p") == 0 ||
|
} else if (strcmp(argv[i], "-p") == 0 ||
|
||||||
strcmp(argv[i], "--port") == 0) {
|
strcmp(argv[i], "--port") == 0) {
|
||||||
if (i + 1 >= argc || !is_valid_port(argv[i + 1])) {
|
if (i + 1 >= argc || !is_valid_port(argv[i + 1])) {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_PORT);
|
fprintf(stderr, "tntctl: invalid port\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
port = argv[++i];
|
port = argv[++i];
|
||||||
|
|
@ -184,27 +183,26 @@ int main(int argc, char **argv) {
|
||||||
strcmp(argv[i], "--login") == 0) {
|
strcmp(argv[i], "--login") == 0) {
|
||||||
if (i + 1 >= argc || is_safe_ssh_token(argv[i + 1]) ||
|
if (i + 1 >= argc || is_safe_ssh_token(argv[i + 1]) ||
|
||||||
strchr(argv[i + 1], '@')) {
|
strchr(argv[i + 1], '@')) {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_LOGIN);
|
fprintf(stderr, "tntctl: invalid login\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
login = argv[++i];
|
login = argv[++i];
|
||||||
} else if (strcmp(argv[i], "--host-key-checking") == 0) {
|
} else if (strcmp(argv[i], "--host-key-checking") == 0) {
|
||||||
if (i + 1 >= argc || !is_host_key_checking_mode(argv[i + 1])) {
|
if (i + 1 >= argc || !is_host_key_checking_mode(argv[i + 1])) {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_HOST_KEY_MODE);
|
fprintf(stderr, "tntctl: invalid host-key checking mode\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
host_key_checking = argv[++i];
|
host_key_checking = argv[++i];
|
||||||
} else if (strcmp(argv[i], "--known-hosts") == 0) {
|
} else if (strcmp(argv[i], "--known-hosts") == 0) {
|
||||||
if (i + 1 >= argc || argv[i + 1][0] == '\0' ||
|
if (i + 1 >= argc || argv[i + 1][0] == '\0' ||
|
||||||
has_newline(argv[i + 1])) {
|
has_newline(argv[i + 1])) {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_KNOWN_HOSTS);
|
fprintf(stderr, "tntctl: invalid known_hosts path\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
known_hosts = argv[++i];
|
known_hosts = argv[++i];
|
||||||
} else if (argv[i][0] == '-') {
|
} else if (argv[i][0] == '-') {
|
||||||
print_error_format(lang, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT,
|
fprintf(stderr, "tntctl: unknown option: %s\n", argv[i]);
|
||||||
argv[i]);
|
print_usage(stderr);
|
||||||
print_usage(stderr, lang);
|
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
|
@ -212,29 +210,29 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i >= argc) {
|
if (i >= argc) {
|
||||||
print_error(lang, TNTCTL_TEXT_MISSING_HOST);
|
fprintf(stderr, "tntctl: missing host\n");
|
||||||
print_usage(stderr, lang);
|
print_usage(stderr);
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
host = argv[i++];
|
host = argv[i++];
|
||||||
if (is_safe_ssh_token(host)) {
|
if (is_safe_ssh_token(host)) {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_HOST);
|
fprintf(stderr, "tntctl: invalid host\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
if (login && strchr(host, '@')) {
|
if (login && strchr(host, '@')) {
|
||||||
print_error(lang, TNTCTL_TEXT_LOGIN_HOST_CONFLICT);
|
fprintf(stderr, "tntctl: use either --login or user@host, not both\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i >= argc || !is_known_exec_command(argv[i])) {
|
if (i >= argc || !is_known_exec_command(argv[i])) {
|
||||||
print_error(lang, TNTCTL_TEXT_UNKNOWN_COMMAND);
|
fprintf(stderr, "tntctl: unknown or missing command\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (build_remote_command(remote_command, sizeof(remote_command), argc,
|
if (build_remote_command(remote_command, sizeof(remote_command), argc,
|
||||||
argv, i) < 0) {
|
argv, i) < 0) {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_REMOTE_COMMAND);
|
fprintf(stderr, "tntctl: invalid or too-long command\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,24 +240,24 @@ int main(int argc, char **argv) {
|
||||||
int n = snprintf(destination, sizeof(destination), "%s@%s", login,
|
int n = snprintf(destination, sizeof(destination), "%s@%s", login,
|
||||||
host);
|
host);
|
||||||
if (n < 0 || n >= (int)sizeof(destination)) {
|
if (n < 0 || n >= (int)sizeof(destination)) {
|
||||||
print_error(lang, TNTCTL_TEXT_DESTINATION_TOO_LONG);
|
fprintf(stderr, "tntctl: destination too long\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int n = snprintf(destination, sizeof(destination), "%s", host);
|
int n = snprintf(destination, sizeof(destination), "%s", host);
|
||||||
if (n < 0 || n >= (int)sizeof(destination)) {
|
if (n < 0 || n >= (int)sizeof(destination)) {
|
||||||
print_error(lang, TNTCTL_TEXT_DESTINATION_TOO_LONG);
|
fprintf(stderr, "tntctl: destination too long\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (destination[0] == '-') {
|
if (destination[0] == '-') {
|
||||||
print_error(lang, TNTCTL_TEXT_INVALID_DESTINATION);
|
fprintf(stderr, "tntctl: invalid destination\n");
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh_argv = calloc((size_t)argc * 2u + 8u, sizeof(*ssh_argv));
|
ssh_argv = calloc((size_t)argc * 2u + 8u, sizeof(*ssh_argv));
|
||||||
if (!ssh_argv) {
|
if (!ssh_argv) {
|
||||||
print_error(lang, TNTCTL_TEXT_OUT_OF_MEMORY);
|
fprintf(stderr, "tntctl: out of memory\n");
|
||||||
return TNT_EXIT_ERROR;
|
return TNT_EXIT_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +268,7 @@ int main(int argc, char **argv) {
|
||||||
int n = snprintf(host_key_option, sizeof(host_key_option),
|
int n = snprintf(host_key_option, sizeof(host_key_option),
|
||||||
"StrictHostKeyChecking=%s", host_key_checking);
|
"StrictHostKeyChecking=%s", host_key_checking);
|
||||||
if (n < 0 || n >= (int)sizeof(host_key_option)) {
|
if (n < 0 || n >= (int)sizeof(host_key_option)) {
|
||||||
print_error(lang, TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG);
|
fprintf(stderr, "tntctl: host-key option too long\n");
|
||||||
free(ssh_argv);
|
free(ssh_argv);
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +279,7 @@ int main(int argc, char **argv) {
|
||||||
int n = snprintf(known_hosts_option, sizeof(known_hosts_option),
|
int n = snprintf(known_hosts_option, sizeof(known_hosts_option),
|
||||||
"UserKnownHostsFile=%s", known_hosts);
|
"UserKnownHostsFile=%s", known_hosts);
|
||||||
if (n < 0 || n >= (int)sizeof(known_hosts_option)) {
|
if (n < 0 || n >= (int)sizeof(known_hosts_option)) {
|
||||||
print_error(lang, TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG);
|
fprintf(stderr, "tntctl: known_hosts option too long\n");
|
||||||
free(ssh_argv);
|
free(ssh_argv);
|
||||||
return TNT_EXIT_USAGE;
|
return TNT_EXIT_USAGE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
#include "tntctl_text.h"
|
|
||||||
|
|
||||||
#include "config_defaults.h"
|
|
||||||
#include "exec_catalog.h"
|
|
||||||
#include "i18n.h"
|
|
||||||
|
|
||||||
static const i18n_string_t text_catalog[TNTCTL_TEXT_COUNT] = {
|
|
||||||
[TNTCTL_TEXT_INVALID_PORT] = I18N_STRING(
|
|
||||||
"invalid port", "端口无效"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_INVALID_LOGIN] = I18N_STRING(
|
|
||||||
"invalid login", "登录名无效"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_INVALID_HOST_KEY_MODE] = I18N_STRING(
|
|
||||||
"invalid host-key checking mode", "主机密钥检查模式无效"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_INVALID_KNOWN_HOSTS] = I18N_STRING(
|
|
||||||
"invalid known_hosts path", "known_hosts 路径无效"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT] = I18N_STRING(
|
|
||||||
"unknown option: %s", "未知选项: %s"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_MISSING_HOST] = I18N_STRING(
|
|
||||||
"missing host", "缺少 host"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_INVALID_HOST] = I18N_STRING(
|
|
||||||
"invalid host", "host 无效"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_LOGIN_HOST_CONFLICT] = I18N_STRING(
|
|
||||||
"use either --login or user@host, not both",
|
|
||||||
"只能使用 --login 或 user@host 之一"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_UNKNOWN_COMMAND] = I18N_STRING(
|
|
||||||
"unknown or missing command", "未知命令或缺少命令"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_INVALID_REMOTE_COMMAND] = I18N_STRING(
|
|
||||||
"invalid or too-long command", "命令无效或过长"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_DESTINATION_TOO_LONG] = I18N_STRING(
|
|
||||||
"destination too long", "目标地址过长"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_INVALID_DESTINATION] = I18N_STRING(
|
|
||||||
"invalid destination", "目标地址无效"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_OUT_OF_MEMORY] = I18N_STRING(
|
|
||||||
"out of memory", "内存不足"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG] = I18N_STRING(
|
|
||||||
"host-key option too long", "主机密钥选项过长"
|
|
||||||
),
|
|
||||||
[TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG] = I18N_STRING(
|
|
||||||
"known_hosts option too long", "known_hosts 选项过长"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
typedef char text_catalog_must_cover_enum[
|
|
||||||
sizeof(text_catalog) / sizeof(text_catalog[0]) == TNTCTL_TEXT_COUNT ? 1 : -1
|
|
||||||
];
|
|
||||||
|
|
||||||
void tntctl_text_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
|
||||||
ui_lang_t lang) {
|
|
||||||
static const i18n_string_t before_commands = I18N_STRING(
|
|
||||||
"Usage: tntctl [options] host command [args...]\n"
|
|
||||||
"\n"
|
|
||||||
"Options:\n"
|
|
||||||
" -p, --port PORT SSH port (default: " TNT_DEFAULT_PORT_TEXT ")\n"
|
|
||||||
" -l, --login USER SSH login name for exec identity\n"
|
|
||||||
" --host-key-checking MODE\n"
|
|
||||||
" OpenSSH host-key mode: yes, accept-new, no\n"
|
|
||||||
" --known-hosts FILE OpenSSH known_hosts file\n"
|
|
||||||
" -V, --version Print version and exit\n"
|
|
||||||
" -h, --help Print this help and exit\n"
|
|
||||||
"\n"
|
|
||||||
"Commands:\n"
|
|
||||||
" ",
|
|
||||||
"用法: tntctl [options] host command [args...]\n"
|
|
||||||
"\n"
|
|
||||||
"选项:\n"
|
|
||||||
" -p, --port PORT SSH 端口 (默认: " TNT_DEFAULT_PORT_TEXT ")\n"
|
|
||||||
" -l, --login USER SSH 登录名,用作 exec 身份\n"
|
|
||||||
" --host-key-checking MODE\n"
|
|
||||||
" OpenSSH 主机密钥模式: yes, accept-new, no\n"
|
|
||||||
" --known-hosts FILE OpenSSH known_hosts 文件\n"
|
|
||||||
" -V, --version 输出版本并退出\n"
|
|
||||||
" -h, --help 输出此帮助并退出\n"
|
|
||||||
"\n"
|
|
||||||
"命令:\n"
|
|
||||||
" "
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer_appendf(buffer, buf_size, pos, "%s",
|
|
||||||
i18n_string(before_commands, lang));
|
|
||||||
exec_catalog_append_command_list(buffer, buf_size, pos);
|
|
||||||
buffer_appendf(buffer, buf_size, pos, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) {
|
|
||||||
if (id < 0 || id >= TNTCTL_TEXT_COUNT) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return i18n_string(text_catalog[id], lang);
|
|
||||||
}
|
|
||||||
11
src/tui.c
11
src/tui.c
|
|
@ -373,9 +373,7 @@ void tui_render_screen(client_t *client) {
|
||||||
chips[chip_count].value_color = mode_color;
|
chips[chip_count].value_color = mode_color;
|
||||||
chip_count++;
|
chip_count++;
|
||||||
|
|
||||||
const char *hint = client->mode == MODE_NORMAL
|
const char *hint = i18n_text(client->ui_lang, I18N_TITLE_HELP_HINT);
|
||||||
? i18n_text(client->ui_lang, I18N_TITLE_HELP_HINT)
|
|
||||||
: "";
|
|
||||||
int hint_width = utf8_string_width(hint);
|
int hint_width = utf8_string_width(hint);
|
||||||
const char *mute_label = i18n_text(client->ui_lang, I18N_TITLE_MUTED);
|
const char *mute_label = i18n_text(client->ui_lang, I18N_TITLE_MUTED);
|
||||||
int mute_width = client->mute_joins ? utf8_string_width(mute_label) + 2 : 0;
|
int mute_width = client->mute_joins ? utf8_string_width(mute_label) + 2 : 0;
|
||||||
|
|
@ -403,7 +401,7 @@ void tui_render_screen(client_t *client) {
|
||||||
|
|
||||||
/* Decide what fits. Reserve at least 1 col of gap between left and
|
/* Decide what fits. Reserve at least 1 col of gap between left and
|
||||||
* right halves so they never visually touch. */
|
* right halves so they never visually touch. */
|
||||||
int show_hint = hint[0] != '\0';
|
int show_hint = 1;
|
||||||
int show_mute = client->mute_joins ? 1 : 0;
|
int show_mute = client->mute_joins ? 1 : 0;
|
||||||
int show_unread = unread_count > 0 ? 1 : 0;
|
int show_unread = unread_count > 0 ? 1 : 0;
|
||||||
int show_whisper = whisper_count > 0 ? 1 : 0;
|
int show_whisper = whisper_count > 0 ? 1 : 0;
|
||||||
|
|
@ -679,10 +677,7 @@ void tui_render_command_output(client_t *client) {
|
||||||
|
|
||||||
buffer_appendf(buffer, sizeof(buffer), &pos,
|
buffer_appendf(buffer, sizeof(buffer), &pos,
|
||||||
i18n_text(client->ui_lang,
|
i18n_text(client->ui_lang,
|
||||||
client->command_output_kind ==
|
I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
||||||
TNT_COMMAND_OUTPUT_INBOX
|
|
||||||
? I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT
|
|
||||||
: I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
|
||||||
start + 1, max_scroll + 1);
|
start + 1, max_scroll + 1);
|
||||||
|
|
||||||
client_send(client, buffer, pos);
|
client_send(client, buffer, pos);
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,6 @@
|
||||||
#include "tui_status.h"
|
#include "tui_status.h"
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
#include "ssh_server.h"
|
#include "ssh_server.h"
|
||||||
#include "utf8.h"
|
|
||||||
|
|
||||||
static void format_command_input_tail(const char *input, int avail_width,
|
|
||||||
char *display, size_t display_size) {
|
|
||||||
if (!input || !display || display_size == 0) return;
|
|
||||||
|
|
||||||
display[0] = '\0';
|
|
||||||
if (avail_width < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utf8_string_width(input) <= avail_width) {
|
|
||||||
strncpy(display, input, display_size - 1);
|
|
||||||
display[display_size - 1] = '\0';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *marker = "<";
|
|
||||||
int marker_width = 1;
|
|
||||||
int tail_width = avail_width - marker_width;
|
|
||||||
if (tail_width < 1) {
|
|
||||||
snprintf(display, display_size, "%s", marker);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *p = input + strlen(input);
|
|
||||||
const char *tail = p;
|
|
||||||
int width = 0;
|
|
||||||
|
|
||||||
while (p > input && width < tail_width) {
|
|
||||||
const char *q = p - 1;
|
|
||||||
while (q > input && ((*q & 0xC0) == 0x80)) {
|
|
||||||
q--;
|
|
||||||
}
|
|
||||||
|
|
||||||
int bytes_read = 0;
|
|
||||||
uint32_t cp = utf8_decode(q, &bytes_read);
|
|
||||||
int char_width = utf8_char_width(cp);
|
|
||||||
if (width + char_width > tail_width) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
width += char_width;
|
|
||||||
tail = q;
|
|
||||||
p = q;
|
|
||||||
}
|
|
||||||
|
|
||||||
snprintf(display, display_size, "%s%s", marker, tail);
|
|
||||||
}
|
|
||||||
|
|
||||||
void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
||||||
const struct client *client, int msg_count,
|
const struct client *client, int msg_count,
|
||||||
|
|
@ -96,12 +48,7 @@ void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
||||||
i18n_text(client->ui_lang, I18N_NORMAL_LATEST));
|
i18n_text(client->ui_lang, I18N_NORMAL_LATEST));
|
||||||
}
|
}
|
||||||
} else if (client->mode == MODE_COMMAND) {
|
} else if (client->mode == MODE_COMMAND) {
|
||||||
char display[sizeof(client->command_input) + 2];
|
|
||||||
int avail = client->width - 1;
|
|
||||||
if (avail < 1) avail = 1;
|
|
||||||
format_command_input_tail(client->command_input, avail, display,
|
|
||||||
sizeof(display));
|
|
||||||
buffer_appendf(buffer, buf_size, pos,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"\033[35m:\033[0m%s\033[K", display);
|
"\033[35m:\033[0m%s\033[K", client->command_input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# CLI option parsing regression tests.
|
|
||||||
|
|
||||||
BIN="../tnt"
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
pass() {
|
|
||||||
echo "✓ $1"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "✗ $1"
|
|
||||||
if [ -n "$2" ]; then
|
|
||||||
printf '%s\n' "$2"
|
|
||||||
fi
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -f "$BIN" ]; then
|
|
||||||
echo "Error: Binary $BIN not found. Run make first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
expect_missing_arg() {
|
|
||||||
opt="$1"
|
|
||||||
output=$("$BIN" "$opt" 2>&1)
|
|
||||||
status=$?
|
|
||||||
|
|
||||||
if [ "$status" -eq 64 ] &&
|
|
||||||
printf '%s\n' "$output" | grep -q "Option requires argument: $opt"; then
|
|
||||||
pass "$opt reports missing argument"
|
|
||||||
else
|
|
||||||
fail "$opt missing argument diagnostic unexpected" "$output"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== TNT CLI Option Tests ==="
|
|
||||||
|
|
||||||
for opt in \
|
|
||||||
-p \
|
|
||||||
--port \
|
|
||||||
-d \
|
|
||||||
--state-dir \
|
|
||||||
--bind \
|
|
||||||
--public-host \
|
|
||||||
--max-connections \
|
|
||||||
--max-conn-per-ip \
|
|
||||||
--max-conn-rate-per-ip \
|
|
||||||
--rate-limit \
|
|
||||||
--idle-timeout \
|
|
||||||
--ssh-log-level \
|
|
||||||
--log-check \
|
|
||||||
--log-recover
|
|
||||||
do
|
|
||||||
expect_missing_arg "$opt"
|
|
||||||
done
|
|
||||||
|
|
||||||
ZH_OUTPUT=$(TNT_LANG=zh "$BIN" --bind 2>&1)
|
|
||||||
ZH_STATUS=$?
|
|
||||||
if [ "$ZH_STATUS" -eq 64 ] &&
|
|
||||||
printf '%s\n' "$ZH_OUTPUT" | grep -q '选项需要参数: --bind'; then
|
|
||||||
pass "missing argument diagnostic follows TNT_LANG"
|
|
||||||
else
|
|
||||||
fail "localized missing argument diagnostic unexpected" "$ZH_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
BAD_PORT_OUTPUT=$("$BIN" --port abc 2>&1)
|
|
||||||
BAD_PORT_STATUS=$?
|
|
||||||
if [ "$BAD_PORT_STATUS" -eq 64 ] &&
|
|
||||||
printf '%s\n' "$BAD_PORT_OUTPUT" | grep -q 'Invalid port: abc'; then
|
|
||||||
pass "invalid port still reports invalid value"
|
|
||||||
else
|
|
||||||
fail "invalid port diagnostic unexpected" "$BAD_PORT_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "PASSED: $PASS"
|
|
||||||
echo "FAILED: $FAIL"
|
|
||||||
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
|
|
||||||
exit "$FAIL"
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Regression checks for active help/manual surfaces.
|
|
||||||
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
||||||
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
|
||||||
|
|
||||||
pass() {
|
|
||||||
echo "✓ $1"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "✗ $1"
|
|
||||||
if [ -n "$2" ]; then
|
|
||||||
printf '%s\n' "$2"
|
|
||||||
fi
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
require_fixed() {
|
|
||||||
file="$1"
|
|
||||||
text="$2"
|
|
||||||
label="$3"
|
|
||||||
|
|
||||||
if grep -F -q "$text" "$REPO_ROOT/$file"; then
|
|
||||||
pass "$label"
|
|
||||||
else
|
|
||||||
fail "$label missing" "$file: $text"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
forbid_fixed() {
|
|
||||||
file="$1"
|
|
||||||
text="$2"
|
|
||||||
label="$3"
|
|
||||||
|
|
||||||
if grep -F -q "$text" "$REPO_ROOT/$file"; then
|
|
||||||
fail "$label still mentions $text" "$file"
|
|
||||||
else
|
|
||||||
pass "$label excludes $text"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== TNT Help Surface Tests ==="
|
|
||||||
|
|
||||||
require_fixed "tnt.1" "/ Search message history" "manual documents NORMAL search"
|
|
||||||
require_fixed "tnt.1" "Space/b Scroll full page down/up" "manual documents space/b paging"
|
|
||||||
require_fixed "tnt.1" "PageDown/PageUp Scroll full page down/up" "manual documents page keys"
|
|
||||||
require_fixed "tnt.1" "End/Home Jump to bottom/top" "manual documents end/home"
|
|
||||||
require_fixed "tnt.1" "g/G Jump to top/bottom" "manual documents g/G"
|
|
||||||
require_fixed "tnt.1" ":lang Show current UI language" "manual documents current language"
|
|
||||||
require_fixed "tnt.1" ":lang \fIen|zh\fR Switch UI language for this session" "manual documents language codes"
|
|
||||||
|
|
||||||
for file in \
|
|
||||||
README.md \
|
|
||||||
docs/EASY_SETUP.md \
|
|
||||||
docs/DEPLOYMENT.md \
|
|
||||||
docs/INTERFACE.md \
|
|
||||||
docs/QUICKREF.md \
|
|
||||||
docs/USER_LIFECYCLE.md \
|
|
||||||
tnt.1 \
|
|
||||||
tntctl.1 \
|
|
||||||
src/command_catalog.c \
|
|
||||||
src/help_text.c \
|
|
||||||
src/manual_text.c
|
|
||||||
do
|
|
||||||
forbid_fixed "$file" ":support" "$file"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "PASSED: $PASS"
|
|
||||||
echo "FAILED: $FAIL"
|
|
||||||
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
|
|
||||||
exit "$FAIL"
|
|
||||||
|
|
@ -140,19 +140,6 @@ else
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DUMP_USAGE=$(ssh $SSH_OPTS localhost "dump -n nope" 2>/dev/null)
|
|
||||||
DUMP_USAGE_STATUS=$?
|
|
||||||
printf '%s\n' "$DUMP_USAGE" | grep -q '^dump: 用法: dump \[N\] | dump -n N$'
|
|
||||||
if [ $? -eq 0 ] && [ "$DUMP_USAGE_STATUS" -eq 64 ]; then
|
|
||||||
echo "✓ dump usage follows TNT_LANG and exits 64"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ dump usage output unexpected"
|
|
||||||
printf '%s\n' "$DUMP_USAGE"
|
|
||||||
echo "exit status: $DUMP_USAGE_STATUS"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
POST_OUTPUT=$(ssh $SSH_OPTS execposter@localhost post "hello from exec" 2>/dev/null || true)
|
POST_OUTPUT=$(ssh $SSH_OPTS execposter@localhost post "hello from exec" 2>/dev/null || true)
|
||||||
if [ "$POST_OUTPUT" = "posted" ]; then
|
if [ "$POST_OUTPUT" = "posted" ]; then
|
||||||
echo "✓ post publishes a message"
|
echo "✓ post publishes a message"
|
||||||
|
|
@ -174,17 +161,6 @@ else
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DUMP_OUTPUT=$(ssh $SSH_OPTS localhost "dump -n 1" 2>/dev/null || true)
|
|
||||||
printf '%s\n' "$DUMP_OUTPUT" | grep -q '|execposter|hello from exec$'
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "✓ dump returns persisted message log records"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ dump output unexpected"
|
|
||||||
printf '%s\n' "$DUMP_OUTPUT"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
PERSIST_FAIL_MARKER="persist-fail-marker"
|
PERSIST_FAIL_MARKER="persist-fail-marker"
|
||||||
rm -f "$STATE_DIR/messages.log"
|
rm -f "$STATE_DIR/messages.log"
|
||||||
mkdir "$STATE_DIR/messages.log"
|
mkdir "$STATE_DIR/messages.log"
|
||||||
|
|
@ -285,17 +261,6 @@ else
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TNTCTL_DUMP=$("../tntctl" -p "$PORT" $TNTCTL_OPTS localhost "dump" "-n" "1" 2>/dev/null || true)
|
|
||||||
printf '%s\n' "$TNTCTL_DUMP" | grep -q '|ctlposter|hello from tntctl$'
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "✓ tntctl dump returns persisted message log records"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ tntctl dump output unexpected"
|
|
||||||
printf '%s\n' "$TNTCTL_DUMP"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXPECT_SCRIPT="${STATE_DIR}/watcher.expect"
|
EXPECT_SCRIPT="${STATE_DIR}/watcher.expect"
|
||||||
WATCHER_READY="${STATE_DIR}/watcher.ready"
|
WATCHER_READY="${STATE_DIR}/watcher.ready"
|
||||||
cat >"$EXPECT_SCRIPT" <<EOF
|
cat >"$EXPECT_SCRIPT" <<EOF
|
||||||
|
|
@ -372,7 +337,7 @@ set timeout 10
|
||||||
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT sender@localhost
|
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT sender@localhost
|
||||||
expect "请输入用户名"
|
expect "请输入用户名"
|
||||||
send "sender\r"
|
send "sender\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send "\033"
|
send "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send ":"
|
send ":"
|
||||||
|
|
|
||||||
|
|
@ -58,58 +58,13 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
USERNAME_CANCEL_SCRIPT="$STATE_DIR/username-cancel.expect"
|
|
||||||
cat >"$USERNAME_CANCEL_SCRIPT" <<EOF
|
|
||||||
set timeout 10
|
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
|
||||||
sleep 1
|
|
||||||
send -- "\003"
|
|
||||||
expect eof
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if expect "$USERNAME_CANCEL_SCRIPT" >"$STATE_DIR/username-cancel.log" 2>&1; then
|
|
||||||
echo "✓ Ctrl+C cancels before username join"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "x Ctrl+C before username failed"
|
|
||||||
sed -n '1,120p' "$STATE_DIR/username-cancel.log"
|
|
||||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
USERNAME_EDIT_SCRIPT="$STATE_DIR/username-edit.expect"
|
|
||||||
cat >"$USERNAME_EDIT_SCRIPT" <<EOF
|
|
||||||
set timeout 10
|
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
|
||||||
sleep 1
|
|
||||||
send -- "wrong\025editeduser\r"
|
|
||||||
expect "Esc NORMAL"
|
|
||||||
send -- "\003"
|
|
||||||
sleep 0.2
|
|
||||||
send -- "\003"
|
|
||||||
expect eof
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if expect "$USERNAME_EDIT_SCRIPT" >"$STATE_DIR/username-edit.log" 2>&1 &&
|
|
||||||
grep -q 'editeduser' "$STATE_DIR/messages.log" &&
|
|
||||||
! grep -q 'wrongediteduser' "$STATE_DIR/messages.log"; then
|
|
||||||
echo "✓ Ctrl+U edits username before join"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "x username line editing failed"
|
|
||||||
sed -n '1,120p' "$STATE_DIR/username-edit.log" 2>/dev/null || true
|
|
||||||
cat "$STATE_DIR/messages.log" 2>/dev/null || true
|
|
||||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXPECT_SCRIPT="$STATE_DIR/bracketed-paste.expect"
|
EXPECT_SCRIPT="$STATE_DIR/bracketed-paste.expect"
|
||||||
cat >"$EXPECT_SCRIPT" <<EOF
|
cat >"$EXPECT_SCRIPT" <<EOF
|
||||||
set timeout 10
|
set timeout 10
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "tester\r"
|
send -- "tester\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033\[200~"
|
send -- "\033\[200~"
|
||||||
send -- "line1\nline2\nline3"
|
send -- "line1\nline2\nline3"
|
||||||
send -- "\033\[201~"
|
send -- "\033\[201~"
|
||||||
|
|
@ -184,28 +139,21 @@ set timeout 10
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "helper\r"
|
send -- "helper\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
expect ":"
|
expect ":"
|
||||||
send -- ":help\r"
|
send -- "help\r"
|
||||||
expect "TNT\\(1\\) 帮助"
|
expect "TNT\\(1\\) 帮助"
|
||||||
expect "Tab 补全 @mention"
|
|
||||||
expect "q:关闭"
|
expect "q:关闭"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- "?"
|
send -- "?"
|
||||||
expect "TNT 按键参考"
|
expect "TNT 按键参考"
|
||||||
expect "Tab - 补全 @mention"
|
|
||||||
expect "l:语言"
|
expect "l:语言"
|
||||||
send -- "\003"
|
|
||||||
expect "NORMAL"
|
|
||||||
send -- "?"
|
|
||||||
expect "TNT 按键参考"
|
|
||||||
send -- "l"
|
send -- "l"
|
||||||
expect "TNT KEY REFERENCE"
|
expect "TNT KEY REFERENCE"
|
||||||
expect "Complete @mention"
|
|
||||||
expect "l:lang"
|
expect "l:lang"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
|
|
@ -232,52 +180,13 @@ else
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HELP_PAGER_KEYS_SCRIPT="$STATE_DIR/help-pager-keys.expect"
|
|
||||||
cat >"$HELP_PAGER_KEYS_SCRIPT" <<EOF
|
|
||||||
set timeout 10
|
|
||||||
stty rows 8 columns 80
|
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
|
||||||
sleep 1
|
|
||||||
send -- "helppager\r"
|
|
||||||
expect "Esc NORMAL"
|
|
||||||
send -- "\033"
|
|
||||||
expect "NORMAL"
|
|
||||||
send -- "?"
|
|
||||||
expect -re {\(1/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[6~"
|
|
||||||
expect -re {\([2-9][0-9]*/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[5~"
|
|
||||||
expect -re {\(1/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[F"
|
|
||||||
expect -re {\([2-9][0-9]*/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[H"
|
|
||||||
expect -re {\(1/[2-9][0-9]*\)}
|
|
||||||
send -- "q"
|
|
||||||
expect "NORMAL"
|
|
||||||
sleep 0.2
|
|
||||||
send -- "\003"
|
|
||||||
sleep 0.2
|
|
||||||
send -- "\003"
|
|
||||||
expect eof
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if expect "$HELP_PAGER_KEYS_SCRIPT" >"$STATE_DIR/help-pager-keys.log" 2>&1; then
|
|
||||||
echo "✓ help pager accepts terminal paging keys"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "x help pager terminal keys failed"
|
|
||||||
sed -n '1,220p' "$STATE_DIR/help-pager-keys.log"
|
|
||||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
UNKNOWN_SCRIPT="$STATE_DIR/unknown-command.expect"
|
UNKNOWN_SCRIPT="$STATE_DIR/unknown-command.expect"
|
||||||
cat >"$UNKNOWN_SCRIPT" <<EOF
|
cat >"$UNKNOWN_SCRIPT" <<EOF
|
||||||
set timeout 10
|
set timeout 10
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "mistype\r"
|
send -- "mistype\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
|
|
@ -309,7 +218,7 @@ set timeout 10
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "localized\r"
|
send -- "localized\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
|
|
@ -359,7 +268,7 @@ set timeout 10
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "usageuser\r"
|
send -- "usageuser\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
|
|
@ -395,9 +304,6 @@ expect ":"
|
||||||
send -- "inbox\r"
|
send -- "inbox\r"
|
||||||
expect "Private messages"
|
expect "Private messages"
|
||||||
expect "(empty)"
|
expect "(empty)"
|
||||||
expect "r:refresh"
|
|
||||||
send -- "r"
|
|
||||||
expect "Private messages"
|
|
||||||
expect "q:close"
|
expect "q:close"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
|
|
@ -452,7 +358,7 @@ stty rows 8 columns 80
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "pageruser\r"
|
send -- "pageruser\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
|
|
@ -462,14 +368,6 @@ expect "j/k:滚动"
|
||||||
expect -re {\(1/[2-9][0-9]*\)}
|
expect -re {\(1/[2-9][0-9]*\)}
|
||||||
send -- "j"
|
send -- "j"
|
||||||
expect -re {\(2/[2-9][0-9]*\)}
|
expect -re {\(2/[2-9][0-9]*\)}
|
||||||
send -- "\033\[6~"
|
|
||||||
expect -re {\([3-9][0-9]*/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[5~"
|
|
||||||
expect -re {\([1-9][0-9]*/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[F"
|
|
||||||
expect -re {\([2-9][0-9]*/[2-9][0-9]*\)}
|
|
||||||
send -- "\033\[H"
|
|
||||||
expect -re {\(1/[2-9][0-9]*\)}
|
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
|
|
@ -489,44 +387,13 @@ else
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COMMAND_INPUT_WRAP_SCRIPT="$STATE_DIR/command-input-wrap.expect"
|
|
||||||
cat >"$COMMAND_INPUT_WRAP_SCRIPT" <<EOF
|
|
||||||
set timeout 10
|
|
||||||
stty rows 10 columns 40
|
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
|
||||||
sleep 1
|
|
||||||
send -- "wrapcmd\r"
|
|
||||||
expect "Esc NORMAL"
|
|
||||||
send -- "\033"
|
|
||||||
expect "NORMAL"
|
|
||||||
send -- ":"
|
|
||||||
expect ":"
|
|
||||||
send -- "search aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaatail"
|
|
||||||
expect -re {<a+tail}
|
|
||||||
send -- "\003"
|
|
||||||
expect "NORMAL"
|
|
||||||
sleep 0.2
|
|
||||||
send -- "\003"
|
|
||||||
expect eof
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if expect "$COMMAND_INPUT_WRAP_SCRIPT" >"$STATE_DIR/command-input-wrap.log" 2>&1; then
|
|
||||||
echo "✓ long command input stays on one status line"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "x long command input display failed"
|
|
||||||
sed -n '1,220p' "$STATE_DIR/command-input-wrap.log"
|
|
||||||
sed -n '1,120p' "$STATE_DIR/server.log"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
SYSTEM_MESSAGES_SCRIPT="$STATE_DIR/system-messages.expect"
|
SYSTEM_MESSAGES_SCRIPT="$STATE_DIR/system-messages.expect"
|
||||||
cat >"$SYSTEM_MESSAGES_SCRIPT" <<EOF
|
cat >"$SYSTEM_MESSAGES_SCRIPT" <<EOF
|
||||||
set timeout 10
|
set timeout 10
|
||||||
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
spawn ssh $SSH_OPTS anonymous@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "systemuser\r"
|
send -- "systemuser\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
|
|
@ -573,7 +440,7 @@ expect "公告"
|
||||||
expect "维护窗口"
|
expect "维护窗口"
|
||||||
expect "按任意键继续"
|
expect "按任意键继续"
|
||||||
send -- "x"
|
send -- "x"
|
||||||
expect "INSERT"
|
expect "NORMAL"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
send -- "\003"
|
send -- "\003"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
|
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Maintenance-script regression tests for scripts/logrotate.sh.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
SCRIPT="../scripts/logrotate.sh"
|
|
||||||
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-logrotate-test.XXXXXX")
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$STATE_DIR"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
pass() {
|
|
||||||
echo "✓ $1"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "✗ $1"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
archive_payload() {
|
|
||||||
archive=$1
|
|
||||||
case "$archive" in
|
|
||||||
*.gz) gzip -cd "$archive" ;;
|
|
||||||
*) cat "$archive" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== TNT Logrotate Tests ==="
|
|
||||||
|
|
||||||
if [ ! -x "$SCRIPT" ]; then
|
|
||||||
echo "Error: script $SCRIPT not found or not executable."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
MISSING_OUTPUT=$("$SCRIPT" "$STATE_DIR/missing.log" 100 10 2>&1)
|
|
||||||
MISSING_STATUS=$?
|
|
||||||
printf '%s\n' "$MISSING_OUTPUT" | grep -q 'does not exist'
|
|
||||||
if [ "$MISSING_STATUS" -eq 0 ] && [ $? -eq 0 ]; then
|
|
||||||
pass "missing log is a successful no-op"
|
|
||||||
else
|
|
||||||
fail "missing log handling"
|
|
||||||
printf '%s\n' "$MISSING_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOG="$STATE_DIR/messages.log"
|
|
||||||
cat > "$LOG" <<'EOF'
|
|
||||||
2026-01-01T00:00:01Z|alice|one
|
|
||||||
2026-01-01T00:00:02Z|bob|two
|
|
||||||
2026-01-01T00:00:03Z|carol|three
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if "$SCRIPT" "$LOG" 100 2 >/dev/null 2>&1 &&
|
|
||||||
grep -q 'alice|one' "$LOG" &&
|
|
||||||
[ "$(ls "$LOG".* 2>/dev/null | wc -l | tr -d ' ')" -eq 0 ]; then
|
|
||||||
pass "small log stays unmodified"
|
|
||||||
else
|
|
||||||
fail "small log no-op"
|
|
||||||
cat "$LOG" 2>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
ROTATE_OUTPUT=$("$SCRIPT" "$LOG" 0 2 2>&1)
|
|
||||||
ROTATE_STATUS=$?
|
|
||||||
ARCHIVE=$(ls "$LOG".*.gz "$LOG".[0-9]* 2>/dev/null | head -n 1)
|
|
||||||
if [ "$ROTATE_STATUS" -eq 0 ] &&
|
|
||||||
printf '%s\n' "$ROTATE_OUTPUT" | grep -q 'kept last 2 lines' &&
|
|
||||||
! grep -q 'alice|one' "$LOG" &&
|
|
||||||
grep -q 'bob|two' "$LOG" &&
|
|
||||||
grep -q 'carol|three' "$LOG" &&
|
|
||||||
[ -n "$ARCHIVE" ] &&
|
|
||||||
archive_payload "$ARCHIVE" | grep -q 'alice|one'; then
|
|
||||||
pass "oversize log is archived and compacted"
|
|
||||||
else
|
|
||||||
fail "oversize rotation"
|
|
||||||
printf '%s\n' "$ROTATE_OUTPUT"
|
|
||||||
cat "$LOG" 2>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
DRY_LOG="$STATE_DIR/dry.log"
|
|
||||||
printf 'line1\nline2\nline3\n' > "$DRY_LOG"
|
|
||||||
DRY_BEFORE=$(cat "$DRY_LOG")
|
|
||||||
DRY_OUTPUT=$("$SCRIPT" --dry-run "$DRY_LOG" 0 1 2>&1)
|
|
||||||
DRY_STATUS=$?
|
|
||||||
if [ "$DRY_STATUS" -eq 0 ] &&
|
|
||||||
[ "$(cat "$DRY_LOG")" = "$DRY_BEFORE" ] &&
|
|
||||||
printf '%s\n' "$DRY_OUTPUT" | grep -q 'would archive'; then
|
|
||||||
pass "dry run does not modify the log"
|
|
||||||
else
|
|
||||||
fail "dry run handling"
|
|
||||||
printf '%s\n' "$DRY_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
INVALID_OUTPUT=$("$SCRIPT" "$LOG" nope 2 2>&1)
|
|
||||||
INVALID_STATUS=$?
|
|
||||||
if [ "$INVALID_STATUS" -eq 64 ] &&
|
|
||||||
printf '%s\n' "$INVALID_OUTPUT" | grep -q 'invalid max size'; then
|
|
||||||
pass "invalid arguments exit 64"
|
|
||||||
else
|
|
||||||
fail "invalid argument status"
|
|
||||||
printf '%s\n' "$INVALID_OUTPUT"
|
|
||||||
echo "exit status: $INVALID_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DIR_OUTPUT=$("$SCRIPT" "$STATE_DIR" 0 1 2>&1)
|
|
||||||
DIR_STATUS=$?
|
|
||||||
if [ "$DIR_STATUS" -eq 1 ] &&
|
|
||||||
printf '%s\n' "$DIR_OUTPUT" | grep -q 'not a regular file'; then
|
|
||||||
pass "non-regular log path is rejected"
|
|
||||||
else
|
|
||||||
fail "non-regular path handling"
|
|
||||||
printf '%s\n' "$DIR_OUTPUT"
|
|
||||||
echo "exit status: $DIR_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
RET_LOG="$STATE_DIR/retention.log"
|
|
||||||
printf 'a\nb\nc\n' > "$RET_LOG"
|
|
||||||
printf old1 > "$RET_LOG.20000101T000000Z.gz"
|
|
||||||
sleep 1
|
|
||||||
printf old2 > "$RET_LOG.20010101T000000Z.gz"
|
|
||||||
sleep 1
|
|
||||||
printf old3 > "$RET_LOG.20020101T000000Z.gz"
|
|
||||||
|
|
||||||
if "$SCRIPT" --keep-archives 2 "$RET_LOG" 100 2 >/dev/null 2>&1 &&
|
|
||||||
[ "$(ls "$RET_LOG".*.gz 2>/dev/null | wc -l | tr -d ' ')" -eq 2 ]; then
|
|
||||||
pass "archive retention removes older archives"
|
|
||||||
else
|
|
||||||
fail "archive retention"
|
|
||||||
ls "$RET_LOG".* 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "PASSED: $PASS"
|
|
||||||
echo "FAILED: $FAIL"
|
|
||||||
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
|
|
||||||
exit "$FAIL"
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Offline messages.log check/recover regression tests.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
BIN="../tnt"
|
|
||||||
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-log-tool-test.XXXXXX")
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
rm -rf "$STATE_DIR"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
pass() {
|
|
||||||
echo "✓ $1"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "✗ $1"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
ts_now() {
|
|
||||||
date -u +%Y-%m-%dT%H:%M:%SZ
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== TNT Message Log Tool Tests ==="
|
|
||||||
|
|
||||||
if [ ! -x "$BIN" ]; then
|
|
||||||
echo "Error: binary $BIN not found. Run make first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TS=$(ts_now)
|
|
||||||
CLEAN_LOG="$STATE_DIR/clean.log"
|
|
||||||
cat > "$CLEAN_LOG" <<EOF
|
|
||||||
$TS|alice|one
|
|
||||||
$TS|bob|two
|
|
||||||
EOF
|
|
||||||
|
|
||||||
CHECK_OUTPUT=$("$BIN" --log-check "$CLEAN_LOG" 2>&1)
|
|
||||||
CHECK_STATUS=$?
|
|
||||||
printf '%s\n' "$CHECK_OUTPUT" | grep -q '^valid_records 2$' &&
|
|
||||||
printf '%s\n' "$CHECK_OUTPUT" | grep -q '^invalid_records 0$'
|
|
||||||
if [ "$CHECK_STATUS" -eq 0 ] && [ $? -eq 0 ]; then
|
|
||||||
pass "clean log check exits 0"
|
|
||||||
else
|
|
||||||
fail "clean log check"
|
|
||||||
printf '%s\n' "$CHECK_OUTPUT"
|
|
||||||
echo "exit status: $CHECK_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
BAD_LOG="$STATE_DIR/bad.log"
|
|
||||||
cat > "$BAD_LOG" <<EOF
|
|
||||||
$TS|alice|one
|
|
||||||
$TS|mallory|extra|pipe
|
|
||||||
$TS|bob|two
|
|
||||||
EOF
|
|
||||||
printf '%s|partial|unterminated' "$TS" >> "$BAD_LOG"
|
|
||||||
|
|
||||||
BAD_CHECK_OUTPUT=$("$BIN" --log-check "$BAD_LOG" 2>&1)
|
|
||||||
BAD_CHECK_STATUS=$?
|
|
||||||
printf '%s\n' "$BAD_CHECK_OUTPUT" | grep -q '^records_seen 4$' &&
|
|
||||||
printf '%s\n' "$BAD_CHECK_OUTPUT" | grep -q '^valid_records 2$' &&
|
|
||||||
printf '%s\n' "$BAD_CHECK_OUTPUT" | grep -q '^invalid_records 2$' &&
|
|
||||||
printf '%s\n' "$BAD_CHECK_OUTPUT" | grep -q '^first_invalid_line 2$'
|
|
||||||
if [ "$BAD_CHECK_STATUS" -eq 1 ] && [ $? -eq 0 ]; then
|
|
||||||
pass "bad log check reports skipped records"
|
|
||||||
else
|
|
||||||
fail "bad log check"
|
|
||||||
printf '%s\n' "$BAD_CHECK_OUTPUT"
|
|
||||||
echo "exit status: $BAD_CHECK_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
RECOVERED="$STATE_DIR/recovered.log"
|
|
||||||
RECOVER_REPORT="$STATE_DIR/recover.report"
|
|
||||||
"$BIN" --log-recover "$BAD_LOG" > "$RECOVERED" 2> "$RECOVER_REPORT"
|
|
||||||
RECOVER_STATUS=$?
|
|
||||||
if [ "$RECOVER_STATUS" -eq 1 ] &&
|
|
||||||
grep -q '^valid_records 2$' "$RECOVER_REPORT" &&
|
|
||||||
grep -q '^invalid_records 2$' "$RECOVER_REPORT" &&
|
|
||||||
grep -q "$TS|alice|one" "$RECOVERED" &&
|
|
||||||
grep -q "$TS|bob|two" "$RECOVERED" &&
|
|
||||||
! grep -q 'mallory' "$RECOVERED" &&
|
|
||||||
! grep -q 'partial' "$RECOVERED"; then
|
|
||||||
pass "recover writes valid records and reports skipped records"
|
|
||||||
else
|
|
||||||
fail "bad log recovery"
|
|
||||||
cat "$RECOVERED" 2>/dev/null
|
|
||||||
cat "$RECOVER_REPORT" 2>/dev/null
|
|
||||||
echo "exit status: $RECOVER_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
MISSING_OUTPUT=$("$BIN" --log-check "$STATE_DIR/missing.log" 2>&1)
|
|
||||||
MISSING_STATUS=$?
|
|
||||||
if [ "$MISSING_STATUS" -eq 1 ] &&
|
|
||||||
printf '%s\n' "$MISSING_OUTPUT" | grep -q 'No such file'; then
|
|
||||||
pass "missing log exits 1"
|
|
||||||
else
|
|
||||||
fail "missing log handling"
|
|
||||||
printf '%s\n' "$MISSING_OUTPUT"
|
|
||||||
echo "exit status: $MISSING_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
USAGE_OUTPUT=$("$BIN" --log-check 2>&1)
|
|
||||||
USAGE_STATUS=$?
|
|
||||||
if [ "$USAGE_STATUS" -eq 64 ] &&
|
|
||||||
printf '%s\n' "$USAGE_OUTPUT" | grep -q 'Option requires argument: --log-check'; then
|
|
||||||
pass "missing log-check argument exits 64"
|
|
||||||
else
|
|
||||||
fail "missing log-check argument"
|
|
||||||
printf '%s\n' "$USAGE_OUTPUT"
|
|
||||||
echo "exit status: $USAGE_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CONFLICT_OUTPUT=$("$BIN" --log-check "$CLEAN_LOG" --log-recover "$CLEAN_LOG" 2>&1)
|
|
||||||
CONFLICT_STATUS=$?
|
|
||||||
if [ "$CONFLICT_STATUS" -eq 64 ] &&
|
|
||||||
printf '%s\n' "$CONFLICT_OUTPUT" | grep -q 'Invalid --log-check: --log-recover'; then
|
|
||||||
pass "conflicting log modes exit 64"
|
|
||||||
else
|
|
||||||
fail "conflicting log modes"
|
|
||||||
printf '%s\n' "$CONFLICT_OUTPUT"
|
|
||||||
echo "exit status: $CONFLICT_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "PASSED: $PASS"
|
|
||||||
echo "FAILED: $FAIL"
|
|
||||||
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
|
|
||||||
exit "$FAIL"
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Slow interactive-client regression test for TNT.
|
|
||||||
# Usage: ./test_slow_client.sh [hold_seconds] [burst_chars]
|
|
||||||
|
|
||||||
PORT=${PORT:-2222}
|
|
||||||
HOLD_SECONDS=${1:-8}
|
|
||||||
BURST_CHARS=${2:-1600}
|
|
||||||
BIN="../tnt"
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-slow-client-test.XXXXXX")
|
|
||||||
SERVER_PID=""
|
|
||||||
SLOW_PID=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ -n "$SLOW_PID" ]; then
|
|
||||||
kill "$SLOW_PID" 2>/dev/null || true
|
|
||||||
wait "$SLOW_PID" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
exec 3>&- 2>/dev/null || true
|
|
||||||
if [ -n "$SERVER_PID" ]; then
|
|
||||||
kill "$SERVER_PID" 2>/dev/null || true
|
|
||||||
wait "$SERVER_PID" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
rm -rf "$STATE_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
case "$HOLD_SECONDS" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
echo "Error: hold_seconds must be a positive integer"
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$BURST_CHARS" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
echo "Error: burst_chars must be a positive integer"
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "$HOLD_SECONDS" -lt 1 ] || [ "$BURST_CHARS" -lt 1 ]; then
|
|
||||||
echo "Error: hold_seconds and burst_chars must be positive"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$BIN" ]; then
|
|
||||||
echo "Error: Binary $BIN not found. Run make first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SSH_EXEC_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR -o ConnectTimeout=5 -p $PORT"
|
|
||||||
SSH_TTY_OPTS="-e none -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5 -p $PORT"
|
|
||||||
|
|
||||||
run_ssh_timeout() {
|
|
||||||
seconds=$1
|
|
||||||
outfile=$2
|
|
||||||
shift 2
|
|
||||||
|
|
||||||
ssh $SSH_EXEC_OPTS "$@" >"$outfile" 2>&1 &
|
|
||||||
cmd_pid=$!
|
|
||||||
elapsed=0
|
|
||||||
|
|
||||||
while [ "$elapsed" -lt "$seconds" ]; do
|
|
||||||
if ! kill -0 "$cmd_pid" 2>/dev/null; then
|
|
||||||
wait "$cmd_pid"
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
elapsed=$((elapsed + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if kill -0 "$cmd_pid" 2>/dev/null; then
|
|
||||||
kill "$cmd_pid" 2>/dev/null || true
|
|
||||||
wait "$cmd_pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
return 124
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_health() {
|
|
||||||
out=""
|
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
|
||||||
if [ -n "$SERVER_PID" ] && ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
out=$(ssh $SSH_EXEC_OPTS localhost health 2>/dev/null || true)
|
|
||||||
[ "$out" = "ok" ] && return 0
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_slow_user() {
|
|
||||||
out=""
|
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
|
||||||
if [ -n "$SERVER_PID" ] && ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
out=$(ssh $SSH_EXEC_OPTS localhost users --json 2>/dev/null || true)
|
|
||||||
printf '%s\n' "$out" | grep -q '"slow"' && return 0
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== TNT Slow Client Test ==="
|
|
||||||
echo "hold=${HOLD_SECONDS}s burst_chars=$BURST_CHARS port=$PORT"
|
|
||||||
|
|
||||||
TNT_LANG=en "$BIN" \
|
|
||||||
--bind 127.0.0.1 \
|
|
||||||
--public-host slow.local \
|
|
||||||
--max-connections 32 \
|
|
||||||
--max-conn-per-ip 32 \
|
|
||||||
--max-conn-rate-per-ip 64 \
|
|
||||||
--rate-limit 0 \
|
|
||||||
--idle-timeout 0 \
|
|
||||||
--ssh-log-level 1 \
|
|
||||||
-p "$PORT" \
|
|
||||||
-d "$STATE_DIR" >"$STATE_DIR/server.log" 2>&1 &
|
|
||||||
SERVER_PID=$!
|
|
||||||
|
|
||||||
if wait_for_health; then
|
|
||||||
echo "✓ server started"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ server failed to start"
|
|
||||||
sed -n '1,160p' "$STATE_DIR/server.log"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SLOW_FIFO="$STATE_DIR/slow.out"
|
|
||||||
mkfifo "$SLOW_FIFO"
|
|
||||||
exec 3<>"$SLOW_FIFO"
|
|
||||||
|
|
||||||
(
|
|
||||||
printf 'slow\n'
|
|
||||||
sleep 2
|
|
||||||
i=0
|
|
||||||
while [ "$i" -lt "$BURST_CHARS" ]; do
|
|
||||||
printf 'x'
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
sleep "$HOLD_SECONDS"
|
|
||||||
) | ssh $SSH_TTY_OPTS slow@127.0.0.1 >"$SLOW_FIFO" 2>"$STATE_DIR/slow.err" &
|
|
||||||
SLOW_PID=$!
|
|
||||||
|
|
||||||
if wait_for_slow_user; then
|
|
||||||
echo "✓ deliberately unread interactive client reached chat"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ slow client did not reach chat"
|
|
||||||
sed -n '1,120p' "$STATE_DIR/slow.err"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
if run_ssh_timeout 5 "$STATE_DIR/health.out" localhost health &&
|
|
||||||
grep -qx 'ok' "$STATE_DIR/health.out"; then
|
|
||||||
echo "✓ health stayed responsive while slow client was pressured"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ health blocked or returned unexpected output"
|
|
||||||
cat "$STATE_DIR/health.out" 2>/dev/null || true
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if run_ssh_timeout 5 "$STATE_DIR/stats.out" localhost stats --json &&
|
|
||||||
grep -q '"status":"ok"' "$STATE_DIR/stats.out"; then
|
|
||||||
echo "✓ stats stayed responsive while slow client was pressured"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ stats blocked or returned unexpected output"
|
|
||||||
cat "$STATE_DIR/stats.out" 2>/dev/null || true
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
FLOOD_FAIL=0
|
|
||||||
i=1
|
|
||||||
while [ "$i" -le 8 ]; do
|
|
||||||
msg=$(printf 'slow-client responsive post %02d %0900d' "$i" 0)
|
|
||||||
if ! run_ssh_timeout 5 "$STATE_DIR/post-$i.out" probe@localhost post "$msg" ||
|
|
||||||
! grep -qx 'posted' "$STATE_DIR/post-$i.out"; then
|
|
||||||
echo "✗ post blocked or failed during slow-client pressure at $i/8"
|
|
||||||
cat "$STATE_DIR/post-$i.out" 2>/dev/null || true
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
FLOOD_FAIL=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$FLOOD_FAIL" -eq 0 ]; then
|
|
||||||
echo "✓ post path stayed responsive during slow-client pressure"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if run_ssh_timeout 5 "$STATE_DIR/tail.out" localhost "tail -n 5" &&
|
|
||||||
grep -q 'slow-client responsive post 08' "$STATE_DIR/tail.out"; then
|
|
||||||
echo "✓ tail sees messages posted during slow-client pressure"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ tail missing slow-client pressure messages"
|
|
||||||
cat "$STATE_DIR/tail.out" 2>/dev/null || true
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
||||||
echo "✓ server survived slow-client pressure"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ server exited during slow-client pressure"
|
|
||||||
sed -n '1,160p' "$STATE_DIR/server.log"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "PASSED: $PASS"
|
|
||||||
echo "FAILED: $FAIL"
|
|
||||||
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
|
|
||||||
exit "$FAIL"
|
|
||||||
|
|
@ -73,33 +73,6 @@ case "$VERSION_OUTPUT" in
|
||||||
*) echo "✗ version output unexpected: $VERSION_OUTPUT"; FAIL=$((FAIL + 1)) ;;
|
*) echo "✗ version output unexpected: $VERSION_OUTPUT"; FAIL=$((FAIL + 1)) ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
HELP_ZH=$(TNT_LANG=zh "$BIN" --help 2>/dev/null || true)
|
|
||||||
printf '%s\n' "$HELP_ZH" | grep -q '^用法: tntctl \[options\] host command \[args...\]' &&
|
|
||||||
printf '%s\n' "$HELP_ZH" | grep -q '^选项:$'
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "✓ local help follows TNT_LANG"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ localized help output unexpected"
|
|
||||||
printf '%s\n' "$HELP_ZH"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$SSH_LOG"
|
|
||||||
BAD_PORT_ZH=$(PATH="$FAKE_BIN:$PATH" TNTCTL_SSH_LOG="$SSH_LOG" TNT_LANG=zh "$BIN" -p nope example.com health 2>&1)
|
|
||||||
BAD_PORT_STATUS=$?
|
|
||||||
if [ "$BAD_PORT_STATUS" -eq 64 ] &&
|
|
||||||
[ ! -f "$SSH_LOG" ] &&
|
|
||||||
printf '%s\n' "$BAD_PORT_ZH" | grep -q '^tntctl: 端口无效$'; then
|
|
||||||
echo "✓ local diagnostics follow TNT_LANG"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ localized diagnostic unexpected"
|
|
||||||
printf '%s\n' "$BAD_PORT_ZH"
|
|
||||||
[ -f "$SSH_LOG" ] && echo "fake ssh was invoked"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_ok "basic argv shape" "$BIN" -p 2222 example.com health
|
run_ok "basic argv shape" "$BIN" -p 2222 example.com health
|
||||||
grep -q '^example.com$' "$SSH_LOG" &&
|
grep -q '^example.com$' "$SSH_LOG" &&
|
||||||
grep -q '^health$' "$SSH_LOG"
|
grep -q '^health$' "$SSH_LOG"
|
||||||
|
|
@ -135,28 +108,6 @@ else
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
run_ok "dump command is accepted" "$BIN" example.com dump -n 1
|
|
||||||
grep -q '^dump -n 1$' "$SSH_LOG"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "✓ dump argv is forwarded as one remote command"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ dump argv unexpected"
|
|
||||||
cat "$SSH_LOG"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_ok "remote help alias is accepted" "$BIN" example.com --help
|
|
||||||
grep -q '^--help$' "$SSH_LOG"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "✓ --help after host is forwarded as exec help"
|
|
||||||
PASS=$((PASS + 1))
|
|
||||||
else
|
|
||||||
echo "✗ remote --help command unexpected"
|
|
||||||
cat "$SSH_LOG"
|
|
||||||
FAIL=$((FAIL + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
PATH="$FAKE_BIN:$PATH" TNTCTL_SSH_LOG="$SSH_LOG" "$BIN" example.com users --xml >/dev/null 2>&1
|
PATH="$FAKE_BIN:$PATH" TNTCTL_SSH_LOG="$SSH_LOG" "$BIN" example.com users --xml >/dev/null 2>&1
|
||||||
REMOTE_STATUS=$?
|
REMOTE_STATUS=$?
|
||||||
if [ "$REMOTE_STATUS" -eq 64 ]; then
|
if [ "$REMOTE_STATUS" -eq 64 ]; then
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ fi
|
||||||
SSH_OPTS="-e none -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p $PORT"
|
SSH_OPTS="-e none -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p $PORT"
|
||||||
SSH_EXEC_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
|
SSH_EXEC_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
|
||||||
BOB_READY="$STATE_DIR/bob.ready"
|
BOB_READY="$STATE_DIR/bob.ready"
|
||||||
PRIVATE_SENT="$STATE_DIR/private.sent"
|
ALICE_DONE="$STATE_DIR/alice.done"
|
||||||
|
|
||||||
wait_for_health() {
|
wait_for_health() {
|
||||||
out=""
|
out=""
|
||||||
|
|
@ -79,18 +79,15 @@ set timeout 30
|
||||||
spawn ssh $SSH_OPTS bob@127.0.0.1
|
spawn ssh $SSH_OPTS bob@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "bob\r"
|
send -- "bob\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
|
exec touch "$BOB_READY"
|
||||||
|
exec sh -c "while \[ ! -f '$ALICE_DONE' \]; do sleep 1; done"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- ":"
|
send -- ":"
|
||||||
expect ":"
|
expect ":"
|
||||||
send -- "inbox\r"
|
send -- "inbox\r"
|
||||||
expect "私信"
|
expect "私信"
|
||||||
expect "(空)"
|
|
||||||
expect "r:刷新"
|
|
||||||
exec touch "$BOB_READY"
|
|
||||||
exec sh -c "while \[ ! -f '$PRIVATE_SENT' \]; do sleep 1; done"
|
|
||||||
expect "私信"
|
|
||||||
expect "alice"
|
expect "alice"
|
||||||
expect "private lifecycle ping"
|
expect "private lifecycle ping"
|
||||||
expect "q:关闭"
|
expect "q:关闭"
|
||||||
|
|
@ -143,7 +140,7 @@ set timeout 30
|
||||||
spawn ssh $SSH_OPTS alice@127.0.0.1
|
spawn ssh $SSH_OPTS alice@127.0.0.1
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "alice\r"
|
send -- "alice\r"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- "?"
|
send -- "?"
|
||||||
|
|
@ -160,7 +157,7 @@ expect "q:关闭"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- "i"
|
send -- "i"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "hello lifecycle alpha\r"
|
send -- "hello lifecycle alpha\r"
|
||||||
sleep 1
|
sleep 1
|
||||||
send -- "\033"
|
send -- "\033"
|
||||||
|
|
@ -185,12 +182,6 @@ expect "alpha"
|
||||||
expect "q:关闭"
|
expect "q:关闭"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- "/alpha\r"
|
|
||||||
expect "搜索"
|
|
||||||
expect "alpha"
|
|
||||||
expect "q:关闭"
|
|
||||||
send -- "q"
|
|
||||||
expect "NORMAL"
|
|
||||||
send -- ":"
|
send -- ":"
|
||||||
expect ":"
|
expect ":"
|
||||||
send -- "mute-joins\r"
|
send -- "mute-joins\r"
|
||||||
|
|
@ -203,7 +194,6 @@ send -- ":"
|
||||||
expect ":"
|
expect ":"
|
||||||
send -- "msg bob private lifecycle ping\r"
|
send -- "msg bob private lifecycle ping\r"
|
||||||
expect "私信已发送给 bob"
|
expect "私信已发送给 bob"
|
||||||
exec touch "$PRIVATE_SENT"
|
|
||||||
expect "q:关闭"
|
expect "q:关闭"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
|
|
@ -215,9 +205,10 @@ expect "q:关闭"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
expect "NORMAL"
|
expect "NORMAL"
|
||||||
send -- "i"
|
send -- "i"
|
||||||
expect "Esc NORMAL"
|
expect ":help"
|
||||||
send -- "/me ships lifecycle\r"
|
send -- "/me ships lifecycle\r"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
exec touch "$ALICE_DONE"
|
||||||
send -- "\003"
|
send -- "\003"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
send -- "\003"
|
send -- "\003"
|
||||||
|
|
@ -231,11 +222,11 @@ else
|
||||||
echo "✗ primary user lifecycle failed"
|
echo "✗ primary user lifecycle failed"
|
||||||
sed -n '1,240p' "$STATE_DIR/alice.log"
|
sed -n '1,240p' "$STATE_DIR/alice.log"
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
touch "$PRIVATE_SENT"
|
touch "$ALICE_DONE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if wait "$BOB_PID" 2>/dev/null; then
|
if wait "$BOB_PID" 2>/dev/null; then
|
||||||
echo "✓ recipient inbox auto-refreshed after private message"
|
echo "✓ recipient read private-message inbox"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
else
|
else
|
||||||
echo "✗ recipient inbox journey failed"
|
echo "✗ recipient inbox journey failed"
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,9 @@ endif
|
||||||
# Source files
|
# Source files
|
||||||
UTF8_SRC = ../../src/utf8.c
|
UTF8_SRC = ../../src/utf8.c
|
||||||
MESSAGE_SRC = ../../src/message.c
|
MESSAGE_SRC = ../../src/message.c
|
||||||
MESSAGE_LOG_SRC = ../../src/message_log.c
|
|
||||||
COMMON_SRC = ../../src/common.c
|
COMMON_SRC = ../../src/common.c
|
||||||
CONFIG_DEFAULTS_SRC = ../../src/config_defaults.c
|
|
||||||
COMMAND_CATALOG_SRC = ../../src/command_catalog.c
|
COMMAND_CATALOG_SRC = ../../src/command_catalog.c
|
||||||
CLI_TEXT_SRC = ../../src/cli_text.c
|
CLI_TEXT_SRC = ../../src/cli_text.c
|
||||||
TNTCTL_TEXT_SRC = ../../src/tntctl_text.c
|
|
||||||
CHAT_ROOM_SRC = ../../src/chat_room.c
|
CHAT_ROOM_SRC = ../../src/chat_room.c
|
||||||
HISTORY_VIEW_SRC = ../../src/history_view.c
|
HISTORY_VIEW_SRC = ../../src/history_view.c
|
||||||
I18N_SRC = ../../src/i18n.c
|
I18N_SRC = ../../src/i18n.c
|
||||||
|
|
@ -28,7 +25,7 @@ HELP_TEXT_SRC = ../../src/help_text.c
|
||||||
MANUAL_TEXT_SRC = ../../src/manual_text.c
|
MANUAL_TEXT_SRC = ../../src/manual_text.c
|
||||||
RATELIMIT_SRC = ../../src/ratelimit.c
|
RATELIMIT_SRC = ../../src/ratelimit.c
|
||||||
|
|
||||||
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_exec_catalog test_help_text test_manual_text test_cli_text test_tntctl_text test_ratelimit test_config_defaults
|
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_exec_catalog test_help_text test_manual_text test_cli_text test_ratelimit
|
||||||
|
|
||||||
.PHONY: all clean run
|
.PHONY: all clean run
|
||||||
|
|
||||||
|
|
@ -37,10 +34,10 @@ all: $(TESTS)
|
||||||
test_utf8: test_utf8.c $(UTF8_SRC)
|
test_utf8: test_utf8.c $(UTF8_SRC)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
test_message: test_message.c $(MESSAGE_SRC) $(MESSAGE_LOG_SRC) $(UTF8_SRC) $(COMMON_SRC)
|
test_message: test_message.c $(MESSAGE_SRC) $(UTF8_SRC) $(COMMON_SRC)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
test_chat_room: test_chat_room.c $(CHAT_ROOM_SRC) $(MESSAGE_SRC) $(MESSAGE_LOG_SRC) $(UTF8_SRC) $(COMMON_SRC) $(CONFIG_DEFAULTS_SRC)
|
test_chat_room: test_chat_room.c $(CHAT_ROOM_SRC) $(MESSAGE_SRC) $(UTF8_SRC) $(COMMON_SRC)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
test_history_view: test_history_view.c $(HISTORY_VIEW_SRC)
|
test_history_view: test_history_view.c $(HISTORY_VIEW_SRC)
|
||||||
|
|
@ -67,13 +64,7 @@ test_manual_text: test_manual_text.c $(MANUAL_TEXT_SRC) $(COMMAND_CATALOG_SRC) $
|
||||||
test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC)
|
test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
test_tntctl_text: test_tntctl_text.c $(TNTCTL_TEXT_SRC) $(EXEC_CATALOG_SRC) $(COMMON_SRC)
|
test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
||||||
|
|
||||||
test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC) $(CONFIG_DEFAULTS_SRC)
|
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
||||||
|
|
||||||
test_config_defaults: test_config_defaults.c $(CONFIG_DEFAULTS_SRC) $(COMMON_SRC)
|
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
run: all
|
run: all
|
||||||
|
|
@ -110,14 +101,8 @@ run: all
|
||||||
@echo "=== Running CLI Text Tests ==="
|
@echo "=== Running CLI Text Tests ==="
|
||||||
./test_cli_text
|
./test_cli_text
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "=== Running tntctl Text Tests ==="
|
|
||||||
./test_tntctl_text
|
|
||||||
@echo ""
|
|
||||||
@echo "=== Running Rate Limit Tests ==="
|
@echo "=== Running Rate Limit Tests ==="
|
||||||
./test_ratelimit
|
./test_ratelimit
|
||||||
@echo ""
|
|
||||||
@echo "=== Running Config Defaults Tests ==="
|
|
||||||
./test_config_defaults
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(TESTS) *.o test_messages.log
|
rm -f $(TESTS) *.o test_messages.log
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ TEST(help_matches_language) {
|
||||||
assert(strstr(output, "Usage: tnt [options]") != NULL);
|
assert(strstr(output, "Usage: tnt [options]") != NULL);
|
||||||
assert(strstr(output, "--bind ADDR") != NULL);
|
assert(strstr(output, "--bind ADDR") != NULL);
|
||||||
assert(strstr(output, "--max-connections N") != NULL);
|
assert(strstr(output, "--max-connections N") != NULL);
|
||||||
assert(strstr(output, "--log-check FILE") != NULL);
|
|
||||||
assert(strstr(output, "--log-recover FILE") != NULL);
|
|
||||||
assert(strstr(output, "TNT_LANG") != NULL);
|
assert(strstr(output, "TNT_LANG") != NULL);
|
||||||
|
|
||||||
memset(output, 0, sizeof(output));
|
memset(output, 0, sizeof(output));
|
||||||
|
|
@ -41,7 +39,6 @@ TEST(help_matches_language) {
|
||||||
assert(strstr(output, "[选项]") == NULL);
|
assert(strstr(output, "[选项]") == NULL);
|
||||||
assert(strstr(output, "--public-host HOST") != NULL);
|
assert(strstr(output, "--public-host HOST") != NULL);
|
||||||
assert(strstr(output, "--idle-timeout SECONDS") != NULL);
|
assert(strstr(output, "--idle-timeout SECONDS") != NULL);
|
||||||
assert(strstr(output, "--log-check FILE") != NULL);
|
|
||||||
assert(strstr(output, "TNT_LANG") != NULL);
|
assert(strstr(output, "TNT_LANG") != NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,10 +51,6 @@ TEST(error_formats_match_language) {
|
||||||
"Invalid %s: %s\n") == 0);
|
"Invalid %s: %s\n") == 0);
|
||||||
assert(strcmp(cli_text_invalid_value_format(UI_LANG_ZH),
|
assert(strcmp(cli_text_invalid_value_format(UI_LANG_ZH),
|
||||||
"%s 无效: %s\n") == 0);
|
"%s 无效: %s\n") == 0);
|
||||||
assert(strcmp(cli_text_option_requires_arg_format(UI_LANG_EN),
|
|
||||||
"Option requires argument: %s\n") == 0);
|
|
||||||
assert(strcmp(cli_text_option_requires_arg_format(UI_LANG_ZH),
|
|
||||||
"选项需要参数: %s\n") == 0);
|
|
||||||
assert(strcmp(cli_text_unknown_option_format(UI_LANG_EN),
|
assert(strcmp(cli_text_unknown_option_format(UI_LANG_EN),
|
||||||
"Unknown option: %s\n") == 0);
|
"Unknown option: %s\n") == 0);
|
||||||
assert(strcmp(cli_text_unknown_option_format(UI_LANG_ZH),
|
assert(strcmp(cli_text_unknown_option_format(UI_LANG_ZH),
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
#include "config_defaults.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#define TEST(name) static void test_##name(void)
|
|
||||||
#define RUN_TEST(name) do { \
|
|
||||||
printf("Running %s... ", #name); \
|
|
||||||
test_##name(); \
|
|
||||||
printf("ok\n"); \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
TEST(specs_expose_runtime_defaults) {
|
|
||||||
assert(TNT_CONFIG_PORT.fallback == TNT_DEFAULT_PORT);
|
|
||||||
assert(TNT_CONFIG_MAX_CONNECTIONS.fallback ==
|
|
||||||
TNT_DEFAULT_MAX_CONNECTIONS);
|
|
||||||
assert(TNT_CONFIG_MAX_CONN_PER_IP.fallback ==
|
|
||||||
TNT_DEFAULT_MAX_CONN_PER_IP);
|
|
||||||
assert(TNT_CONFIG_MAX_CONN_RATE_PER_IP.fallback ==
|
|
||||||
TNT_DEFAULT_MAX_CONN_RATE_PER_IP);
|
|
||||||
assert(TNT_CONFIG_RATE_LIMIT.fallback ==
|
|
||||||
TNT_DEFAULT_RATE_LIMIT_ENABLED);
|
|
||||||
assert(TNT_CONFIG_IDLE_TIMEOUT.fallback == TNT_DEFAULT_IDLE_TIMEOUT);
|
|
||||||
assert(TNT_CONFIG_PORT.min_value == TNT_MIN_PORT);
|
|
||||||
assert(TNT_CONFIG_PORT.max_value == TNT_MAX_PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(parse_uses_spec_ranges) {
|
|
||||||
int out = 0;
|
|
||||||
|
|
||||||
assert(tnt_config_parse_int("2222", &TNT_CONFIG_PORT, &out));
|
|
||||||
assert(out == 2222);
|
|
||||||
assert(!tnt_config_parse_int("0", &TNT_CONFIG_PORT, &out));
|
|
||||||
assert(!tnt_config_parse_int("65536", &TNT_CONFIG_PORT, &out));
|
|
||||||
assert(!tnt_config_parse_int("abc", &TNT_CONFIG_PORT, &out));
|
|
||||||
assert(!tnt_config_parse_int("", &TNT_CONFIG_PORT, &out));
|
|
||||||
|
|
||||||
assert(tnt_config_parse_int("0", &TNT_CONFIG_IDLE_TIMEOUT, &out));
|
|
||||||
assert(out == 0);
|
|
||||||
assert(!tnt_config_parse_int("86401", &TNT_CONFIG_IDLE_TIMEOUT, &out));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(env_reader_uses_fallback_and_range) {
|
|
||||||
unsetenv(TNT_CONFIG_MAX_CONNECTIONS.env_name);
|
|
||||||
assert(tnt_config_env_int(&TNT_CONFIG_MAX_CONNECTIONS) ==
|
|
||||||
TNT_DEFAULT_MAX_CONNECTIONS);
|
|
||||||
|
|
||||||
setenv(TNT_CONFIG_MAX_CONNECTIONS.env_name, "128", 1);
|
|
||||||
assert(tnt_config_env_int(&TNT_CONFIG_MAX_CONNECTIONS) == 128);
|
|
||||||
|
|
||||||
setenv(TNT_CONFIG_MAX_CONNECTIONS.env_name, "0", 1);
|
|
||||||
assert(tnt_config_env_int(&TNT_CONFIG_MAX_CONNECTIONS) ==
|
|
||||||
TNT_DEFAULT_MAX_CONNECTIONS);
|
|
||||||
|
|
||||||
unsetenv(TNT_CONFIG_MAX_CONNECTIONS.env_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
printf("Running config defaults unit tests...\n\n");
|
|
||||||
RUN_TEST(specs_expose_runtime_defaults);
|
|
||||||
RUN_TEST(parse_uses_spec_ranges);
|
|
||||||
RUN_TEST(env_reader_uses_fallback_and_range);
|
|
||||||
printf("\nAll 3 tests passed!\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -28,14 +28,12 @@ TEST(generates_localized_exec_help) {
|
||||||
assert(strstr(en, "TNT exec interface") != NULL);
|
assert(strstr(en, "TNT exec interface") != NULL);
|
||||||
assert(strstr(en, "Commands:") != NULL);
|
assert(strstr(en, "Commands:") != NULL);
|
||||||
assert(strstr(en, "users [--json]") != NULL);
|
assert(strstr(en, "users [--json]") != NULL);
|
||||||
assert(strstr(en, "dump [N]") != NULL);
|
|
||||||
assert(strstr(en, "post MESSAGE") != NULL);
|
assert(strstr(en, "post MESSAGE") != NULL);
|
||||||
assert(strstr(en, "support") == NULL);
|
assert(strstr(en, "support") == NULL);
|
||||||
|
|
||||||
assert(strstr(zh, "TNT exec 接口") != NULL);
|
assert(strstr(zh, "TNT exec 接口") != NULL);
|
||||||
assert(strstr(zh, "命令:") != NULL);
|
assert(strstr(zh, "命令:") != NULL);
|
||||||
assert(strstr(zh, "users [--json]") != NULL);
|
assert(strstr(zh, "users [--json]") != NULL);
|
||||||
assert(strstr(zh, "dump [N]") != NULL);
|
|
||||||
assert(strstr(zh, "post MESSAGE") != NULL);
|
assert(strstr(zh, "post MESSAGE") != NULL);
|
||||||
assert(strstr(zh, "support") == NULL);
|
assert(strstr(zh, "support") == NULL);
|
||||||
assert_ascii_angle_placeholders(zh);
|
assert_ascii_angle_placeholders(zh);
|
||||||
|
|
@ -67,10 +65,6 @@ TEST(matches_exec_commands_and_args) {
|
||||||
assert(id == TNT_EXEC_COMMAND_TAIL);
|
assert(id == TNT_EXEC_COMMAND_TAIL);
|
||||||
assert(strcmp(args, "-n 20") == 0);
|
assert(strcmp(args, "-n 20") == 0);
|
||||||
|
|
||||||
assert(exec_catalog_match("dump -n 20", &id, &args));
|
|
||||||
assert(id == TNT_EXEC_COMMAND_DUMP);
|
|
||||||
assert(strcmp(args, "-n 20") == 0);
|
|
||||||
|
|
||||||
assert(exec_catalog_match("post hello world", &id, &args));
|
assert(exec_catalog_match("post hello world", &id, &args));
|
||||||
assert(id == TNT_EXEC_COMMAND_POST);
|
assert(id == TNT_EXEC_COMMAND_POST);
|
||||||
assert(strcmp(args, "hello world") == 0);
|
assert(strcmp(args, "hello world") == 0);
|
||||||
|
|
@ -96,9 +90,6 @@ TEST(validates_argument_shapes) {
|
||||||
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_TAIL, NULL));
|
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_TAIL, NULL));
|
||||||
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_TAIL, "-n 20"));
|
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_TAIL, "-n 20"));
|
||||||
|
|
||||||
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_DUMP, NULL));
|
|
||||||
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_DUMP, "-n 20"));
|
|
||||||
|
|
||||||
assert(!exec_catalog_args_valid(TNT_EXEC_COMMAND_POST, NULL));
|
assert(!exec_catalog_args_valid(TNT_EXEC_COMMAND_POST, NULL));
|
||||||
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_POST, "hello"));
|
assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_POST, "hello"));
|
||||||
}
|
}
|
||||||
|
|
@ -120,18 +111,8 @@ TEST(generates_localized_usage) {
|
||||||
memset(en, 0, sizeof(en));
|
memset(en, 0, sizeof(en));
|
||||||
en_pos = 0;
|
en_pos = 0;
|
||||||
exec_catalog_append_usage(en, sizeof(en), &en_pos,
|
exec_catalog_append_usage(en, sizeof(en), &en_pos,
|
||||||
TNT_EXEC_COMMAND_DUMP, (ui_lang_t)99);
|
TNT_EXEC_COMMAND_TAIL, (ui_lang_t)99);
|
||||||
assert(strcmp(en, "dump: usage: dump [N] | dump -n N\n") == 0);
|
assert(strcmp(en, "tail: usage: tail [N] | tail -n N\n") == 0);
|
||||||
}
|
|
||||||
|
|
||||||
TEST(generates_unique_command_list) {
|
|
||||||
char output[256] = {0};
|
|
||||||
size_t pos = 0;
|
|
||||||
|
|
||||||
exec_catalog_append_command_list(output, sizeof(output), &pos);
|
|
||||||
|
|
||||||
assert(strcmp(output,
|
|
||||||
"help, health, users, stats, tail, dump, post, exit") == 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
|
|
@ -141,7 +122,6 @@ int main(void) {
|
||||||
RUN_TEST(matches_exec_commands_and_args);
|
RUN_TEST(matches_exec_commands_and_args);
|
||||||
RUN_TEST(validates_argument_shapes);
|
RUN_TEST(validates_argument_shapes);
|
||||||
RUN_TEST(generates_localized_usage);
|
RUN_TEST(generates_localized_usage);
|
||||||
RUN_TEST(generates_unique_command_list);
|
|
||||||
|
|
||||||
printf("\n✓ All %d tests passed!\n", tests_passed);
|
printf("\n✓ All %d tests passed!\n", tests_passed);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ TEST(full_help_matches_language) {
|
||||||
assert(strstr(en, "AVAILABLE COMMANDS") != NULL);
|
assert(strstr(en, "AVAILABLE COMMANDS") != NULL);
|
||||||
assert(strstr(en, "COMMAND OUTPUT KEYS") != NULL);
|
assert(strstr(en, "COMMAND OUTPUT KEYS") != NULL);
|
||||||
assert(strstr(en, ":inbox") != NULL);
|
assert(strstr(en, ":inbox") != NULL);
|
||||||
assert(strstr(en, "Refresh live output") != NULL);
|
|
||||||
assert(strstr(en, ":support") == NULL);
|
assert(strstr(en, ":support") == NULL);
|
||||||
assert(strstr(en, ":commands") == NULL);
|
assert(strstr(en, ":commands") == NULL);
|
||||||
assert(strstr(en, "Cycle UI language") != NULL);
|
assert(strstr(en, "Cycle UI language") != NULL);
|
||||||
|
|
@ -39,7 +38,6 @@ TEST(full_help_matches_language) {
|
||||||
assert(strstr(zh, "可用命令") != NULL);
|
assert(strstr(zh, "可用命令") != NULL);
|
||||||
assert(strstr(zh, "命令输出按键") != NULL);
|
assert(strstr(zh, "命令输出按键") != NULL);
|
||||||
assert(strstr(zh, ":inbox") != NULL);
|
assert(strstr(zh, ":inbox") != NULL);
|
||||||
assert(strstr(zh, "刷新动态输出") != NULL);
|
|
||||||
assert(strstr(zh, "/me <action>") != NULL);
|
assert(strstr(zh, "/me <action>") != NULL);
|
||||||
assert(strstr(zh, "@username") != NULL);
|
assert(strstr(zh, "@username") != NULL);
|
||||||
assert(strstr(zh, "<动作>") == NULL);
|
assert(strstr(zh, "<动作>") == NULL);
|
||||||
|
|
|
||||||
|
|
@ -80,21 +80,10 @@ TEST(default_uses_locale_when_no_tnt_lang) {
|
||||||
|
|
||||||
TEST(text_lookup_matches_language) {
|
TEST(text_lookup_matches_language) {
|
||||||
i18n_string_t sample = I18N_STRING("fallback", "替代");
|
i18n_string_t sample = I18N_STRING("fallback", "替代");
|
||||||
i18n_string_t mapped = I18N_STRING_MAP(
|
|
||||||
I18N_EN("mapped fallback"),
|
|
||||||
I18N_ZH("映射替代")
|
|
||||||
);
|
|
||||||
i18n_string_t english_only = I18N_STRING_MAP(
|
|
||||||
I18N_EN("english only")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(strcmp(i18n_string(sample, UI_LANG_EN), "fallback") == 0);
|
assert(strcmp(i18n_string(sample, UI_LANG_EN), "fallback") == 0);
|
||||||
assert(strcmp(i18n_string(sample, UI_LANG_ZH), "替代") == 0);
|
assert(strcmp(i18n_string(sample, UI_LANG_ZH), "替代") == 0);
|
||||||
assert(strcmp(i18n_string(sample, (ui_lang_t)99), "fallback") == 0);
|
assert(strcmp(i18n_string(sample, (ui_lang_t)99), "fallback") == 0);
|
||||||
assert(strcmp(i18n_string(mapped, UI_LANG_EN), "mapped fallback") == 0);
|
|
||||||
assert(strcmp(i18n_string(mapped, UI_LANG_ZH), "映射替代") == 0);
|
|
||||||
assert(strcmp(i18n_string(english_only, UI_LANG_ZH),
|
|
||||||
"english only") == 0);
|
|
||||||
|
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_USERNAME_PROMPT),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_USERNAME_PROMPT),
|
||||||
"display name") != NULL);
|
"display name") != NULL);
|
||||||
|
|
@ -122,12 +111,6 @@ TEST(text_lookup_matches_language) {
|
||||||
"q:close") != NULL);
|
"q:close") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_COMMAND_OUTPUT_STATUS_FORMAT),
|
||||||
"q:关闭") != NULL);
|
"q:关闭") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_EN,
|
|
||||||
I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT),
|
|
||||||
"r:refresh") != NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH,
|
|
||||||
I18N_COMMAND_OUTPUT_REFRESH_STATUS_FORMAT),
|
|
||||||
"r:刷新") != NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_MOTD_CONTINUE_HINT),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_MOTD_CONTINUE_HINT),
|
||||||
"Press any key") != NULL);
|
"Press any key") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MOTD_CONTINUE_HINT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MOTD_CONTINUE_HINT),
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,8 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <limits.h>
|
|
||||||
|
|
||||||
#define TEST(name) static void test_##name()
|
#define TEST(name) static void test_##name()
|
||||||
#define RUN_TEST(name) do { \
|
#define RUN_TEST(name) do { \
|
||||||
|
|
@ -18,45 +16,12 @@
|
||||||
|
|
||||||
static int tests_passed = 0;
|
static int tests_passed = 0;
|
||||||
static const char *test_log = "test_messages.log";
|
static const char *test_log = "test_messages.log";
|
||||||
static char test_state_dir[PATH_MAX];
|
|
||||||
|
|
||||||
/* Helper: Clean up test log file */
|
/* Helper: Clean up test log file */
|
||||||
static void cleanup_test_log(void) {
|
static void cleanup_test_log(void) {
|
||||||
unlink(test_log);
|
unlink(test_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void cleanup_state_dir(void) {
|
|
||||||
if (test_state_dir[0] != '\0') {
|
|
||||||
char log_path[PATH_MAX];
|
|
||||||
snprintf(log_path, sizeof(log_path), "%s/messages.log", test_state_dir);
|
|
||||||
unlink(log_path);
|
|
||||||
rmdir(test_state_dir);
|
|
||||||
test_state_dir[0] = '\0';
|
|
||||||
}
|
|
||||||
unsetenv("TNT_STATE_DIR");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setup_state_dir(void) {
|
|
||||||
const char *tmp = getenv("TMPDIR");
|
|
||||||
|
|
||||||
cleanup_state_dir();
|
|
||||||
if (!tmp || tmp[0] == '\0') {
|
|
||||||
tmp = "/tmp";
|
|
||||||
}
|
|
||||||
snprintf(test_state_dir, sizeof(test_state_dir),
|
|
||||||
"%s/tnt-message-test.XXXXXX", tmp);
|
|
||||||
assert(mkdtemp(test_state_dir) != NULL);
|
|
||||||
assert(setenv("TNT_STATE_DIR", test_state_dir, 1) == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void format_rfc3339_now(char *buffer, size_t buf_size) {
|
|
||||||
time_t now = time(NULL);
|
|
||||||
struct tm tm_info;
|
|
||||||
|
|
||||||
gmtime_r(&now, &tm_info);
|
|
||||||
strftime(buffer, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test message initialization */
|
/* Test message initialization */
|
||||||
TEST(message_init) {
|
TEST(message_init) {
|
||||||
message_init();
|
message_init();
|
||||||
|
|
@ -157,104 +122,6 @@ TEST(message_save_basic) {
|
||||||
cleanup_test_log();
|
cleanup_test_log();
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(message_load_skips_malformed_records) {
|
|
||||||
char ts[64];
|
|
||||||
char log_path[PATH_MAX];
|
|
||||||
message_t *messages = NULL;
|
|
||||||
|
|
||||||
setup_state_dir();
|
|
||||||
format_rfc3339_now(ts, sizeof(ts));
|
|
||||||
snprintf(log_path, sizeof(log_path), "%s/messages.log", test_state_dir);
|
|
||||||
|
|
||||||
FILE *fp = fopen(log_path, "wb");
|
|
||||||
assert(fp != NULL);
|
|
||||||
fprintf(fp, "%s|alice|valid one\n", ts);
|
|
||||||
fprintf(fp, "not-a-date|bob|bad date\n");
|
|
||||||
fprintf(fp, "%s||empty user\n", ts);
|
|
||||||
fprintf(fp, "%s|mallory|extra|pipe\n", ts);
|
|
||||||
fprintf(fp, "%s|badutf|bad \xC3\x28\n", ts);
|
|
||||||
fprintf(fp, "%s|partial|truncated record", ts);
|
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
int count = message_load(&messages, 10);
|
|
||||||
assert(count == 1);
|
|
||||||
assert(strcmp(messages[0].username, "alice") == 0);
|
|
||||||
assert(strcmp(messages[0].content, "valid one") == 0);
|
|
||||||
free(messages);
|
|
||||||
cleanup_state_dir();
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(message_search_skips_malformed_records) {
|
|
||||||
char ts[64];
|
|
||||||
char log_path[PATH_MAX];
|
|
||||||
message_t *results = NULL;
|
|
||||||
|
|
||||||
setup_state_dir();
|
|
||||||
format_rfc3339_now(ts, sizeof(ts));
|
|
||||||
snprintf(log_path, sizeof(log_path), "%s/messages.log", test_state_dir);
|
|
||||||
|
|
||||||
FILE *fp = fopen(log_path, "wb");
|
|
||||||
assert(fp != NULL);
|
|
||||||
fprintf(fp, "%s|alice|needle valid\n", ts);
|
|
||||||
fprintf(fp, "%s|mallory|needle extra|pipe\n", ts);
|
|
||||||
fprintf(fp, "%s|partial|needle truncated", ts);
|
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
int count = message_search("needle", &results, 10);
|
|
||||||
assert(count == 1);
|
|
||||||
assert(strcmp(results[0].username, "alice") == 0);
|
|
||||||
assert(strcmp(results[0].content, "needle valid") == 0);
|
|
||||||
free(results);
|
|
||||||
cleanup_state_dir();
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(message_dump_exports_valid_records) {
|
|
||||||
char ts[64];
|
|
||||||
char log_path[PATH_MAX];
|
|
||||||
char expected_all[512];
|
|
||||||
char expected_last_two[512];
|
|
||||||
char *dump = NULL;
|
|
||||||
size_t dump_len = 0;
|
|
||||||
|
|
||||||
setup_state_dir();
|
|
||||||
format_rfc3339_now(ts, sizeof(ts));
|
|
||||||
snprintf(log_path, sizeof(log_path), "%s/messages.log", test_state_dir);
|
|
||||||
|
|
||||||
FILE *fp = fopen(log_path, "wb");
|
|
||||||
assert(fp != NULL);
|
|
||||||
fprintf(fp, "%s|alice|first valid\n", ts);
|
|
||||||
fprintf(fp, "%s|mallory|extra|pipe\n", ts);
|
|
||||||
fprintf(fp, "%s|bob|second valid\n", ts);
|
|
||||||
fprintf(fp, "%s|carol|third valid\n", ts);
|
|
||||||
fprintf(fp, "%s|partial|truncated record", ts);
|
|
||||||
fclose(fp);
|
|
||||||
|
|
||||||
snprintf(expected_all, sizeof(expected_all),
|
|
||||||
"%s|alice|first valid\n"
|
|
||||||
"%s|bob|second valid\n"
|
|
||||||
"%s|carol|third valid\n",
|
|
||||||
ts, ts, ts);
|
|
||||||
assert(message_dump_text(&dump, &dump_len, 0) == 0);
|
|
||||||
assert(dump != NULL);
|
|
||||||
assert(dump_len == strlen(expected_all));
|
|
||||||
assert(strcmp(dump, expected_all) == 0);
|
|
||||||
free(dump);
|
|
||||||
|
|
||||||
dump = NULL;
|
|
||||||
dump_len = 0;
|
|
||||||
snprintf(expected_last_two, sizeof(expected_last_two),
|
|
||||||
"%s|bob|second valid\n"
|
|
||||||
"%s|carol|third valid\n",
|
|
||||||
ts, ts);
|
|
||||||
assert(message_dump_text(&dump, &dump_len, 2) == 0);
|
|
||||||
assert(dump != NULL);
|
|
||||||
assert(dump_len == strlen(expected_last_two));
|
|
||||||
assert(strcmp(dump, expected_last_two) == 0);
|
|
||||||
free(dump);
|
|
||||||
|
|
||||||
cleanup_state_dir();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test edge cases */
|
/* Test edge cases */
|
||||||
TEST(message_edge_cases) {
|
TEST(message_edge_cases) {
|
||||||
message_t msg;
|
message_t msg;
|
||||||
|
|
@ -348,16 +215,12 @@ int main(void) {
|
||||||
RUN_TEST(message_format_unicode);
|
RUN_TEST(message_format_unicode);
|
||||||
RUN_TEST(message_format_width_limits);
|
RUN_TEST(message_format_width_limits);
|
||||||
RUN_TEST(message_save_basic);
|
RUN_TEST(message_save_basic);
|
||||||
RUN_TEST(message_load_skips_malformed_records);
|
|
||||||
RUN_TEST(message_search_skips_malformed_records);
|
|
||||||
RUN_TEST(message_dump_exports_valid_records);
|
|
||||||
RUN_TEST(message_edge_cases);
|
RUN_TEST(message_edge_cases);
|
||||||
RUN_TEST(message_special_characters);
|
RUN_TEST(message_special_characters);
|
||||||
RUN_TEST(message_buffer_safety);
|
RUN_TEST(message_buffer_safety);
|
||||||
RUN_TEST(message_timestamp_formats);
|
RUN_TEST(message_timestamp_formats);
|
||||||
|
|
||||||
cleanup_test_log();
|
cleanup_test_log();
|
||||||
cleanup_state_dir();
|
|
||||||
|
|
||||||
printf("\n✓ All %d tests passed!\n", tests_passed);
|
printf("\n✓ All %d tests passed!\n", tests_passed);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
/* Unit tests for tntctl local help and diagnostic text */
|
|
||||||
|
|
||||||
#include "../../include/tntctl_text.h"
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define TEST(name) static void test_##name()
|
|
||||||
#define RUN_TEST(name) do { \
|
|
||||||
printf("Running %s... ", #name); \
|
|
||||||
test_##name(); \
|
|
||||||
printf("✓\n"); \
|
|
||||||
tests_passed++; \
|
|
||||||
} while(0)
|
|
||||||
|
|
||||||
static int tests_passed = 0;
|
|
||||||
|
|
||||||
TEST(usage_matches_language) {
|
|
||||||
char en[2048] = {0};
|
|
||||||
char zh[2048] = {0};
|
|
||||||
size_t en_pos = 0;
|
|
||||||
size_t zh_pos = 0;
|
|
||||||
|
|
||||||
tntctl_text_append_usage(en, sizeof(en), &en_pos, UI_LANG_EN);
|
|
||||||
tntctl_text_append_usage(zh, sizeof(zh), &zh_pos, UI_LANG_ZH);
|
|
||||||
|
|
||||||
assert(strstr(en, "Usage: tntctl [options] host command [args...]") != NULL);
|
|
||||||
assert(strstr(en, "--host-key-checking MODE") != NULL);
|
|
||||||
assert(strstr(en,
|
|
||||||
"help, health, users, stats, tail, dump, post, exit") != NULL);
|
|
||||||
assert(strstr(zh, "用法: tntctl [options] host command [args...]") != NULL);
|
|
||||||
assert(strstr(zh, "OpenSSH 主机密钥模式") != NULL);
|
|
||||||
assert(strstr(zh,
|
|
||||||
"help, health, users, stats, tail, dump, post, exit") != NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(errors_match_language) {
|
|
||||||
assert(strcmp(tntctl_text(UI_LANG_EN, TNTCTL_TEXT_INVALID_PORT),
|
|
||||||
"invalid port") == 0);
|
|
||||||
assert(strcmp(tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_INVALID_PORT),
|
|
||||||
"端口无效") == 0);
|
|
||||||
assert(strcmp(tntctl_text(UI_LANG_EN, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT),
|
|
||||||
"unknown option: %s") == 0);
|
|
||||||
assert(strcmp(tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT),
|
|
||||||
"未知选项: %s") == 0);
|
|
||||||
assert(strcmp(tntctl_text((ui_lang_t)99, TNTCTL_TEXT_INVALID_PORT),
|
|
||||||
"invalid port") == 0);
|
|
||||||
assert(strcmp(tntctl_text(UI_LANG_EN,
|
|
||||||
(tntctl_text_id_t)TNTCTL_TEXT_COUNT), "") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
printf("Running tntctl text unit tests...\n\n");
|
|
||||||
|
|
||||||
RUN_TEST(usage_matches_language);
|
|
||||||
RUN_TEST(errors_match_language);
|
|
||||||
|
|
||||||
printf("\n✓ All %d tests passed!\n", tests_passed);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
61
tnt.1
61
tnt.1
|
|
@ -26,14 +26,6 @@ tnt \- anonymous SSH chat server with Vim\-style TUI
|
||||||
.IR level ]
|
.IR level ]
|
||||||
.RB [ \-V | \-\-version ]
|
.RB [ \-V | \-\-version ]
|
||||||
.RB [ \-h | \-\-help ]
|
.RB [ \-h | \-\-help ]
|
||||||
.br
|
|
||||||
.B tnt
|
|
||||||
.B \-\-log\-check
|
|
||||||
.I file
|
|
||||||
.br
|
|
||||||
.B tnt
|
|
||||||
.B \-\-log\-recover
|
|
||||||
.I file
|
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
.B tnt
|
.B tnt
|
||||||
is a multi\-user anonymous chat server accessed over SSH.
|
is a multi\-user anonymous chat server accessed over SSH.
|
||||||
|
|
@ -42,13 +34,6 @@ COMMAND modes.
|
||||||
Users connect with any standard SSH client; no account or registration is
|
Users connect with any standard SSH client; no account or registration is
|
||||||
needed.
|
needed.
|
||||||
.PP
|
.PP
|
||||||
In the 1.x series,
|
|
||||||
.B tnt
|
|
||||||
is the stable server process name.
|
|
||||||
Use
|
|
||||||
.BR tntctl (1)
|
|
||||||
for local control commands against a running server.
|
|
||||||
.PP
|
|
||||||
Messages are persisted to a log file and restored on server restart.
|
Messages are persisted to a log file and restored on server restart.
|
||||||
The server supports CJK and emoji input, rate limiting, access tokens, and
|
The server supports CJK and emoji input, rate limiting, access tokens, and
|
||||||
a non\-interactive exec interface for scripting.
|
a non\-interactive exec interface for scripting.
|
||||||
|
|
@ -125,18 +110,6 @@ Overrides the
|
||||||
.B TNT_SSH_LOG_LEVEL
|
.B TNT_SSH_LOG_LEVEL
|
||||||
environment variable.
|
environment variable.
|
||||||
.TP
|
.TP
|
||||||
.BR \-\-log\-check " " \fIfile\fR
|
|
||||||
Check a
|
|
||||||
.I messages.log
|
|
||||||
v1 file and print record counts.
|
|
||||||
Exits non-zero when invalid records are found or the file cannot be read.
|
|
||||||
.TP
|
|
||||||
.BR \-\-log\-recover " " \fIfile\fR
|
|
||||||
Write valid
|
|
||||||
.I messages.log
|
|
||||||
v1 records to standard output and print a recovery summary to standard error.
|
|
||||||
The source file is not modified.
|
|
||||||
.TP
|
|
||||||
.BR \-V ", " \-\-version
|
.BR \-V ", " \-\-version
|
||||||
Print version and exit.
|
Print version and exit.
|
||||||
.TP
|
.TP
|
||||||
|
|
@ -167,8 +140,6 @@ Press
|
||||||
to return to INSERT,
|
to return to INSERT,
|
||||||
.B :
|
.B :
|
||||||
to enter COMMAND mode,
|
to enter COMMAND mode,
|
||||||
.B /
|
|
||||||
to search message history,
|
|
||||||
.B ?
|
.B ?
|
||||||
to open the full key reference.
|
to open the full key reference.
|
||||||
.TP
|
.TP
|
||||||
|
|
@ -184,8 +155,6 @@ ESC Switch to NORMAL
|
||||||
Ctrl+W Delete last word
|
Ctrl+W Delete last word
|
||||||
Ctrl+U Clear input line
|
Ctrl+U Clear input line
|
||||||
Ctrl+C Switch to NORMAL
|
Ctrl+C Switch to NORMAL
|
||||||
Up/Down Browse sent message history
|
|
||||||
Tab Complete @mention
|
|
||||||
Paste Keep multi-line paste in the input buffer
|
Paste Keep multi-line paste in the input buffer
|
||||||
/me \fIaction\fR Send action message (e.g. /me waves)
|
/me \fIaction\fR Send action message (e.g. /me waves)
|
||||||
@\fIusername\fR Mention user (bell notification + highlight)
|
@\fIusername\fR Mention user (bell notification + highlight)
|
||||||
|
|
@ -202,7 +171,6 @@ Ctrl+F/Ctrl+B Scroll full page down/up
|
||||||
PageDown/PageUp Scroll full page down/up
|
PageDown/PageUp Scroll full page down/up
|
||||||
End/Home Jump to bottom/top
|
End/Home Jump to bottom/top
|
||||||
g/G Jump to top/bottom
|
g/G Jump to top/bottom
|
||||||
/ Search message history
|
|
||||||
i Switch to INSERT
|
i Switch to INSERT
|
||||||
: Enter COMMAND mode
|
: Enter COMMAND mode
|
||||||
? Open full key reference
|
? Open full key reference
|
||||||
|
|
@ -222,9 +190,8 @@ l l.
|
||||||
:w \fIuser text\fR Short alias for :msg
|
:w \fIuser text\fR Short alias for :msg
|
||||||
:inbox Show private messages
|
:inbox Show private messages
|
||||||
:last [\fIN\fR] Show last N messages from history (1\-50, default 10)
|
:last [\fIN\fR] Show last N messages from history (1\-50, default 10)
|
||||||
:search \fIkeyword\fR Case\-insensitive search; shows the last 15 matches
|
:search \fIkeyword\fR Case\-insensitive search across full message history
|
||||||
:mute\-joins Toggle join/leave system notifications on/off
|
:mute\-joins Toggle join/leave system notifications on/off
|
||||||
:lang Show current UI language
|
|
||||||
:lang \fIen|zh\fR Switch UI language for this session
|
:lang \fIen|zh\fR Switch UI language for this session
|
||||||
:help Show concise manual
|
:help Show concise manual
|
||||||
:clear Clear command output
|
:clear Clear command output
|
||||||
|
|
@ -232,25 +199,6 @@ l l.
|
||||||
Up/Down Browse command history
|
Up/Down Browse command history
|
||||||
ESC Cancel and return to NORMAL
|
ESC Cancel and return to NORMAL
|
||||||
.TE
|
.TE
|
||||||
.PP
|
|
||||||
Command output pages use the same paging keys as the help screen.
|
|
||||||
.TS
|
|
||||||
l l.
|
|
||||||
q, ESC Close output
|
|
||||||
j/k, arrows Scroll down/up
|
|
||||||
Ctrl+D/Ctrl+U Scroll half page down/up
|
|
||||||
Ctrl+F/Ctrl+B Scroll full page down/up
|
|
||||||
Space/b Scroll full page down/up
|
|
||||||
PageDown/PageUp Scroll full page down/up
|
|
||||||
End/Home Jump to bottom/top
|
|
||||||
g/G Jump to top/bottom
|
|
||||||
r Refresh live output (:inbox)
|
|
||||||
.TE
|
|
||||||
.PP
|
|
||||||
The
|
|
||||||
.B :inbox
|
|
||||||
page refreshes automatically when a new private message arrives while it is
|
|
||||||
open.
|
|
||||||
.SH EXEC INTERFACE
|
.SH EXEC INTERFACE
|
||||||
Commands can be run non\-interactively for scripting:
|
Commands can be run non\-interactively for scripting:
|
||||||
.PP
|
.PP
|
||||||
|
|
@ -259,7 +207,6 @@ ssh host \-p 2222 help
|
||||||
ssh host \-p 2222 users \-\-json
|
ssh host \-p 2222 users \-\-json
|
||||||
ssh host \-p 2222 stats \-\-json
|
ssh host \-p 2222 stats \-\-json
|
||||||
ssh host \-p 2222 tail 20
|
ssh host \-p 2222 tail 20
|
||||||
ssh host \-p 2222 dump \-n 100
|
|
||||||
ssh host \-p 2222 post "Hello from a script"
|
ssh host \-p 2222 post "Hello from a script"
|
||||||
ssh host \-p 2222 post "/me deploys v2.0"
|
ssh host \-p 2222 post "/me deploys v2.0"
|
||||||
ssh host \-p 2222 health
|
ssh host \-p 2222 health
|
||||||
|
|
@ -340,13 +287,9 @@ libssh log verbosity from 0 to 4 (default: 1).
|
||||||
.SH FILES
|
.SH FILES
|
||||||
.TP
|
.TP
|
||||||
.I messages.log
|
.I messages.log
|
||||||
Chat history in the TNT message log v1 format:
|
Chat history in RFC\ 3339 pipe\-delimited format
|
||||||
RFC\ 3339 UTC pipe\-delimited records
|
|
||||||
.RI ( timestamp | username | content ).
|
.RI ( timestamp | username | content ).
|
||||||
Stored in the state directory.
|
Stored in the state directory.
|
||||||
See
|
|
||||||
.I docs/MESSAGE_LOG.md
|
|
||||||
in the source distribution for parser and recovery rules.
|
|
||||||
.TP
|
.TP
|
||||||
.I host_key
|
.I host_key
|
||||||
RSA 4096\-bit host key, auto\-generated on first run.
|
RSA 4096\-bit host key, auto\-generated on first run.
|
||||||
|
|
|
||||||
7
tntctl.1
7
tntctl.1
|
|
@ -73,12 +73,6 @@ Print recent messages.
|
||||||
.B tail -n N
|
.B tail -n N
|
||||||
Print recent messages.
|
Print recent messages.
|
||||||
.TP
|
.TP
|
||||||
.B dump [N]
|
|
||||||
Export persisted messages.
|
|
||||||
.TP
|
|
||||||
.B dump -n N
|
|
||||||
Export persisted messages.
|
|
||||||
.TP
|
|
||||||
.B post MESSAGE
|
.B post MESSAGE
|
||||||
Post a message non-interactively.
|
Post a message non-interactively.
|
||||||
.TP
|
.TP
|
||||||
|
|
@ -88,7 +82,6 @@ Print the server exec help.
|
||||||
.nf
|
.nf
|
||||||
tntctl chat.example.com health
|
tntctl chat.example.com health
|
||||||
tntctl -p 2222 chat.example.com stats --json
|
tntctl -p 2222 chat.example.com stats --json
|
||||||
tntctl -p 2222 chat.example.com dump -n 100
|
|
||||||
tntctl -l operator chat.example.com post "service notice"
|
tntctl -l operator chat.example.com post "service notice"
|
||||||
tntctl --host-key-checking accept-new chat.example.com users
|
tntctl --host-key-checking accept-new chat.example.com users
|
||||||
.fi
|
.fi
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue