[FL-3953] Application chaining (#4105)

* feat: app chaining

* add `launch_current_app_after_deferred`, remove `get_referring_application`

* fix naming

* new api

* fix f18

* fix deferred launches after errors

* fix: memory leak

* Updater: MIN_GAP_PAGES = 0

* loader: loader_get_application_launch_path doc

* loader: fix freeze

* loader: reject mlib, reduce code size

* loader: generic synchronous call, reduce size

* loader: reject furi_string, reduce size

* apps: debug: removed order field from manifests since it is no longer meaningful

---------

Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
Co-authored-by: hedger <hedger@nanode.su>
This commit is contained in:
Anna Antonenko
2025-04-05 20:22:05 +04:00
committed by GitHub
parent dac1457f0a
commit 6b5d006690
37 changed files with 512 additions and 73 deletions

View File

@@ -27,9 +27,7 @@ static void desktop_loader_callback(const void* message, void* context) {
if(event->type == LoaderEventTypeApplicationBeforeLoad) {
view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalBeforeAppStarted);
furi_check(furi_semaphore_acquire(desktop->animation_semaphore, 3000) == FuriStatusOk);
} else if(
event->type == LoaderEventTypeApplicationLoadFailed ||
event->type == LoaderEventTypeApplicationStopped) {
} else if(event->type == LoaderEventTypeNoMoreAppsInQueue) {
view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalAfterAppFinished);
}
}

View File

