mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2025-12-12 20:49:49 +04:00
[FL-3928, FL-3929] CLI commands in fals and threads (#4116)
* feat: FuriThread stdin * ci: fix f18 * feat: stdio callback context * feat: FuriPipe * POTENTIALLY EXPLOSIVE pipe welding * fix: non-explosive welding * Revert welding * docs: furi_pipe * feat: pipe event loop integration * update f18 sdk * f18 * docs: make doxygen happy * fix: event loop not triggering when pipe attached to stdio * fix: partial stdout in pipe * allow simultaneous in and out subscription in event loop * feat: vcp i/o * feat: cli ansi stuffs and history * feat: more line editing * working but slow cli rewrite * restore previous speed after 4 days of debugging 🥲 * fix: cli_app_should_stop * fix: cli and event_loop memory leaks * style: remove commented out code * ci: fix pvs warnings * fix: unit tests, event_loop crash * ci: fix build * ci: silence pvs warning * feat: cli gpio * ci: fix formatting * Fix memory leak during event loop unsubscription * Event better memory leak fix * feat: cli completions * Merge remote-tracking branch 'origin/dev' into portasynthinca3/3928-cli-threads * merge fixups * temporarily exclude speaker_debug app * pvs and unit tests fixups * feat: commands in fals * move commands out of flash, code cleanup * ci: fix errors * fix: run commands in buffer when stopping session * speedup cli file transfer * fix f18 * separate cli_shell into modules * fix pvs warning * fix qflipper refusing to connect * remove temp debug logs * remove erroneous conclusion * Fix memory leak during event loop unsubscription * Event better memory leak fix * unit test for the fix * improve thread stdio callback signatures * pipe stdout timeout * update api symbols * fix f18, formatting * fix pvs warnings * increase stack size, hope to fix unit tests * cli completions * more key combos * commands in fals * move commands out of flash * ci: fix errors * speedup cli file transfer * merge fixups * fix f18 * cli: revert flag changes * cli: fix formatting * cli, fbt: loopback perf benchmark * thread, event_loop: subscribing to thread flags * cli: signal internal events using thread flags, improve performance * fix f18, formatting * event_loop: fix crash * storage_cli: increase write_chunk buffer size again * cli: explanation for order=0 * thread, event_loop: thread flags callback refactor * cli: increase stack size * cli: rename cli_app_should_stop -> cli_is_pipe_broken_or_is_etx_next_char * cli: use plain array instead of mlib for history * cli: prepend file name to static fns * cli: fix formatting * cli_shell: increase stack size * cli_shell: give up pipe to command thread * fix formatting * fix: format * fix merge * fix. merge. * cli_shell: fix detach ordering * desktop: record_cli -> record_cli_vcp * cli: fix spelling, reload/remove ext cmds on card mount/unmount * cli: fix race conditions and formatting * scripts: wait for CTS to go high before starting flipper * scripts: better race condition fix * REVERT THIS: test script race condition fix * Revert "REVERT THIS: test script race condition fix" This reverts commit 3b028d29b07212755872c5706c8c6a58be551636. * REVERT THIS: test script fix * scripts: sleep? * cli: updated oplist for CliCommandTree * Revert "REVERT THIS: test script fix" This reverts commit e9846318549ce092ef422ff97522ba51916163be. * cli: mention memory leak in FL ticket --------- Co-authored-by: Georgii Surkov <georgii.surkov@outlook.com> Co-authored-by: あく <alleteam@gmail.com> Co-authored-by: hedger <hedger@nanode.su>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include <toolbox/pipe.h>
|
||||
#include <flipper_application/plugins/plugin_manager.h>
|
||||
#include <loader/firmware_api/firmware_api.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#define TAG "CliShell"
|
||||
|
||||
@@ -29,6 +30,11 @@ CliShellKeyComboSet* component_key_combo_sets[] = {
|
||||
};
|
||||
static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets));
|
||||
|
||||
typedef enum {
|
||||
CliShellStorageEventMount,
|
||||
CliShellStorageEventUnmount,
|
||||
} CliShellStorageEvent;
|
||||
|
||||
struct CliShell {
|
||||
Cli* cli;
|
||||
FuriEventLoop* event_loop;
|
||||
@@ -37,6 +43,10 @@ struct CliShell {
|
||||
CliAnsiParser* ansi_parser;
|
||||
FuriEventLoopTimer* ansi_parsing_timer;
|
||||
|
||||
Storage* storage;
|
||||
FuriPubSubSubscription* storage_subscription;
|
||||
FuriMessageQueue* storage_event_queue;
|
||||
|
||||
void* components[CliShellComponentMAX];
|
||||
};
|
||||
|
||||
@@ -46,10 +56,39 @@ typedef struct {
|
||||
FuriString* args;
|
||||
} CliCommandThreadData;
|
||||
|
||||
static void cli_shell_data_available(PipeSide* pipe, void* context);
|
||||
static void cli_shell_pipe_broken(PipeSide* pipe, void* context);
|
||||
|
||||
static void cli_shell_install_pipe(CliShell* cli_shell) {
|
||||
pipe_install_as_stdio(cli_shell->pipe);
|
||||
pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop);
|
||||
pipe_set_callback_context(cli_shell->pipe, cli_shell);
|
||||
pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0);
|
||||
pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0);
|
||||
}
|
||||
|
||||
static void cli_shell_detach_pipe(CliShell* cli_shell) {
|
||||
pipe_detach_from_event_loop(cli_shell->pipe);
|
||||
furi_thread_set_stdin_callback(NULL, NULL);
|
||||
furi_thread_set_stdout_callback(NULL, NULL);
|
||||
}
|
||||
|
||||
// =========
|
||||
// Execution
|
||||
// =========
|
||||
|
||||
static int32_t cli_command_thread(void* context) {
|
||||
CliCommandThreadData* thread_data = context;
|
||||
if(!(thread_data->command->flags & CliCommandFlagDontAttachStdio))
|
||||
pipe_install_as_stdio(thread_data->pipe);
|
||||
|
||||
thread_data->command->execute_callback(
|
||||
thread_data->pipe, thread_data->args, thread_data->command->context);
|
||||
|
||||
fflush(stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
|
||||
// split command into command and args
|
||||
size_t space = furi_string_search_char(command, ' ');
|
||||
@@ -59,6 +98,7 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
|
||||
FuriString* args = furi_string_alloc_set(command);
|
||||
furi_string_right(args, space + 1);
|
||||
|
||||
PluginManager* plugin_manager = NULL;
|
||||
Loader* loader = NULL;
|
||||
CliCommand command_data;
|
||||
|
||||
@@ -71,6 +111,34 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
|
||||
break;
|
||||
}
|
||||
|
||||
// load external command
|
||||
if(command_data.flags & CliCommandFlagExternal) {
|
||||
plugin_manager =
|
||||
plugin_manager_alloc(PLUGIN_APP_ID, PLUGIN_API_VERSION, firmware_api_interface);
|
||||
FuriString* path = furi_string_alloc_printf(
|
||||
"%s/cli_%s.fal", CLI_COMMANDS_PATH, furi_string_get_cstr(command_name));
|
||||
uint32_t plugin_cnt_last = plugin_manager_get_count(plugin_manager);
|
||||
PluginManagerError error =
|
||||
plugin_manager_load_single(plugin_manager, furi_string_get_cstr(path));
|
||||
furi_string_free(path);
|
||||
|
||||
if(error != PluginManagerErrorNone) {
|
||||
printf(ANSI_FG_RED "failed to load external command" ANSI_RESET);
|
||||
break;
|
||||
}
|
||||
|
||||
const CliCommandDescriptor* plugin =
|
||||
plugin_manager_get_ep(plugin_manager, plugin_cnt_last);
|
||||
furi_assert(plugin);
|
||||
furi_check(furi_string_cmp_str(command_name, plugin->name) == 0);
|
||||
command_data.execute_callback = plugin->execute_callback;
|
||||
command_data.flags = plugin->flags | CliCommandFlagExternal;
|
||||
command_data.stack_depth = plugin->stack_depth;
|
||||
|
||||
// external commands have to run in an external thread
|
||||
furi_check(!(command_data.flags & CliCommandFlagUseShellThread));
|
||||
}
|
||||
|
||||
// lock loader
|
||||
if(!(command_data.flags & CliCommandFlagParallelSafe)) {
|
||||
loader = furi_record_open(RECORD_LOADER);
|
||||
@@ -82,7 +150,27 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
|
||||
}
|
||||
}
|
||||
|
||||
command_data.execute_callback(cli_shell->pipe, args, command_data.context);
|
||||
if(command_data.flags & CliCommandFlagUseShellThread) {
|
||||
// run command in this thread
|
||||
command_data.execute_callback(cli_shell->pipe, args, command_data.context);
|
||||
} else {
|
||||
// run command in separate thread
|
||||
cli_shell_detach_pipe(cli_shell);
|
||||
CliCommandThreadData thread_data = {
|
||||
.command = &command_data,
|
||||
.pipe = cli_shell->pipe,
|
||||
.args = args,
|
||||
};
|
||||
FuriThread* thread = furi_thread_alloc_ex(
|
||||
furi_string_get_cstr(command_name),
|
||||
command_data.stack_depth,
|
||||
cli_command_thread,
|
||||
&thread_data);
|
||||
furi_thread_start(thread);
|
||||
furi_thread_join(thread);
|
||||
furi_thread_free(thread);
|
||||
cli_shell_install_pipe(cli_shell);
|
||||
}
|
||||
} while(0);
|
||||
|
||||
furi_string_free(command_name);
|
||||
@@ -91,13 +179,51 @@ void cli_shell_execute_command(CliShell* cli_shell, FuriString* command) {
|
||||
// unlock loader
|
||||
if(loader) loader_unlock(loader);
|
||||
furi_record_close(RECORD_LOADER);
|
||||
|
||||
// unload external command
|
||||
if(plugin_manager) plugin_manager_free(plugin_manager);
|
||||
}
|
||||
|
||||
// ==============
|
||||
// Event handlers
|
||||
// ==============
|
||||
|
||||
static void cli_shell_process_key(CliShell* cli_shell, CliKeyCombo key_combo) {
|
||||
static void cli_shell_storage_event(const void* message, void* context) {
|
||||
CliShell* cli_shell = context;
|
||||
const StorageEvent* event = message;
|
||||
|
||||
if(event->type == StorageEventTypeCardMount) {
|
||||
CliShellStorageEvent cli_event = CliShellStorageEventMount;
|
||||
furi_check(
|
||||
furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk);
|
||||
} else if(event->type == StorageEventTypeCardUnmount) {
|
||||
CliShellStorageEvent cli_event = CliShellStorageEventUnmount;
|
||||
furi_check(
|
||||
furi_message_queue_put(cli_shell->storage_event_queue, &cli_event, 0) == FuriStatusOk);
|
||||
}
|
||||
}
|
||||
|
||||
static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) {
|
||||
CliShell* cli_shell = context;
|
||||
FuriMessageQueue* queue = object;
|
||||
CliShellStorageEvent event;
|
||||
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
|
||||
|
||||
if(event == CliShellStorageEventMount) {
|
||||
cli_enumerate_external_commands(cli_shell->cli);
|
||||
} else if(event == CliShellStorageEventUnmount) {
|
||||
cli_remove_external_commands(cli_shell->cli);
|
||||
} else {
|
||||
furi_crash();
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
cli_shell_process_parser_result(CliShell* cli_shell, CliAnsiParserResult parse_result) {
|
||||
if(!parse_result.is_done) return;
|
||||
CliKeyCombo key_combo = parse_result.result;
|
||||
if(key_combo.key == CliKeyUnrecognized) return;
|
||||
|
||||
for(size_t i = 0; i < CliShellComponentMAX; i++) { // -V1008
|
||||
CliShellKeyComboSet* set = component_key_combo_sets[i];
|
||||
void* component_context = cli_shell->components[i];
|
||||
@@ -130,22 +256,13 @@ static void cli_shell_data_available(PipeSide* pipe, void* context) {
|
||||
// process ANSI escape sequences
|
||||
int c = getchar();
|
||||
furi_assert(c >= 0);
|
||||
CliAnsiParserResult parse_result = cli_ansi_parser_feed(cli_shell->ansi_parser, c);
|
||||
if(!parse_result.is_done) return;
|
||||
CliKeyCombo key_combo = parse_result.result;
|
||||
if(key_combo.key == CliKeyUnrecognized) return;
|
||||
|
||||
cli_shell_process_key(cli_shell, key_combo);
|
||||
cli_shell_process_parser_result(cli_shell, cli_ansi_parser_feed(cli_shell->ansi_parser, c));
|
||||
}
|
||||
|
||||
static void cli_shell_timer_expired(void* context) {
|
||||
CliShell* cli_shell = context;
|
||||
CliAnsiParserResult parse_result = cli_ansi_parser_feed_timeout(cli_shell->ansi_parser);
|
||||
if(!parse_result.is_done) return;
|
||||
CliKeyCombo key_combo = parse_result.result;
|
||||
if(key_combo.key == CliKeyUnrecognized) return;
|
||||
|
||||
cli_shell_process_key(cli_shell, key_combo);
|
||||
cli_shell_process_parser_result(
|
||||
cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser));
|
||||
}
|
||||
|
||||
// =======
|
||||
@@ -158,7 +275,6 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) {
|
||||
cli_shell->cli = furi_record_open(RECORD_CLI);
|
||||
cli_shell->ansi_parser = cli_ansi_parser_alloc();
|
||||
cli_shell->pipe = pipe;
|
||||
pipe_install_as_stdio(cli_shell->pipe);
|
||||
|
||||
cli_shell->components[CliShellComponentLine] = cli_shell_line_alloc(cli_shell);
|
||||
cli_shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc(
|
||||
@@ -167,20 +283,34 @@ static CliShell* cli_shell_alloc(PipeSide* pipe) {
|
||||
cli_shell->event_loop = furi_event_loop_alloc();
|
||||
cli_shell->ansi_parsing_timer = furi_event_loop_timer_alloc(
|
||||
cli_shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, cli_shell);
|
||||
pipe_attach_to_event_loop(cli_shell->pipe, cli_shell->event_loop);
|
||||
|
||||
pipe_set_callback_context(cli_shell->pipe, cli_shell);
|
||||
pipe_set_data_arrived_callback(cli_shell->pipe, cli_shell_data_available, 0);
|
||||
pipe_set_broken_callback(cli_shell->pipe, cli_shell_pipe_broken, 0);
|
||||
cli_shell_install_pipe(cli_shell);
|
||||
|
||||
cli_shell->storage_event_queue = furi_message_queue_alloc(1, sizeof(CliShellStorageEvent));
|
||||
furi_event_loop_subscribe_message_queue(
|
||||
cli_shell->event_loop,
|
||||
cli_shell->storage_event_queue,
|
||||
FuriEventLoopEventIn,
|
||||
cli_shell_storage_internal_event,
|
||||
cli_shell);
|
||||
cli_shell->storage = furi_record_open(RECORD_STORAGE);
|
||||
cli_shell->storage_subscription = furi_pubsub_subscribe(
|
||||
storage_get_pubsub(cli_shell->storage), cli_shell_storage_event, cli_shell);
|
||||
|
||||
return cli_shell;
|
||||
}
|
||||
|
||||
static void cli_shell_free(CliShell* cli_shell) {
|
||||
furi_pubsub_unsubscribe(
|
||||
storage_get_pubsub(cli_shell->storage), cli_shell->storage_subscription);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_event_loop_unsubscribe(cli_shell->event_loop, cli_shell->storage_event_queue);
|
||||
furi_message_queue_free(cli_shell->storage_event_queue);
|
||||
|
||||
cli_shell_completions_free(cli_shell->components[CliShellComponentCompletions]);
|
||||
cli_shell_line_free(cli_shell->components[CliShellComponentLine]);
|
||||
|
||||
pipe_detach_from_event_loop(cli_shell->pipe);
|
||||
cli_shell_detach_pipe(cli_shell);
|
||||
furi_event_loop_timer_free(cli_shell->ansi_parsing_timer);
|
||||
furi_event_loop_free(cli_shell->event_loop);
|
||||
pipe_free(cli_shell->pipe);
|
||||
|
||||
Reference in New Issue
Block a user