Add Typed SSH Service Controls

Provide admin-only SSH status plus generation-safe start, stop, and
single-session disconnect operations through the bounded dispatcher.
Include
Settings UI coverage, lifecycle safeguards, and host-side regression
tests.
This commit is contained in:
2026-09-13 16:07:04 +02:00
parent 7ccc8799e9
commit 737bd29f9e
25 changed files with 1049 additions and 47 deletions
+67 -2
View File
@@ -100,6 +100,8 @@ static bool s_running;
static bool s_transitioning;
static bool s_desired_running;
static bool s_cleanup_pending;
/* Saturates independently of the internal completion sequence; never reset by counters. */
static uint32_t s_management_generation = 1U;
static uint32_t s_requested_sequence;
static uint32_t s_completed_sequence;
static esp_err_t s_command_result = ESP_ERR_INVALID_STATE;
@@ -837,7 +839,8 @@ static ssh_slot_t *find_free_slot(size_t *slot_index)
}
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
if (s_slots[index].state == SSH_TRANSPORT_SESSION_FREE) {
if (s_slots[index].state == SSH_TRANSPORT_SESSION_FREE &&
s_slots[index].generation < SSH_TRANSPORT_GENERATION_MAX) {
*slot_index = index;
return &s_slots[index];
}
@@ -907,7 +910,8 @@ static void accept_connections(void)
(void)setsockopt(socket_fd, IPPROTO_TCP, TCP_NODELAY,
&enabled, sizeof(enabled));
uint32_t generation = next_generation(slot->generation);
/* Exhausted slots are retired by find_free_slot(), never reused after wrap. */
uint32_t generation = slot->generation + 1U;
memset(slot, 0, sizeof(*slot));
slot->state = SSH_TRANSPORT_SESSION_HANDSHAKE;
slot->generation = generation;
@@ -1512,6 +1516,7 @@ static esp_err_t request_running_locked(bool desired)
return ESP_OK;
}
s_desired_running = desired;
if (s_management_generation != UINT32_MAX) ++s_management_generation;
s_transitioning = true;
s_requested_sequence = next_generation(s_requested_sequence);
sequence = s_requested_sequence;
@@ -1633,6 +1638,66 @@ esp_err_t ssh_transport_get_snapshot(ssh_transport_snapshot_t *snapshot)
return ESP_OK;
}
esp_err_t ssh_transport_get_management_snapshot(ssh_transport_management_snapshot_t *snapshot)
{
if (snapshot == NULL) return ESP_ERR_INVALID_ARG;
taskENTER_CRITICAL(&s_lock);
if (!s_initialized) {
taskEXIT_CRITICAL(&s_lock);
return ESP_ERR_INVALID_STATE;
}
memset(snapshot, 0, sizeof(*snapshot));
snapshot->generation = s_management_generation;
snapshot->running = s_running;
snapshot->transitioning = s_transitioning || s_cleanup_pending;
for (size_t i = 0; i < SSH_TRANSPORT_MAX_SESSIONS; ++i) {
snapshot->sessions[i] = s_session_snapshots[i];
snapshot->sessions[i].close_requested |=
snapshot->sessions[i].active && s_external_close_id[i] == snapshot->sessions[i].session_id;
}
taskEXIT_CRITICAL(&s_lock);
return ESP_OK;
}
esp_err_t ssh_transport_manage_current(ssh_transport_management_action_t action,
uint32_t target, uint32_t generation)
{
if (!generation || generation == UINT32_MAX ||
action < SSH_TRANSPORT_MANAGE_START || action > SSH_TRANSPORT_MANAGE_DISCONNECT ||
((action == SSH_TRANSPORT_MANAGE_DISCONNECT) != (target != 0U))) return ESP_ERR_INVALID_ARG;
if (s_command_mutex == NULL) return ESP_ERR_INVALID_STATE;
if (xSemaphoreTake(s_command_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT;
esp_err_t error = ESP_ERR_INVALID_STATE;
taskENTER_CRITICAL(&s_lock);
if (s_initialized && !s_transitioning && !s_cleanup_pending &&
generation == s_management_generation) {
if (action == SSH_TRANSPORT_MANAGE_DISCONNECT) {
error = ESP_ERR_NOT_FOUND;
for (size_t i = 0; i < SSH_TRANSPORT_MAX_SESSIONS; ++i) {
const ssh_transport_session_snapshot_t *session = &s_session_snapshots[i];
if (session->active && session->session_id == target &&
!session->close_requested && session->state != SSH_TRANSPORT_SESSION_CLOSING &&
s_external_close_id[i] != target) {
s_external_close_id[i] = target;
error = ESP_OK;
break;
}
}
} else if (s_running != (action == SSH_TRANSPORT_MANAGE_START)) {
error = ESP_OK;
}
}
taskEXIT_CRITICAL(&s_lock);
/* The command mutex spans comparison and canonical lifecycle admission.
* No HTTPD work/lock is involved; only the SSH owner touches sockets/wolfSSH. */
if (error == ESP_OK) {
if (action == SSH_TRANSPORT_MANAGE_DISCONNECT) notify_task();
else error = request_running_locked(action == SSH_TRANSPORT_MANAGE_START);
}
xSemaphoreGive(s_command_mutex);
return error;
}
esp_err_t ssh_transport_clear_counters(void)
{
taskENTER_CRITICAL(&s_lock);