@@ -167,6 +167,13 @@ static void loader_show_gui_error(
furi_record_close(RECORD_DIALOGS);
}
static void loader_generic_synchronous_request(Loader* loader, LoaderMessage* message) {
furi_check(loader);
message->api_lock = api_lock_alloc_locked();
furi_message_queue_put(loader->queue, message, FuriWaitForever);
api_lock_wait_unlock_and_free(message->api_lock);
}
LoaderStatus
loader_start(Loader* loader, const char* name, const char* args, FuriString* error_message) {
furi_check(loader);
@@ -202,16 +209,12 @@ void loader_start_detached_with_gui_error(Loader* loader, const char* name, cons
}
bool loader_lock(Loader* loader) {
furi_check(loader);
LoaderMessage message;
LoaderMessageBoolResult result;
message.type = LoaderMessageTypeLock;
message.api_lock = api_lock_alloc_locked();
message.bool_value = &result;
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
api_lock_wait_unlock_and_free(message.api_lock);
LoaderMessage message = {
.type = LoaderMessageTypeLock,
.bool_value = &result,
};
loader_generic_synchronous_request(loader, &message);
return result.value;
}
@@ -225,16 +228,12 @@ void loader_unlock(Loader* loader) {
}
bool loader_is_locked(Loader* loader) {
furi_check(loader);
LoaderMessage message;
LoaderMessageBoolResult result;
message.type = LoaderMessageTypeIsLocked;
message.api_lock = api_lock_alloc_locked();
message.bool_value = &result;
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
api_lock_wait_unlock_and_free(message.api_lock);
LoaderMessage message = {
.type = LoaderMessageTypeIsLocked,
.bool_value = &result,
};
loader_generic_synchronous_request(loader, &message);
return result.value;
}
@@ -256,42 +255,63 @@ FuriPubSub* loader_get_pubsub(Loader* loader) {
}
bool loader_signal(Loader* loader, uint32_t signal, void* arg) {
furi_check(loader);
LoaderMessageBoolResult result;
LoaderMessage message = {
.type = LoaderMessageTypeSignal,
.api_lock = api_lock_alloc_locked(),
.signal.signal = signal,
.signal.arg = arg,
.bool_value = &result,
};
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
api_lock_wait_unlock_and_free(message.api_lock);
loader_generic_synchronous_request(loader, &message);
return result.value;
}
bool loader_get_application_name(Loader* loader, FuriString* name) {
furi_check(loader);
LoaderMessageBoolResult result;
LoaderMessage message = {
.type = LoaderMessageTypeGetApplicationName,
.api_lock = api_lock_alloc_locked(),
.application_name = name,
.bool_value = &result,
};
furi_message_queue_put(loader->queue, &message, FuriWaitForever);
api_lock_wait_unlock_and_free(message.api_lock);
loader_generic_synchronous_request(loader, &message);
return result.value;
}
bool loader_get_application_launch_path(Loader* loader, FuriString* name) {
LoaderMessageBoolResult result;
LoaderMessage message = {
.type = LoaderMessageTypeGetApplicationLaunchPath,
.application_name = name,
.bool_value = &result,
};
loader_generic_synchronous_request(loader, &message);
return result.value;
}
void loader_enqueue_launch(
Loader* loader,
const char* name,
const char* args,
LoaderDeferredLaunchFlag flags) {
LoaderMessage message = {
.type = LoaderMessageTypeEnqueueLaunch,
.defer_start =
{
.name_or_path = strdup(name),
.args = args ? strdup(args) : NULL,
.flags = flags,
},
};
loader_generic_synchronous_request(loader, &message);
}
void loader_clear_launch_queue(Loader* loader) {
LoaderMessage message = {
.type = LoaderMessageTypeClearLaunchQueue,
};
loader_generic_synchronous_request(loader, &message);
}
// callbacks
static void loader_menu_closed_callback(void* context) {
@@ -328,12 +348,10 @@ static Loader* loader_alloc(void) {
Loader* loader = malloc(sizeof(Loader));
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.thread = NULL;
loader->app.insomniac = false;
loader->app.fap = NULL;
loader->gui = furi_record_open(RECORD_GUI);
loader->view_holder = view_holder_alloc();
loader->loading = loading_alloc();
view_holder_attach_to_gui(loader->view_holder, loader->gui);
return loader;
}
@@ -656,6 +674,10 @@ static LoaderMessageLoaderStatusResult loader_do_start_by_name(
LoaderStatusErrorUnknownApp, error_message, "Application \"%s\" not found", name);
} while(false);
if(status.value == LoaderStatusOk) {
loader->app.launch_path = furi_string_alloc_set_str(name);
}
return status;
}
@@ -673,6 +695,57 @@ static void loader_do_unlock(Loader* loader) {
loader->app.thread = NULL;
}
static void loader_do_emit_queue_empty_event(Loader* loader) {
FURI_LOG_I(TAG, "Launch queue empty");
LoaderEvent event;
event.type = LoaderEventTypeNoMoreAppsInQueue;
furi_pubsub_publish(loader->pubsub, &event);
}
static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record);
static void loader_do_next_deferred_launch_if_available(Loader* loader) {
LoaderDeferredLaunchRecord record;
if(loader_queue_pop(&loader->launch_queue, &record)) {
loader_do_deferred_launch(loader, &record);
loader_queue_item_clear(&record);
} else {
loader_do_emit_queue_empty_event(loader);
}
}
static bool loader_do_deferred_launch(Loader* loader, LoaderDeferredLaunchRecord* record) {
furi_assert(loader);
furi_assert(record);
bool is_successful = false;
FuriString* error_message = furi_string_alloc();
view_holder_set_view(loader->view_holder, loading_get_view(loader->loading));
view_holder_send_to_front(loader->view_holder);
do {
const char* app_name_str = record->name_or_path;
const char* app_args = record->args;
FURI_LOG_I(TAG, "Deferred launch: %s", app_name_str);
LoaderMessageLoaderStatusResult result =
loader_do_start_by_name(loader, app_name_str, app_args, error_message);
if(result.value == LoaderStatusOk) {
is_successful = true;
break;
}
if(record->flags & LoaderDeferredLaunchFlagGui)
loader_show_gui_error(result, app_name_str, error_message);
loader_do_next_deferred_launch_if_available(loader);
} while(false);
view_holder_set_view(loader->view_holder, NULL);
furi_string_free(error_message);
return is_successful;
}
static void loader_do_app_closed(Loader* loader) {
furi_assert(loader->app.thread);
@@ -697,11 +770,15 @@ static void loader_do_app_closed(Loader* loader) {
loader->app.thread = NULL;
}
furi_string_free(loader->app.launch_path);
FURI_LOG_I(TAG, "Application stopped. Free heap: %zu", memmgr_get_free_heap());
LoaderEvent event;
event.type = LoaderEventTypeApplicationStopped;
furi_pubsub_publish(loader->pubsub, &event);
loader_do_next_deferred_launch_if_available(loader);
}
static bool loader_is_application_running(Loader* loader) {
@@ -726,6 +803,15 @@ static bool loader_do_get_application_name(Loader* loader, FuriString* name) {
return false;
}
static bool loader_do_get_application_launch_path(Loader* loader, FuriString* path) {
if(loader_is_application_running(loader)) {
furi_string_set(path, loader->app.launch_path);
return true;
}
return false;
}
// app
int32_t loader_srv(void* p) {
@@ -748,16 +834,20 @@ int32_t loader_srv(void* p) {
while(true) {
if(furi_message_queue_get(loader->queue, &message, FuriWaitForever) == FuriStatusOk) {
switch(message.type) {
case LoaderMessageTypeStartByName:
*(message.status_value) = loader_do_start_by_name(
case LoaderMessageTypeStartByName: {
LoaderMessageLoaderStatusResult status = loader_do_start_by_name(
loader, message.start.name, message.start.args, message.start.error_message);
*(message.status_value) = status;
if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader);
api_lock_unlock(message.api_lock);
break;
}
case LoaderMessageTypeStartByNameDetachedWithGuiError: {
FuriString* error_message = furi_string_alloc();
LoaderMessageLoaderStatusResult status = loader_do_start_by_name(
loader, message.start.name, message.start.args, error_message);
loader_show_gui_error(status, message.start.name, error_message);
if(status.value != LoaderStatusOk) loader_do_emit_queue_empty_event(loader);
if(message.start.name) free((void*)message.start.name);
if(message.start.args) free((void*)message.start.args);
furi_string_free(error_message);
@@ -796,6 +886,19 @@ int32_t loader_srv(void* p) {
loader_do_get_application_name(loader, message.application_name);
api_lock_unlock(message.api_lock);
break;
case LoaderMessageTypeGetApplicationLaunchPath:
message.bool_value->value =
loader_do_get_application_launch_path(loader, message.application_name);
api_lock_unlock(message.api_lock);
break;
case LoaderMessageTypeEnqueueLaunch:
furi_check(loader_queue_push(&loader->launch_queue, &message.defer_start));
api_lock_unlock(message.api_lock);
break;
case LoaderMessageTypeClearLaunchQueue:
loader_queue_clear(&loader->launch_queue);
api_lock_unlock(message.api_lock);
break;
}
}
}

View File

@@ -20,13 +20,19 @@ typedef enum {
typedef enum {
LoaderEventTypeApplicationBeforeLoad,
LoaderEventTypeApplicationLoadFailed,
LoaderEventTypeApplicationStopped
LoaderEventTypeApplicationStopped,
LoaderEventTypeNoMoreAppsInQueue, //<! The normal `Stopped` event still fires before this one
} LoaderEventType;
typedef struct {
LoaderEventType type;
} LoaderEvent;
typedef enum {
LoaderDeferredLaunchFlagNone = 0,
LoaderDeferredLaunchFlagGui = (1 << 1), //<! Report launch to the user via a dialog
} LoaderDeferredLaunchFlag;
/**
* @brief Start application
* @param[in] instance loader instance
@@ -93,7 +99,7 @@ FuriPubSub* loader_get_pubsub(Loader* instance);
*
* @param[in] instance pointer to the loader instance
* @param[in] signal signal value to be sent
* @param[in,out] arg optional argument (can be of any value, including NULL)
* @param[inout] arg optional argument (can be of any value, including NULL)
*
* @return true if the signal was handled by the application, false otherwise
*/
@@ -103,11 +109,49 @@ bool loader_signal(Loader* instance, uint32_t signal, void* arg);
* @brief Get the name of the currently running application
*
* @param[in] instance pointer to the loader instance
* @param[in,out] name pointer to the string to contain the name (must be allocated)
* @param[inout] name pointer to the string to contain the name (must be allocated)
* @return true if it was possible to get an application name, false otherwise
*/
bool loader_get_application_name(Loader* instance, FuriString* name);
/**
* @brief Get the launch path or name of the currently running application
*
* This is the string that was supplied to `loader_start` such that the current
* app is running now. It might be a name (in the case of internal apps) or a
* path (in the case of external apps). This value can be used to launch the
* same app again.
*
* @param[in] instance pointer to the loader instance
* @param[inout] name pointer to the string to contain the path or name
* (must be allocated)
* @return true if it was possible to get an application path, false otherwise
*/
bool loader_get_application_launch_path(Loader* instance, FuriString* name);
/**
* @brief Enqueues a request to launch an application after the current one
*
* @param[in] instance pointer to the loader instance
* @param[in] name pointer to the name or path of the application, or NULL to
* cancel a previous request
* @param[in] args pointer to argument to provide to the next app
* @param[in] flags additional flags. see enum documentation for more info
*/
void loader_enqueue_launch(
Loader* instance,
const char* name,
const char* args,
LoaderDeferredLaunchFlag flags);
/**
* @brief Removes all requests to launch applications after the current one
* exits
*
* @param[in] instance pointer to the loader instance
*/
void loader_clear_launch_queue(Loader* instance);
#ifdef __cplusplus
}
#endif

View File

@@ -120,7 +120,7 @@ static void loader_pubsub_callback(const void* message, void* context) {
const LoaderEvent* event = message;
const FuriThreadId thread_id = (FuriThreadId)context;
if(event->type == LoaderEventTypeApplicationStopped) {
if(event->type == LoaderEventTypeNoMoreAppsInQueue) {
furi_thread_flags_set(thread_id, APPLICATION_STOP_EVENT);
}
}

View File

@@ -2,11 +2,20 @@
#include <furi.h>
#include <toolbox/api_lock.h>
#include <flipper_application/flipper_application.h>
#include <gui/gui.h>
#include <gui/view_holder.h>
#include <gui/modules/loading.h>
#include <m-array.h>
#include "loader.h"
#include "loader_menu.h"
#include "loader_applications.h"
#include "loader_queue.h"
typedef struct {
FuriString* launch_path;
char* args;
FuriThread* thread;
bool insomniac;
@@ -19,6 +28,12 @@ struct Loader {
LoaderMenu* loader_menu;
LoaderApplications* loader_applications;
LoaderAppData app;
LoaderLaunchQueue launch_queue;
Gui* gui;
ViewHolder* view_holder;
Loading* loading;
};
typedef enum {
@@ -33,6 +48,9 @@ typedef enum {
LoaderMessageTypeStartByNameDetachedWithGuiError,
LoaderMessageTypeSignal,
LoaderMessageTypeGetApplicationName,
LoaderMessageTypeGetApplicationLaunchPath,
LoaderMessageTypeEnqueueLaunch,
LoaderMessageTypeClearLaunchQueue,
} LoaderMessageType;
typedef struct {
@@ -72,6 +90,7 @@ typedef struct {
union {
LoaderMessageStartByName start;
LoaderDeferredLaunchRecord defer_start;
LoaderMessageSignal signal;
FuriString* application_name;
};

View File

@@ -0,0 +1,32 @@
#include "loader_queue.h"
void loader_queue_item_clear(LoaderDeferredLaunchRecord* item) {
free(item->args);
free(item->name_or_path);
}
bool loader_queue_pop(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item) {
if(!queue->item_cnt) return false;
*item = queue->items[0];
queue->item_cnt--;
memmove(
&queue->items[0], &queue->items[1], queue->item_cnt * sizeof(LoaderDeferredLaunchRecord));
return true;
}
bool loader_queue_push(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item) {
if(queue->item_cnt == LOADER_QUEUE_MAX_SIZE) return false;
queue->items[queue->item_cnt] = *item;
queue->item_cnt++;
return true;
}
void loader_queue_clear(LoaderLaunchQueue* queue) {
for(size_t i = 0; i < queue->item_cnt; i++)
loader_queue_item_clear(&queue->items[i]);
queue->item_cnt = 0;
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include <furi.h>
#include "loader.h"
#define LOADER_QUEUE_MAX_SIZE 4
typedef struct {
char* name_or_path;
char* args;
LoaderDeferredLaunchFlag flags;
} LoaderDeferredLaunchRecord;
typedef struct {
LoaderDeferredLaunchRecord items[LOADER_QUEUE_MAX_SIZE];
size_t item_cnt;
} LoaderLaunchQueue;
/**
* @brief Frees internal data in a `DeferredLaunchRecord`
*
* @param[out] item Record to clear
*/
void loader_queue_item_clear(LoaderDeferredLaunchRecord* item);
/**
* @brief Fetches the next item from the launch queue
*
* @param[inout] queue Queue instance
* @param[out] item Item output
*
* @return `true` if `item` was populated, `false` if queue is empty
*/
bool loader_queue_pop(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item);
/**
* @brief Puts an item into the launch queue
*
* @param[inout] queue Queue instance
* @param[in] item Item to put in the queue
*
* @return `true` if the item was put into the queue, `false` if there's no more
* space left
*/
bool loader_queue_push(LoaderLaunchQueue* queue, LoaderDeferredLaunchRecord* item);
/**
* @brief Clears the launch queue
*
* @param[inout] queue Queue instance
*/
void loader_queue_clear(LoaderLaunchQueue* queue);