mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 04:34:38 +08:00
Localize tntctl help and diagnostics
This commit is contained in:
parent
f0499c32f6
commit
b23b1ba194
4 changed files with 172 additions and 35 deletions
2
Makefile
2
Makefile
|
|
@ -25,7 +25,7 @@ OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
|
|||
DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d)
|
||||
TARGET = tnt
|
||||
CTL_TARGET = tntctl
|
||||
CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o
|
||||
CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o $(OBJ_DIR)/i18n.o
|
||||
TARGETS = $(TARGET) $(CTL_TARGET)
|
||||
|
||||
PREFIX ?= /usr/local
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@
|
|||
- `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.
|
||||
- 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
|
||||
catalog with coverage checks for every message ID.
|
||||
|
|
|
|||
176
src/tntctl.c
176
src/tntctl.c
|
|
@ -1,5 +1,6 @@
|
|||
#include "common.h"
|
||||
#include "exec_catalog.h"
|
||||
#include "i18n.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
|
@ -7,21 +8,126 @@
|
|||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void print_usage(FILE *stream) {
|
||||
fprintf(stream,
|
||||
"Usage: tntctl [options] host command [args...]\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" -p, --port PORT SSH port (default: 2222)\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 mirror the TNT SSH exec interface: health, stats, users,\n"
|
||||
"tail, dump, post, help, and exit.\n");
|
||||
typedef enum {
|
||||
TNTCTL_TEXT_USAGE,
|
||||
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;
|
||||
|
||||
static const i18n_string_t tntctl_text_catalog[TNTCTL_TEXT_COUNT] = {
|
||||
[TNTCTL_TEXT_USAGE] = I18N_STRING(
|
||||
"Usage: tntctl [options] host command [args...]\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" -p, --port PORT SSH port (default: 2222)\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 mirror the TNT SSH exec interface: health, stats, users,\n"
|
||||
"tail, dump, post, help, and exit.\n",
|
||||
"用法: tntctl [options] host command [args...]\n"
|
||||
"\n"
|
||||
"选项:\n"
|
||||
" -p, --port PORT SSH 端口 (默认: 2222)\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"
|
||||
"命令对应 TNT SSH exec 接口: health, stats, users,\n"
|
||||
"tail, dump, post, help 和 exit.\n"
|
||||
),
|
||||
[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 tntctl_text_catalog_must_cover_enum[
|
||||
sizeof(tntctl_text_catalog) / sizeof(tntctl_text_catalog[0]) ==
|
||||
TNTCTL_TEXT_COUNT ? 1 : -1];
|
||||
|
||||
static const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) {
|
||||
if (id < 0 || id >= TNTCTL_TEXT_COUNT) {
|
||||
return "";
|
||||
}
|
||||
return i18n_string(tntctl_text_catalog[id], lang);
|
||||
}
|
||||
|
||||
static void print_usage(FILE *stream, ui_lang_t lang) {
|
||||
fputs(tntctl_text(lang, TNTCTL_TEXT_USAGE), stream);
|
||||
}
|
||||
|
||||
static void print_error(ui_lang_t lang, tntctl_text_id_t id) {
|
||||
fprintf(stderr, "tntctl: %s\n", tntctl_text(lang, id));
|
||||
}
|
||||
|
||||
static void print_error_format(ui_lang_t lang, tntctl_text_id_t id,
|
||||
const char *value) {
|
||||
fprintf(stderr, "tntctl: ");
|
||||
fprintf(stderr, tntctl_text(lang, id), value);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
static bool is_valid_port(const char *value) {
|
||||
|
|
@ -153,6 +259,7 @@ int main(int argc, char **argv) {
|
|||
char **ssh_argv = NULL;
|
||||
int ssh_argc = 0;
|
||||
int rc;
|
||||
ui_lang_t lang = i18n_default_ui_lang();
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--") == 0) {
|
||||
|
|
@ -160,7 +267,7 @@ int main(int argc, char **argv) {
|
|||
break;
|
||||
} else if (strcmp(argv[i], "-h") == 0 ||
|
||||
strcmp(argv[i], "--help") == 0) {
|
||||
print_usage(stdout);
|
||||
print_usage(stdout, lang);
|
||||
return TNT_EXIT_OK;
|
||||
} else if (strcmp(argv[i], "-V") == 0 ||
|
||||
strcmp(argv[i], "--version") == 0) {
|
||||
|
|
@ -169,7 +276,7 @@ int main(int argc, char **argv) {
|
|||
} else if (strcmp(argv[i], "-p") == 0 ||
|
||||
strcmp(argv[i], "--port") == 0) {
|
||||
if (i + 1 >= argc || !is_valid_port(argv[i + 1])) {
|
||||
fprintf(stderr, "tntctl: invalid port\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_PORT);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
port = argv[++i];
|
||||
|
|
@ -177,26 +284,27 @@ int main(int argc, char **argv) {
|
|||
strcmp(argv[i], "--login") == 0) {
|
||||
if (i + 1 >= argc || is_safe_ssh_token(argv[i + 1]) ||
|
||||
strchr(argv[i + 1], '@')) {
|
||||
fprintf(stderr, "tntctl: invalid login\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_LOGIN);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
login = argv[++i];
|
||||
} else if (strcmp(argv[i], "--host-key-checking") == 0) {
|
||||
if (i + 1 >= argc || !is_host_key_checking_mode(argv[i + 1])) {
|
||||
fprintf(stderr, "tntctl: invalid host-key checking mode\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_HOST_KEY_MODE);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
host_key_checking = argv[++i];
|
||||
} else if (strcmp(argv[i], "--known-hosts") == 0) {
|
||||
if (i + 1 >= argc || argv[i + 1][0] == '\0' ||
|
||||
has_newline(argv[i + 1])) {
|
||||
fprintf(stderr, "tntctl: invalid known_hosts path\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_KNOWN_HOSTS);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
known_hosts = argv[++i];
|
||||
} else if (argv[i][0] == '-') {
|
||||
fprintf(stderr, "tntctl: unknown option: %s\n", argv[i]);
|
||||
print_usage(stderr);
|
||||
print_error_format(lang, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT,
|
||||
argv[i]);
|
||||
print_usage(stderr, lang);
|
||||
return TNT_EXIT_USAGE;
|
||||
} else {
|
||||
break;
|
||||
|
|
@ -204,29 +312,29 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
|
||||
if (i >= argc) {
|
||||
fprintf(stderr, "tntctl: missing host\n");
|
||||
print_usage(stderr);
|
||||
print_error(lang, TNTCTL_TEXT_MISSING_HOST);
|
||||
print_usage(stderr, lang);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
||||
host = argv[i++];
|
||||
if (is_safe_ssh_token(host)) {
|
||||
fprintf(stderr, "tntctl: invalid host\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_HOST);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
if (login && strchr(host, '@')) {
|
||||
fprintf(stderr, "tntctl: use either --login or user@host, not both\n");
|
||||
print_error(lang, TNTCTL_TEXT_LOGIN_HOST_CONFLICT);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
||||
if (i >= argc || !is_known_exec_command(argv[i])) {
|
||||
fprintf(stderr, "tntctl: unknown or missing command\n");
|
||||
print_error(lang, TNTCTL_TEXT_UNKNOWN_COMMAND);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
||||
if (build_remote_command(remote_command, sizeof(remote_command), argc,
|
||||
argv, i) < 0) {
|
||||
fprintf(stderr, "tntctl: invalid or too-long command\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_REMOTE_COMMAND);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
||||
|
|
@ -234,24 +342,24 @@ int main(int argc, char **argv) {
|
|||
int n = snprintf(destination, sizeof(destination), "%s@%s", login,
|
||||
host);
|
||||
if (n < 0 || n >= (int)sizeof(destination)) {
|
||||
fprintf(stderr, "tntctl: destination too long\n");
|
||||
print_error(lang, TNTCTL_TEXT_DESTINATION_TOO_LONG);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
} else {
|
||||
int n = snprintf(destination, sizeof(destination), "%s", host);
|
||||
if (n < 0 || n >= (int)sizeof(destination)) {
|
||||
fprintf(stderr, "tntctl: destination too long\n");
|
||||
print_error(lang, TNTCTL_TEXT_DESTINATION_TOO_LONG);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
}
|
||||
if (destination[0] == '-') {
|
||||
fprintf(stderr, "tntctl: invalid destination\n");
|
||||
print_error(lang, TNTCTL_TEXT_INVALID_DESTINATION);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
||||
ssh_argv = calloc((size_t)argc * 2u + 8u, sizeof(*ssh_argv));
|
||||
if (!ssh_argv) {
|
||||
fprintf(stderr, "tntctl: out of memory\n");
|
||||
print_error(lang, TNTCTL_TEXT_OUT_OF_MEMORY);
|
||||
return TNT_EXIT_ERROR;
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +370,7 @@ int main(int argc, char **argv) {
|
|||
int n = snprintf(host_key_option, sizeof(host_key_option),
|
||||
"StrictHostKeyChecking=%s", host_key_checking);
|
||||
if (n < 0 || n >= (int)sizeof(host_key_option)) {
|
||||
fprintf(stderr, "tntctl: host-key option too long\n");
|
||||
print_error(lang, TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG);
|
||||
free(ssh_argv);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
|
@ -273,7 +381,7 @@ int main(int argc, char **argv) {
|
|||
int n = snprintf(known_hosts_option, sizeof(known_hosts_option),
|
||||
"UserKnownHostsFile=%s", known_hosts);
|
||||
if (n < 0 || n >= (int)sizeof(known_hosts_option)) {
|
||||
fprintf(stderr, "tntctl: known_hosts option too long\n");
|
||||
print_error(lang, TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG);
|
||||
free(ssh_argv);
|
||||
return TNT_EXIT_USAGE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,33 @@ case "$VERSION_OUTPUT" in
|
|||
*) echo "✗ version output unexpected: $VERSION_OUTPUT"; FAIL=$((FAIL + 1)) ;;
|
||||
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
|
||||
grep -q '^example.com$' "$SSH_LOG" &&
|
||||
grep -q '^health$' "$SSH_LOG"
|
||||
|
|
|
|||
Loading…
Reference in a new issue