Files
ESP32_Serial_Swiss_Army_Knife/tests/ssh_management/run.py
T
Commander1024 737bd29f9e 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.
2026-09-13 16:07:04 +02:00

141 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""Exact SSH management/lifecycle/close/slot-selection functions, deterministic RTOS.
No wolfSSH, sockets, real scheduling or target execution is claimed.
"""
from pathlib import Path
import re
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[2]
source = (ROOT / 'src/ssh_transport.c').read_text()
def function(name):
match = re.search(r'^(?:static )?[^\n]+\b' + name + r'\([^;]*?\n\{.*?^\}', source, re.M | re.S)
assert match, name
return match.group() + '\n'
header = '\n'.join(line for line in (ROOT / 'src/ssh_transport.h').read_text().splitlines() if not line.startswith(('#include', '#pragma once')))
constants = '\n'.join(re.search(r'^#define ' + name + r' .+$', source, re.M).group() for name in ('SSH_TRANSPORT_GENERATION_MAX', 'SSH_TRANSPORT_COMMAND_TIMEOUT_MS', 'SSH_TRANSPORT_MAX_PENDING_HANDSHAKES'))
username = re.search(r'^#define USER_DATABASE_USERNAME_CAPACITY .+$', (ROOT / 'src/user_database.h').read_text(), re.M).group()
fakes = r'''
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>
typedef int esp_err_t;
enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE, ESP_ERR_TIMEOUT, ESP_ERR_NOT_FOUND };
typedef uint32_t session_broker_client_id_t;
typedef int user_role_t;
typedef int user_auth_method_t;
'''
state = r'''
typedef struct { ssh_transport_session_state_t state; uint32_t generation, session_id; } ssh_slot_t;
static ssh_slot_t s_slots[SSH_TRANSPORT_MAX_SESSIONS];
static ssh_transport_session_snapshot_t s_session_snapshots[SSH_TRANSPORT_MAX_SESSIONS];
static uint32_t s_external_close_id[SSH_TRANSPORT_MAX_SESSIONS];
static unsigned depth, mutex_storage, notifications, ticks;
static unsigned *s_command_mutex = &mutex_storage;
static bool s_initialized, s_running, s_transitioning, s_cleanup_pending, s_desired_running;
static uint32_t s_management_generation, s_requested_sequence, s_completed_sequence;
static int s_command_result;
static bool owner_stalled, owner_fail;
static int s_lock;
#define taskENTER_CRITICAL(p) do { (void)(p); assert(depth++ == 0); } while(0)
#define taskEXIT_CRITICAL(p) do { (void)(p); assert(--depth == 0); } while(0)
#define pdTRUE 1
#define portMAX_DELAY 99999U
#define pdMS_TO_TICKS(n) (n)
typedef unsigned TickType_t;
static int xSemaphoreTake(unsigned *m, unsigned wait) { assert(!depth); (void)wait; if (*m) return 0; *m = 1; return 1; }
static void xSemaphoreGive(unsigned *m) { assert(!depth && *m); *m = 0; }
static void notify_task(void) { assert(!depth); ++notifications; }
static unsigned xTaskGetTickCount(void) { return ticks; }
static void vTaskDelay(unsigned n) {
assert(!depth && mutex_storage); ticks += n;
if (!owner_stalled) {
s_completed_sequence = s_requested_sequence; s_command_result = owner_fail ? ESP_FAIL : ESP_OK;
s_running = owner_fail ? false : s_desired_running; s_transitioning = false; s_cleanup_pending = owner_fail;
}
}
'''
tests = r'''
static void reset(void) {
memset(s_slots, 0, sizeof(s_slots)); memset(s_session_snapshots, 0, sizeof(s_session_snapshots));
memset(s_external_close_id, 0, sizeof(s_external_close_id));
s_initialized = s_running = true; s_transitioning = s_cleanup_pending = owner_stalled = owner_fail = false;
mutex_storage = notifications = ticks = 0; s_management_generation = 7; s_requested_sequence = s_completed_sequence = 0;
for (unsigned i = 0; i < 2; ++i) {
s_slots[i] = (ssh_slot_t){SSH_TRANSPORT_SESSION_ACTIVE, 2, make_session_id(i, 2)};
s_session_snapshots[i] = (ssh_transport_session_snapshot_t){.active=true, .session_id=s_slots[i].session_id, .generation=2, .state=SSH_TRANSPORT_SESSION_ACTIVE};
}
}
int main(void) {
reset(); ssh_transport_management_snapshot_t v;
assert(ssh_transport_get_management_snapshot(NULL) == ESP_ERR_INVALID_ARG);
assert(ssh_transport_get_management_snapshot(&v) == ESP_OK && v.generation == 7 && v.running && !v.transitioning);
assert(!notifications && !depth && !mutex_storage);
s_initialized = false; assert(ssh_transport_get_management_snapshot(&v) == ESP_ERR_INVALID_STATE); s_initialized = true;
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 9, 7) == ESP_OK);
assert(s_external_close_id[0] == 9 && !s_external_close_id[1] && notifications == 1);
assert(ssh_transport_get_management_snapshot(&v) == ESP_OK && v.sessions[0].close_requested && !v.sessions[1].close_requested);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 9, 7) == ESP_ERR_NOT_FOUND);
assert(consume_external_close(&s_slots[0], 0) && !s_external_close_id[0] && s_session_snapshots[0].close_requested);
assert(!consume_external_close(&s_slots[1], 1));
puts("PASS SSH atomic published snapshot/target close, duplicate rejection and unrelated-slot isolation");
reset();
s_external_close_id[0] = 5; assert(!consume_external_close(&s_slots[0], 0));
s_slots[0].state = SSH_TRANSPORT_SESSION_FREE; assert(!consume_external_close(&s_slots[0], 0) && !s_external_close_id[0]);
s_session_snapshots[0].active = false;
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 9, 7) == ESP_ERR_NOT_FOUND);
s_session_snapshots[0].active = true; s_session_snapshots[0].session_id = 13;
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 9, 7) == ESP_ERR_NOT_FOUND);
assert(!s_external_close_id[0] && !notifications);
size_t index; s_slots[0].generation = SSH_TRANSPORT_GENERATION_MAX; s_slots[1].state = SSH_TRANSPORT_SESSION_FREE;
assert(find_free_slot(&index) == &s_slots[1] && index == 1);
s_slots[1].generation = SSH_TRANSPORT_GENERATION_MAX; assert(find_free_slot(&index) == NULL);
assert(make_session_id(1, SSH_TRANSPORT_GENERATION_MAX) != 0);
puts("PASS SSH disconnect/reuse/late owner close safety and generation exhaustion retires slots");
reset();
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, 6) == ESP_ERR_INVALID_STATE);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_START, 0, 7) == ESP_ERR_INVALID_STATE);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 9, 7) == ESP_ERR_INVALID_ARG);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 0, 7) == ESP_ERR_INVALID_ARG);
assert(ssh_transport_manage_current(99, 0, 7) == ESP_ERR_INVALID_ARG);
mutex_storage = 1; assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, 7) == ESP_ERR_TIMEOUT); mutex_storage = 0;
s_transitioning = true; assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 9, 7) == ESP_ERR_INVALID_STATE); s_transitioning = false;
assert(!notifications);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, 7) == ESP_OK && !s_running && s_management_generation == 8);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_START, 0, 7) == ESP_ERR_INVALID_STATE);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_START, 0, 8) == ESP_OK && s_running && s_management_generation == 9);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_DISCONNECT, 9, 7) == ESP_ERR_INVALID_STATE);
assert(ssh_transport_stop() == ESP_OK && s_management_generation == 10);
assert(ssh_transport_start() == ESP_OK && s_management_generation == 11);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, 9) == ESP_ERR_INVALID_STATE);
puts("PASS SSH conditional start/stop, command mutex, CLI transitions and stop/start ABA fencing");
reset(); owner_stalled = true;
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, 7) == ESP_ERR_TIMEOUT && s_transitioning && !mutex_storage && s_management_generation == 8);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_START, 0, 8) == ESP_ERR_INVALID_STATE);
assert(ssh_transport_get_management_snapshot(&v) == ESP_OK && v.transitioning);
reset(); owner_fail = true;
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, 7) == ESP_FAIL && s_cleanup_pending);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_START, 0, 8) == ESP_ERR_INVALID_STATE);
owner_fail = false; assert(ssh_transport_stop() == ESP_OK && !s_cleanup_pending);
s_management_generation = UINT32_MAX - 1;
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_START, 0, UINT32_MAX - 1) == ESP_OK && s_management_generation == UINT32_MAX);
assert(ssh_transport_manage_current(SSH_TRANSPORT_MANAGE_STOP, 0, UINT32_MAX) == ESP_ERR_INVALID_ARG);
assert(ssh_transport_stop() == ESP_OK && s_management_generation == UINT32_MAX);
puts("PASS SSH admitted timeout is not cancellation; failed cleanup and saturated versions preserve CLI recovery");
}
'''
names = ('next_generation', 'make_session_id', 'consume_external_close', 'find_free_slot', 'request_running_locked', 'request_running', 'ssh_transport_start', 'ssh_transport_stop', 'ssh_transport_get_management_snapshot', 'ssh_transport_manage_current')
# Guard the accept path, which is not executed with the socket double here.
assert 'uint32_t generation = slot->generation + 1U;' in function('accept_connections')
assert 'next_generation(slot->generation)' not in source
with tempfile.TemporaryDirectory(prefix='ssh-management-') as directory:
tmp = Path(directory)
unit = fakes + username + '\n' + header + '\n' + constants + '\n' + state + '\n'.join(function(n) for n in names) + tests
(tmp / 'test.c').write_text(unit)
subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', str(tmp / 'test.c'), '-o', str(tmp / 'test')], check=True, timeout=30)
subprocess.run([str(tmp / 'test')], check=True, timeout=10)