1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-12 12:42:30 +04:00
Files
RebornedBrain eea53491de [FL-3569] NFC CLI commands (#4158)
* 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

* Now cli_shell can be customized with another motd and another command set

* Added custom motd callback definition

* Now user can alloc and free his own cli command set

* cli_vcp can now restart shell with another command set

* Help command modified to show available commands from different command sets

* Api adjustement

* Reworked nfc_cli to start new shell with another command set

* Revert custom shell changes from vcp

* Custom motd callback moved to cli_shell

* Cli Shell now can be started from ongoing cli command

* Help command moved to a separate function so it can be used for custom shell

* Now nfc command spawns separate shell for further nfc commands

* cli_shell: give up pipe to command thread

* fix formatting

* cli_shell: separate into toolbox

* speaker_debug: fix

* fix: format

* Merge branch 'portasynthinca3/3928-3929-cli-fals-threads' into portasynthinca3/3965-cli_shell-toolbox

* fix merge

* fix. merge.

* fix formatting

* fix: cmd flags

* fix: formatting

* Added basic command descriptor structs and macros

* Basic nfc commands definitions added

* Nfc cli commands collection and functions added

* Raw skeleton of nfc cli processor added

* cli: increase default stack depth

* New callbacks for ctx alloc / free added

* nfc_cli moved to cli folder

* Some more logic for command processor

* Scanner command no works via command_processor

* plugin manifest adj

* Argument descriptors were removed, now only keys left

* Some helper command function implemented

* Command processor logic now mostly works

* Added all parsers and dummy implementation of raw cmd

* Now processor checks duplicated keys and treat them as errors

* Some renamings

* Arguments processing moved to separate function

* Now command processor can reuse context of previuos command for the next one if it's allowed

* can_reuse callback added for checking if context can be reused

* command processor is now freed on nfc cli exit

* Some cleanups

* First working version of raw command

* Now input data are placed directly to bit buffer

* Added tag

* Introduced request/response structs

* Moved raw command to a separate folder

* Moved some common types to header

* Added protocol specific handlers for iso14a and felica

* Opened felica crc header for referencing

* Added handler for iso14443_3b

* Opened iso15693_3_poller for referencing

* Added iso15693_3 handler for raw command

* NfcCliRawError enum introduced for response result

* Refactored handlers implementation

* Formatting functions now added as helpers

* New printing result logic

* Not present error value added to enum

* Timeout added to raw command

* Command processor now supports multivalue keys

* Apdu command implementation added

* NfcScanner moved to helpers and command now uses it

* Helper now can format protocol names

* Dump command added

* Added some more functions to scanner helper

* Dump main logic simplified

* Dump handlers moved to protocols folder

* Protocol parser added to simplify searching protocol by name

* Protocol and key arguments added to dump command

* Cleanups

* Apdu now parses protocol using helper parser

* Raw now parses protocol using helper parser

* Wrong naming fix

* Emulate command added to cli

* Description added to action descriptor and command macros

* Description field added to all commands

* Removed unnecessary enum for commands

* Added functions for formatting command and action info

* Proper error messages and help added

* Fix for unsupported single action command

* Function renamed to more appropriate

* Field command moved to all other commands

* Cleanups

* Nfc commands modified with new cli shell

* Removed previous nfc_cli.c after merge

* Removed nfc_cli.h header

* Some renamings and cleanups

* Some comments and instructions added

* Some comments and instructions added

* TODOs removed

* Fix for missing parse callback

* Added not implemented dummy for mfu actions, for now

* Fix name mismatch

* Remove unneeded header

* Mfu command moved to separate folder, also raw info action logic added

* Dictionary with id/vendors added to assets. It is used by nfc_cli_mfu_info_get_vendor function

* One more unneeded header removed

* Moved mfu info action to a separate file

* Info action now uses sync mfu poller

* mfu rdbl action added

* wrbl action added for mfu command

* Some formatting for rdbl command

* Function for formatting mfu errors added

* All mfu actions now show errors in the same way

* Fix error with sync poller. Previously when read failed function returned ErrorNone, now it processes iso14a error to get proper value

* Make PVS happy

* Nfc cli now doesn't start if desktop app is running

* Make action description look more common

* Scanner now has -t key and can show detected protocol hierarchies

* Apdu now checks max input payload data

* Proper format

* Proper error handling added to dump command

* Timeout key added dump command

* Fix merge issue

* formatting

* Pragma pack replaced with FURI_PACKED

* Fix felica memory leak

---------

Co-authored-by: Anna Antonenko <portasynthinca3@gmail.com>
Co-authored-by: Georgii Surkov <georgii.surkov@outlook.com>
Co-authored-by: あく <alleteam@gmail.com>
Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: hedger <hedger@nanode.su>
2025-09-29 14:34:49 +04:00

491 lines
16 KiB
C

#include "cli_shell.h"
#include "cli_shell_i.h"
#include "../cli_ansi.h"
#include "../cli_registry_i.h"
#include "../cli_command.h"
#include "cli_shell_line.h"
#include "cli_shell_completions.h"
#include <stdio.h>
#include <furi_hal_version.h>
#include <m-array.h>
#include <loader/loader.h>
#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"
#define ANSI_TIMEOUT_MS 10
#define TRANSIENT_SESSION_WINDOW_MS 100
typedef enum {
CliShellComponentCompletions,
CliShellComponentLine,
CliShellComponentMAX, //<! do not use
} CliShellComponent;
CliShellKeyComboSet* component_key_combo_sets[] = {
[CliShellComponentCompletions] = &cli_shell_completions_key_combo_set,
[CliShellComponentLine] = &cli_shell_line_key_combo_set,
};
static_assert(CliShellComponentMAX == COUNT_OF(component_key_combo_sets));
typedef enum {
CliShellStorageEventMount = (1 << 0),
CliShellStorageEventUnmount = (1 << 1),
} CliShellStorageEvent;
#define CliShellStorageEventAll (CliShellStorageEventMount | CliShellStorageEventUnmount)
typedef struct {
Storage* storage;
FuriPubSubSubscription* subscription;
FuriEventFlag* event_flag;
} CliShellStorage;
struct CliShell {
// Set and freed by external thread
CliShellMotd motd;
void* callback_context;
PipeSide* pipe;
CliRegistry* registry;
const CliCommandExternalConfig* ext_config;
FuriThread* thread;
const char* prompt;
// Set and freed by shell thread
FuriEventLoop* event_loop;
CliAnsiParser* ansi_parser;
FuriEventLoopTimer* ansi_parsing_timer;
CliShellStorage storage;
void* components[CliShellComponentMAX];
};
typedef struct {
CliRegistryCommand* command;
PipeSide* pipe;
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);
}
// =================
// Built-in commands
// =================
void cli_command_reload_external(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(pipe);
UNUSED(args);
CliShell* shell = context;
furi_check(shell->ext_config);
cli_registry_reload_external_commands(shell->registry, shell->ext_config);
printf("OK!");
}
void cli_command_help(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(pipe);
UNUSED(args);
CliShell* shell = context;
CliRegistry* registry = shell->registry;
const size_t columns = 3;
printf("Available commands:\r\n" ANSI_FG_GREEN);
cli_registry_lock(registry);
CliCommandDict_t* commands = cli_registry_get_commands(registry);
size_t commands_count = CliCommandDict_size(*commands);
CliCommandDict_it_t iterator;
CliCommandDict_it(iterator, *commands);
for(size_t i = 0; i < commands_count; i++) {
const CliCommandDict_itref_t* item = CliCommandDict_cref(iterator);
printf("%-30s", furi_string_get_cstr(item->key));
CliCommandDict_next(iterator);
if(i % columns == columns - 1) printf("\r\n");
}
if(shell->ext_config)
printf(
ANSI_RESET
"\r\nIf you added a new external command and can't see it above, run `reload_ext_cmds`");
printf(ANSI_RESET "\r\nFind out more: https://docs.flipper.net/development/cli");
cli_registry_unlock(registry);
}
void cli_command_exit(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(pipe);
UNUSED(args);
CliShell* shell = context;
cli_shell_line_set_about_to_exit(shell->components[CliShellComponentLine]);
furi_event_loop_stop(shell->event_loop);
}
// ==================
// Internal functions
// ==================
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, ' ');
if(space == FURI_STRING_FAILURE) space = furi_string_size(command);
FuriString* command_name = furi_string_alloc_set(command);
furi_string_left(command_name, space);
FuriString* args = furi_string_alloc_set(command);
furi_string_right(args, space + 1);
PluginManager* plugin_manager = NULL;
Loader* loader = furi_record_open(RECORD_LOADER);
bool loader_locked = false;
CliRegistryCommand command_data;
do {
// find handler
if(!cli_registry_get_command(cli_shell->registry, command_name, &command_data)) {
printf(
ANSI_FG_RED "could not find command `%s`, try `help`" ANSI_RESET,
furi_string_get_cstr(command_name));
break;
}
// load external command
if(command_data.flags & CliCommandFlagExternal) {
const CliCommandExternalConfig* ext_config = cli_shell->ext_config;
plugin_manager = plugin_manager_alloc(
ext_config->appid, CLI_PLUGIN_API_VERSION, firmware_api_interface);
FuriString* path = furi_string_alloc_printf(
"%s/%s%s.fal",
ext_config->search_directory,
ext_config->fal_prefix,
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_locked = loader_lock(loader);
if(!loader_locked) {
printf(ANSI_FG_RED
"this command cannot be run while an application is open" ANSI_RESET);
break;
}
}
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);
furi_string_free(args);
// unlock loader
if(loader_locked) loader_unlock(loader);
furi_record_close(RECORD_LOADER);
// unload external command
if(plugin_manager) plugin_manager_free(plugin_manager);
}
const char* cli_shell_get_prompt(CliShell* cli_shell) {
return cli_shell->prompt;
}
// ==============
// Event handlers
// ==============
static void cli_shell_signal_storage_event(CliShell* cli_shell, CliShellStorageEvent event) {
furi_check(!(furi_event_flag_set(cli_shell->storage.event_flag, event) & FuriFlagError));
}
static void cli_shell_storage_event(const void* message, void* context) {
CliShell* cli_shell = context;
const StorageEvent* event = message;
if(event->type == StorageEventTypeCardMount) {
cli_shell_signal_storage_event(cli_shell, CliShellStorageEventMount);
} else if(event->type == StorageEventTypeCardUnmount) {
cli_shell_signal_storage_event(cli_shell, CliShellStorageEventUnmount);
}
}
static void cli_shell_storage_internal_event(FuriEventLoopObject* object, void* context) {
CliShell* cli_shell = context;
FuriEventFlag* event_flag = object;
CliShellStorageEvent event =
furi_event_flag_wait(event_flag, FuriFlagWaitAll, FuriFlagWaitAny, 0);
furi_check(!(event & FuriFlagError));
if(event & CliShellStorageEventUnmount) {
cli_registry_remove_external_commands(cli_shell->registry);
} else if(event & CliShellStorageEventMount) {
cli_registry_reload_external_commands(cli_shell->registry, cli_shell->ext_config);
} 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];
for(size_t j = 0; j < set->count; j++) {
if(set->records[j].combo.modifiers == key_combo.modifiers &&
set->records[j].combo.key == key_combo.key)
if(set->records[j].action(key_combo, component_context)) return;
}
if(set->fallback)
if(set->fallback(key_combo, component_context)) return;
}
}
static void cli_shell_pipe_broken(PipeSide* pipe, void* context) {
// allow commands to be processed before we stop the shell
if(pipe_bytes_available(pipe)) return;
CliShell* cli_shell = context;
furi_event_loop_stop(cli_shell->event_loop);
}
static void cli_shell_data_available(PipeSide* pipe, void* context) {
UNUSED(pipe);
CliShell* cli_shell = context;
furi_event_loop_timer_start(cli_shell->ansi_parsing_timer, furi_ms_to_ticks(ANSI_TIMEOUT_MS));
// process ANSI escape sequences
int c = getchar();
furi_assert(c >= 0);
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;
cli_shell_process_parser_result(
cli_shell, cli_ansi_parser_feed_timeout(cli_shell->ansi_parser));
}
// ===========
// Thread code
// ===========
static void cli_shell_init(CliShell* shell) {
cli_registry_add_command(
shell->registry,
"help",
CliCommandFlagUseShellThread | CliCommandFlagParallelSafe,
cli_command_help,
shell);
cli_registry_add_command(
shell->registry,
"?",
CliCommandFlagUseShellThread | CliCommandFlagParallelSafe,
cli_command_help,
shell);
cli_registry_add_command(
shell->registry,
"exit",
CliCommandFlagUseShellThread | CliCommandFlagParallelSafe,
cli_command_exit,
shell);
if(shell->ext_config) {
cli_registry_add_command(
shell->registry,
"reload_ext_cmds",
CliCommandFlagUseShellThread,
cli_command_reload_external,
shell);
cli_registry_reload_external_commands(shell->registry, shell->ext_config);
}
shell->components[CliShellComponentLine] = cli_shell_line_alloc(shell);
shell->components[CliShellComponentCompletions] = cli_shell_completions_alloc(
shell->registry, shell, shell->components[CliShellComponentLine]);
shell->ansi_parser = cli_ansi_parser_alloc();
shell->event_loop = furi_event_loop_alloc();
shell->ansi_parsing_timer = furi_event_loop_timer_alloc(
shell->event_loop, cli_shell_timer_expired, FuriEventLoopTimerTypeOnce, shell);
shell->storage.event_flag = furi_event_flag_alloc();
furi_event_loop_subscribe_event_flag(
shell->event_loop,
shell->storage.event_flag,
FuriEventLoopEventIn,
cli_shell_storage_internal_event,
shell);
shell->storage.storage = furi_record_open(RECORD_STORAGE);
shell->storage.subscription = furi_pubsub_subscribe(
storage_get_pubsub(shell->storage.storage), cli_shell_storage_event, shell);
cli_shell_install_pipe(shell);
}
static void cli_shell_deinit(CliShell* shell) {
furi_pubsub_unsubscribe(
storage_get_pubsub(shell->storage.storage), shell->storage.subscription);
furi_record_close(RECORD_STORAGE);
furi_event_loop_unsubscribe(shell->event_loop, shell->storage.event_flag);
furi_event_flag_free(shell->storage.event_flag);
cli_shell_completions_free(shell->components[CliShellComponentCompletions]);
cli_shell_line_free(shell->components[CliShellComponentLine]);
cli_shell_detach_pipe(shell);
furi_event_loop_timer_free(shell->ansi_parsing_timer);
furi_event_loop_free(shell->event_loop);
cli_ansi_parser_free(shell->ansi_parser);
}
static int32_t cli_shell_thread(void* context) {
CliShell* shell = context;
// Sometimes, the other side (e.g. qFlipper) closes the pipe even before our thread is started.
// Although the rest of the code will eventually find this out if this check is removed,
// there's no point in wasting time. This gives qFlipper a chance to quickly close and re-open
// the session.
const size_t delay_step = 10;
for(size_t i = 0; i < TRANSIENT_SESSION_WINDOW_MS / delay_step; i++) {
furi_delay_ms(delay_step);
if(pipe_state(shell->pipe) == PipeStateBroken) return 0;
}
cli_shell_init(shell);
FURI_LOG_D(TAG, "Started");
shell->motd(shell->callback_context);
cli_shell_line_prompt(shell->components[CliShellComponentLine]);
furi_event_loop_run(shell->event_loop);
FURI_LOG_D(TAG, "Stopped");
cli_shell_deinit(shell);
return 0;
}
// ==========
// Public API
// ==========
CliShell* cli_shell_alloc(
CliShellMotd motd,
void* context,
PipeSide* pipe,
CliRegistry* registry,
const CliCommandExternalConfig* ext_config) {
furi_check(motd);
furi_check(pipe);
furi_check(registry);
CliShell* shell = malloc(sizeof(CliShell));
*shell = (CliShell){
.motd = motd,
.callback_context = context,
.pipe = pipe,
.registry = registry,
.ext_config = ext_config,
};
shell->thread =
furi_thread_alloc_ex("CliShell", CLI_SHELL_STACK_SIZE, cli_shell_thread, shell);
return shell;
}
void cli_shell_free(CliShell* shell) {
furi_check(shell);
furi_thread_free(shell->thread);
free(shell);
}
void cli_shell_start(CliShell* shell) {
furi_check(shell);
furi_thread_start(shell->thread);
}
void cli_shell_join(CliShell* shell) {
furi_check(shell);
furi_thread_join(shell->thread);
}
void cli_shell_set_prompt(CliShell* shell, const char* prompt) {
furi_check(shell);
furi_check(furi_thread_get_state(shell->thread) == FuriThreadStateStopped);
shell->prompt = prompt;
}