Implements admin-only Display settings with generation-checked Apply, Save, Load, Defaults, and Reset operations across the web UI, CLI, SSH dispatcher, and local UI owner. Adds bounded HTTP handling, session-isolated operation results, browser lifecycle support, and comprehensive host tests and documentation.
384 lines
22 KiB
C
384 lines
22 KiB
C
|
|
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;i<sizeof(answer);++i) assert(!answer[i]);
|
|
assert(!s_sessions[0].active && !s_sessions[0].prompt_length);
|
|
for (size_t i=0;i<sizeof(s_sessions[0].prompt_input);++i) assert(!s_sessions[0].prompt_input[i]);
|
|
}
|
|
static void test_currentness(void)
|
|
{
|
|
++a.slot_generation;
|
|
admin_console_owner_t missing=owner; missing.is_current=NULL;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&missing)==ESP_ERR_INVALID_ARG);
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
unsigned before=runs;
|
|
feed(&a,"owner stale\r"); live[0]=false; pump(worker_task);
|
|
assert(runs==before && !s_sessions[0].active && principal_current);
|
|
feed(&b,"isolated\r"); pump(worker_task); assert(runs==++before);
|
|
live[0]=true; ++a.slot_generation;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
feed(&a,"reuse\r"); current_hook=reopen_during_current; pump(worker_task);
|
|
assert(runs==before && token_matches(&s_sessions[0],&a));
|
|
live[0]=true;
|
|
feed(&a,"last check\r"); checks=0; current_hook=stale_at_execution;
|
|
pump(worker_task); current_hook=NULL;
|
|
assert(checks==2 && runs==before && !s_sessions[0].active);
|
|
live[0]=true; ++a.slot_generation;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
void (*hooks[])(void)={revoked_reply,account_revoked_reply,closed_reply,unanswered};
|
|
for (size_t i=0;i<sizeof(hooks)/sizeof(hooks[0]);++i) {
|
|
clear_output(&a); ticks=0; waits=0;
|
|
feed(&a,"prompt\r"); prompt_hook=hooks[i]; command_hook=prompt_command;
|
|
pump(worker_task); prompt_hook=NULL; command_hook=NULL;
|
|
assert(runs==++before);
|
|
admin_session_t empty={0}; assert(!memcmp(&empty,&s_sessions[0],sizeof(empty)));
|
|
if (i==3) assert(waits==3 && ticks==750);
|
|
/* Dispatcher recovered, so trusted UART0 work still runs. */
|
|
admin_request_t uart={.origin=ADMIN_REQUEST_UART0};
|
|
assert(xQueueSend(s_request_queue,&uart,0)); pump(worker_task); assert(runs==++before);
|
|
live[0]=true; principal_current=true; ++a.slot_generation;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
}
|
|
setup_dispatch(); clear_output(&a); live[0]=false;
|
|
uint8_t answer[32]; size_t n=99;
|
|
assert(admin_ssh_console_dispatch_read_input("Not published",answer,sizeof(answer),true,&n)==ESP_ERR_NOT_FOUND);
|
|
assert(!n && !s_sessions[0].output_length);
|
|
secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); live[0]=true;
|
|
++a.slot_generation; assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
clear_output(&a);
|
|
s_sessions[0].output_start=4094;
|
|
assert(worker_write(&a,"abcdef"));
|
|
uint8_t out[8];
|
|
assert(admin_ssh_console_read_output(&a,out,3,&n)==ESP_OK && n==3 && !memcmp(out,"abc",3));
|
|
assert(!s_sessions[0].output[4094] && !s_sessions[0].output[4095] && !s_sessions[0].output[0]);
|
|
assert(!memcmp(s_sessions[0].output+1,"def",3));
|
|
assert(admin_ssh_console_read_output(&a,out,sizeof(out),&n)==ESP_OK && n==3 && !memcmp(out,"def",3));
|
|
for (size_t i=0;i<sizeof(s_sessions[0].output);++i) assert(!s_sessions[0].output[i]);
|
|
admin_ssh_console_close(&a);
|
|
puts("PASS: owner stale/account current isolation, callback close/reuse, revoked submitted prompts, periodic unanswered invalidation/stale wake, UART recovery, consumed output wiping");
|
|
}
|
|
static void test_dispatch_currentness(void)
|
|
{
|
|
++a.slot_generation; live[0]=true; principal_current=true;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
setup_dispatch();
|
|
assert(admin_ssh_console_dispatch_is_current());
|
|
current_task=(void *)2;
|
|
assert(!admin_ssh_console_dispatch_is_current());
|
|
current_task=s_task;
|
|
live[0]=false;
|
|
assert(!admin_ssh_console_dispatch_is_current());
|
|
assert(!s_sessions[0].active);
|
|
secure_wipe(&s_sessions[0],sizeof(s_sessions[0]));
|
|
++a.slot_generation; live[0]=true;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
setup_dispatch(); principal_current=false;
|
|
assert(!admin_ssh_console_dispatch_is_current());
|
|
assert(!s_sessions[0].active);
|
|
secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); principal_current=true;
|
|
++a.slot_generation;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
setup_dispatch(); s_dispatch_token.slot_generation--;
|
|
assert(!admin_ssh_console_dispatch_is_current());
|
|
assert(s_sessions[0].active); /* Stale dispatch cannot close replacement. */
|
|
secure_wipe(&s_sessions[0],sizeof(s_sessions[0]));
|
|
s_dispatch_remote=false;
|
|
assert(admin_ssh_console_dispatch_is_current()); /* Trusted UART0. */
|
|
puts("PASS: handler currentness API rejects wrong task, stale owner/account/token; preserves replacement and UART0");
|
|
}
|
|
|
|
static void test_shared_admission(void)
|
|
{
|
|
admin_ssh_console_token_t web={.slot_index=255, .session_id=7,
|
|
.slot_generation=42, .transport=ADMIN_CONSOLE_TRANSPORT_WEB};
|
|
admin_ssh_console_token_t ssh=web; ssh.transport=ADMIN_CONSOLE_TRANSPORT_SSH;
|
|
static const admin_console_owner_t second_owner={
|
|
.supported_actions=1U << ADMIN_CONSOLE_DEFER_SELF_CLOSE,
|
|
.drained=drained, .perform=perform, .is_current=is_current,
|
|
};
|
|
assert(admin_ssh_console_open_available(&web,&admin,&owner)==ESP_OK);
|
|
assert(web.slot_index==0 && web.session_id==7 && web.slot_generation==42 &&
|
|
web.transport==ADMIN_CONSOLE_TRANSPORT_WEB);
|
|
assert(admin_ssh_console_open_available(&ssh,&admin,&second_owner)==ESP_OK);
|
|
assert(ssh.slot_index==1 && ssh.session_id==7 && ssh.slot_generation==42 && !ssh.transport);
|
|
assert(s_sessions[0].owner==&owner && s_sessions[1].owner==&second_owner);
|
|
unsigned before=runs;
|
|
clear_output(&web);
|
|
feed(&web,"\"web\" \"reset\" --force\r"); pump(worker_task);
|
|
assert(runs==before && !s_control_queue->count);
|
|
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;i<sizeof(s_sessions[0].prompt_input);++i) assert(!s_sessions[0].prompt_input[i]);
|
|
assert(n==6 && !memcmp(secret,"secret",6));
|
|
assert(s_sessions[0].output_length==strlen("Password: \r\n"));
|
|
clear_output(&a);
|
|
assert(admin_ssh_console_dispatch_read_input("Visible: ",secret,sizeof(secret),false,&n)==ESP_OK);
|
|
assert(s_sessions[0].output_length==strlen("Visible: secret\r\n"));
|
|
prompt_hook=cancel_reply;
|
|
assert(admin_ssh_console_dispatch_read_input("Password: ",secret,sizeof(secret),true,&n)==ESP_ERR_INVALID_STATE);
|
|
assert(n==0 && secret[0]==0 && s_sessions[0].prompt_input[0]==0);
|
|
prompt_hook=close_prompt;
|
|
assert(admin_ssh_console_dispatch_read_input("Password: ",secret,sizeof(secret),true,&n)==ESP_ERR_NOT_FOUND);
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_ERR_INVALID_STATE);
|
|
secure_wipe(&s_sessions[0],sizeof(s_sessions[0])); prompt_hook=NULL;
|
|
assert(admin_ssh_console_open_owned(&a,&admin,&owner)==ESP_OK);
|
|
setup_dispatch(); clear_output(&a);
|
|
assert(!admin_ssh_console_dispatch_is_web());
|
|
s_dispatch_token.transport = ADMIN_CONSOLE_TRANSPORT_WEB;
|
|
assert(admin_ssh_console_dispatch_is_web());
|
|
s_dispatch_remote = false; assert(!admin_ssh_console_dispatch_is_web());
|
|
s_dispatch_remote = true; s_dispatch_token = a;
|
|
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_WEB_STOP,0)==ESP_ERR_NOT_SUPPORTED);
|
|
assert(admin_ssh_console_dispatch_defer((admin_ssh_deferred_action_type_t)32,0)==ESP_ERR_NOT_SUPPORTED);
|
|
assert(admin_ssh_console_dispatch_defer(ADMIN_SSH_DEFER_STOP,0)==ESP_ERR_NOT_SUPPORTED);
|
|
assert(!s_sessions[0].deferred_action_pending);
|
|
queue_full=true;
|
|
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_SELF_CLOSE,0)==ESP_ERR_TIMEOUT);
|
|
assert(!s_sessions[0].deferred_action_pending); queue_full=false;
|
|
assert(admin_ssh_console_dispatch_defer(ADMIN_CONSOLE_DEFER_SELF_CLOSE,0)==ESP_OK);
|
|
admin_ssh_console_session_snapshot_t pending;
|
|
assert(admin_ssh_console_get_session_snapshot(&a, &pending)==ESP_OK && pending.deferred_action_pending);
|
|
assert(!admin_ssh_console_feed_input(&a,(const uint8_t *)"x",1,&n) && n==0);
|
|
s_sessions[0].command_pending=false; owner_drained=false; ticks=0;
|
|
pump(control_task); assert(ticks==10000 && actions==0);
|
|
clear_output(&a); owner_drained=true;
|
|
/* The fake esp_console_run does not invoke registered command callbacks. */
|
|
assert(command_exit(1,NULL)==0);
|
|
assert(s_control_queue->count==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");
|
|
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");
|
|
}
|