[FL-2837][FL-3270] Loader refaptoring: second encounter (#2779)
* Core: rename internal FlipperApplication to FlipperInternalApplication * FAP Loader: move load_name_and_icon to flipper_application library * Loader menu: rework api * View holder: move to gui service * Loader: simple "loading" worker * Loader: applications dialog * Loader: fapping * Update f18 api * Apps: remove fap_loader * Libs, flipper application: store args, rename thread allocation * Loader: error handling * Apps: use loader error handling * Loader: documentation * FBT: accomodate loader * Loader: do not raise gui error if loader is locked * Archive: accomodate loader * Loader: fix loading message * Flipper: drop some old dolphin legacy * Loader: generalize error construction Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									4ddfe05a59
								
							
						
					
					
						commit
						761a14e6e2
					
				| @ -26,7 +26,6 @@ Applications for main Flipper menu. | ||||
| 
 | ||||
| - `archive`             - Archive and file manager  | ||||
| - `bad_usb`             - Bad USB application | ||||
| - `fap_loader`          - External applications loader | ||||
| - `gpio`                - GPIO application: includes USART bridge and GPIO control | ||||
| - `ibutton`             - iButton application, onewire keys and more | ||||
| - `infrared`            - Infrared application, controls your IR devices | ||||
|  | ||||
| @ -12,7 +12,6 @@ App( | ||||
|         "subghz", | ||||
|         "bad_usb", | ||||
|         "u2f", | ||||
|         "fap_loader", | ||||
|         "archive", | ||||
|     ], | ||||
| ) | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
| #include <core/common_defines.h> | ||||
| #include <core/log.h> | ||||
| #include <gui/modules/file_browser_worker.h> | ||||
| #include <fap_loader/fap_loader_app.h> | ||||
| #include <flipper_application/flipper_application.h> | ||||
| #include <math.h> | ||||
| 
 | ||||
