1
mirror of https://github.com/DarkFlippers/unleashed-firmware.git synced 2025-12-12 04:34:43 +04:00
Files
unleashed-firmware/applications/services/storage/storage_cli.c
Anna Antonenko 13333edd30 [FL-3954, FL-3955] New CLI architecture (#4111)
* 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: 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: fix rpc lockup

* cli: better lockup fix

* cli: fix f18

* fix merge

---------

Co-authored-by: Georgii Surkov <georgii.surkov@outlook.com>
Co-authored-by: あく <alleteam@gmail.com>
2025-04-02 22:10:10 +04:00

705 lines
21 KiB
C

#include <furi.h>
#include <furi_hal.h>
#include <cli/cli.h>
#include <lib/toolbox/args.h>
#include <lib/toolbox/dir_walk.h>
#include <lib/toolbox/md5_calc.h>
#include <lib/toolbox/strint.h>
#include <lib/toolbox/tar/tar_archive.h>
#include <storage/storage.h>
#include <storage/storage_sd_api.h>
#include <power/power_service/power.h>
#include <toolbox/pipe.h>
#define MAX_NAME_LENGTH 255
static void storage_cli_print_usage(void);
static void storage_cli_print_error(FS_Error error) {
printf("Storage error: %s\r\n", storage_error_get_desc(error));
}
static void storage_cli_info(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
if(furi_string_cmp_str(path, STORAGE_INT_PATH_PREFIX) == 0) {
uint64_t total_space;
uint64_t free_space;
FS_Error error =
storage_common_fs_info(api, STORAGE_INT_PATH_PREFIX, &total_space, &free_space);
if(error != FSE_OK) {
storage_cli_print_error(error);
} else {
printf(
"Label: %s\r\nType: Virtual\r\n%luKiB total\r\n%luKiB free\r\n",
furi_hal_version_get_name_ptr() ? furi_hal_version_get_name_ptr() : "Unknown",
(uint32_t)(total_space / 1024),
(uint32_t)(free_space / 1024));
}
} else if(furi_string_cmp_str(path, STORAGE_EXT_PATH_PREFIX) == 0) {
SDInfo sd_info;
FS_Error error = storage_sd_info(api, &sd_info);
if(error != FSE_OK) {
storage_cli_print_error(error);
} else {
printf(
"Label: %s\r\nType: %s\r\n%luKiB total\r\n%luKiB free\r\n"
"%02x%s %s v%i.%i\r\nSN:%04lx %02i/%i\r\n",
sd_info.label,
sd_api_get_fs_type_text(sd_info.fs_type),
sd_info.kb_total,
sd_info.kb_free,
sd_info.manufacturer_id,
sd_info.oem_id,
sd_info.product_name,
sd_info.product_revision_major,
sd_info.product_revision_minor,
sd_info.product_serial_number,
sd_info.manufacturing_month,
sd_info.manufacturing_year);
}
} else {
storage_cli_print_usage();
}
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_format(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
if(furi_string_cmp_str(path, STORAGE_INT_PATH_PREFIX) == 0) {
storage_cli_print_error(FSE_NOT_IMPLEMENTED);
} else if(furi_string_cmp_str(path, STORAGE_EXT_PATH_PREFIX) == 0) {
printf("Formatting SD card, All data will be lost! Are you sure (y/n)?\r\n");
char answer = getchar();
if(answer == 'y' || answer == 'Y') {
Storage* api = furi_record_open(RECORD_STORAGE);
printf("Formatting, please wait...\r\n");
FS_Error error = storage_sd_format(api);
if(error != FSE_OK) {
storage_cli_print_error(error);
} else {
printf("SD card was successfully formatted.\r\n");
}
furi_record_close(RECORD_STORAGE);
} else {
printf("Cancelled.\r\n");
}
} else {
storage_cli_print_usage();
}
}
static void storage_cli_list(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
if(furi_string_cmp_str(path, "/") == 0) {
printf("\t[D] int\r\n");
printf("\t[D] ext\r\n");
printf("\t[D] any\r\n");
} else {
Storage* api = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(api);
if(storage_dir_open(file, furi_string_get_cstr(path))) {
FileInfo fileinfo;
char name[MAX_NAME_LENGTH];
bool read_done = false;
while(storage_dir_read(file, &fileinfo, name, MAX_NAME_LENGTH)) {
read_done = true;
if(file_info_is_dir(&fileinfo)) {
printf("\t[D] %s\r\n", name);
} else {
printf("\t[F] %s %lub\r\n", name, (uint32_t)(fileinfo.size));
}
}
if(!read_done) {
printf("\tEmpty\r\n");
}
} else {
storage_cli_print_error(storage_file_get_error(file));
}
storage_dir_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
}
static void storage_cli_tree(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(args);
if(furi_string_cmp_str(path, "/") == 0) {
furi_string_set(path, STORAGE_INT_PATH_PREFIX);
storage_cli_tree(pipe, path, NULL);
furi_string_set(path, STORAGE_EXT_PATH_PREFIX);
storage_cli_tree(pipe, path, NULL);
} else {
Storage* api = furi_record_open(RECORD_STORAGE);
DirWalk* dir_walk = dir_walk_alloc(api);
FuriString* name;
name = furi_string_alloc();
if(dir_walk_open(dir_walk, furi_string_get_cstr(path))) {
FileInfo fileinfo;
bool read_done = false;
while(dir_walk_read(dir_walk, name, &fileinfo) == DirWalkOK) {
read_done = true;
if(file_info_is_dir(&fileinfo)) {
printf("\t[D] %s\r\n", furi_string_get_cstr(name));
} else {
printf(
"\t[F] %s %lub\r\n",
furi_string_get_cstr(name),
(uint32_t)(fileinfo.size));
}
}
if(!read_done) {
printf("\tEmpty\r\n");
}
} else {
storage_cli_print_error(dir_walk_get_error(dir_walk));
}
furi_string_free(name);
dir_walk_free(dir_walk);
furi_record_close(RECORD_STORAGE);
}
}
static void storage_cli_read(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(api);
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
const size_t buffer_size = 128;
size_t read_size = 0;
uint8_t* data = malloc(buffer_size);
printf("Size: %lu\r\n", (uint32_t)storage_file_size(file));
do {
read_size = storage_file_read(file, data, buffer_size);
for(size_t i = 0; i < read_size; i++) {
printf("%c", data[i]);
}
} while(read_size > 0);
printf("\r\n");
free(data);
} else {
storage_cli_print_error(storage_file_get_error(file));
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_write(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(api);
const size_t buffer_size = 512;
uint8_t* buffer = malloc(buffer_size);
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_OPEN_APPEND)) {
printf("Just write your text data. New line by Ctrl+Enter, exit by Ctrl+C.\r\n");
uint32_t read_index = 0;
while(true) {
uint8_t symbol = getchar();
if(symbol == CliKeyETX) {
size_t write_size = read_index % buffer_size;
if(write_size > 0) {
size_t written_size = storage_file_write(file, buffer, write_size);
if(written_size != write_size) {
storage_cli_print_error(storage_file_get_error(file));
}
break;
}
}
buffer[read_index % buffer_size] = symbol;
printf("%c", buffer[read_index % buffer_size]);
fflush(stdout);
read_index++;
if((read_index % buffer_size) == 0) {
size_t written_size = storage_file_write(file, buffer, buffer_size);
if(written_size != buffer_size) {
storage_cli_print_error(storage_file_get_error(file));
break;
}
}
}
printf("\r\n");
} else {
storage_cli_print_error(storage_file_get_error(file));
}
storage_file_close(file);
free(buffer);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_read_chunks(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
Storage* api = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(api);
uint32_t buffer_size;
if(strint_to_uint32(furi_string_get_cstr(args), NULL, &buffer_size, 10) !=
StrintParseNoError) {
storage_cli_print_usage();
} else if(storage_file_open(file, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
uint64_t file_size = storage_file_size(file);
printf("Size: %llu\r\n", file_size);
if(buffer_size) {
uint8_t* data = malloc(buffer_size);
while(file_size > 0) {
printf("\r\nReady?\r\n");
getchar();
size_t read_size = storage_file_read(file, data, buffer_size);
for(size_t i = 0; i < read_size; i++) {
putchar(data[i]);
}
file_size -= read_size;
}
free(data);
}
printf("\r\n");
} else {
storage_cli_print_error(storage_file_get_error(file));
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_write_chunk(PipeSide* pipe, FuriString* path, FuriString* args) {
Storage* api = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(api);
uint32_t need_to_read;
if(strint_to_uint32(furi_string_get_cstr(args), NULL, &need_to_read, 10) !=
StrintParseNoError) {
storage_cli_print_usage();
} else {
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_OPEN_APPEND)) {
printf("Ready\r\n");
const size_t buffer_size = 1024;
uint8_t* buffer = malloc(buffer_size);
while(need_to_read) {
size_t read_this_time = pipe_receive(pipe, buffer, MIN(buffer_size, need_to_read));
size_t wrote_this_time = storage_file_write(file, buffer, read_this_time);
if(wrote_this_time != read_this_time) {
storage_cli_print_error(storage_file_get_error(file));
break;
}
need_to_read -= read_this_time;
}
free(buffer);
} else {
storage_cli_print_error(storage_file_get_error(file));
}
storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_stat(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
if(furi_string_cmp_str(path, "/") == 0) {
printf("Storage\r\n");
} else if(
furi_string_cmp_str(path, STORAGE_EXT_PATH_PREFIX) == 0 ||
furi_string_cmp_str(path, STORAGE_INT_PATH_PREFIX) == 0 ||
furi_string_cmp_str(path, STORAGE_ANY_PATH_PREFIX) == 0) {
uint64_t total_space;
uint64_t free_space;
FS_Error error =
storage_common_fs_info(api, furi_string_get_cstr(path), &total_space, &free_space);
if(error != FSE_OK) {
storage_cli_print_error(error);
} else {
printf(
"Storage, %luKiB total, %luKiB free\r\n",
(uint32_t)(total_space / 1024),
(uint32_t)(free_space / 1024));
}
} else {
FileInfo fileinfo;
FS_Error error = storage_common_stat(api, furi_string_get_cstr(path), &fileinfo);
if(error == FSE_OK) {
if(file_info_is_dir(&fileinfo)) {
printf("Directory\r\n");
} else {
printf("File, size: %lub\r\n", (uint32_t)(fileinfo.size));
}
} else {
storage_cli_print_error(error);
}
}
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_timestamp(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
uint32_t timestamp = 0;
FS_Error error = storage_common_timestamp(api, furi_string_get_cstr(path), &timestamp);
if(error != FSE_OK) {
printf("Invalid arguments\r\n");
} else {
printf("Timestamp %lu\r\n", timestamp);
}
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_copy(PipeSide* pipe, FuriString* old_path, FuriString* args) {
UNUSED(pipe);
Storage* api = furi_record_open(RECORD_STORAGE);
FuriString* new_path;
new_path = furi_string_alloc();
if(!args_read_probably_quoted_string_and_trim(args, new_path)) {
storage_cli_print_usage();
} else {
FS_Error error = storage_common_copy(
api, furi_string_get_cstr(old_path), furi_string_get_cstr(new_path));
if(error != FSE_OK) {
storage_cli_print_error(error);
}
}
furi_string_free(new_path);
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_remove(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
FS_Error error = storage_common_remove(api, furi_string_get_cstr(path));
if(error != FSE_OK) {
storage_cli_print_error(error);
}
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_rename(PipeSide* pipe, FuriString* old_path, FuriString* args) {
UNUSED(pipe);
Storage* api = furi_record_open(RECORD_STORAGE);
FuriString* new_path;
new_path = furi_string_alloc();
if(!args_read_probably_quoted_string_and_trim(args, new_path)) {
storage_cli_print_usage();
} else {
FS_Error error = storage_common_rename(
api, furi_string_get_cstr(old_path), furi_string_get_cstr(new_path));
if(error != FSE_OK) {
storage_cli_print_error(error);
}
}
furi_string_free(new_path);
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_mkdir(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
FS_Error error = storage_common_mkdir(api, furi_string_get_cstr(path));
if(error != FSE_OK) {
storage_cli_print_error(error);
}
furi_record_close(RECORD_STORAGE);
}
static void storage_cli_md5(PipeSide* pipe, FuriString* path, FuriString* args) {
UNUSED(pipe);
UNUSED(args);
Storage* api = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(api);
FuriString* md5 = furi_string_alloc();
FS_Error file_error;
if(md5_string_calc_file(file, furi_string_get_cstr(path), md5, &file_error)) {
printf("%s\r\n", furi_string_get_cstr(md5));
} else {
storage_cli_print_error(file_error);
}
furi_string_free(md5);
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
static bool tar_extract_file_callback(const char* name, bool is_directory, void* context) {
UNUSED(context);
printf("\t%s %s\r\n", is_directory ? "D" : "F", name);
return true;
}
static void storage_cli_extract(PipeSide* pipe, FuriString* old_path, FuriString* args) {
UNUSED(pipe);
FuriString* new_path = furi_string_alloc();
if(!args_read_probably_quoted_string_and_trim(args, new_path)) {
storage_cli_print_usage();
furi_string_free(new_path);
return;
}
Storage* api = furi_record_open(RECORD_STORAGE);
TarArchive* archive = tar_archive_alloc(api);
TarOpenMode tar_mode = tar_archive_get_mode_for_path(furi_string_get_cstr(old_path));
do {
if(!tar_archive_open(archive, furi_string_get_cstr(old_path), tar_mode)) {
printf("Failed to open archive\r\n");
break;
}
uint32_t start_tick = furi_get_tick();
tar_archive_set_file_callback(archive, tar_extract_file_callback, NULL);
printf("Unpacking to %s\r\n", furi_string_get_cstr(new_path));
bool success = tar_archive_unpack_to(archive, furi_string_get_cstr(new_path), NULL);
uint32_t end_tick = furi_get_tick();
printf(
"Decompression %s in %lu ticks \r\n",
success ? "success" : "failed",
end_tick - start_tick);
} while(false);
tar_archive_free(archive);
furi_string_free(new_path);
furi_record_close(RECORD_STORAGE);
}
typedef void (*StorageCliCommandCallback)(PipeSide* pipe, FuriString* path, FuriString* args);
typedef struct {
const char* command;
const char* help;
const StorageCliCommandCallback impl;
} StorageCliCommand;
static const StorageCliCommand storage_cli_commands[] = {
{
"write_chunk",
"read data from cli and append it to file, <args> should contain how many bytes you want to write",
&storage_cli_write_chunk,
},
{
"read_chunks",
"read data from file and print file size and content to cli, <args> should contain how many bytes you want to read in block",
&storage_cli_read_chunks,
},
{
"list",
"list files and dirs",
&storage_cli_list,
},
{
"md5",
"md5 hash of the file",
&storage_cli_md5,
},
{
"stat",
"info about file or dir",
&storage_cli_stat,
},
{
"info",
"get FS info",
&storage_cli_info,
},
{
"tree",
"list files and dirs, recursive",
&storage_cli_tree,
},
{
"read",
"read text from file and print file size and content to cli",
&storage_cli_read,
},
{
"write",
"read text from cli and append it to file, stops by ctrl+c",
&storage_cli_write,
},
{
"copy",
"copy file to new file, <args> must contain new path",
&storage_cli_copy,
},
{
"remove",
"delete the file or directory",
&storage_cli_remove,
},
{
"rename",
"move file to new file, <args> must contain new path",
&storage_cli_rename,
},
{
"mkdir",
"creates a new directory",
&storage_cli_mkdir,
},
{
"timestamp",
"last modification timestamp",
&storage_cli_timestamp,
},
{
"extract",
"extract tar archive to destination",
&storage_cli_extract,
},
{
"format",
"format filesystem",
&storage_cli_format,
},
};
static void storage_cli_print_usage(void) {
printf("Usage:\r\n");
printf("storage <cmd> <path> <args>\r\n");
printf("The path must start with /int or /ext\r\n");
printf("Cmd list:\r\n");
for(size_t i = 0; i < COUNT_OF(storage_cli_commands); ++i) {
const StorageCliCommand* command_descr = &storage_cli_commands[i];
const char* cli_cmd = command_descr->command;
printf(
"\t%s%s - %s\r\n", cli_cmd, strlen(cli_cmd) > 8 ? "\t" : "\t\t", command_descr->help);
}
}
void storage_cli(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(context);
FuriString* cmd;
FuriString* path;
cmd = furi_string_alloc();
path = furi_string_alloc();
do {
if(!args_read_string_and_trim(args, cmd)) {
storage_cli_print_usage();
break;
}
if(!args_read_probably_quoted_string_and_trim(args, path)) {
storage_cli_print_usage();
break;
}
size_t i = 0;
for(; i < COUNT_OF(storage_cli_commands); ++i) {
const StorageCliCommand* command_descr = &storage_cli_commands[i];
if(furi_string_cmp_str(cmd, command_descr->command) == 0) {
command_descr->impl(pipe, path, args);
break;
}
}
if(i == COUNT_OF(storage_cli_commands)) {
storage_cli_print_usage();
}
} while(false);
furi_string_free(path);
furi_string_free(cmd);
}
static void storage_cli_factory_reset(PipeSide* pipe, FuriString* args, void* context) {
UNUSED(pipe);
UNUSED(args);
UNUSED(context);
printf("All data will be lost! Are you sure (y/n)?\r\n");
char c = getchar();
if(c == 'y' || c == 'Y') {
printf("Data will be wiped after reboot.\r\n");
furi_hal_rtc_reset_registers();
furi_hal_rtc_set_flag(FuriHalRtcFlagStorageFormatInternal);
Power* power = furi_record_open(RECORD_POWER);
power_reboot(power, PowerBootModeNormal);
} else {
printf("Safe choice.\r\n");
}
}
void storage_on_system_start(void) {
#ifdef SRV_CLI
Cli* cli = furi_record_open(RECORD_CLI);
cli_add_command_ex(cli, "storage", CliCommandFlagParallelSafe, storage_cli, NULL, 512);
cli_add_command(
cli, "factory_reset", CliCommandFlagParallelSafe, storage_cli_factory_reset, NULL);
furi_record_close(RECORD_CLI);
#else
UNUSED(storage_cli_factory_reset);
#endif
}