static admin_ssh_console_token_t a = { .slot_index=0, .session_id=7, .slot_generation=1 }; static admin_ssh_console_token_t b = { .slot_index=1, .session_id=7, .slot_generation=1, .transport=1 }; static user_principal_t admin = { .role=USER_ROLE_ADMIN }; static bool live[2] = {true, true}; static void (*current_hook)(void); static bool is_current(const admin_ssh_console_token_t *t, const user_principal_t *p) { assert(!lock_depth && p->role==USER_ROLE_ADMIN); if (current_hook) current_hook(); return live[t->slot_index]; } static bool drained(const admin_ssh_console_token_t *t) { assert(!lock_depth); assert(t->session_id==7); return owner_drained; } static esp_err_t perform(const admin_ssh_console_token_t *t, admin_ssh_deferred_action_type_t action, uint32_t arg) { (void)t; (void)arg; assert(!lock_depth); assert(action==ADMIN_CONSOLE_DEFER_SELF_CLOSE); ++actions; return ESP_OK; } static const admin_console_owner_t owner = { .supported_actions=1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE, .drained=drained, .perform=perform, .is_current=is_current, }; static void pump(void (*task)(void *)) { if (!setjmp(loop_done)) task(NULL); } static void feed(const admin_ssh_console_token_t *t, const char *s) { size_t n=0; assert(admin_ssh_console_feed_input(t,(const uint8_t *)s,strlen(s),&n)); assert(n==strlen(s)); } static void clear_output(const admin_ssh_console_token_t *t) { uint8_t out[4096]; size_t n; assert(admin_ssh_console_read_output(t,out,sizeof(out),&n)==ESP_OK); } static void competing_completion(void) { size_t n=99; assert(!admin_ssh_console_feed_input(&b,(const uint8_t *)"\t",1,&n)); assert(n==0 && s_completion_busy); } static void reopen_during_completion(void) { assert(!lock_depth && s_completion_busy); admin_ssh_console_close(&a); ++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); clear_output(&a); } static void hidden_reply(void) { feed(&a,"secret\r"); } static void cancel_reply(void) { feed(&a,"secret\x03"); } static void close_prompt(void) { admin_ssh_console_close(&a); } static void close_during_delay(void) { if (ticks>=200) admin_ssh_console_close(&a); } static void close_during_command(void) { assert(s_sessions[0].executing); admin_ssh_console_close(&a); assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE); } static void setup_dispatch(void) { s_dispatch_remote=true; s_dispatch_token=a; s_dispatch_principal=admin; s_sessions[0].executing=true; s_sessions[0].command_pending=true; } static void reopen_during_current(void) { current_hook=NULL; admin_ssh_console_close(&a); ++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); live[0]=false; /* Failed old validation must not close the replacement. */ } static void revoked_reply(void) { hidden_reply(); live[0]=false; } static unsigned checks; static void stale_at_execution(void) { if (++checks==2) live[0]=false; } static void account_revoked_reply(void) { hidden_reply(); principal_current=false; } static void closed_reply(void) { hidden_reply(); close_prompt(); } static unsigned waits; static void unanswered(void) { ++waits; if (waits==1) xSemaphoreGive(s_prompt_done); /* Stale wake while still waiting. */ if (waits==3) live[0]=false; /* No close notification. */ } static void prompt_command(void) { uint8_t answer[32]; size_t n=99; assert(admin_ssh_console_dispatch_read_input("Password: ",answer,sizeof(answer),true,&n)==ESP_ERR_NOT_FOUND); assert(n==0); for (size_t i=0;icount); uint8_t diagnostic[512]={0}; size_t received=0; assert(admin_ssh_console_read_output(&web,diagnostic,sizeof(diagnostic)-1,&received)==ESP_OK); assert(strstr((char *)diagnostic,"unavailable from the web console")); feed(&web,"\"user\" \"password\" admin --generate\r"); pump(worker_task); assert(runs==before && !s_control_queue->count); feed(&web,"\"web\" \"status\"\r"); pump(worker_task); assert(runs==before+1); /* UART0 bypasses remote policy and remains the recovery path. */ admin_request_t uart={.origin=ADMIN_REQUEST_UART0, .line="user recover --force"}; assert(xQueueSend(s_request_queue,&uart,0)); pump(worker_task); assert(runs==before+2); runs=before; admin_ssh_console_token_t full=web; full.slot_index=99; assert(admin_ssh_console_open_available(&full,&admin,&owner)==ESP_ERR_INVALID_STATE); assert(full.slot_index==99); admin_ssh_console_token_t stale=web; s_sessions[0].executing=true; admin_ssh_console_close(&web); assert(admin_ssh_console_open_available(&full,&admin,&owner)==ESP_ERR_INVALID_STATE); assert(full.slot_index==99); /* Inactive executing slots still consume capacity. */ s_sessions[0].executing=false; ++web.slot_generation; assert(admin_ssh_console_open_available(&web,&admin,&owner)==ESP_OK); admin_ssh_console_close(&stale); assert(!admin_ssh_console_accepts_input(&stale) && admin_ssh_console_accepts_input(&web)); assert(admin_ssh_console_accepts_input(&ssh)); admin_ssh_console_close(&web); admin_ssh_console_close(&ssh); puts("PASS: two-owner shared admission, colliding preferred indices/IDs, full capacity, executing reservation and stale tokens"); } int main(void) { assert(admin_ssh_console_init()==ESP_OK); assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE); assert(admin_ssh_console_start_uart_frontend()==ESP_OK); test_shared_admission(); principal_current=false; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE); principal_current=true; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); assert(admin_ssh_console_open_owned(&b,&admin,&owner)==ESP_OK); admin_ssh_console_token_t other=a; other.transport=1; assert(admin_ssh_console_open_owned(&other,&admin,&owner)==ESP_ERR_INVALID_STATE); admin_ssh_console_close(&other); assert(!admin_ssh_console_accepts_input(&other)); other=a; ++other.slot_generation; admin_ssh_console_close(&other); assert(admin_ssh_console_accepts_input(&a)); clear_output(&a); clear_output(&b); completion_hook=competing_completion; feed(&a,"\t"); completion_hook=NULL; assert(!s_completion_busy && s_sessions[0].output_length && !s_sessions[1].output_length); /* Keep the in-flight token unchanged; only the reopened session advances. */ admin_ssh_console_token_t completing=a; completion_hook=reopen_during_completion; feed(&completing,"\t"); completion_hook=NULL; assert(a.slot_generation==completing.slot_generation+1); assert(!s_completion_busy && !s_sessions[0].output_length && !s_sessions[0].input_length); assert(!admin_ssh_console_accepts_input(&completing)); feed(&a,"\t"); assert(s_sessions[0].output_length); clear_output(&a); feed(&a,"help\r"); assert(runs==0); pump(worker_task); assert(runs==1); feed(&a,"\x1b[A"); assert(!strcmp((char *)s_sessions[0].input,"help")); feed(&a,"\x03"); feed(&a,"stale\r"); admin_ssh_console_close(&a); ++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); pump(worker_task); assert(runs==1); feed(&a,"revoked\r"); principal_current=false; pump(worker_task); assert(runs==1); principal_current=true; ++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); admin_request_t uart={ .origin=ADMIN_REQUEST_UART0 }; assert(xQueueSend(s_request_queue,&uart,0)); pump(worker_task); assert(runs==2); feed(&a,"close\r"); command_hook=close_during_command; pump(worker_task); command_hook=NULL; admin_session_t empty={0}; assert(!memcmp(&empty,&s_sessions[0],sizeof(empty))); ++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); setup_dispatch(); clear_output(&a); prompt_hook=hidden_reply; uint8_t secret[32]; size_t n; uint8_t history_before[sizeof(s_sessions[0].history)]; memcpy(history_before,s_sessions[0].history,sizeof(history_before)); assert(admin_ssh_console_dispatch_is_current()); assert(admin_ssh_console_dispatch_read_input("Password: ",secret,sizeof(secret),true,&n)==ESP_OK); assert(!memcmp(history_before,s_sessions[0].history,sizeof(history_before))); for (size_t i=0;icount==1); admin_control_request_t exit_request; memcpy(&exit_request,s_control_queue->bytes,sizeof(exit_request)); assert(exit_request.action==ADMIN_CONSOLE_DEFER_SELF_CLOSE); assert(token_matches(&s_sessions[0],&exit_request.token)); assert(exit_request.owner==&owner && exit_request.argument==a.session_id); ticks=0; pump(control_task); assert(ticks==200 && actions==1); clear_output(&a); assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_SELF_CLOSE,0)==ESP_OK); ticks=0; delay_hook=close_during_delay; pump(control_task); delay_hook=NULL; assert(actions==1); secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK); s_sessions[0].output_length=4096; ticks=0; assert(ssh_output_write(&a,"x",1)==-1 && errno==EAGAIN && ticks==5000); clear_output(&a); s_dispatch_output_previous_cr=false; assert(ssh_output_write(&a,"a\nb\r\n",6)==6); assert(s_sessions[0].output_length==7); admin_ssh_console_close(&a); assert(ssh_output_write(&a,"x",1)==-1 && errno==EPIPE); assert(!lock_depth); test_currentness(); test_dispatch_currentness(); unsigned before_serial = runs; assert(admin_ssh_console_submit_serial_settings(0) == ESP_ERR_INVALID_STATE); s_dispatch_ready = false; assert(admin_ssh_console_submit_serial_settings(1) == ESP_ERR_INVALID_STATE); s_dispatch_ready = true; queue_full = true; assert(admin_ssh_console_submit_serial_settings(1) == ESP_ERR_TIMEOUT); queue_full = false; admin_request_t preceding_uart = {.origin = ADMIN_REQUEST_UART0}; assert(xQueueSend(s_request_queue, &preceding_uart, 0)); queue_send_wait = portMAX_DELAY; assert(admin_ssh_console_submit_serial_settings(17) == ESP_OK && !serial_settings_executed); assert(queue_send_wait == 0); admin_request_t typed; memcpy(&typed, s_request_queue->bytes + sizeof(typed), sizeof(typed)); assert(typed.origin == ADMIN_REQUEST_SERIAL_SETTINGS && typed.serial_settings_id == 17); assert(s_request_queue->capacity == 4 && sizeof(typed.line) == 257); pump(worker_task); assert(serial_settings_executed == 17 && runs == before_serial + 1 && !s_request_queue->count); assert(serial_settings_preceding_runs == before_serial + 1); for (unsigned i = 0; i < s_request_queue->capacity; ++i) assert(xQueueSend(s_request_queue, &preceding_uart, 0)); queue_send_wait = portMAX_DELAY; assert(admin_ssh_console_submit_serial_settings(18) == ESP_ERR_TIMEOUT); assert(queue_send_wait == 0 && s_request_queue->count == 4 && serial_settings_executed == 17); pump(worker_task); assert(runs == before_serial + 5 && serial_settings_executed == 17 && !s_request_queue->count); puts("PASS: typed Serial admission uses zero wait on success/full queue, preserves all four queued UART requests and FIFO execution, no command-string dispatch"); assert(admin_ssh_console_submit_account_settings(0) == ESP_ERR_INVALID_STATE); s_dispatch_ready = false; assert(admin_ssh_console_submit_account_settings(1) == ESP_ERR_INVALID_STATE); s_dispatch_ready = true; queue_full = true; assert(admin_ssh_console_submit_account_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0); queue_full = false; assert(admin_ssh_console_submit_serial_settings(21) == ESP_OK); assert(admin_ssh_console_submit_account_settings(22) == ESP_OK && queue_send_wait == 0); pump(worker_task); assert(serial_settings_executed == 21 && account_settings_executed == 22 && runs == before_serial + 5); puts("PASS: typed Accounts uses same bounded queue with nonblocking admission and isolated dispatcher routing"); assert(admin_ssh_console_submit_network_settings(0) == ESP_ERR_INVALID_STATE); s_dispatch_ready = false; assert(admin_ssh_console_submit_network_settings(1) == ESP_ERR_INVALID_STATE); s_dispatch_ready = true; queue_full = true; assert(admin_ssh_console_submit_network_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0); queue_full = false; assert(admin_ssh_console_submit_serial_settings(31) == ESP_OK); assert(admin_ssh_console_submit_network_settings(32) == ESP_OK && queue_send_wait == 0); assert(admin_ssh_console_submit_account_settings(33) == ESP_OK); pump(worker_task); assert(serial_settings_executed == 31 && network_settings_executed == 32 && account_settings_executed == 33); assert(runs == before_serial + 5 && s_request_queue->capacity == 4); puts("PASS: typed Network queues only an ID, shares unchanged queue, executes outside lock without command runner"); assert(admin_ssh_console_submit_display_settings(0) == ESP_ERR_INVALID_STATE); s_dispatch_ready = false; assert(admin_ssh_console_submit_display_settings(1) == ESP_ERR_INVALID_STATE); s_dispatch_ready = true; queue_full = true; assert(admin_ssh_console_submit_display_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0); queue_full = false; assert(admin_ssh_console_submit_serial_settings(41) == ESP_OK); assert(admin_ssh_console_submit_network_settings(42) == ESP_OK); assert(admin_ssh_console_submit_account_settings(43) == ESP_OK); assert(admin_ssh_console_submit_display_settings(44) == ESP_OK && queue_send_wait == 0); assert(admin_ssh_console_submit_display_settings(45) == ESP_ERR_TIMEOUT && s_request_queue->count == 4); pump(worker_task); assert(serial_settings_executed == 41 && network_settings_executed == 42 && account_settings_executed == 43 && display_settings_executed == 44); assert(runs == before_serial + 5 && s_request_queue->capacity == 4); puts("PASS: typed Display IDs share all four unchanged queue slots; full/not-ready admission fails, routing never invokes command runner"); assert(admin_ssh_console_submit_broker_settings(0) == ESP_ERR_INVALID_STATE); s_dispatch_ready = false; assert(admin_ssh_console_submit_broker_settings(1) == ESP_ERR_INVALID_STATE); s_dispatch_ready = true; queue_full = true; assert(admin_ssh_console_submit_broker_settings(1) == ESP_ERR_TIMEOUT && queue_send_wait == 0); queue_full = false; assert(admin_ssh_console_submit_broker_settings(51) == ESP_OK); assert(admin_ssh_console_submit_display_settings(52) == ESP_OK); assert(admin_ssh_console_submit_network_settings(53) == ESP_OK); assert(admin_ssh_console_submit_account_settings(54) == ESP_OK); assert(admin_ssh_console_submit_broker_settings(55) == ESP_ERR_TIMEOUT); pump(worker_task); assert(broker_settings_executed == 51 && display_settings_executed == 52 && network_settings_executed == 53 && account_settings_executed == 54); assert(runs == before_serial + 5 && s_request_queue->capacity == 4); puts("PASS: Broker typed IDs, not-ready/full queue, routing and unchanged dispatcher capacity"); puts("PASS: admission/identity, two owners, completion contention/reopen, history, queued stale/revoked work, UART dispatch, hidden/disconnected prompts, exit-to-SELF_CLOSE, deferred rejection/drain/close, 5s output backpressure"); }