Add Explicit Session Exit Controls Mini-Features ä2+3

This commit is contained in:
2026-08-31 04:02:19 +02:00
parent c37fab67db
commit 06bf33b3cf
9 changed files with 77 additions and 16 deletions
+46 -5
View File
@@ -460,6 +460,28 @@ static void report_command_result(esp_err_t error, int command_result)
}
}
static int command_exit(int argc, char **argv)
{
(void)argv;
if (argc != 1) {
printf("Usage: exit\n");
return 1;
}
if (!admin_ssh_console_dispatch_is_remote()) {
printf("The exit command is available only from an administrative SSH session.\n");
return 1;
}
esp_err_t error = admin_ssh_console_dispatch_defer(
ADMIN_SSH_DEFER_DISCONNECT, s_dispatch_token.session_id);
if (error != ESP_OK) {
printf("Could not schedule SSH session close: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH session close scheduled after output drains.\n");
return 0;
}
static void dispatch_registered_command(admin_request_t *request)
{
FILE *saved_stdout = stdout;
@@ -724,6 +746,18 @@ esp_err_t admin_ssh_console_init(void)
return ESP_OK;
}
esp_err_t admin_ssh_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "exit",
.help = "Close the current administrative SSH session",
.hint = NULL,
.func = &command_exit,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
esp_err_t admin_ssh_console_start_uart_frontend(void)
{
taskENTER_CRITICAL(&s_lock);
@@ -1003,10 +1037,15 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
secure_wipe(completed, sizeof(completed));
continue;
}
if (value == '\r' || value == '\n') {
bool exit_requested = value == 0x04U && session->input_length == 0U;
if (value == '\r' || value == '\n' || exit_requested) {
session->discard_next_lf = value == '\r';
history_commit_locked(session);
memcpy(request.line, session->input, session->input_length);
if (exit_requested) {
memcpy(request.line, "exit", sizeof("exit"));
} else {
history_commit_locked(session);
memcpy(request.line, session->input, session->input_length);
}
request.origin = ADMIN_REQUEST_SSH;
request.token = *token;
request.principal = session->principal;
@@ -1015,8 +1054,10 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
session->input_cursor = 0U;
session->history_position = -1;
session->command_pending = true;
(void)append_output_locked(session, (const uint8_t *)"\r\n",
sizeof("\r\n") - 1U);
(void)append_output_locked(session,
(const uint8_t *)(exit_requested ? "^D\r\n" : "\r\n"),
exit_requested ? sizeof("^D\r\n") - 1U
: sizeof("\r\n") - 1U);
submit = true;
} else if (value == 0x03U) {
secure_wipe(session->input, sizeof(session->input));
+2
View File
@@ -43,6 +43,8 @@ typedef struct {
/* Starts the single command worker. It is the sole esp_console_run() caller. */
esp_err_t admin_ssh_console_init(void);
/* Register administration-shell-only root commands after esp_console initialization. */
esp_err_t admin_ssh_console_register_commands(void);
/* Called after all ESP-IDF commands are registered; starts the UART0 frontend. */
esp_err_t admin_ssh_console_start_uart_frontend(void);
+1 -1
View File
@@ -11,7 +11,7 @@
#include "linenoise/linenoise.h"
static const char *const s_root_candidates[] = {
"help", "debug", "display", "serial", "broker", "usb", "user",
"help", "exit", "debug", "display", "serial", "broker", "usb", "user",
"wifi", "web", "ssh", "ping", "nslookup", "traceroute", "reboot", "memory",
};
+1
View File
@@ -305,6 +305,7 @@ void app_main(void)
ESP_ERROR_CHECK(ssh_console_register_commands());
ESP_ERROR_CHECK(network_console_register_root_commands());
ESP_ERROR_CHECK(system_console_register_commands());
ESP_ERROR_CHECK(admin_ssh_console_register_commands());
/* Upgrade late UART terminals safely and add nested completion. */
console_completion_install();
ESP_ERROR_CHECK(admin_ssh_console_start_uart_frontend());
+18 -2
View File
@@ -119,6 +119,7 @@ static const char s_index_html[] =
"<div class=\"button-row\">\n"
"<button id=\"request-control\" class=\"button primary\" type=\"button\" disabled>Request control</button>\n"
"<button id=\"release-control\" class=\"button danger\" type=\"button\" disabled>Release control</button>\n"
"<button id=\"disconnect\" class=\"button danger\" type=\"button\">Disconnect</button>\n"
"<button id=\"reconnect\" class=\"button\" type=\"button\">Reconnect</button>\n"
"</div>\n"
"<p id=\"input-state\" class=\"input-state\" data-enabled=\"false\" aria-live=\"polite\">"
@@ -151,6 +152,7 @@ static const char s_app_js[] =
"const connectionDetail = element('connection-detail');\n"
"const requestControl = element('request-control');\n"
"const releaseControl = element('release-control');\n"
"const disconnectButton = element('disconnect');\n"
"const reconnectButton = element('reconnect');\n"
"const terminalHost = element('terminal');\n"
"const terminal = new Terminal({\n"
@@ -171,6 +173,7 @@ static const char s_app_js[] =
"let ticketAbort = null;\n"
"let reconnectTimer = null;\n"
"let reconnectDelay = 1000;\n"
"let reconnectEnabled = true;\n"
"let connectionGeneration = 0;\n"
"let role = 'observer';\n"
"let clientId = null;\n"
@@ -193,6 +196,7 @@ static const char s_app_js[] =
" terminal.options.disableStdin = !writer;\n"
" requestControl.disabled = !socketOpen() || writer;\n"
" releaseControl.disabled = !socketOpen() || !writer;\n"
" disconnectButton.disabled = unloading || (!reconnectEnabled && socket === null && ticketAbort === null);\n"
" inputState.dataset.enabled = writer ? 'true' : 'false';\n"
" inputState.textContent = writer\n"
" ? 'Writer mode — terminal input is enabled.'\n"
@@ -215,7 +219,7 @@ static const char s_app_js[] =
" }\n"
"};\n"
"const scheduleReconnect = () => {\n"
" if (unloading || reconnectTimer !== null) return;\n"
" if (unloading || !reconnectEnabled || reconnectTimer !== null) return;\n"
" const delay = reconnectDelay;\n"
" reconnectDelay = Math.min(reconnectDelay * 2, 10000);\n"
" setConnection('Disconnected', 'bad', `Reconnecting in ${Math.ceil(delay / 1000)} second(s)…`);\n"
@@ -267,7 +271,7 @@ static const char s_app_js[] =
" return ticket;\n"
"}\n"
"async function connect() {\n"
" if (unloading) return;\n"
" if (unloading || !reconnectEnabled) return;\n"
" clearReconnectTimer();\n"
" const generation = ++connectionGeneration;\n"
" if (ticketAbort !== null) ticketAbort.abort();\n"
@@ -331,7 +335,19 @@ static const char s_app_js[] =
"releaseControl.addEventListener('click', () => {\n"
" if (role === 'writer' && socketOpen()) socket.send('release-writer');\n"
"});\n"
"disconnectButton.addEventListener('click', () => {\n"
" reconnectEnabled = false;\n"
" ++connectionGeneration;\n"
" clearReconnectTimer();\n"
" if (ticketAbort !== null) { ticketAbort.abort(); ticketAbort = null; }\n"
" if (socket !== null) { const previous = socket; socket = null; previous.close(); }\n"
" clientId = null;\n"
" clientIdField.textContent = '—';\n"
" setRole('observer');\n"
" setConnection('Disconnected', 'warn', 'Disconnected by user. Automatic reconnect is paused.');\n"
"});\n"
"reconnectButton.addEventListener('click', () => {\n"
" reconnectEnabled = true;\n"
" reconnectDelay = 1000;\n"
" connect();\n"
"});\n"