mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 04:34:38 +08:00
Add persisted message dump command
This commit is contained in:
parent
7b5a683557
commit
8b55a3d9ab
19 changed files with 395 additions and 5 deletions
|
|
@ -197,6 +197,7 @@ ssh -p 2222 chat.example.com health
|
|||
ssh -p 2222 chat.example.com stats --json
|
||||
ssh -p 2222 chat.example.com users
|
||||
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 chat.example.com post "/me deploys v2.0"
|
||||
```
|
||||
|
|
@ -212,6 +213,7 @@ around the same SSH exec interface:
|
|||
```sh
|
||||
tntctl chat.example.com health
|
||||
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"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
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 a public security policy, supported-version guidance, and GitHub issue
|
||||
templates for bug reports and feature requests.
|
||||
- Added `tntctl`, a thin local wrapper around the documented SSH exec
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ ssh -p 2222 chat.example.com health
|
|||
ssh -p 2222 chat.example.com stats --json
|
||||
ssh -p 2222 chat.example.com users --json
|
||||
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"
|
||||
```
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ The same commands can be run through `tntctl`:
|
|||
```sh
|
||||
tntctl chat.example.com health
|
||||
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 --host-key-checking accept-new chat.example.com users
|
||||
```
|
||||
|
|
@ -128,6 +130,22 @@ Prints recent in-memory messages as tab-separated lines:
|
|||
The current upper bound is `MAX_MESSAGES`. This command reads the live
|
||||
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`
|
||||
|
||||
Posts a message as the SSH login name and prints:
|
||||
|
|
|
|||
|
|
@ -53,6 +53,13 @@ Replay and search use the same strict parser. TNT skips records that are:
|
|||
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.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The v1 record format is stable for TNT 1.x. Future incompatible storage
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ INSERT MODE
|
|||
limit 1023 bytes/message; over-limit input rings bell
|
||||
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
|
||||
|
||||
STRUCTURE
|
||||
src/main.c entry, signals
|
||||
src/cli_text.c startup CLI text
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ Goal: make stored history durable, inspectable, and recoverable.
|
|||
- ✅ formalize the message log v1 format
|
||||
- ✅ keep persisted timestamps in UTC throughout write and replay
|
||||
- ✅ validate persisted UTF-8 and record structure before replay/search
|
||||
- ✅ provide an inspection/export command for persisted records
|
||||
- add log rotation and compaction tooling
|
||||
- provide an offline inspection/export command
|
||||
- define broader recovery tooling for truncated or partially corrupted logs
|
||||
|
||||
## Stage 4: Interactive UX
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ The product path should stay short:
|
|||
7. User uses commands only when needed: `:users`, `:msg`, `:inbox`, `:last`,
|
||||
`:search`, `:nick`, `:mute-joins`, and `:q`.
|
||||
8. Scripts and operators use `tntctl` or SSH exec commands for `health`,
|
||||
`stats`, `users`, `tail`, and `post`.
|
||||
`stats`, `users`, `tail`, `dump`, and `post`.
|
||||
|
||||
## TUI Experience Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ typedef enum {
|
|||
TNT_EXEC_COMMAND_USERS,
|
||||
TNT_EXEC_COMMAND_STATS,
|
||||
TNT_EXEC_COMMAND_TAIL,
|
||||
TNT_EXEC_COMMAND_DUMP,
|
||||
TNT_EXEC_COMMAND_POST,
|
||||
TNT_EXEC_COMMAND_EXIT
|
||||
} tnt_exec_command_id_t;
|
||||
|
|
|
|||
|
|
@ -26,4 +26,9 @@ 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. */
|
||||
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 */
|
||||
|
|
|
|||
62
src/exec.c
62
src/exec.c
|
|
@ -291,6 +291,45 @@ static int parse_tail_count(const char *args, int *count) {
|
|||
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) {
|
||||
int requested = 20;
|
||||
int total_messages;
|
||||
|
|
@ -347,6 +386,27 @@ static int exec_command_tail(client_t *client, const char *args) {
|
|||
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) {
|
||||
char content[MAX_MESSAGE_LEN];
|
||||
char username[MAX_USERNAME_LEN];
|
||||
|
|
@ -451,6 +511,8 @@ int exec_dispatch(client_t *client) {
|
|||
return exec_command_stats(client, args != NULL);
|
||||
case TNT_EXEC_COMMAND_TAIL:
|
||||
return exec_command_tail(client, args);
|
||||
case TNT_EXEC_COMMAND_DUMP:
|
||||
return exec_command_dump(client, args);
|
||||
case TNT_EXEC_COMMAND_POST:
|
||||
return exec_command_post(client, args);
|
||||
case TNT_EXEC_COMMAND_EXIT:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ static const exec_catalog_entry_t entries[] = {
|
|||
"tail -n N", "tail [N] | tail -n N",
|
||||
I18N_STRING("Print recent messages", "输出最近消息"),
|
||||
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,
|
||||
"post MESSAGE", "post MESSAGE",
|
||||
I18N_STRING("Post a message non-interactively", "非交互发送消息"),
|
||||
|
|
|
|||
165
src/message.c
165
src/message.c
|
|
@ -6,6 +6,7 @@
|
|||
#endif
|
||||
#include "message.h"
|
||||
#include "utf8.h"
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
|
|
@ -26,6 +27,17 @@ static time_t parse_rfc3339_utc(const char *timestamp_str) {
|
|||
return timegm(&tm);
|
||||
}
|
||||
|
||||
static void format_rfc3339_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);
|
||||
}
|
||||
|
||||
static void discard_line_remainder(FILE *fp) {
|
||||
int c;
|
||||
|
||||
|
|
@ -102,6 +114,47 @@ static bool parse_log_record(const char *line, message_t *out,
|
|||
return true;
|
||||
}
|
||||
|
||||
static int append_dump_record(char **output, size_t *capacity,
|
||||
size_t *len, const message_t *msg) {
|
||||
char timestamp[64];
|
||||
int needed;
|
||||
size_t available;
|
||||
|
||||
if (!output || !capacity || !len || !msg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
format_rfc3339_utc(msg->timestamp, timestamp, sizeof(timestamp));
|
||||
needed = snprintf(NULL, 0, "%s|%s|%s\n", timestamp, msg->username,
|
||||
msg->content);
|
||||
if (needed < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
available = *capacity > *len ? *capacity - *len : 0;
|
||||
if ((size_t)needed + 1 > available) {
|
||||
size_t new_capacity = *capacity ? *capacity : 1024;
|
||||
while ((size_t)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;
|
||||
}
|
||||
|
||||
snprintf(*output + *len, *capacity - *len, "%s|%s|%s\n", timestamp,
|
||||
msg->username, msg->content);
|
||||
*len += (size_t)needed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Initialize message subsystem */
|
||||
void message_init(void) {
|
||||
/* Nothing to initialize for now */
|
||||
|
|
@ -339,6 +392,118 @@ int message_search(const char *query, message_t **results, int 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[2048];
|
||||
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 (!parse_log_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 */
|
||||
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
||||
struct tm tm_info;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ static void print_usage(FILE *stream) {
|
|||
" -h, --help Print this help and exit\n"
|
||||
"\n"
|
||||
"Commands mirror the TNT SSH exec interface: health, stats, users,\n"
|
||||
"tail, post, help, and exit.\n");
|
||||
"tail, dump, post, help, and exit.\n");
|
||||
}
|
||||
|
||||
static bool is_valid_port(const char *value) {
|
||||
|
|
@ -78,6 +78,7 @@ static bool is_known_exec_command(const char *command) {
|
|||
strcmp(command, "stats") == 0 ||
|
||||
strcmp(command, "users") == 0 ||
|
||||
strcmp(command, "tail") == 0 ||
|
||||
strcmp(command, "dump") == 0 ||
|
||||
strcmp(command, "post") == 0 ||
|
||||
strcmp(command, "help") == 0 ||
|
||||
strcmp(command, "exit") == 0);
|
||||
|
|
|
|||
|
|
@ -140,6 +140,19 @@ else
|
|||
FAIL=$((FAIL + 1))
|
||||
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)
|
||||
if [ "$POST_OUTPUT" = "posted" ]; then
|
||||
echo "✓ post publishes a message"
|
||||
|
|
@ -161,6 +174,17 @@ else
|
|||
FAIL=$((FAIL + 1))
|
||||
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"
|
||||
rm -f "$STATE_DIR/messages.log"
|
||||
mkdir "$STATE_DIR/messages.log"
|
||||
|
|
@ -261,6 +285,17 @@ else
|
|||
FAIL=$((FAIL + 1))
|
||||
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"
|
||||
WATCHER_READY="${STATE_DIR}/watcher.ready"
|
||||
cat >"$EXPECT_SCRIPT" <<EOF
|
||||
|
|
|
|||
|
|
@ -108,6 +108,17 @@ else
|
|||
FAIL=$((FAIL + 1))
|
||||
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
|
||||
|
||||
PATH="$FAKE_BIN:$PATH" TNTCTL_SSH_LOG="$SSH_LOG" "$BIN" example.com users --xml >/dev/null 2>&1
|
||||
REMOTE_STATUS=$?
|
||||
if [ "$REMOTE_STATUS" -eq 64 ]; then
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ TEST(generates_localized_exec_help) {
|
|||
assert(strstr(en, "TNT exec interface") != NULL);
|
||||
assert(strstr(en, "Commands:") != NULL);
|
||||
assert(strstr(en, "users [--json]") != NULL);
|
||||
assert(strstr(en, "dump [N]") != NULL);
|
||||
assert(strstr(en, "post MESSAGE") != NULL);
|
||||
assert(strstr(en, "support") == NULL);
|
||||
|
||||
assert(strstr(zh, "TNT exec 接口") != NULL);
|
||||
assert(strstr(zh, "命令:") != NULL);
|
||||
assert(strstr(zh, "users [--json]") != NULL);
|
||||
assert(strstr(zh, "dump [N]") != NULL);
|
||||
assert(strstr(zh, "post MESSAGE") != NULL);
|
||||
assert(strstr(zh, "support") == NULL);
|
||||
assert_ascii_angle_placeholders(zh);
|
||||
|
|
@ -65,6 +67,10 @@ TEST(matches_exec_commands_and_args) {
|
|||
assert(id == TNT_EXEC_COMMAND_TAIL);
|
||||
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(id == TNT_EXEC_COMMAND_POST);
|
||||
assert(strcmp(args, "hello world") == 0);
|
||||
|
|
@ -90,6 +96,9 @@ TEST(validates_argument_shapes) {
|
|||
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_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, "hello"));
|
||||
}
|
||||
|
|
@ -111,8 +120,8 @@ TEST(generates_localized_usage) {
|
|||
memset(en, 0, sizeof(en));
|
||||
en_pos = 0;
|
||||
exec_catalog_append_usage(en, sizeof(en), &en_pos,
|
||||
TNT_EXEC_COMMAND_TAIL, (ui_lang_t)99);
|
||||
assert(strcmp(en, "tail: usage: tail [N] | tail -n N\n") == 0);
|
||||
TNT_EXEC_COMMAND_DUMP, (ui_lang_t)99);
|
||||
assert(strcmp(en, "dump: usage: dump [N] | dump -n N\n") == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,53 @@ TEST(message_search_skips_malformed_records) {
|
|||
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(message_edge_cases) {
|
||||
message_t msg;
|
||||
|
|
@ -303,6 +350,7 @@ int main(void) {
|
|||
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_special_characters);
|
||||
RUN_TEST(message_buffer_safety);
|
||||
|
|
|
|||
1
tnt.1
1
tnt.1
|
|
@ -222,6 +222,7 @@ ssh host \-p 2222 help
|
|||
ssh host \-p 2222 users \-\-json
|
||||
ssh host \-p 2222 stats \-\-json
|
||||
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 "/me deploys v2.0"
|
||||
ssh host \-p 2222 health
|
||||
|
|
|
|||
7
tntctl.1
7
tntctl.1
|
|
@ -73,6 +73,12 @@ Print recent messages.
|
|||
.B tail -n N
|
||||
Print recent messages.
|
||||
.TP
|
||||
.B dump [N]
|
||||
Export persisted messages.
|
||||
.TP
|
||||
.B dump -n N
|
||||
Export persisted messages.
|
||||
.TP
|
||||
.B post MESSAGE
|
||||
Post a message non-interactively.
|
||||
.TP
|
||||
|
|
@ -82,6 +88,7 @@ Print the server exec help.
|
|||
.nf
|
||||
tntctl chat.example.com health
|
||||
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 --host-key-checking accept-new chat.example.com users
|
||||
.fi
|
||||
|
|
|
|||
Loading…
Reference in a new issue