Display SSH completion candidates for ambiguous prefixes
This commit is contained in:
@@ -142,7 +142,7 @@ SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25
|
||||
|
||||
UART0 and admin SSH submit to one bounded queue, and one dispatcher task is the sole caller of `esp_console_run()`. Consequently, SSH commands execute the canonical UART0 handlers and produce the same status and mutation behavior rather than using a second command implementation. Remote output is routed into the authenticated session's bounded output ring; only the SSH transport task accesses wolfSSH.
|
||||
|
||||
Admin SSH supports four-entry per-session command history with Up/Down, shared whole-line Tab completion using the same hints as UART0, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. History is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
||||
Admin SSH supports four-entry per-session command history with Up/Down, shared whole-line Tab completion using the same hints as UART0, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. A Tab that cannot extend an ambiguous command prefix prints the same matching candidates supplied to the UART0 completion callback, then redraws the current input line. History is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
||||
|
||||
`reboot`, `ssh stop`, session disconnect, and SSH host-key reset/rotation are deferred until the command acknowledgement has left both the administration output ring and transport TX buffer. The shell stops accepting another command while such an action is pending. SSH host-key replacement or service stop closes all SSH sessions; reconnect and verify the new fingerprint where applicable. Web recovery credentials/certificates, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. `user bootstrap` and `user recover --force` remain UART0-only. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ Use one disposable `admin` and one disposable `user`. Keep UART0 attached throug
|
||||
|
||||
Exercise printable input, backspace, Ctrl-C, CR/LF, an empty line, and a line longer than the documented limit. Confirm the command line is bounded, overflow is discarded through a clear diagnostic, and a new prompt remains usable. Run `help`, `user list`, and `broker clients` in a normal ANSI terminal and confirm every line starts in column zero: canonical LF output must be normalized to CRLF without doubling handlers that already emit CRLF.
|
||||
|
||||
Run at least five distinct commands, then use Up/Down to navigate the four-entry per-session history, return to a saved draft with Down, and confirm older entries are bounded out. Verify history does not survive reconnect and is not shared with a second administrator. Exercise Tab on root and nested prefixes such as `us`, `user l`, `wifi ap sh`, `wifi next`, and `ssh host-key i`; confirm the same hints are offered by UART0 and SSH, and that unique/common prefixes redraw cleanly without inserting escape-sequence bytes into the command. Type a command, use Left/Right to insert and delete characters in its middle, then use Home/End and the terminal's Pos1/Ende keys; verify cursor placement, Backspace, and Delete change exactly the intended byte before submission.
|
||||
Run at least five distinct commands, then use Up/Down to navigate the four-entry per-session history, return to a saved draft with Down, and confirm older entries are bounded out. Verify history does not survive reconnect and is not shared with a second administrator. Exercise Tab on root and nested prefixes such as `us`, `user l`, `wifi ap sh`, `wifi next`, and `ssh host-key i`; confirm the same hints are offered by UART0 and SSH. For an ambiguous prefix, confirm SSH prints the matching candidates and redraws the unchanged input line, matching UART0's candidate set; unique/common prefixes must redraw cleanly without inserting escape-sequence bytes into the command. Type a command, use Left/Right to insert and delete characters in its middle, then use Home/End and the terminal's Pos1/Ende keys; verify cursor placement, Backspace, and Delete change exactly the intended byte before submission.
|
||||
|
||||
Run an unsupported command and confirm it is rejected without affecting UART0 or the serial broker. Run the full root `help` output to exercise output-ring draining. With the SSH client temporarily unable to read output, confirm the worker applies input backpressure rather than accepting an unbounded command/output backlog; inspect `ssh counters` for admin-console admission and input-backpressure values.
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#define ADMIN_UART_CONSOLE_TASK_PRIORITY 3U
|
||||
#define ADMIN_SSH_CONSOLE_MAX_ARGUMENTS 10U
|
||||
#define ADMIN_SSH_CONSOLE_HISTORY_DEPTH 4U
|
||||
#define ADMIN_SSH_CONSOLE_COMPLETION_OUTPUT_CAPACITY 2048U
|
||||
#define ADMIN_SSH_CONTROL_QUEUE_LENGTH 2U
|
||||
#define ADMIN_SSH_CONTROL_TASK_STACK_SIZE 4096U
|
||||
#define ADMIN_SSH_CONTROL_TASK_PRIORITY 3U
|
||||
@@ -92,6 +93,8 @@ typedef struct {
|
||||
|
||||
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static admin_session_t s_sessions[ADMIN_SSH_CONSOLE_MAX_SESSIONS];
|
||||
/* admin_ssh_console_feed_input() is called only by the sole SSH owner task. */
|
||||
static char s_completion_output[ADMIN_SSH_CONSOLE_COMPLETION_OUTPUT_CAPACITY];
|
||||
|
||||
static StaticQueue_t s_request_queue_storage;
|
||||
static uint8_t s_request_queue_bytes[ADMIN_SSH_CONSOLE_REQUEST_QUEUE_LENGTH *
|
||||
@@ -967,8 +970,13 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
++*consumed;
|
||||
taskEXIT_CRITICAL(&s_lock);
|
||||
char completed[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U] = {0};
|
||||
size_t candidates_length = 0U;
|
||||
bool expanded = console_completion_expand(current, completed,
|
||||
sizeof(completed));
|
||||
bool candidates_formatted = !expanded &&
|
||||
console_completion_format_matches(current, s_completion_output,
|
||||
sizeof(s_completion_output),
|
||||
&candidates_length);
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
session = &s_sessions[token->slot_index];
|
||||
if (token_matches(session, token) && !session->command_pending &&
|
||||
@@ -979,6 +987,14 @@ bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
session->input_cursor = session->input_length;
|
||||
session->history_position = -1;
|
||||
(void)redraw_line_locked(session);
|
||||
} else if (candidates_formatted && candidates_length > 0U &&
|
||||
candidates_length + 2U + sizeof("admin@serial-tool> ") - 1U +
|
||||
session->input_length <=
|
||||
ADMIN_SSH_CONSOLE_OUTPUT_CAPACITY - session->output_length) {
|
||||
(void)append_output_locked(session, (const uint8_t *)"\r\n", 2U);
|
||||
(void)append_output_locked(session, (const uint8_t *)s_completion_output,
|
||||
candidates_length);
|
||||
(void)redraw_line_locked(session);
|
||||
} else {
|
||||
(void)append_output_locked(session, (const uint8_t *)"\a", 1U);
|
||||
}
|
||||
|
||||
@@ -270,6 +270,47 @@ bool console_completion_expand(const char *line, char *completed, size_t capacit
|
||||
return true;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char *output;
|
||||
size_t capacity;
|
||||
size_t length;
|
||||
bool complete;
|
||||
} completion_format_context_t;
|
||||
|
||||
static bool format_completion_candidate(const char *candidate, void *context)
|
||||
{
|
||||
completion_format_context_t *result = context;
|
||||
size_t candidate_length = strlen(candidate);
|
||||
if (candidate_length + 2U > result->capacity - result->length) {
|
||||
result->complete = false;
|
||||
return false;
|
||||
}
|
||||
memcpy(result->output + result->length, candidate, candidate_length);
|
||||
result->length += candidate_length;
|
||||
result->output[result->length++] = '\r';
|
||||
result->output[result->length++] = '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
bool console_completion_format_matches(const char *line, char *output, size_t capacity,
|
||||
size_t *output_length)
|
||||
{
|
||||
if (line == NULL || output == NULL || output_length == NULL || capacity == 0U) {
|
||||
return false;
|
||||
}
|
||||
completion_format_context_t result = {
|
||||
.output = output,
|
||||
.capacity = capacity,
|
||||
.complete = true,
|
||||
};
|
||||
console_completion_visit(line, format_completion_candidate, &result);
|
||||
if (!result.complete) {
|
||||
return false;
|
||||
}
|
||||
*output_length = result.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
|
||||
void *buffer,
|
||||
size_t size)
|
||||
|
||||
@@ -22,6 +22,15 @@ void console_completion_visit(const char *line,
|
||||
/* Bounded longest-prefix completion shared by the UART and admin SSH frontends. */
|
||||
bool console_completion_expand(const char *line, char *completed, size_t capacity);
|
||||
|
||||
/*
|
||||
* Format the matching candidates as CRLF-terminated lines for a frontend that
|
||||
* cannot use linenoise's native completion display. A successful empty result
|
||||
* means no candidate matched; false means the supplied output buffer was too
|
||||
* small or an argument was invalid.
|
||||
*/
|
||||
bool console_completion_format_matches(const char *line, char *output, size_t capacity,
|
||||
size_t *output_length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user