| static void | ||||
| @ -367,7 +367,7 @@ void archive_add_app_item(ArchiveBrowserView* browser, const char* name) { | ||||
| static bool archive_get_fap_meta(FuriString* file_path, FuriString* fap_name, uint8_t** icon_ptr) { | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     bool success = false; | ||||
|     if(fap_loader_load_name_and_icon(file_path, storage, icon_ptr, fap_name)) { | ||||
|     if(flipper_application_load_name_and_icon(file_path, storage, icon_ptr, fap_name)) { | ||||
|         success = true; | ||||
|     } | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|  | ||||
| @ -11,17 +11,28 @@ | ||||
| #define SCENE_STATE_DEFAULT (0) | ||||
| #define SCENE_STATE_NEED_REFRESH (1) | ||||
| 
 | ||||
| static const char* flipper_app_name[] = { | ||||
|     [ArchiveFileTypeIButton] = "iButton", | ||||
|     [ArchiveFileTypeNFC] = "NFC", | ||||
|     [ArchiveFileTypeSubGhz] = "Sub-GHz", | ||||
|     [ArchiveFileTypeLFRFID] = "125 kHz RFID", | ||||
|     [ArchiveFileTypeInfrared] = "Infrared", | ||||
|     [ArchiveFileTypeBadUsb] = "Bad USB", | ||||
|     [ArchiveFileTypeU2f] = "U2F", | ||||
|     [ArchiveFileTypeUpdateManifest] = "UpdaterApp", | ||||
|     [ArchiveFileTypeApplication] = "Applications", | ||||
| }; | ||||
| const char* archive_get_flipper_app_name(ArchiveFileTypeEnum file_type) { | ||||
|     switch(file_type) { | ||||
|     case ArchiveFileTypeIButton: | ||||
|         return "iButton"; | ||||
|     case ArchiveFileTypeNFC: | ||||
|         return "NFC"; | ||||
|     case ArchiveFileTypeSubGhz: | ||||
|         return "Sub-GHz"; | ||||
|     case ArchiveFileTypeLFRFID: | ||||
|         return "125 kHz RFID"; | ||||
|     case ArchiveFileTypeInfrared: | ||||
|         return "Infrared"; | ||||
|     case ArchiveFileTypeBadUsb: | ||||
|         return "Bad USB"; | ||||
|     case ArchiveFileTypeU2f: | ||||
|         return "U2F"; | ||||
|     case ArchiveFileTypeUpdateManifest: | ||||
|         return "UpdaterApp"; | ||||
|     default: | ||||
|         return NULL; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void archive_loader_callback(const void* message, void* context) { | ||||
|     furi_assert(message); | ||||
| @ -39,20 +50,20 @@ static void archive_run_in_app(ArchiveBrowserView* browser, ArchiveFile_t* selec | ||||
|     UNUSED(browser); | ||||
|     Loader* loader = furi_record_open(RECORD_LOADER); | ||||
| 
 | ||||
|     LoaderStatus status; | ||||
|     const char* app_name = archive_get_flipper_app_name(selected->type); | ||||
| 
 | ||||
|     if(app_name) { | ||||
|         if(selected->is_app) { | ||||
|             char* param = strrchr(furi_string_get_cstr(selected->path), '/'); | ||||
|             if(param != NULL) { | ||||
|                 param++; | ||||
|             } | ||||
|         status = loader_start(loader, flipper_app_name[selected->type], param); | ||||
|             loader_start_with_gui_error(loader, app_name, param); | ||||
|         } else { | ||||
|         status = loader_start( | ||||
|             loader, flipper_app_name[selected->type], furi_string_get_cstr(selected->path)); | ||||
|             loader_start_with_gui_error(loader, app_name, furi_string_get_cstr(selected->path)); | ||||
|         } | ||||
| 
 | ||||
|     if(status != LoaderStatusOk) { | ||||
|         FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
|     } else { | ||||
|         loader_start_with_gui_error(loader, furi_string_get_cstr(selected->path), NULL); | ||||
|     } | ||||
| 
 | ||||
|     furi_record_close(RECORD_LOADER); | ||||
|  | ||||
| @ -1,15 +0,0 @@ | ||||
| App( | ||||
|     appid="fap_loader", | ||||
|     name="Applications", | ||||
|     apptype=FlipperAppType.APP, | ||||
|     entry_point="fap_loader_app", | ||||
|     cdefines=["APP_FAP_LOADER"], | ||||
|     requires=[ | ||||
|         "gui", | ||||
|         "storage", | ||||
|         "loader", | ||||
|     ], | ||||
|     stack_size=int(1.5 * 1024), | ||||
|     icon="A_Plugins_14", | ||||
|     order=90, | ||||
| ) | ||||
| @ -1,216 +0,0 @@ | ||||
| #include "fap_loader_app.h" | ||||
| 
 | ||||
| #include <furi.h> | ||||
| #include <furi_hal_debug.h> | ||||
| 
 | ||||
| #include <assets_icons.h> | ||||
| #include <gui/gui.h> | ||||
| #include <gui/view_dispatcher.h> | ||||
| #include <gui/modules/loading.h> | ||||
| #include <dialogs/dialogs.h> | ||||
| #include <toolbox/path.h> | ||||
| #include <flipper_application/flipper_application.h> | ||||
| #include <loader/firmware_api/firmware_api.h> | ||||
| 
 | ||||
| #define TAG "FapLoader" | ||||
| 
 | ||||
| struct FapLoader { | ||||
|     FlipperApplication* app; | ||||
|     Storage* storage; | ||||
|     DialogsApp* dialogs; | ||||
|     Gui* gui; | ||||
|     FuriString* fap_path; | ||||
|     ViewDispatcher* view_dispatcher; | ||||
|     Loading* loading; | ||||
| }; | ||||
| 
 | ||||
| bool fap_loader_load_name_and_icon( | ||||
|     FuriString* path, | ||||
|     Storage* storage, | ||||
|     uint8_t** icon_ptr, | ||||
|     FuriString* item_name) { | ||||
|     FlipperApplication* app = flipper_application_alloc(storage, firmware_api_interface); | ||||
| 
 | ||||
|     FlipperApplicationPreloadStatus preload_res = | ||||
|         flipper_application_preload_manifest(app, furi_string_get_cstr(path)); | ||||
| 
 | ||||
|     bool load_success = false; | ||||
| 
 | ||||
|     if(preload_res == FlipperApplicationPreloadStatusSuccess) { | ||||
|         const FlipperApplicationManifest* manifest = flipper_application_get_manifest(app); | ||||
|         if(manifest->has_icon) { | ||||
|             memcpy(*icon_ptr, manifest->icon, FAP_MANIFEST_MAX_ICON_SIZE); | ||||
|         } | ||||
|         furi_string_set(item_name, manifest->name); | ||||
|         load_success = true; | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "FAP Loader failed to preload %s", furi_string_get_cstr(path)); | ||||
|         load_success = false; | ||||
|     } | ||||
| 
 | ||||
|     flipper_application_free(app); | ||||
|     return load_success; | ||||
| } | ||||
| 
 | ||||
| static bool fap_loader_item_callback( | ||||
|     FuriString* path, | ||||
|     void* context, | ||||
|     uint8_t** icon_ptr, | ||||
|     FuriString* item_name) { | ||||
|     FapLoader* fap_loader = context; | ||||
|     furi_assert(fap_loader); | ||||
|     return fap_loader_load_name_and_icon(path, fap_loader->storage, icon_ptr, item_name); | ||||
| } | ||||
| 
 | ||||
| static bool fap_loader_run_selected_app(FapLoader* loader) { | ||||
|     furi_assert(loader); | ||||
| 
 | ||||
|     FuriString* error_message; | ||||
| 
 | ||||
|     error_message = furi_string_alloc_set("unknown error"); | ||||
| 
 | ||||
|     bool file_selected = false; | ||||
|     bool show_error = true; | ||||
|     do { | ||||
|         file_selected = true; | ||||
|         loader->app = flipper_application_alloc(loader->storage, firmware_api_interface); | ||||
|         size_t start = furi_get_tick(); | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "FAP Loader is loading %s", furi_string_get_cstr(loader->fap_path)); | ||||
| 
 | ||||
|         FlipperApplicationPreloadStatus preload_res = | ||||
|             flipper_application_preload(loader->app, furi_string_get_cstr(loader->fap_path)); | ||||
|         if(preload_res != FlipperApplicationPreloadStatusSuccess) { | ||||
|             const char* err_msg = flipper_application_preload_status_to_string(preload_res); | ||||
|             furi_string_printf(error_message, "Preload failed: %s", err_msg); | ||||
|             FURI_LOG_E( | ||||
|                 TAG, | ||||
|                 "FAP Loader failed to preload %s: %s", | ||||
|                 furi_string_get_cstr(loader->fap_path), | ||||
|                 err_msg); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "FAP Loader is mapping"); | ||||
|         FlipperApplicationLoadStatus load_status = flipper_application_map_to_memory(loader->app); | ||||
|         if(load_status != FlipperApplicationLoadStatusSuccess) { | ||||
|             const char* err_msg = flipper_application_load_status_to_string(load_status); | ||||
|             furi_string_printf(error_message, "Load failed: %s", err_msg); | ||||
|             FURI_LOG_E( | ||||
|                 TAG, | ||||
|                 "FAP Loader failed to map to memory %s: %s", | ||||
|                 furi_string_get_cstr(loader->fap_path), | ||||
|                 err_msg); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "Loaded in %ums", (size_t)(furi_get_tick() - start)); | ||||
|         FURI_LOG_I(TAG, "FAP Loader is starting app"); | ||||
| 
 | ||||
|         FuriThread* thread = flipper_application_spawn(loader->app, NULL); | ||||
| 
 | ||||
|         /* This flag is set by the debugger - to break on app start */ | ||||
|         if(furi_hal_debug_is_gdb_session_active()) { | ||||
|             FURI_LOG_W(TAG, "Triggering BP for debugger"); | ||||
|             /* After hitting this, you can set breakpoints in your .fap's code
 | ||||
|              * Note that you have to toggle breakpoints that were set before */ | ||||
|             __asm volatile("bkpt 0"); | ||||
|         } | ||||
| 
 | ||||
|         FuriString* app_name = furi_string_alloc(); | ||||
|         path_extract_filename_no_ext(furi_string_get_cstr(loader->fap_path), app_name); | ||||
|         furi_thread_set_appid(thread, furi_string_get_cstr(app_name)); | ||||
|         furi_string_free(app_name); | ||||
| 
 | ||||
|         furi_thread_start(thread); | ||||
|         furi_thread_join(thread); | ||||
| 
 | ||||
|         show_error = false; | ||||
|         int ret = furi_thread_get_return_code(thread); | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "FAP app returned: %i", ret); | ||||
|     } while(0); | ||||
| 
 | ||||
|     if(show_error) { | ||||
|         DialogMessage* message = dialog_message_alloc(); | ||||
|         dialog_message_set_header(message, "Error", 64, 0, AlignCenter, AlignTop); | ||||
|         dialog_message_set_buttons(message, NULL, NULL, NULL); | ||||
| 
 | ||||
|         FuriString* buffer; | ||||
|         buffer = furi_string_alloc(); | ||||
|         furi_string_printf(buffer, "%s", furi_string_get_cstr(error_message)); | ||||
|         furi_string_replace(buffer, ":", "\n"); | ||||
|         dialog_message_set_text( | ||||
|             message, furi_string_get_cstr(buffer), 64, 32, AlignCenter, AlignCenter); | ||||
| 
 | ||||
|         dialog_message_show(loader->dialogs, message); | ||||
|         dialog_message_free(message); | ||||
|         furi_string_free(buffer); | ||||
|     } | ||||
| 
 | ||||
|     furi_string_free(error_message); | ||||
| 
 | ||||
|     if(file_selected) { | ||||
|         flipper_application_free(loader->app); | ||||
|     } | ||||
| 
 | ||||
|     return file_selected; | ||||
| } | ||||
| 
 | ||||
| static bool fap_loader_select_app(FapLoader* loader) { | ||||
|     const DialogsFileBrowserOptions browser_options = { | ||||
|         .extension = ".fap", | ||||
|         .skip_assets = true, | ||||
|         .icon = &I_unknown_10px, | ||||
|         .hide_ext = true, | ||||
|         .item_loader_callback = fap_loader_item_callback, | ||||
|         .item_loader_context = loader, | ||||
|         .base_path = EXT_PATH("apps"), | ||||
|     }; | ||||
| 
 | ||||
|     return dialog_file_browser_show( | ||||
|         loader->dialogs, loader->fap_path, loader->fap_path, &browser_options); | ||||
| } | ||||
| 
 | ||||
| static FapLoader* fap_loader_alloc(const char* path) { | ||||
|     FapLoader* loader = malloc(sizeof(FapLoader)); //-V799
 | ||||
|     loader->fap_path = furi_string_alloc_set(path); | ||||
|     loader->storage = furi_record_open(RECORD_STORAGE); | ||||
|     loader->dialogs = furi_record_open(RECORD_DIALOGS); | ||||
|     loader->gui = furi_record_open(RECORD_GUI); | ||||
|     loader->view_dispatcher = view_dispatcher_alloc(); | ||||
|     loader->loading = loading_alloc(); | ||||
|     view_dispatcher_attach_to_gui( | ||||
|         loader->view_dispatcher, loader->gui, ViewDispatcherTypeFullscreen); | ||||
|     view_dispatcher_add_view(loader->view_dispatcher, 0, loading_get_view(loader->loading)); | ||||
|     return loader; | ||||
| } //-V773
 | ||||
| 
 | ||||
| static void fap_loader_free(FapLoader* loader) { | ||||
|     view_dispatcher_remove_view(loader->view_dispatcher, 0); | ||||
|     loading_free(loader->loading); | ||||
|     view_dispatcher_free(loader->view_dispatcher); | ||||
|     furi_string_free(loader->fap_path); | ||||
|     furi_record_close(RECORD_GUI); | ||||
|     furi_record_close(RECORD_DIALOGS); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|     free(loader); | ||||
| } | ||||
| 
 | ||||
| int32_t fap_loader_app(void* p) { | ||||
|     FapLoader* loader; | ||||
|     if(p) { | ||||
|         loader = fap_loader_alloc((const char*)p); | ||||
|         view_dispatcher_switch_to_view(loader->view_dispatcher, 0); | ||||
|         fap_loader_run_selected_app(loader); | ||||
|     } else { | ||||
|         loader = fap_loader_alloc(EXT_PATH("apps")); | ||||
|         while(fap_loader_select_app(loader)) { | ||||
|             view_dispatcher_switch_to_view(loader->view_dispatcher, 0); | ||||
|             fap_loader_run_selected_app(loader); | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     fap_loader_free(loader); | ||||
|     return 0; | ||||
| } | ||||
| @ -1,27 +0,0 @@ | ||||
| #pragma once | ||||
| #include <storage/storage.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| typedef struct FapLoader FapLoader; | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Load name and icon from FAP file. | ||||
|  *  | ||||
|  * @param path Path to FAP file. | ||||
|  * @param storage Storage instance. | ||||
|  * @param icon_ptr Icon pointer. | ||||
|  * @param item_name Application name. | ||||
|  * @return true if icon and name were loaded successfully. | ||||
|  */ | ||||
| bool fap_loader_load_name_and_icon( | ||||
|     FuriString* path, | ||||
|     Storage* storage, | ||||
|     uint8_t** icon_ptr, | ||||
|     FuriString* item_name); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
| @ -4,9 +4,9 @@ | ||||
| #include <gui/icon.h> | ||||
| 
 | ||||
| typedef enum { | ||||
|     FlipperApplicationFlagDefault = 0, | ||||
|     FlipperApplicationFlagInsomniaSafe = (1 << 0), | ||||
| } FlipperApplicationFlag; | ||||
|     FlipperInternalApplicationFlagDefault = 0, | ||||
|     FlipperInternalApplicationFlagInsomniaSafe = (1 << 0), | ||||
| } FlipperInternalApplicationFlag; | ||||
| 
 | ||||
| typedef struct { | ||||
|     const FuriThreadCallback app; | ||||
| @ -14,48 +14,41 @@ typedef struct { | ||||
|     const char* appid; | ||||
|     const size_t stack_size; | ||||
|     const Icon* icon; | ||||
|     const FlipperApplicationFlag flags; | ||||
| } FlipperApplication; | ||||
|     const FlipperInternalApplicationFlag flags; | ||||
| } FlipperInternalApplication; | ||||
| 
 | ||||
| typedef void (*FlipperOnStartHook)(void); | ||||
| typedef void (*FlipperInternalOnStartHook)(void); | ||||
| 
 | ||||
| extern const char* FLIPPER_AUTORUN_APP_NAME; | ||||
| 
 | ||||
| /* Services list
 | ||||
|  * Spawned on startup | ||||
|  */ | ||||
| extern const FlipperApplication FLIPPER_SERVICES[]; | ||||
| extern const FlipperInternalApplication FLIPPER_SERVICES[]; | ||||
| extern const size_t FLIPPER_SERVICES_COUNT; | ||||
| 
 | ||||
| /* Apps list
 | ||||
|  * Spawned by loader | ||||
|  */ | ||||
| extern const FlipperApplication FLIPPER_APPS[]; | ||||
| extern const FlipperInternalApplication FLIPPER_APPS[]; | ||||
| extern const size_t FLIPPER_APPS_COUNT; | ||||
| 
 | ||||
| /* On system start hooks
 | ||||
|  * Called by loader, after OS initialization complete | ||||
|  */ | ||||
| extern const FlipperOnStartHook FLIPPER_ON_SYSTEM_START[]; | ||||
| extern const FlipperInternalOnStartHook FLIPPER_ON_SYSTEM_START[]; | ||||
| extern const size_t FLIPPER_ON_SYSTEM_START_COUNT; | ||||
| 
 | ||||
| /* System apps
 | ||||
|  * Can only be spawned by loader by name | ||||
|  */ | ||||
| extern const FlipperApplication FLIPPER_SYSTEM_APPS[]; | ||||
| extern const FlipperInternalApplication FLIPPER_SYSTEM_APPS[]; | ||||
| extern const size_t FLIPPER_SYSTEM_APPS_COUNT; | ||||
| 
 | ||||
| /* Separate scene app holder
 | ||||
|  * Spawned by loader | ||||
|  */ | ||||
| extern const FlipperApplication FLIPPER_SCENE; | ||||
| extern const FlipperApplication FLIPPER_SCENE_APPS[]; | ||||
| extern const size_t FLIPPER_SCENE_APPS_COUNT; | ||||
| 
 | ||||
| extern const FlipperApplication FLIPPER_ARCHIVE; | ||||
| extern const FlipperInternalApplication FLIPPER_ARCHIVE; | ||||
| 
 | ||||
| /* Settings list
 | ||||
|  * Spawned by loader | ||||
|  */ | ||||
| extern const FlipperApplication FLIPPER_SETTINGS_APPS[]; | ||||
| extern const FlipperInternalApplication FLIPPER_SETTINGS_APPS[]; | ||||
| extern const size_t FLIPPER_SETTINGS_APPS_COUNT; | ||||
|  | ||||
| @ -36,6 +36,7 @@ static void desktop_loader_callback(const void* message, void* context) { | ||||
|         view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalAfterAppFinished); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void desktop_lock_icon_draw_callback(Canvas* canvas, void* context) { | ||||
|     UNUSED(context); | ||||
|     furi_assert(canvas); | ||||
|  | ||||
| @ -16,8 +16,6 @@ | ||||
| #define SNAKE_GAME_APP EXT_PATH("/apps/Games/snake_game.fap") | ||||
| #define CLOCK_APP EXT_PATH("/apps/Tools/clock.fap") | ||||
| 
 | ||||
| #define FAP_LOADER_APP_NAME "Applications" | ||||
| 
 | ||||
| static void desktop_scene_main_new_idle_animation_callback(void* context) { | ||||
|     furi_assert(context); | ||||
|     Desktop* desktop = context; | ||||
| @ -40,7 +38,8 @@ static void desktop_scene_main_interact_animation_callback(void* context) { | ||||
| } | ||||
| 
 | ||||
| #ifdef APP_ARCHIVE | ||||
| static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* flipper_app) { | ||||
| static void | ||||
|     desktop_switch_to_app(Desktop* desktop, const FlipperInternalApplication* flipper_app) { | ||||
|     furi_assert(desktop); | ||||
|     furi_assert(flipper_app); | ||||
|     furi_assert(flipper_app->app); | ||||
| @ -67,30 +66,16 @@ static void desktop_switch_to_app(Desktop* desktop, const FlipperApplication* fl | ||||
| #endif | ||||
| 
 | ||||
| static void desktop_scene_main_open_app_or_profile(Desktop* desktop, const char* path) { | ||||
|     do { | ||||
|         LoaderStatus status = loader_start(desktop->loader, FAP_LOADER_APP_NAME, path); | ||||
|         if(status == LoaderStatusOk) break; | ||||
|         FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
| 
 | ||||
|         status = loader_start(desktop->loader, "Passport", NULL); | ||||
|         if(status != LoaderStatusOk) { | ||||
|             FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
|     if(loader_start_with_gui_error(desktop->loader, path, NULL) != LoaderStatusOk) { | ||||
|         loader_start(desktop->loader, "Passport", NULL, NULL); | ||||
|     } | ||||
|     } while(false); | ||||
| } | ||||
| 
 | ||||
| static void desktop_scene_main_start_favorite(Desktop* desktop, FavoriteApp* application) { | ||||
|     LoaderStatus status = LoaderStatusErrorInternal; | ||||
|     if(application->is_external) { | ||||
|         status = loader_start(desktop->loader, FAP_LOADER_APP_NAME, application->name_or_path); | ||||
|     } else if(strlen(application->name_or_path) > 0) { | ||||
|         status = loader_start(desktop->loader, application->name_or_path, NULL); | ||||
|     if(strlen(application->name_or_path) > 0) { | ||||
|         loader_start_with_gui_error(desktop->loader, application->name_or_path, NULL); | ||||
|     } else { | ||||
|         status = loader_start(desktop->loader, FAP_LOADER_APP_NAME, NULL); | ||||
|     } | ||||
| 
 | ||||
|     if(status != LoaderStatusOk) { | ||||
|         FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
|         loader_start(desktop->loader, LOADER_APPLICATIONS_NAME, NULL, NULL); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -148,10 +133,7 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { | ||||
|             break; | ||||
| 
 | ||||
|         case DesktopMainEventOpenPowerOff: { | ||||
|             LoaderStatus status = loader_start(desktop->loader, "Power", "off"); | ||||
|             if(status != LoaderStatusOk) { | ||||
|                 FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
|             } | ||||
|             loader_start(desktop->loader, "Power", "off", NULL); | ||||
|             consumed = true; | ||||
|             break; | ||||
|         } | ||||
| @ -176,18 +158,12 @@ bool desktop_scene_main_on_event(void* context, SceneManagerEvent event) { | ||||
|             break; | ||||
|         case DesktopAnimationEventInteractAnimation: | ||||
|             if(!animation_manager_interact_process(desktop->animation_manager)) { | ||||
|                 LoaderStatus status = loader_start(desktop->loader, "Passport", NULL); | ||||
|                 if(status != LoaderStatusOk) { | ||||
|                     FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
|                 } | ||||
|                 loader_start(desktop->loader, "Passport", NULL, NULL); | ||||
|             } | ||||
|             consumed = true; | ||||
|             break; | ||||
|         case DesktopMainEventOpenPassport: { | ||||
|             LoaderStatus status = loader_start(desktop->loader, "Passport", NULL); | ||||
|             if(status != LoaderStatusOk) { | ||||
|                 FURI_LOG_E(TAG, "loader_start failed: %d", status); | ||||
|             } | ||||
|             loader_start(desktop->loader, "Passport", NULL, NULL); | ||||
|             break; | ||||
|         } | ||||
|         case DesktopMainEventOpenGame: { | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| #pragma once | ||||
| #include "dialogs.h" | ||||
| #include "dialogs_message.h" | ||||
| #include "view_holder.h" | ||||
| #include <gui/view_holder.h> | ||||
| #include <gui/modules/file_browser.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
|  | ||||
| @ -1,20 +1,27 @@ | ||||
| #include "loader.h" | ||||
| #include "loader_i.h" | ||||
| #include "loader_menu.h" | ||||
| #include <applications.h> | ||||
| #include <storage/storage.h> | ||||
| #include <furi_hal.h> | ||||
| 
 | ||||
| #include <dialogs/dialogs.h> | ||||
| #include <toolbox/path.h> | ||||
| #include <flipper_application/flipper_application.h> | ||||
| #include <loader/firmware_api/firmware_api.h> | ||||
| 
 | ||||
| #define TAG "Loader" | ||||
| #define LOADER_MAGIC_THREAD_VALUE 0xDEADBEEF | ||||
| // api
 | ||||
| 
 | ||||
| LoaderStatus loader_start(Loader* loader, const char* name, const char* args) { | ||||
| LoaderStatus | ||||
|     loader_start(Loader* loader, const char* name, const char* args, FuriString* error_message) { | ||||
|     LoaderMessage message; | ||||
|     LoaderMessageLoaderStatusResult result; | ||||
| 
 | ||||
|     message.type = LoaderMessageTypeStartByName; | ||||
|     message.start.name = name; | ||||
|     message.start.args = args; | ||||
|     message.start.error_message = error_message; | ||||
|     message.api_lock = api_lock_alloc_locked(); | ||||
|     message.status_value = &result; | ||||
|     furi_message_queue_put(loader->queue, &message, FuriWaitForever); | ||||
| @ -22,6 +29,31 @@ LoaderStatus loader_start(Loader* loader, const char* name, const char* args) { | ||||
|     return result.value; | ||||
| } | ||||
| 
 | ||||
| LoaderStatus loader_start_with_gui_error(Loader* loader, const char* name, const char* args) { | ||||
|     FuriString* error_message = furi_string_alloc(); | ||||
|     LoaderStatus status = loader_start(loader, name, args, error_message); | ||||
| 
 | ||||
|     // TODO: we have many places where we can emit a double start, ex: desktop, menu
 | ||||
|     // so i prefer to not show LoaderStatusErrorAppStarted error message for now
 | ||||
|     if(status == LoaderStatusErrorUnknownApp || status == LoaderStatusErrorInternal) { | ||||
|         DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); | ||||
|         DialogMessage* message = dialog_message_alloc(); | ||||
|         dialog_message_set_header(message, "Error", 64, 0, AlignCenter, AlignTop); | ||||
|         dialog_message_set_buttons(message, NULL, NULL, NULL); | ||||
| 
 | ||||
|         furi_string_replace(error_message, ":", "\n"); | ||||
|         dialog_message_set_text( | ||||
|             message, furi_string_get_cstr(error_message), 64, 32, AlignCenter, AlignCenter); | ||||
| 
 | ||||
|         dialog_message_show(dialogs, message); | ||||
|         dialog_message_free(message); | ||||
|         furi_record_close(RECORD_DIALOGS); | ||||
|     } | ||||
| 
 | ||||
|     furi_string_free(error_message); | ||||
|     return status; | ||||
| } | ||||
| 
 | ||||
| bool loader_lock(Loader* loader) { | ||||
|     LoaderMessage message; | ||||
|     LoaderMessageBoolResult result; | ||||
| @ -73,27 +105,26 @@ static void loader_menu_closed_callback(void* context) { | ||||
|     furi_message_queue_put(loader->queue, &message, FuriWaitForever); | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_click_callback(const char* name, void* context) { | ||||
| static void loader_applications_closed_callback(void* context) { | ||||
|     Loader* loader = context; | ||||
|     loader_start(loader, name, NULL); | ||||
|     LoaderMessage message; | ||||
|     message.type = LoaderMessageTypeApplicationsClosed; | ||||
|     furi_message_queue_put(loader->queue, &message, FuriWaitForever); | ||||
| } | ||||
| 
 | ||||
| static void loader_thread_state_callback(FuriThreadState thread_state, void* context) { | ||||
|     furi_assert(context); | ||||
| 
 | ||||
|     Loader* loader = context; | ||||
|     LoaderEvent event; | ||||
| 
 | ||||
|     if(thread_state == FuriThreadStateRunning) { | ||||
|         LoaderEvent event; | ||||
|         event.type = LoaderEventTypeApplicationStarted; | ||||
|         furi_pubsub_publish(loader->pubsub, &event); | ||||
|     } else if(thread_state == FuriThreadStateStopped) { | ||||
|         LoaderMessage message; | ||||
|         message.type = LoaderMessageTypeAppClosed; | ||||
|         furi_message_queue_put(loader->queue, &message, FuriWaitForever); | ||||
| 
 | ||||
|         event.type = LoaderEventTypeApplicationStopped; | ||||
|         furi_pubsub_publish(loader->pubsub, &event); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -104,16 +135,17 @@ static Loader* loader_alloc() { | ||||
|     loader->pubsub = furi_pubsub_alloc(); | ||||
|     loader->queue = furi_message_queue_alloc(1, sizeof(LoaderMessage)); | ||||
|     loader->loader_menu = NULL; | ||||
|     loader->loader_applications = NULL; | ||||
|     loader->app.args = NULL; | ||||
|     loader->app.name = NULL; | ||||
|     loader->app.thread = NULL; | ||||
|     loader->app.insomniac = false; | ||||
|     loader->app.fap = NULL; | ||||
|     return loader; | ||||
| } | ||||
| 
 | ||||
| static FlipperApplication const* loader_find_application_by_name_in_list( | ||||
| static FlipperInternalApplication const* loader_find_application_by_name_in_list( | ||||
|     const char* name, | ||||
|     const FlipperApplication* list, | ||||
|     const FlipperInternalApplication* list, | ||||
|     const uint32_t n_apps) { | ||||
|     for(size_t i = 0; i < n_apps; i++) { | ||||
|         if(strcmp(name, list[i].name) == 0) { | ||||
| @ -123,8 +155,8 @@ static FlipperApplication const* loader_find_application_by_name_in_list( | ||||
|     return NULL; | ||||
| } | ||||
| 
 | ||||
| static const FlipperApplication* loader_find_application_by_name(const char* name) { | ||||
|     const FlipperApplication* application = NULL; | ||||
| static const FlipperInternalApplication* loader_find_application_by_name(const char* name) { | ||||
|     const FlipperInternalApplication* application = NULL; | ||||
|     application = loader_find_application_by_name_in_list(name, FLIPPER_APPS, FLIPPER_APPS_COUNT); | ||||
|     if(!application) { | ||||
|         application = loader_find_application_by_name_in_list( | ||||
| @ -138,25 +170,7 @@ static const FlipperApplication* loader_find_application_by_name(const char* nam | ||||
|     return application; | ||||
| } | ||||
| 
 | ||||
| static void | ||||
|     loader_start_internal_app(Loader* loader, const FlipperApplication* app, const char* args) { | ||||
|     FURI_LOG_I(TAG, "Starting %s", app->name); | ||||
| 
 | ||||
|     // store args
 | ||||
|     furi_assert(loader->app.args == NULL); | ||||
|     if(args && strlen(args) > 0) { | ||||
|         loader->app.args = strdup(args); | ||||
|     } | ||||
| 
 | ||||
|     // store name
 | ||||
|     furi_assert(loader->app.name == NULL); | ||||
|     loader->app.name = strdup(app->name); | ||||
| 
 | ||||
|     // setup app thread
 | ||||
|     loader->app.thread = | ||||
|         furi_thread_alloc_ex(app->name, app->stack_size, app->app, loader->app.args); | ||||
|     furi_thread_set_appid(loader->app.thread, app->appid); | ||||
| 
 | ||||
| static void loader_start_app_thread(Loader* loader, FlipperInternalApplicationFlag flags) { | ||||
|     // setup heap trace
 | ||||
|     FuriHalRtcHeapTrackMode mode = furi_hal_rtc_get_heap_track_mode(); | ||||
|     if(mode > FuriHalRtcHeapTrackModeNone) { | ||||
| @ -166,14 +180,14 @@ static void | ||||
|     } | ||||
| 
 | ||||
|     // setup insomnia
 | ||||
|     if(!(app->flags & FlipperApplicationFlagInsomniaSafe)) { | ||||
|     if(!(flags & FlipperInternalApplicationFlagInsomniaSafe)) { | ||||
|         furi_hal_power_insomnia_enter(); | ||||
|         loader->app.insomniac = true; | ||||
|     } else { | ||||
|         loader->app.insomniac = false; | ||||
|     } | ||||
| 
 | ||||
|     // setup app thread callbacks
 | ||||
|     // setup thread state callbacks
 | ||||
|     furi_thread_set_state_context(loader->app.thread, loader); | ||||
|     furi_thread_set_state_callback(loader->app.thread, loader_thread_state_callback); | ||||
| 
 | ||||
| @ -181,42 +195,206 @@ static void | ||||
|     furi_thread_start(loader->app.thread); | ||||
| } | ||||
| 
 | ||||
| static void loader_start_internal_app( | ||||
|     Loader* loader, | ||||
|     const FlipperInternalApplication* app, | ||||
|     const char* args) { | ||||
|     FURI_LOG_I(TAG, "Starting %s", app->name); | ||||
| 
 | ||||
|     // store args
 | ||||
|     furi_assert(loader->app.args == NULL); | ||||
|     if(args && strlen(args) > 0) { | ||||
|         loader->app.args = strdup(args); | ||||
|     } | ||||
| 
 | ||||
|     loader->app.thread = | ||||
|         furi_thread_alloc_ex(app->name, app->stack_size, app->app, loader->app.args); | ||||
|     furi_thread_set_appid(loader->app.thread, app->appid); | ||||
| 
 | ||||
|     loader_start_app_thread(loader, app->flags); | ||||
| } | ||||
| 
 | ||||
| static void loader_log_status_error( | ||||
|     LoaderStatus status, | ||||
|     FuriString* error_message, | ||||
|     const char* format, | ||||
|     va_list args) { | ||||
|     if(error_message) { | ||||
|         furi_string_vprintf(error_message, format, args); | ||||
|         FURI_LOG_E(TAG, "Status [%d]: %s", status, furi_string_get_cstr(error_message)); | ||||
|     } else { | ||||
|         FuriString* tmp = furi_string_alloc(); | ||||
|         FURI_LOG_E(TAG, "Status [%d]: %s", status, furi_string_get_cstr(tmp)); | ||||
|         furi_string_free(tmp); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static LoaderStatus loader_make_status_error( | ||||
|     LoaderStatus status, | ||||
|     FuriString* error_message, | ||||
|     const char* format, | ||||
|     ...) { | ||||
|     va_list args; | ||||
|     va_start(args, format); | ||||
|     loader_log_status_error(status, error_message, format, args); | ||||
|     va_end(args); | ||||
|     return status; | ||||
| } | ||||
| 
 | ||||
| static LoaderStatus loader_make_success_status(FuriString* error_message) { | ||||
|     if(error_message) { | ||||
|         furi_string_set(error_message, "App started"); | ||||
|     } | ||||
| 
 | ||||
|     return LoaderStatusOk; | ||||
| } | ||||
| 
 | ||||
| static LoaderStatus loader_start_external_app( | ||||
|     Loader* loader, | ||||
|     Storage* storage, | ||||
|     const char* path, | ||||
|     const char* args, | ||||
|     FuriString* error_message) { | ||||
|     LoaderStatus status = loader_make_success_status(error_message); | ||||
| 
 | ||||
|     do { | ||||
|         loader->app.fap = flipper_application_alloc(storage, firmware_api_interface); | ||||
|         size_t start = furi_get_tick(); | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "Loading %s", path); | ||||
| 
 | ||||
|         FlipperApplicationPreloadStatus preload_res = | ||||
|             flipper_application_preload(loader->app.fap, path); | ||||
|         if(preload_res != FlipperApplicationPreloadStatusSuccess) { | ||||
|             const char* err_msg = flipper_application_preload_status_to_string(preload_res); | ||||
|             status = loader_make_status_error( | ||||
|                 LoaderStatusErrorInternal, error_message, "Preload failed %s: %s", path, err_msg); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "Mapping"); | ||||
|         FlipperApplicationLoadStatus load_status = | ||||
|             flipper_application_map_to_memory(loader->app.fap); | ||||
|         if(load_status != FlipperApplicationLoadStatusSuccess) { | ||||
|             const char* err_msg = flipper_application_load_status_to_string(load_status); | ||||
|             status = loader_make_status_error( | ||||
|                 LoaderStatusErrorInternal, error_message, "Load failed %s: %s", path, err_msg); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         FURI_LOG_I(TAG, "Loaded in %zums", (size_t)(furi_get_tick() - start)); | ||||
|         FURI_LOG_I(TAG, "Starting app"); | ||||
| 
 | ||||
|         loader->app.thread = flipper_application_alloc_thread(loader->app.fap, args); | ||||
|         FuriString* app_name = furi_string_alloc(); | ||||
|         path_extract_filename_no_ext(path, app_name); | ||||
|         furi_thread_set_appid(loader->app.thread, furi_string_get_cstr(app_name)); | ||||
|         furi_string_free(app_name); | ||||
| 
 | ||||
|         /* This flag is set by the debugger - to break on app start */ | ||||
|         if(furi_hal_debug_is_gdb_session_active()) { | ||||
|             FURI_LOG_W(TAG, "Triggering BP for debugger"); | ||||
|             /* After hitting this, you can set breakpoints in your .fap's code
 | ||||
|              * Note that you have to toggle breakpoints that were set before */ | ||||
|             __asm volatile("bkpt 0"); | ||||
|         } | ||||
| 
 | ||||
|         loader_start_app_thread(loader, FlipperInternalApplicationFlagDefault); | ||||
|     } while(0); | ||||
| 
 | ||||
|     if(status != LoaderStatusOk) { | ||||
|         flipper_application_free(loader->app.fap); | ||||
|         loader->app.fap = NULL; | ||||
|     } | ||||
| 
 | ||||
|     return status; | ||||
| } | ||||
| 
 | ||||
| // process messages
 | ||||
| 
 | ||||
| static void loader_do_menu_show(Loader* loader) { | ||||
|     if(!loader->loader_menu) { | ||||
|         loader->loader_menu = loader_menu_alloc(); | ||||
|         loader_menu_set_closed_callback(loader->loader_menu, loader_menu_closed_callback, loader); | ||||
|         loader_menu_set_click_callback(loader->loader_menu, loader_menu_click_callback, loader); | ||||
|         loader_menu_start(loader->loader_menu); | ||||
|         loader->loader_menu = loader_menu_alloc(loader_menu_closed_callback, loader); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void loader_do_menu_closed(Loader* loader) { | ||||
|     if(loader->loader_menu) { | ||||
|         loader_menu_stop(loader->loader_menu); | ||||
|         loader_menu_free(loader->loader_menu); | ||||
|         loader->loader_menu = NULL; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void loader_do_applications_show(Loader* loader) { | ||||
|     if(!loader->loader_applications) { | ||||
|         loader->loader_applications = | ||||
|             loader_applications_alloc(loader_applications_closed_callback, loader); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void loader_do_applications_closed(Loader* loader) { | ||||
|     if(loader->loader_applications) { | ||||
|         loader_applications_free(loader->loader_applications); | ||||
|         loader->loader_applications = NULL; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static bool loader_do_is_locked(Loader* loader) { | ||||
|     return loader->app.thread != NULL; | ||||
| } | ||||
| 
 | ||||
| static LoaderStatus loader_do_start_by_name(Loader* loader, const char* name, const char* args) { | ||||
| static LoaderStatus loader_do_start_by_name( | ||||
|     Loader* loader, | ||||
|     const char* name, | ||||
|     const char* args, | ||||
|     FuriString* error_message) { | ||||
|     LoaderStatus status; | ||||
|     do { | ||||
|         // check lock
 | ||||
|         if(loader_do_is_locked(loader)) { | ||||
|         return LoaderStatusErrorAppStarted; | ||||
|     } | ||||
| 
 | ||||
|     const FlipperApplication* app = loader_find_application_by_name(name); | ||||
| 
 | ||||
|     if(!app) { | ||||
|         return LoaderStatusErrorUnknownApp; | ||||
|             const char* current_thread_name = | ||||
|                 furi_thread_get_name(furi_thread_get_id(loader->app.thread)); | ||||
|             status = loader_make_status_error( | ||||
|                 LoaderStatusErrorAppStarted, | ||||
|                 error_message, | ||||
|                 "Loader is locked, please close the \"%s\" first", | ||||
|                 current_thread_name); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         // check internal apps
 | ||||
|         { | ||||
|             const FlipperInternalApplication* app = loader_find_application_by_name(name); | ||||
|             if(app) { | ||||
|                 loader_start_internal_app(loader, app, args); | ||||
|     return LoaderStatusOk; | ||||
|                 status = loader_make_success_status(error_message); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // check Applications
 | ||||
|         if(strcmp(name, LOADER_APPLICATIONS_NAME) == 0) { | ||||
|             loader_do_applications_show(loader); | ||||
|             status = loader_make_success_status(error_message); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         // check external apps
 | ||||
|         { | ||||
|             Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|             if(storage_file_exists(storage, name)) { | ||||
|                 status = loader_start_external_app(loader, storage, name, args, error_message); | ||||
|                 furi_record_close(RECORD_STORAGE); | ||||
|                 break; | ||||
|             } | ||||
|             furi_record_close(RECORD_STORAGE); | ||||
|         } | ||||
| 
 | ||||
|         status = loader_make_status_error( | ||||
|             LoaderStatusErrorUnknownApp, error_message, "Application \"%s\" not found", name); | ||||
|     } while(false); | ||||
| 
 | ||||
|     return status; | ||||
| } | ||||
| 
 | ||||
| static bool loader_do_lock(Loader* loader) { | ||||
| @ -229,13 +407,16 @@ static bool loader_do_lock(Loader* loader) { | ||||
| } | ||||
| 
 | ||||
| static void loader_do_unlock(Loader* loader) { | ||||
|     furi_assert(loader->app.thread == (FuriThread*)LOADER_MAGIC_THREAD_VALUE); | ||||
|     furi_check(loader->app.thread == (FuriThread*)LOADER_MAGIC_THREAD_VALUE); | ||||
|     loader->app.thread = NULL; | ||||
| } | ||||
| 
 | ||||
| static void loader_do_app_closed(Loader* loader) { | ||||
|     furi_assert(loader->app.thread); | ||||
|     FURI_LOG_I(TAG, "Application stopped. Free heap: %zu", memmgr_get_free_heap()); | ||||
| 
 | ||||
|     furi_thread_join(loader->app.thread); | ||||
|     FURI_LOG_I(TAG, "App returned: %li", furi_thread_get_return_code(loader->app.thread)); | ||||
| 
 | ||||
|     if(loader->app.args) { | ||||
|         free(loader->app.args); | ||||
|         loader->app.args = NULL; | ||||
| @ -245,14 +426,22 @@ static void loader_do_app_closed(Loader* loader) { | ||||
|         furi_hal_power_insomnia_exit(); | ||||
|     } | ||||
| 
 | ||||
|     free(loader->app.name); | ||||
|     loader->app.name = NULL; | ||||
| 
 | ||||
|     furi_thread_join(loader->app.thread); | ||||
|     if(loader->app.fap) { | ||||
|         flipper_application_free(loader->app.fap); | ||||
|         loader->app.fap = NULL; | ||||
|         loader->app.thread = NULL; | ||||
|     } else { | ||||
|         furi_thread_free(loader->app.thread); | ||||
|         loader->app.thread = NULL; | ||||
|     } | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "Application stopped. Free heap: %zu", memmgr_get_free_heap()); | ||||
| 
 | ||||
|     LoaderEvent event; | ||||
|     event.type = LoaderEventTypeApplicationStopped; | ||||
|     furi_pubsub_publish(loader->pubsub, &event); | ||||
| } | ||||
| 
 | ||||
| // app
 | ||||
| 
 | ||||
| int32_t loader_srv(void* p) { | ||||
| @ -266,7 +455,7 @@ int32_t loader_srv(void* p) { | ||||
|     } | ||||
| 
 | ||||
|     if(FLIPPER_AUTORUN_APP_NAME && strlen(FLIPPER_AUTORUN_APP_NAME)) { | ||||
|         loader_do_start_by_name(loader, FLIPPER_AUTORUN_APP_NAME, NULL); | ||||
|         loader_do_start_by_name(loader, FLIPPER_AUTORUN_APP_NAME, NULL, NULL); | ||||
|     } | ||||
| 
 | ||||
|     LoaderMessage message; | ||||
| @ -274,8 +463,8 @@ int32_t loader_srv(void* p) { | ||||
|         if(furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) { | ||||
|             switch(message.type) { | ||||
|             case LoaderMessageTypeStartByName: | ||||
|                 message.status_value->value = | ||||
|                     loader_do_start_by_name(loader, message.start.name, message.start.args); | ||||
|                 message.status_value->value = loader_do_start_by_name( | ||||
|                     loader, message.start.name, message.start.args, message.start.error_message); | ||||
|                 api_lock_unlock(message.api_lock); | ||||
|                 break; | ||||
|             case LoaderMessageTypeShowMenu: | ||||
| @ -297,6 +486,10 @@ int32_t loader_srv(void* p) { | ||||
|                 break; | ||||
|             case LoaderMessageTypeUnlock: | ||||
|                 loader_do_unlock(loader); | ||||
|                 break; | ||||
|             case LoaderMessageTypeApplicationsClosed: | ||||
|                 loader_do_applications_closed(loader); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -6,6 +6,7 @@ extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| #define RECORD_LOADER "loader" | ||||
| #define LOADER_APPLICATIONS_NAME "Applications" | ||||
| 
 | ||||
| typedef struct Loader Loader; | ||||
| 
 | ||||
| @ -25,28 +26,57 @@ typedef struct { | ||||
|     LoaderEventType type; | ||||
| } LoaderEvent; | ||||
| 
 | ||||
| /** Start application
 | ||||
|  * @param name - application name | ||||
|  * @param args - application arguments | ||||
|  * @retval true on success | ||||
| /**
 | ||||
|  * @brief Start application | ||||
|  * @param[in] instance loader instance | ||||
|  * @param[in] name application name | ||||
|  * @param[in] args application arguments | ||||
|  * @param[out] error_message detailed error message, can be NULL | ||||
|  * @return LoaderStatus  | ||||
|  */ | ||||
| LoaderStatus loader_start(Loader* instance, const char* name, const char* args); | ||||
| LoaderStatus | ||||
|     loader_start(Loader* instance, const char* name, const char* args, FuriString* error_message); | ||||
| 
 | ||||
| /** Lock application start
 | ||||
|  * @retval true on success | ||||
| /**
 | ||||
|  * @brief Start application with GUI error message | ||||
|  * @param[in] instance loader instance | ||||
|  * @param[in] name application name | ||||
|  * @param[in] args application arguments | ||||
|  * @return LoaderStatus  | ||||
|  */ | ||||
| LoaderStatus loader_start_with_gui_error(Loader* loader, const char* name, const char* args); | ||||
| 
 | ||||
| /** 
 | ||||
|  * @brief Lock application start | ||||
|  * @param[in] instance loader instance | ||||
|  * @return true on success | ||||
|  */ | ||||
| bool loader_lock(Loader* instance); | ||||
| 
 | ||||
| /** Unlock application start */ | ||||
| /**
 | ||||
|  * @brief Unlock application start | ||||
|  * @param[in] instance loader instance | ||||
|  */ | ||||
| void loader_unlock(Loader* instance); | ||||
| 
 | ||||
| /** Get loader lock status */ | ||||
| /**
 | ||||
|  * @brief Check if loader is locked | ||||
|  * @param[in] instance loader instance | ||||
|  * @return true if locked | ||||
|  */ | ||||
| bool loader_is_locked(Loader* instance); | ||||
| 
 | ||||
| /** Show primary loader */ | ||||
| /**
 | ||||
|  * @brief Show loader menu | ||||
|  * @param[in] instance loader instance | ||||
|  */ | ||||
| void loader_show_menu(Loader* instance); | ||||
| 
 | ||||
| /** Show primary loader */ | ||||
| /**
 | ||||
|  * @brief Get loader pubsub | ||||
|  * @param[in] instance loader instance | ||||
|  * @return FuriPubSub*  | ||||
|  */ | ||||
| FuriPubSub* loader_get_pubsub(Loader* instance); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
|  | ||||
							
								
								
									
										146
									
								
								applications/services/loader/loader_applications.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								applications/services/loader/loader_applications.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | ||||
| #include "loader.h" | ||||
| #include "loader_applications.h" | ||||
| #include <dialogs/dialogs.h> | ||||
| #include <flipper_application/flipper_application.h> | ||||
| #include <assets_icons.h> | ||||
| #include <gui/gui.h> | ||||
| #include <gui/view_holder.h> | ||||
| #include <gui/modules/loading.h> | ||||
| 
 | ||||
| #define TAG "LoaderApplications" | ||||
| 
 | ||||
| struct LoaderApplications { | ||||
|     FuriThread* thread; | ||||
|     void (*closed_cb)(void*); | ||||
|     void* context; | ||||
| }; | ||||
| 
 | ||||
| static int32_t loader_applications_thread(void* p); | ||||
| 
 | ||||
| LoaderApplications* loader_applications_alloc(void (*closed_cb)(void*), void* context) { | ||||
|     LoaderApplications* loader_applications = malloc(sizeof(LoaderApplications)); | ||||
|     loader_applications->thread = | ||||
|         furi_thread_alloc_ex(TAG, 512, loader_applications_thread, (void*)loader_applications); | ||||
|     loader_applications->closed_cb = closed_cb; | ||||
|     loader_applications->context = context; | ||||
|     furi_thread_start(loader_applications->thread); | ||||
|     return loader_applications; | ||||
| } | ||||
| 
 | ||||
| void loader_applications_free(LoaderApplications* loader_applications) { | ||||
|     furi_assert(loader_applications); | ||||
|     furi_thread_join(loader_applications->thread); | ||||
|     furi_thread_free(loader_applications->thread); | ||||
|     free(loader_applications); | ||||
| } | ||||
| 
 | ||||
| typedef struct { | ||||
|     FuriString* fap_path; | ||||
|     DialogsApp* dialogs; | ||||
|     Storage* storage; | ||||
| } LoaderApplicationsApp; | ||||
| 
 | ||||
| static LoaderApplicationsApp* loader_applications_app_alloc() { | ||||
|     LoaderApplicationsApp* app = malloc(sizeof(LoaderApplicationsApp)); //-V799
 | ||||
|     app->fap_path = furi_string_alloc_set(EXT_PATH("apps")); | ||||
|     app->dialogs = furi_record_open(RECORD_DIALOGS); | ||||
|     app->storage = furi_record_open(RECORD_STORAGE); | ||||
|     return app; | ||||
| } //-V773
 | ||||
| 
 | ||||
| static void loader_applications_app_free(LoaderApplicationsApp* loader_applications_app) { | ||||
|     furi_assert(loader_applications_app); | ||||
|     furi_record_close(RECORD_DIALOGS); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|     furi_string_free(loader_applications_app->fap_path); | ||||
|     free(loader_applications_app); | ||||
| } | ||||
| 
 | ||||
| static bool loader_applications_item_callback( | ||||
|     FuriString* path, | ||||
|     void* context, | ||||
|     uint8_t** icon_ptr, | ||||
|     FuriString* item_name) { | ||||
|     LoaderApplicationsApp* loader_applications_app = context; | ||||
|     furi_assert(loader_applications_app); | ||||
|     return flipper_application_load_name_and_icon( | ||||
|         path, loader_applications_app->storage, icon_ptr, item_name); | ||||
| } | ||||
| 
 | ||||
| static bool loader_applications_select_app(LoaderApplicationsApp* loader_applications_app) { | ||||
|     const DialogsFileBrowserOptions browser_options = { | ||||
|         .extension = ".fap", | ||||
|         .skip_assets = true, | ||||
|         .icon = &I_unknown_10px, | ||||
|         .hide_ext = true, | ||||
|         .item_loader_callback = loader_applications_item_callback, | ||||
|         .item_loader_context = loader_applications_app, | ||||
|         .base_path = EXT_PATH("apps"), | ||||
|     }; | ||||
| 
 | ||||
|     return dialog_file_browser_show( | ||||
|         loader_applications_app->dialogs, | ||||
|         loader_applications_app->fap_path, | ||||
|         loader_applications_app->fap_path, | ||||
|         &browser_options); | ||||
| } | ||||
| 
 | ||||
| #define APPLICATION_STOP_EVENT 1 | ||||
| 
 | ||||
| static void loader_pubsub_callback(const void* message, void* context) { | ||||
|     const LoaderEvent* event = message; | ||||
|     const FuriThreadId thread_id = (FuriThreadId)context; | ||||
| 
 | ||||
|     if(event->type == LoaderEventTypeApplicationStopped) { | ||||
|         furi_thread_flags_set(thread_id, APPLICATION_STOP_EVENT); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void loader_applications_start_app(const char* name) { | ||||
|     // start loading animation
 | ||||
|     Gui* gui = furi_record_open(RECORD_GUI); | ||||
|     ViewHolder* view_holder = view_holder_alloc(); | ||||
|     Loading* loading = loading_alloc(); | ||||
| 
 | ||||
|     view_holder_attach_to_gui(view_holder, gui); | ||||
|     view_holder_set_view(view_holder, loading_get_view(loading)); | ||||
|     view_holder_start(view_holder); | ||||
| 
 | ||||
|     // load app
 | ||||
|     FuriThreadId thread_id = furi_thread_get_current_id(); | ||||
|     Loader* loader = furi_record_open(RECORD_LOADER); | ||||
|     FuriPubSubSubscription* subscription = | ||||
|         furi_pubsub_subscribe(loader_get_pubsub(loader), loader_pubsub_callback, thread_id); | ||||
| 
 | ||||
|     LoaderStatus status = loader_start_with_gui_error(loader, name, NULL); | ||||
| 
 | ||||
|     if(status == LoaderStatusOk) { | ||||
|         furi_thread_flags_wait(APPLICATION_STOP_EVENT, FuriFlagWaitAny, FuriWaitForever); | ||||
|     } | ||||
| 
 | ||||
|     furi_pubsub_unsubscribe(loader_get_pubsub(loader), subscription); | ||||
|     furi_record_close(RECORD_LOADER); | ||||
| 
 | ||||
|     // stop loading animation
 | ||||
|     view_holder_stop(view_holder); | ||||
|     view_holder_free(view_holder); | ||||
|     loading_free(loading); | ||||
|     furi_record_close(RECORD_GUI); | ||||
| } | ||||
| 
 | ||||
| static int32_t loader_applications_thread(void* p) { | ||||
|     LoaderApplications* loader_applications = p; | ||||
|     LoaderApplicationsApp* loader_applications_app = loader_applications_app_alloc(); | ||||
| 
 | ||||
|     while(loader_applications_select_app(loader_applications_app)) { | ||||
|         loader_applications_start_app(furi_string_get_cstr(loader_applications_app->fap_path)); | ||||
|     } | ||||
| 
 | ||||
|     loader_applications_app_free(loader_applications_app); | ||||
| 
 | ||||
|     if(loader_applications->closed_cb) { | ||||
|         loader_applications->closed_cb(loader_applications->context); | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
							
								
								
									
										16
									
								
								applications/services/loader/loader_applications.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								applications/services/loader/loader_applications.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| #pragma once | ||||
| #include <furi.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| #endif | ||||
| 
 | ||||
| typedef struct LoaderApplications LoaderApplications; | ||||
| 
 | ||||
| LoaderApplications* loader_applications_alloc(void (*closed_cb)(void*), void* context); | ||||
| 
 | ||||
| void loader_applications_free(LoaderApplications* loader_applications); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
| @ -50,21 +50,11 @@ static void loader_cli_open(FuriString* args, Loader* loader) { | ||||
| 
 | ||||
|         const char* app_name_str = furi_string_get_cstr(app_name); | ||||
| 
 | ||||
|         LoaderStatus status = loader_start(loader, app_name_str, args_str); | ||||
| 
 | ||||
|         switch(status) { | ||||
|         case LoaderStatusOk: | ||||
|             break; | ||||
|         case LoaderStatusErrorAppStarted: | ||||
|             printf("Can't start, application is running"); | ||||
|             break; | ||||
|         case LoaderStatusErrorUnknownApp: | ||||
|             printf("%s doesn't exists\r\n", app_name_str); | ||||
|             break; | ||||
|         case LoaderStatusErrorInternal: | ||||
|             printf("Internal error\r\n"); | ||||
|             break; | ||||
|         FuriString* error_message = furi_string_alloc(); | ||||
|         if(loader_start(loader, app_name_str, args_str, error_message) != LoaderStatusOk) { | ||||
|             printf("%s\r\n", furi_string_get_cstr(error_message)); | ||||
|         } | ||||
|         furi_string_free(error_message); | ||||
|     } while(false); | ||||
| 
 | ||||
|     furi_string_free(app_name); | ||||
|  | ||||
| @ -1,20 +1,23 @@ | ||||
| #pragma once | ||||
| #include <furi.h> | ||||
| #include <toolbox/api_lock.h> | ||||
| #include <flipper_application/flipper_application.h> | ||||
| #include "loader.h" | ||||
| #include "loader_menu.h" | ||||
| #include "loader_applications.h" | ||||
| 
 | ||||
| typedef struct { | ||||
|     char* args; | ||||
|     char* name; | ||||
|     FuriThread* thread; | ||||
|     bool insomniac; | ||||
|     FlipperApplication* fap; | ||||
| } LoaderAppData; | ||||
| 
 | ||||
| struct Loader { | ||||
|     FuriPubSub* pubsub; | ||||
|     FuriMessageQueue* queue; | ||||
|     LoaderMenu* loader_menu; | ||||
|     LoaderApplications* loader_applications; | ||||
|     LoaderAppData app; | ||||
| }; | ||||
| 
 | ||||
| @ -23,6 +26,7 @@ typedef enum { | ||||
|     LoaderMessageTypeAppClosed, | ||||
|     LoaderMessageTypeShowMenu, | ||||
|     LoaderMessageTypeMenuClosed, | ||||
|     LoaderMessageTypeApplicationsClosed, | ||||
|     LoaderMessageTypeLock, | ||||
|     LoaderMessageTypeUnlock, | ||||
|     LoaderMessageTypeIsLocked, | ||||
| @ -31,6 +35,7 @@ typedef enum { | ||||
| typedef struct { | ||||
|     const char* name; | ||||
|     const char* args; | ||||
|     FuriString* error_message; | ||||
| } LoaderMessageStartByName; | ||||
| 
 | ||||
| typedef struct { | ||||
|  | ||||
| @ -5,106 +5,76 @@ | ||||
| #include <assets_icons.h> | ||||
| #include <applications.h> | ||||
| 
 | ||||
| #include "loader.h" | ||||
| #include "loader_menu.h" | ||||
| 
 | ||||
| #define TAG "LoaderMenu" | ||||
| 
 | ||||
| struct LoaderMenu { | ||||
|     Gui* gui; | ||||
|     ViewDispatcher* view_dispatcher; | ||||
|     Menu* primary_menu; | ||||
|     Submenu* settings_menu; | ||||
| 
 | ||||
|     void (*closed_callback)(void*); | ||||
|     void* closed_callback_context; | ||||
| 
 | ||||
|     void (*click_callback)(const char*, void*); | ||||
|     void* click_callback_context; | ||||
| 
 | ||||
|     FuriThread* thread; | ||||
|     void (*closed_cb)(void*); | ||||
|     void* context; | ||||
| }; | ||||
| 
 | ||||
| static int32_t loader_menu_thread(void* p); | ||||
| 
 | ||||
| LoaderMenu* loader_menu_alloc(void (*closed_cb)(void*), void* context) { | ||||
|     LoaderMenu* loader_menu = malloc(sizeof(LoaderMenu)); | ||||
|     loader_menu->closed_cb = closed_cb; | ||||
|     loader_menu->context = context; | ||||
|     loader_menu->thread = furi_thread_alloc_ex(TAG, 1024, loader_menu_thread, loader_menu); | ||||
|     furi_thread_start(loader_menu->thread); | ||||
|     return loader_menu; | ||||
| } | ||||
| 
 | ||||
| void loader_menu_free(LoaderMenu* loader_menu) { | ||||
|     furi_assert(loader_menu); | ||||
|     furi_thread_join(loader_menu->thread); | ||||
|     furi_thread_free(loader_menu->thread); | ||||
|     free(loader_menu); | ||||
| } | ||||
| 
 | ||||
| typedef enum { | ||||
|     LoaderMenuViewPrimary, | ||||
|     LoaderMenuViewSettings, | ||||
| } LoaderMenuView; | ||||
| 
 | ||||
| static int32_t loader_menu_thread(void* p); | ||||
| typedef struct { | ||||
|     Gui* gui; | ||||
|     ViewDispatcher* view_dispatcher; | ||||
|     Menu* primary_menu; | ||||
|     Submenu* settings_menu; | ||||
| } LoaderMenuApp; | ||||
| 
 | ||||
| LoaderMenu* loader_menu_alloc() { | ||||
|     LoaderMenu* loader_menu = malloc(sizeof(LoaderMenu)); | ||||
|     loader_menu->gui = furi_record_open(RECORD_GUI); | ||||
|     loader_menu->view_dispatcher = view_dispatcher_alloc(); | ||||
|     loader_menu->primary_menu = menu_alloc(); | ||||
|     loader_menu->settings_menu = submenu_alloc(); | ||||
|     loader_menu->thread = NULL; | ||||
|     return loader_menu; | ||||
| } | ||||
| 
 | ||||
| void loader_menu_free(LoaderMenu* loader_menu) { | ||||
|     furi_assert(loader_menu); | ||||
|     // check if thread is running
 | ||||
|     furi_assert(!loader_menu->thread); | ||||
| 
 | ||||
|     submenu_free(loader_menu->settings_menu); | ||||
|     menu_free(loader_menu->primary_menu); | ||||
|     view_dispatcher_free(loader_menu->view_dispatcher); | ||||
|     furi_record_close(RECORD_GUI); | ||||
|     free(loader_menu); | ||||
| } | ||||
| 
 | ||||
| void loader_menu_start(LoaderMenu* loader_menu) { | ||||
|     furi_assert(loader_menu); | ||||
|     furi_assert(!loader_menu->thread); | ||||
|     loader_menu->thread = furi_thread_alloc_ex(TAG, 1024, loader_menu_thread, loader_menu); | ||||
|     furi_thread_start(loader_menu->thread); | ||||
| } | ||||
| 
 | ||||
| void loader_menu_stop(LoaderMenu* loader_menu) { | ||||
|     furi_assert(loader_menu); | ||||
|     furi_assert(loader_menu->thread); | ||||
|     view_dispatcher_stop(loader_menu->view_dispatcher); | ||||
|     furi_thread_join(loader_menu->thread); | ||||
|     furi_thread_free(loader_menu->thread); | ||||
|     loader_menu->thread = NULL; | ||||
| } | ||||
| 
 | ||||
| void loader_menu_set_closed_callback( | ||||
|     LoaderMenu* loader_menu, | ||||
|     void (*callback)(void*), | ||||
|     void* context) { | ||||
|     loader_menu->closed_callback = callback; | ||||
|     loader_menu->closed_callback_context = context; | ||||
| } | ||||
| 
 | ||||
| void loader_menu_set_click_callback( | ||||
|     LoaderMenu* loader_menu, | ||||
|     void (*callback)(const char*, void*), | ||||
|     void* context) { | ||||
|     loader_menu->click_callback = callback; | ||||
|     loader_menu->click_callback_context = context; | ||||
| static void loader_menu_start(const char* name) { | ||||
|     Loader* loader = furi_record_open(RECORD_LOADER); | ||||
|     loader_start_with_gui_error(loader, name, NULL); | ||||
|     furi_record_close(RECORD_LOADER); | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_callback(void* context, uint32_t index) { | ||||
|     LoaderMenu* loader_menu = context; | ||||
|     UNUSED(context); | ||||
|     const char* name = FLIPPER_APPS[index].name; | ||||
|     if(loader_menu->click_callback) { | ||||
|         loader_menu->click_callback(name, loader_menu->click_callback_context); | ||||
|     loader_menu_start(name); | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_applications_callback(void* context, uint32_t index) { | ||||
|     UNUSED(index); | ||||
|     UNUSED(context); | ||||
|     const char* name = LOADER_APPLICATIONS_NAME; | ||||
|     loader_menu_start(name); | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_settings_menu_callback(void* context, uint32_t index) { | ||||
|     LoaderMenu* loader_menu = context; | ||||
|     UNUSED(context); | ||||
|     const char* name = FLIPPER_SETTINGS_APPS[index].name; | ||||
|     if(loader_menu->click_callback) { | ||||
|         loader_menu->click_callback(name, loader_menu->click_callback_context); | ||||
|     } | ||||
|     loader_menu_start(name); | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_switch_to_settings(void* context, uint32_t index) { | ||||
|     UNUSED(index); | ||||
|     LoaderMenu* loader_menu = context; | ||||
|     view_dispatcher_switch_to_view(loader_menu->view_dispatcher, LoaderMenuViewSettings); | ||||
|     LoaderMenuApp* app = context; | ||||
|     view_dispatcher_switch_to_view(app->view_dispatcher, LoaderMenuViewSettings); | ||||
| } | ||||
| 
 | ||||
| static uint32_t loader_menu_switch_to_primary(void* context) { | ||||
| @ -117,30 +87,32 @@ static uint32_t loader_menu_exit(void* context) { | ||||
|     return VIEW_NONE; | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_build_menu(LoaderMenu* loader_menu) { | ||||
| static void loader_menu_build_menu(LoaderMenuApp* app, LoaderMenu* menu) { | ||||
|     size_t i; | ||||
|     for(i = 0; i < FLIPPER_APPS_COUNT; i++) { | ||||
|         menu_add_item( | ||||
|             loader_menu->primary_menu, | ||||
|             app->primary_menu, | ||||
|             FLIPPER_APPS[i].name, | ||||
|             FLIPPER_APPS[i].icon, | ||||
|             i, | ||||
|             loader_menu_callback, | ||||
|             (void*)loader_menu); | ||||
|             (void*)menu); | ||||
|     } | ||||
|     menu_add_item( | ||||
|         loader_menu->primary_menu, | ||||
|         "Settings", | ||||
|         &A_Settings_14, | ||||
|         app->primary_menu, "Settings", &A_Settings_14, i++, loader_menu_switch_to_settings, app); | ||||
|     menu_add_item( | ||||
|         app->primary_menu, | ||||
|         LOADER_APPLICATIONS_NAME, | ||||
|         &A_Plugins_14, | ||||
|         i++, | ||||
|         loader_menu_switch_to_settings, | ||||
|         loader_menu); | ||||
|         loader_menu_applications_callback, | ||||
|         (void*)menu); | ||||
| }; | ||||
| 
 | ||||
| static void loader_menu_build_submenu(LoaderMenu* loader_menu) { | ||||
| static void loader_menu_build_submenu(LoaderMenuApp* app, LoaderMenu* loader_menu) { | ||||
|     for(size_t i = 0; i < FLIPPER_SETTINGS_APPS_COUNT; i++) { | ||||
|         submenu_add_item( | ||||
|             loader_menu->settings_menu, | ||||
|             app->settings_menu, | ||||
|             FLIPPER_SETTINGS_APPS[i].name, | ||||
|             i, | ||||
|             loader_menu_settings_menu_callback, | ||||
| @ -148,40 +120,59 @@ static void loader_menu_build_submenu(LoaderMenu* loader_menu) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static LoaderMenuApp* loader_menu_app_alloc(LoaderMenu* loader_menu) { | ||||
|     LoaderMenuApp* app = malloc(sizeof(LoaderMenuApp)); | ||||
|     app->gui = furi_record_open(RECORD_GUI); | ||||
|     app->view_dispatcher = view_dispatcher_alloc(); | ||||
|     app->primary_menu = menu_alloc(); | ||||
|     app->settings_menu = submenu_alloc(); | ||||
| 
 | ||||
|     loader_menu_build_menu(app, loader_menu); | ||||
|     loader_menu_build_submenu(app, loader_menu); | ||||
| 
 | ||||
|     // Primary menu
 | ||||
|     View* primary_view = menu_get_view(app->primary_menu); | ||||
|     view_set_context(primary_view, app->primary_menu); | ||||
|     view_set_previous_callback(primary_view, loader_menu_exit); | ||||
|     view_dispatcher_add_view(app->view_dispatcher, LoaderMenuViewPrimary, primary_view); | ||||
| 
 | ||||
|     // Settings menu
 | ||||
|     View* settings_view = submenu_get_view(app->settings_menu); | ||||
|     view_set_context(settings_view, app->settings_menu); | ||||
|     view_set_previous_callback(settings_view, loader_menu_switch_to_primary); | ||||
|     view_dispatcher_add_view(app->view_dispatcher, LoaderMenuViewSettings, settings_view); | ||||
| 
 | ||||
|     view_dispatcher_enable_queue(app->view_dispatcher); | ||||
|     view_dispatcher_switch_to_view(app->view_dispatcher, LoaderMenuViewPrimary); | ||||
| 
 | ||||
|     return app; | ||||
| } | ||||
| 
 | ||||
| static void loader_menu_app_free(LoaderMenuApp* app) { | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, LoaderMenuViewPrimary); | ||||
|     view_dispatcher_remove_view(app->view_dispatcher, LoaderMenuViewSettings); | ||||
|     view_dispatcher_free(app->view_dispatcher); | ||||
| 
 | ||||
|     menu_free(app->primary_menu); | ||||
|     submenu_free(app->settings_menu); | ||||
|     furi_record_close(RECORD_GUI); | ||||
|     free(app); | ||||
| } | ||||
| 
 | ||||
| static int32_t loader_menu_thread(void* p) { | ||||
|     LoaderMenu* loader_menu = p; | ||||
|     furi_assert(loader_menu); | ||||
| 
 | ||||
|     loader_menu_build_menu(loader_menu); | ||||
|     loader_menu_build_submenu(loader_menu); | ||||
|     LoaderMenuApp* app = loader_menu_app_alloc(loader_menu); | ||||
| 
 | ||||
|     view_dispatcher_attach_to_gui( | ||||
|         loader_menu->view_dispatcher, loader_menu->gui, ViewDispatcherTypeFullscreen); | ||||
|     view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); | ||||
|     view_dispatcher_run(app->view_dispatcher); | ||||
| 
 | ||||
|     // Primary menu
 | ||||
|     View* primary_view = menu_get_view(loader_menu->primary_menu); | ||||
|     view_set_context(primary_view, loader_menu->primary_menu); | ||||
|     view_set_previous_callback(primary_view, loader_menu_exit); | ||||
|     view_dispatcher_add_view(loader_menu->view_dispatcher, LoaderMenuViewPrimary, primary_view); | ||||
| 
 | ||||
|     // Settings menu
 | ||||
|     View* settings_view = submenu_get_view(loader_menu->settings_menu); | ||||
|     view_set_context(settings_view, loader_menu->settings_menu); | ||||
|     view_set_previous_callback(settings_view, loader_menu_switch_to_primary); | ||||
|     view_dispatcher_add_view(loader_menu->view_dispatcher, LoaderMenuViewSettings, settings_view); | ||||
| 
 | ||||
|     view_dispatcher_enable_queue(loader_menu->view_dispatcher); | ||||
|     view_dispatcher_switch_to_view(loader_menu->view_dispatcher, LoaderMenuViewPrimary); | ||||
| 
 | ||||
|     // run view dispatcher
 | ||||
|     view_dispatcher_run(loader_menu->view_dispatcher); | ||||
| 
 | ||||
|     view_dispatcher_remove_view(loader_menu->view_dispatcher, LoaderMenuViewPrimary); | ||||
|     view_dispatcher_remove_view(loader_menu->view_dispatcher, LoaderMenuViewSettings); | ||||
| 
 | ||||
|     if(loader_menu->closed_callback) { | ||||
|         loader_menu->closed_callback(loader_menu->closed_callback_context); | ||||
|     if(loader_menu->closed_cb) { | ||||
|         loader_menu->closed_cb(loader_menu->context); | ||||
|     } | ||||
| 
 | ||||
|     loader_menu_app_free(app); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| @ -7,24 +7,10 @@ extern "C" { | ||||
| 
 | ||||
| typedef struct LoaderMenu LoaderMenu; | ||||
| 
 | ||||
| LoaderMenu* loader_menu_alloc(); | ||||
| LoaderMenu* loader_menu_alloc(void (*closed_cb)(void*), void* context); | ||||
| 
 | ||||
| void loader_menu_free(LoaderMenu* loader_menu); | ||||
| 
 | ||||
| void loader_menu_start(LoaderMenu* loader_menu); | ||||
| 
 | ||||
| void loader_menu_stop(LoaderMenu* loader_menu); | ||||
| 
 | ||||
| void loader_menu_set_closed_callback( | ||||
|     LoaderMenu* loader_menu, | ||||
|     void (*callback)(void*), | ||||
|     void* context); | ||||
| 
 | ||||
| void loader_menu_set_click_callback( | ||||
|     LoaderMenu* loader_menu, | ||||
|     void (*callback)(const char*, void*), | ||||
|     void* context); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
| @ -52,7 +52,7 @@ static void rpc_system_app_start_process(const PB_Main* request, void* context) | ||||
|             snprintf(args_temp, RPC_SYSTEM_APP_TEMP_ARGS_SIZE, "RPC %08lX", (uint32_t)rpc_app); | ||||
|             app_args = args_temp; | ||||
|         } | ||||
|         LoaderStatus status = loader_start(loader, app_name, app_args); | ||||
|         LoaderStatus status = loader_start(loader, app_name, app_args, NULL); | ||||
|         if(status == LoaderStatusErrorAppStarted) { | ||||
|             result = PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED; | ||||
|         } else if(status == LoaderStatusErrorInternal) { | ||||
|  | ||||
| @ -3,7 +3,6 @@ | ||||
| #include "desktop_settings_scene.h" | ||||
| #include <storage/storage.h> | ||||
| #include <dialogs/dialogs.h> | ||||
| #include <fap_loader/fap_loader_app.h> | ||||
| 
 | ||||
| #define EXTERNAL_APPLICATION_NAME ("[External Application]") | ||||
| #define EXTERNAL_APPLICATION_INDEX (FLIPPER_APPS_COUNT + 1) | ||||
| @ -65,7 +64,6 @@ void desktop_settings_scene_favorite_on_enter(void* context) { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| #ifdef APP_FAP_LOADER | ||||
|     submenu_add_item( | ||||
|         submenu, | ||||
|         EXTERNAL_APPLICATION_NAME, | ||||
| @ -75,7 +73,6 @@ void desktop_settings_scene_favorite_on_enter(void* context) { | ||||
|     if(curr_favorite_app->is_external) { | ||||
|         pre_select_item = EXTERNAL_APPLICATION_INDEX; | ||||
|     } | ||||
| #endif | ||||
| 
 | ||||
|     submenu_set_header( | ||||
|         submenu, primary_favorite ? "Primary favorite app:" : "Secondary favorite app:"); | ||||
|  | ||||
| @ -172,7 +172,7 @@ static void storage_move_to_sd_mount_callback(const void* message, void* context | ||||
| 
 | ||||
|     if(storage_event->type == StorageEventTypeCardMount) { | ||||
|         Loader* loader = furi_record_open(RECORD_LOADER); | ||||
|         loader_start(loader, "StorageMoveToSd", NULL); | ||||
|         loader_start(loader, "StorageMoveToSd", NULL, NULL); | ||||
|         furi_record_close(RECORD_LOADER); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -99,7 +99,7 @@ static void updater_start_app(void* context, uint32_t arg) { | ||||
|      * So, accessing its record would cause a deadlock  | ||||
|      */ | ||||
|     Loader* loader = furi_record_open(RECORD_LOADER); | ||||
|     loader_start(loader, "UpdaterApp", NULL); | ||||
|     loader_start(loader, "UpdaterApp", NULL, NULL); | ||||
|     furi_record_close(RECORD_LOADER); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| entry,status,name,type,params | ||||
| Version,+,30.1,, | ||||
| Version,+,31.0,, | ||||
| Header,+,applications/services/bt/bt_service/bt.h,, | ||||
| Header,+,applications/services/cli/cli.h,, | ||||
| Header,+,applications/services/cli/cli_vcp.h,, | ||||
| @ -735,9 +735,11 @@ Function,+,filesystem_api_error_get_desc,const char*,FS_Error | ||||
| Function,-,fiprintf,int,"FILE*, const char*, ..." | ||||
| Function,-,fiscanf,int,"FILE*, const char*, ..." | ||||
| Function,+,flipper_application_alloc,FlipperApplication*,"Storage*, const ElfApiInterface*" | ||||
| Function,+,flipper_application_alloc_thread,FuriThread*,"FlipperApplication*, const char*" | ||||
| Function,+,flipper_application_free,void,FlipperApplication* | ||||
| Function,+,flipper_application_get_manifest,const FlipperApplicationManifest*,FlipperApplication* | ||||
| Function,+,flipper_application_is_plugin,_Bool,FlipperApplication* | ||||
| Function,+,flipper_application_load_name_and_icon,_Bool,"FuriString*, Storage*, uint8_t**, FuriString*" | ||||
| Function,+,flipper_application_load_status_to_string,const char*,FlipperApplicationLoadStatus | ||||
| Function,+,flipper_application_manifest_is_compatible,_Bool,"const FlipperApplicationManifest*, const ElfApiInterface*" | ||||
| Function,+,flipper_application_manifest_is_target_compatible,_Bool,const FlipperApplicationManifest* | ||||
| @ -747,7 +749,6 @@ Function,+,flipper_application_plugin_get_descriptor,const FlipperAppPluginDescr | ||||
| Function,+,flipper_application_preload,FlipperApplicationPreloadStatus,"FlipperApplication*, const char*" | ||||
| Function,+,flipper_application_preload_manifest,FlipperApplicationPreloadStatus,"FlipperApplication*, const char*" | ||||
| Function,+,flipper_application_preload_status_to_string,const char*,FlipperApplicationPreloadStatus | ||||
| Function,+,flipper_application_spawn,FuriThread*,"FlipperApplication*, void*" | ||||
| Function,+,flipper_format_buffered_file_alloc,FlipperFormat*,Storage* | ||||
| Function,+,flipper_format_buffered_file_close,_Bool,FlipperFormat* | ||||
| Function,+,flipper_format_buffered_file_open_always,_Bool,"FlipperFormat*, const char*" | ||||
| @ -1419,7 +1420,8 @@ Function,+,loader_get_pubsub,FuriPubSub*,Loader* | ||||
| Function,+,loader_is_locked,_Bool,Loader* | ||||
| Function,+,loader_lock,_Bool,Loader* | ||||
| Function,+,loader_show_menu,void,Loader* | ||||
| Function,+,loader_start,LoaderStatus,"Loader*, const char*, const char*" | ||||
| Function,+,loader_start,LoaderStatus,"Loader*, const char*, const char*, FuriString*" | ||||
| Function,+,loader_start_with_gui_error,LoaderStatus,"Loader*, const char*, const char*" | ||||
| Function,+,loader_unlock,void,Loader* | ||||
| Function,+,loading_alloc,Loading*, | ||||
| Function,+,loading_free,void,Loading* | ||||
|  | ||||
| 
 | 
| @ -1,5 +1,5 @@ | ||||
| entry,status,name,type,params | ||||
| Version,+,30.1,, | ||||
| Version,+,31.0,, | ||||
| Header,+,applications/services/bt/bt_service/bt.h,, | ||||
| Header,+,applications/services/cli/cli.h,, | ||||
| Header,+,applications/services/cli/cli_vcp.h,, | ||||
| @ -892,9 +892,11 @@ Function,-,finitel,int,long double | ||||
| Function,-,fiprintf,int,"FILE*, const char*, ..." | ||||
| Function,-,fiscanf,int,"FILE*, const char*, ..." | ||||
| Function,+,flipper_application_alloc,FlipperApplication*,"Storage*, const ElfApiInterface*" | ||||
| Function,+,flipper_application_alloc_thread,FuriThread*,"FlipperApplication*, const char*" | ||||
| Function,+,flipper_application_free,void,FlipperApplication* | ||||
| Function,+,flipper_application_get_manifest,const FlipperApplicationManifest*,FlipperApplication* | ||||
| Function,+,flipper_application_is_plugin,_Bool,FlipperApplication* | ||||
| Function,+,flipper_application_load_name_and_icon,_Bool,"FuriString*, Storage*, uint8_t**, FuriString*" | ||||
| Function,+,flipper_application_load_status_to_string,const char*,FlipperApplicationLoadStatus | ||||
| Function,+,flipper_application_manifest_is_compatible,_Bool,"const FlipperApplicationManifest*, const ElfApiInterface*" | ||||
| Function,+,flipper_application_manifest_is_target_compatible,_Bool,const FlipperApplicationManifest* | ||||
| @ -904,7 +906,6 @@ Function,+,flipper_application_plugin_get_descriptor,const FlipperAppPluginDescr | ||||
| Function,+,flipper_application_preload,FlipperApplicationPreloadStatus,"FlipperApplication*, const char*" | ||||
| Function,+,flipper_application_preload_manifest,FlipperApplicationPreloadStatus,"FlipperApplication*, const char*" | ||||
| Function,+,flipper_application_preload_status_to_string,const char*,FlipperApplicationPreloadStatus | ||||
| Function,+,flipper_application_spawn,FuriThread*,"FlipperApplication*, void*" | ||||
| Function,+,flipper_format_buffered_file_alloc,FlipperFormat*,Storage* | ||||
| Function,+,flipper_format_buffered_file_close,_Bool,FlipperFormat* | ||||
| Function,+,flipper_format_buffered_file_open_always,_Bool,"FlipperFormat*, const char*" | ||||
| @ -1825,7 +1826,8 @@ Function,+,loader_get_pubsub,FuriPubSub*,Loader* | ||||
| Function,+,loader_is_locked,_Bool,Loader* | ||||
| Function,+,loader_lock,_Bool,Loader* | ||||
| Function,+,loader_show_menu,void,Loader* | ||||
| Function,+,loader_start,LoaderStatus,"Loader*, const char*, const char*" | ||||
| Function,+,loader_start,LoaderStatus,"Loader*, const char*, const char*, FuriString*" | ||||
| Function,+,loader_start_with_gui_error,LoaderStatus,"Loader*, const char*, const char*" | ||||
| Function,+,loader_unlock,void,Loader* | ||||
| Function,+,loading_alloc,Loading*, | ||||
| Function,+,loading_free,void,Loading* | ||||
|  | ||||
| 
 | 
| @ -2,6 +2,7 @@ | ||||
| #include "elf/elf_file.h" | ||||
| #include <notification/notification_messages.h> | ||||
| #include "application_assets.h" | ||||
| #include <loader/firmware_api/firmware_api.h> | ||||
| 
 | ||||
| #include <m-list.h> | ||||
| 
 | ||||
| @ -81,6 +82,12 @@ void flipper_application_free(FlipperApplication* app) { | ||||
|     } | ||||
| 
 | ||||
|     elf_file_free(app->elf); | ||||
| 
 | ||||
|     if(app->ep_thread_args) { | ||||
|         free(app->ep_thread_args); | ||||
|         app->ep_thread_args = NULL; | ||||
|     } | ||||
| 
 | ||||
|     free(app); | ||||
| } | ||||
| 
 | ||||
| @ -224,10 +231,19 @@ static int32_t flipper_application_thread(void* context) { | ||||
|     return ret_code; | ||||
| } | ||||
| 
 | ||||
| FuriThread* flipper_application_spawn(FlipperApplication* app, void* args) { | ||||
| FuriThread* flipper_application_alloc_thread(FlipperApplication* app, const char* args) { | ||||
|     furi_check(app->thread == NULL); | ||||
|     furi_check(!flipper_application_is_plugin(app)); | ||||
|     app->ep_thread_args = args; | ||||
| 
 | ||||
|     if(app->ep_thread_args) { | ||||
|         free(app->ep_thread_args); | ||||
|     } | ||||
| 
 | ||||
|     if(args) { | ||||
|         app->ep_thread_args = strdup(args); | ||||
|     } else { | ||||
|         app->ep_thread_args = NULL; | ||||
|     } | ||||
| 
 | ||||
|     const FlipperApplicationManifest* manifest = flipper_application_get_manifest(app); | ||||
|     app->thread = furi_thread_alloc_ex( | ||||
| @ -290,3 +306,31 @@ const FlipperAppPluginDescriptor* | ||||
| 
 | ||||
|     return lib_descriptor; | ||||
| } | ||||
| 
 | ||||
| bool flipper_application_load_name_and_icon( | ||||
|     FuriString* path, | ||||
|     Storage* storage, | ||||
|     uint8_t** icon_ptr, | ||||
|     FuriString* item_name) { | ||||
|     FlipperApplication* app = flipper_application_alloc(storage, firmware_api_interface); | ||||
| 
 | ||||
|     FlipperApplicationPreloadStatus preload_res = | ||||
|         flipper_application_preload_manifest(app, furi_string_get_cstr(path)); | ||||
| 
 | ||||
|     bool load_success = false; | ||||
| 
 | ||||
|     if(preload_res == FlipperApplicationPreloadStatusSuccess) { | ||||
|         const FlipperApplicationManifest* manifest = flipper_application_get_manifest(app); | ||||
|         if(manifest->has_icon) { | ||||
|             memcpy(*icon_ptr, manifest->icon, FAP_MANIFEST_MAX_ICON_SIZE); | ||||
|         } | ||||
|         furi_string_set(item_name, manifest->name); | ||||
|         load_success = true; | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "Failed to preload %s", furi_string_get_cstr(path)); | ||||
|         load_success = false; | ||||
|     } | ||||
| 
 | ||||
|     flipper_application_free(app); | ||||
|     return load_success; | ||||
| } | ||||
| @ -106,14 +106,14 @@ const FlipperApplicationManifest* flipper_application_get_manifest(FlipperApplic | ||||
| FlipperApplicationLoadStatus flipper_application_map_to_memory(FlipperApplication* app); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Create application thread at entry point address, using app name and | ||||
|  * @brief Allocate application thread at entry point address, using app name and | ||||
|  * stack size from metadata. Returned thread isn't started yet.  | ||||
|  * Can be only called once for application instance. | ||||
|  * @param app Applicaiton pointer | ||||
|  * @param args Object to pass to app's entry point | ||||
|  * @param args Args to pass to app's entry point | ||||
|  * @return Created thread | ||||
|  */ | ||||
| FuriThread* flipper_application_spawn(FlipperApplication* app, void* args); | ||||
| FuriThread* flipper_application_alloc_thread(FlipperApplication* app, const char* args); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Check if application is a plugin (not a runnable standalone app) | ||||
| @ -149,6 +149,21 @@ typedef const FlipperAppPluginDescriptor* (*FlipperApplicationPluginEntryPoint)( | ||||
| const FlipperAppPluginDescriptor* | ||||
|     flipper_application_plugin_get_descriptor(FlipperApplication* app); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Load name and icon from FAP file. | ||||
|  *  | ||||
|  * @param path Path to FAP file. | ||||
|  * @param storage Storage instance. | ||||
|  * @param icon_ptr Icon pointer. | ||||
|  * @param item_name Application name. | ||||
|  * @return true if icon and name were loaded successfully. | ||||
|  */ | ||||
| bool flipper_application_load_name_and_icon( | ||||
|     FuriString* path, | ||||
|     Storage* storage, | ||||
|     uint8_t** icon_ptr, | ||||
|     FuriString* item_name); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @ -52,9 +52,7 @@ class Main(App): | ||||
|                 if not self.args.launch_app: | ||||
|                     return 0 | ||||
| 
 | ||||
|                 storage.send_and_wait_eol( | ||||
|                     f'loader open "Applications" {fap_dst_path}\r' | ||||
|                 ) | ||||
|                 storage.send_and_wait_eol(f"loader open {fap_dst_path}\r") | ||||
| 
 | ||||
|                 if len(result := storage.read.until(storage.CLI_EOL)): | ||||
|                     self.logger.error(f"Unexpected response: {result.decode('ascii')}") | ||||
|  | ||||
| @ -353,12 +353,18 @@ class AppBuildset: | ||||
| 
 | ||||
| class ApplicationsCGenerator: | ||||
|     APP_TYPE_MAP = { | ||||
|         FlipperAppType.SERVICE: ("FlipperApplication", "FLIPPER_SERVICES"), | ||||
|         FlipperAppType.SYSTEM: ("FlipperApplication", "FLIPPER_SYSTEM_APPS"), | ||||
|         FlipperAppType.APP: ("FlipperApplication", "FLIPPER_APPS"), | ||||
|         FlipperAppType.DEBUG: ("FlipperApplication", "FLIPPER_DEBUG_APPS"), | ||||
|         FlipperAppType.SETTINGS: ("FlipperApplication", "FLIPPER_SETTINGS_APPS"), | ||||
|         FlipperAppType.STARTUP: ("FlipperOnStartHook", "FLIPPER_ON_SYSTEM_START"), | ||||
|         FlipperAppType.SERVICE: ("FlipperInternalApplication", "FLIPPER_SERVICES"), | ||||
|         FlipperAppType.SYSTEM: ("FlipperInternalApplication", "FLIPPER_SYSTEM_APPS"), | ||||
|         FlipperAppType.APP: ("FlipperInternalApplication", "FLIPPER_APPS"), | ||||
|         FlipperAppType.DEBUG: ("FlipperInternalApplication", "FLIPPER_DEBUG_APPS"), | ||||
|         FlipperAppType.SETTINGS: ( | ||||
|             "FlipperInternalApplication", | ||||
|             "FLIPPER_SETTINGS_APPS", | ||||
|         ), | ||||
|         FlipperAppType.STARTUP: ( | ||||
|             "FlipperInternalOnStartHook", | ||||
|             "FLIPPER_ON_SYSTEM_START", | ||||
|         ), | ||||
|     } | ||||
| 
 | ||||
|     def __init__(self, buildset: AppBuildset, autorun_app: str = ""): | ||||
| @ -379,7 +385,7 @@ class ApplicationsCGenerator: | ||||
|      .appid = "{app.appid}",  | ||||
|      .stack_size = {app.stack_size}, | ||||
|      .icon = {f"&{app.icon}" if app.icon else "NULL"}, | ||||
|      .flags = {'|'.join(f"FlipperApplicationFlag{flag}" for flag in app.flags)} }}""" | ||||
|      .flags = {'|'.join(f"FlipperInternalApplicationFlag{flag}" for flag in app.flags)} }}""" | ||||
| 
 | ||||
|     def generate(self): | ||||
|         contents = [ | ||||
| @ -408,7 +414,7 @@ class ApplicationsCGenerator: | ||||
|             contents.extend( | ||||
|                 [ | ||||
|                     self.get_app_ep_forward(archive_app[0]), | ||||
|                     f"const FlipperApplication FLIPPER_ARCHIVE = {self.get_app_descr(archive_app[0])};", | ||||
|                     f"const FlipperInternalApplication FLIPPER_ARCHIVE = {self.get_app_descr(archive_app[0])};", | ||||
|                 ] | ||||
|             ) | ||||
| 
 | ||||
|  | ||||
| @ -63,7 +63,7 @@ class Main(App): | ||||
|                     storage_ops.recursive_send(fap_dst_path, fap_local_path, False) | ||||
| 
 | ||||
|                 fap_host_app = self.args.targets[0] | ||||
|                 startup_command = f'"Applications" {fap_host_app}' | ||||
|                 startup_command = f"{fap_host_app}" | ||||
|                 if self.args.host_app: | ||||
|                     startup_command = self.args.host_app | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Sergey Gavrilov
						Sergey Gavrilov