app-module events & callstack config, crash diagnostics (#630)

- Apps can specify task stack depth and preferred memory placement in their manifests.
- App identifiers are validated against length and character requirements.
- Crash diagnostics now show the crash cause, reason, fault address, call stack, and program-counter details, with logs saved for review.
- App closing is more consistent across built-in screens. There's now a dedicated function, and the old _emit() function is made private.
- Crash diagnostics no longer display a QR code without a call stack.
- Improved memory allocation for unrestricted requests.
This commit is contained in:
Ken Van Hoeylandt
2026-08-27 21:50:12 +02:00
committed by GitHub
parent c656ee9ffd
commit 92ca046681
74 changed files with 654 additions and 381 deletions
+7 -10
View File
@@ -114,16 +114,6 @@ error_t app_event_subscribe_with_app_id(struct AppEventSubscription* sub, struct
*/
error_t app_event_unsubscribe(struct AppEventSubscription* sub);
/**
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
* @warning Does not work in ISR context.
* @retval ERROR_NONE delivered to at least one subscription
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
* dropped for that subscription (still delivered to any other matching subscription)
*/
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
/**
* Non-blocking: pop the next event for @a sub if one is already queued.
* @warning Never blocks. To wait for an event, block in task_event_group_wait()/
@@ -134,6 +124,13 @@ error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* eve
*/
error_t app_event_poll(struct AppEventSubscription* sub, struct AppEvent* out_event);
/**
* Emits a close event to the specified app.
* @param[in] instance_id
* @return ERROR_NONE when event was successfully emitted
*/
error_t app_event_emit_close(AppInstanceId instance_id);
#ifdef __cplusplus
}
#endif
+23
View File
@@ -3,12 +3,16 @@
#include "location.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// Character count, excluding null terminator
#define APP_ID_LENGTH 32
/** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory {
APP_CATEGORY_SYSTEM,
@@ -24,6 +28,21 @@ enum AppManifestFlags {
APP_MANIFEST_FLAG_HIDDEN = 1 >> 0,
};
/** Largest stack depth (in words) an app may request. Keeps `depth * sizeof(StackType_t)` safely
* bounded and stops one app from claiming an unreasonable share of available RAM. A depth beyond
* this must be rejected outright, not silently truncated or clamped. */
#define APP_STACK_SIZE_MAX 16384
struct AppStackConfig {
/** Stack depth (in words, matching FreeRTOS's configSTACK_DEPTH_TYPE) for this app's task.
* 0 uses the scheduler's default. Must not exceed APP_STACK_SIZE_MAX. */
uint16_t depth;
/** Desired memory capability.
* 0 means default.
* Combine one or more of \a MemoryCapability from <tactility/memory.h> with a bitwise OR.*/
uint16_t desired_memory_capability;
};
/** Describes a registrable app. One manifest exists per app id. */
struct AppManifest {
/** Unique app identifier. Should never be NULL. */
@@ -34,8 +53,12 @@ struct AppManifest {
struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
uint8_t flags;
/** Stack allocation config for this app's task. */
struct AppStackConfig stack;
};
bool app_id_is_valid(const char* id);
#ifdef __cplusplus
}
#endif
@@ -50,6 +50,12 @@ struct AppMetadata {
* Must be NULL-terminated.
*/
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
/**
* Stack depth (in words) for the app's task. Optional; 0 means scheduler default.
* @warning Avoid default values: the default is conservative, which wastes memory.
*/
uint32_t stack_depth;
};
/**
@@ -0,0 +1,24 @@
#pragma once
#include <app/event.h>
#include <app/instance.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
* @warning Does not work in ISR context.
* @retval ERROR_NONE delivered to at least one subscription
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
* dropped for that subscription (still delivered to any other matching subscription)
*/
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
#ifdef __cplusplus
}
#endif
@@ -13,10 +13,10 @@
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
bool app_metadata_is_valid_format_version(const std::string& version);
bool app_metadata_is_valid_id(const std::string& id);
bool app_metadata_is_valid_name(const std::string& name);
bool app_metadata_is_valid_version_name(const std::string& version);
bool app_metadata_is_valid_version_code(const std::string& version);
bool app_metadata_is_valid_stack_size(const std::string& value);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_metadata_is_valid_device_id_list(const std::string& value);
@@ -31,3 +31,5 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char));
@@ -21,12 +21,13 @@ extern "C" {
* AppLoaderApi::load()/run(), marking the instance ACTIVE for the duration of run().
* @param[in] app_instance_id id already allocated in the ledger for this instance
* @param[in] location the location of the app
* @param[in] stack stack allocation config for the app's task
* @param[in] argc the amount of arguments to pass to the app's main function
* @param[in] argv the array of arguments to pass to the app's main function - ownership is
* taken by the scheduler regardless of outcome (freed once the spawned task's run() returns, or
* immediately on a failure to start it)
*/
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, int argc, char* argv[]);
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, struct AppStackConfig stack, int argc, char* argv[]);
/**
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its
+3 -2
View File
@@ -4,8 +4,8 @@
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/filesystem/file_mutex.h>
@@ -180,6 +180,7 @@ error_t register_installed_app_locked(const std::string& app_dir_path, const App
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#include "tactility/filesystem/file_mutex.h"
#include <tactility/filesystem/file_mutex.h>
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <app/private/metadata_parsing_internal.h>
#include <tactility/log.h>
@@ -16,8 +14,19 @@
constexpr auto* TAG = "app_metadata";
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_metadata_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
@@ -28,15 +37,6 @@ std::string trim(const std::string& value) {
return value.substr(start, end - start + 1);
}
bool validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
@@ -125,12 +125,6 @@ bool app_metadata_is_valid_format_version(const std::string& version) {
});
}
bool app_metadata_is_valid_id(const std::string& id) {
return id.size() >= 5 && id.size() <= APP_METADATA_APP_ID_LENGTH && validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_name(const std::string& name) {
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
@@ -150,6 +144,13 @@ bool app_metadata_is_valid_version_code(const std::string& version) {
});
}
bool app_metadata_is_valid_stack_size(const std::string& value) {
// 10 digits is the maximum decimal width of uint32_t.
return !value.empty() && value.size() <= 10 && validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
@@ -28,7 +31,7 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
return false;
}
if (!app_metadata_is_valid_id(id)) {
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
@@ -97,5 +100,7 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
return false;
}
out_metadata.stack_depth = 0;
return true;
}
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
@@ -28,7 +31,7 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
return false;
}
if (!app_metadata_is_valid_id(id)) {
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
@@ -114,5 +117,33 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
}
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_metadata_is_valid_stack_size(stack_size_string)) {
LOG_E(TAG, "Invalid app.stack.depth");
return false;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return false;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return false;
}
out_metadata.stack_depth = stack_size;
}
return true;
}
+5
View File
@@ -120,4 +120,9 @@ error_t app_event_poll(AppEventSubscription* sub, AppEvent* out_event) {
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
}
error_t app_event_emit_close(AppInstanceId instance_id) {
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
return app_event_emit(instance_id, &event);
}
} // extern "C"
+11 -7
View File
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/scheduler.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
@@ -103,17 +103,20 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
const AppManifest* manifest = manifest_iterator->second;
AppInstanceId target_id = ledger.next_instance_id++;
AppInstanceRecord record { target_id, manifest, APP_INSTANCE_STATE_STARTING, nullptr };
AppInstanceRecord record { .id = target_id, .manifest = manifest, .state = APP_INSTANCE_STATE_STARTING, .task = nullptr };
record.parent_id = parent_instance_id;
ledger.instances[target_id] = record;
mutex_unlock(&ledger.mutex);
error_t result = app_scheduler_start(target_id, manifest->location, argc, argv);
if (result != ERROR_NONE) {
LOG_I(TAG, "[instance %d] starting %s with parent %d", target_id, manifest->id, parent_instance_id);
error_t error = app_scheduler_start(target_id, manifest->location, manifest->stack, argc, argv);
if (error != ERROR_NONE) {
mutex_lock(&ledger.mutex);
ledger.instances.erase(target_id);
mutex_unlock(&ledger.mutex);
return result;
LOG_I(TAG, "[instance %d] Failed to start: %s", target_id, error_to_string(error));
return error;
}
*out_app_instance_id = target_id;
@@ -289,6 +292,7 @@ void app_manager_install_path_scan(void) {
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
new_records.push_back(std::move(record));
}
+15
View File
@@ -0,0 +1,15 @@
#include <app/manifest.h>
#include <app/private/metadata_parsing_internal.h>
#include <stdlib.h>
#include <string.h>
extern "C" {
bool app_id_is_valid(const char* id) {
auto size = strlen(id);
return size >= 5 && size <= APP_ID_LENGTH && app_metadata_validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
}
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <app/event.h>
#include <app/instance.h>
#include <app/loader.h>
#include <app/private/event.h>
#include <app/private/ledger.h>
#include <app/private/scheduler.h>
#include <app/scheduler.h>
#include <service/instance.h>
@@ -11,6 +11,7 @@
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/memory.h>
#include <cstdint>
#include <cstdio>
@@ -27,6 +28,15 @@ constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
constexpr UBaseType_t APP_TASK_PRIORITY = 4;
// Used when an app's manifest doesn't request a specific stack depth (0). 8192 bytes' worth.
constexpr size_t APP_DEFAULT_STACK_DEPTH = 8192 / sizeof(StackType_t);
// Task control blocks must stay in internal RAM; only the stack itself may live in external memory.
constexpr MemoryPolicy APP_TASK_TCB_POLICY = { MEMORY_CAPABILITY_INTERNAL, 0, 0 };
constexpr auto* APP_REAPER_TASK_NAME = "app_reaper";
constexpr size_t APP_REAPER_STACK_DEPTH = 2048 / sizeof(StackType_t);
namespace {
struct TaskContext {
@@ -36,8 +46,42 @@ struct TaskContext {
int argc;
char** argv;
AppCompletionSignal* completion;
StackType_t* stackBuffer;
StaticTask_t* taskTcb;
};
struct ReaperContext {
TaskHandle_t target;
StackType_t* stackBuffer;
StaticTask_t* taskTcb;
};
void reaper_task_main(void* context) {
auto* ctx = static_cast<ReaperContext*>(context);
while (eTaskGetState(ctx->target) == eRunning) {
taskYIELD();
}
vTaskDelete(ctx->target);
memory_free(ctx->stackBuffer);
memory_free(ctx->taskTcb);
delete ctx;
vTaskDelete(nullptr);
}
// Hands off this task's own statically-allocated stack/TCB (which it can never safely free
// itself - a task can't free the stack it's still running on, see UsbHidInput.cpp for the same
// constraint) to a short-lived helper task, then suspends forever. Never returns.
void reap_self(StackType_t* stack_buffer, StaticTask_t* task_tcb) {
auto* reaper_ctx = new (std::nothrow) ReaperContext { xTaskGetCurrentTaskHandle(), stack_buffer, task_tcb };
if (reaper_ctx == nullptr || xTaskCreate(reaper_task_main, APP_REAPER_TASK_NAME, APP_REAPER_STACK_DEPTH, reaper_ctx, tskIDLE_PRIORITY, nullptr) != pdPASS) {
LOG_E(TAG, "Failed to create app reaper task; leaking stack buffer");
delete reaper_ctx;
}
vTaskSuspend(nullptr);
}
void set_state(AppInstanceId app_instance_id, AppInstanceState state) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
@@ -144,7 +188,7 @@ void app_task_main(void* context) {
check(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX) == nullptr);
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, reinterpret_cast<void*>(static_cast<uintptr_t>(ctx->app_instance_id)));
LOG_I(TAG, "Thread for %d started", ctx->app_instance_id);
LOG_I(TAG, "[instance %lu] Task started", ctx->app_instance_id);
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
@@ -165,9 +209,11 @@ void app_task_main(void* context) {
AppInstanceId app_instance_id = ctx->app_instance_id;
AppCompletionSignal* completion = ctx->completion;
StackType_t* stack_buffer = ctx->stackBuffer;
StaticTask_t* task_tcb = ctx->taskTcb;
delete ctx;
LOG_I(TAG, "Thread for %d finished", app_instance_id);
LOG_I(TAG, "[instance %lu] Task finished", app_instance_id);
// Erase the ledger entry before self-deleting - see "Reap self-terminated app tasks":
// nothing else is guaranteed to ever call app_scheduler_stop() for this instance (the
@@ -187,17 +233,23 @@ void app_task_main(void* context) {
xSemaphoreGive(completion->semaphore);
release_completion_signal(completion); // releases app_task_main()'s own reference
LOG_I(TAG, "[instance %lu] minimum free stack space: %d bytes", app_instance_id, uxTaskGetStackHighWaterMark(nullptr));
#ifdef ESP_PLATFORM
// Statically-allocated stack/TCB (see app_scheduler_start()) - can't self-delete, see reap_self().
reap_self(stack_buffer, task_tcb);
#else
vTaskDelete(nullptr);
#endif
}
} // namespace
extern "C" {
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, int argc, char* argv[]) {
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, AppStackConfig stack, int argc, char* argv[]) {
const AppLoaderApi* loader = find_loader_api(location.type);
if (loader == nullptr) {
LOG_E(TAG, "No app loader is registered (service '%s' not found)", loader_service_id_for(location.type));
LOG_E(TAG, "[instance %lu] No app loader is registered (service '%s' not found)", app_instance_id, loader_service_id_for(location.type));
app_ledger_free_arguments(argc, argv);
return ERROR_NOT_FOUND;
}
@@ -205,30 +257,96 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
void* runtime = nullptr;
error_t load_result = loader->load(location, &runtime);
if (load_result != ERROR_NONE) {
LOG_E(TAG, "Failed to load app: %s", error_to_string(load_result));
LOG_E(TAG, "[instance %lu] Failed to load app: %s", app_instance_id, error_to_string(load_result));
app_ledger_free_arguments(argc, argv);
return load_result;
}
auto* completion = new (std::nothrow) AppCompletionSignal();
if (completion == nullptr) {
LOG_E(TAG, "Failed to allocate app");
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
completion->semaphore = xSemaphoreCreateBinary();
if (completion->semaphore == nullptr) {
LOG_E(TAG, "Failed to allocate app");
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
auto* context = new (std::nothrow) TaskContext { loader, runtime, app_instance_id, argc, argv, completion };
// Same bound app_metadata_parse() enforces on manifest.properties-declared depths - a
// manifest built directly in C++ (not parsed from a file) must be held to it too.
if (stack.depth > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "[instance %lu] stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", app_instance_id, stack.depth, APP_STACK_SIZE_MAX);
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_INVALID_ARGUMENT;
}
if (stack.depth == 0) {
LOG_W(TAG, "[instance %lu] using default stack depth", app_instance_id);
}
size_t effective_stack_depth = stack.depth != 0 ? stack.depth : APP_DEFAULT_STACK_DEPTH;
#ifdef ESP_PLATFORM
// ESP-IDF's FreeRTOS port has configSUPPORT_STATIC_ALLOCATION, POSIX doesn't
// Try the desired capability first (if any). If it fails or isn't specified, use the fallback/default alloc behaviour (use internal memory).
StackType_t* stack_buffer = nullptr;
if (stack.desired_memory_capability != 0) {
MemoryPolicy requested_policy = { .required = stack.desired_memory_capability, .desired = 0, .alignment = 0 };
stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(effective_stack_depth * sizeof(StackType_t), &requested_policy));
}
if (stack_buffer == nullptr) {
MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(effective_stack_depth * sizeof(StackType_t), &internal_policy));
}
if (stack_buffer == nullptr) {
LOG_E(TAG, "[instance %lu] Failed to allocate app stack", app_instance_id);
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
auto* task_tcb = static_cast<StaticTask_t*>(memory_alloc_with_policy(sizeof(StaticTask_t), &APP_TASK_TCB_POLICY));
if (task_tcb == nullptr) {
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
memory_free(stack_buffer);
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
#else
StackType_t* stack_buffer = nullptr;
StaticTask_t* task_tcb = nullptr;
#endif
auto* context = new (std::nothrow) TaskContext {
.loader = loader,
.runtime = runtime,
.app_instance_id = app_instance_id,
.argc = argc,
.argv = argv,
.completion = completion,
.stackBuffer = stack_buffer,
.taskTcb = task_tcb
};
if (context == nullptr) {
LOG_E(TAG, "Failed to allocate app");
LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id);
#ifdef ESP_PLATFORM
memory_free(task_tcb);
memory_free(stack_buffer);
#endif
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
@@ -239,14 +357,23 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
char task_name[16];
snprintf(task_name, sizeof(task_name), "app_%lu", static_cast<unsigned long>(app_instance_id));
TaskHandle_t task_handle = nullptr;
// 8192 bytes -> stack depth in words, matching what TactilityKernel's Thread wrapper does with the stack size it's given.
// Created at idle priority so it can't preempt us before vTaskSuspend() below runs, then suspended immediately -
// the ledger must record the handle (set_task()) before the task can possibly observe or erase its own entry.
// (see app_scheduler_stop()'s liveness check and app_task_main()'s exit path)
BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, tskIDLE_PRIORITY, &task_handle);
if (create_result != pdPASS) {
#ifdef ESP_PLATFORM
TaskHandle_t task_handle = xTaskCreateStatic(app_task_main, task_name, effective_stack_depth, context, tskIDLE_PRIORITY, stack_buffer, task_tcb);
#else
TaskHandle_t task_handle = nullptr;
if (xTaskCreate(app_task_main, task_name, effective_stack_depth, context, tskIDLE_PRIORITY, &task_handle) != pdPASS) {
task_handle = nullptr;
}
#endif
if (task_handle == nullptr) {
delete context;
#ifdef ESP_PLATFORM
memory_free(task_tcb);
memory_free(stack_buffer);
#endif
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
@@ -260,10 +387,17 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
vTaskPrioritySet(task_handle, APP_TASK_PRIORITY);
vTaskResume(task_handle);
memory_print_stats();
return ERROR_NONE;
}
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout) {
if (app_scheduler_current_app_id() == app_instance_id) {
LOG_E(TAG, "Can't call app_scheduler_stop() from the owning task");
return ERROR_NOT_ALLOWED;
}
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
if (completion != nullptr) {
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
+3 -1
View File
@@ -2,6 +2,7 @@
#include <app/event.h>
#include <app/install.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/metadata.h>
#include <app/paths.h>
#include <app/scheduler.h>
@@ -21,7 +22,6 @@ const ModuleSymbol app_module_symbols[] = {
DEFINE_MODULE_SYMBOL(app_event_subscribe),
DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id),
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
DEFINE_MODULE_SYMBOL(app_event_emit),
DEFINE_MODULE_SYMBOL(app_event_poll),
// app/install
DEFINE_MODULE_SYMBOL(app_get_install_path),
@@ -42,6 +42,8 @@ const ModuleSymbol app_module_symbols[] = {
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall),
// app/manifest
DEFINE_MODULE_SYMBOL(app_id_is_valid),
// app/metadata
DEFINE_MODULE_SYMBOL(app_metadata_parse),
// app/paths
@@ -6,6 +6,11 @@
#include <tactility/delay.h>
#include <tactility/time.h>
// app_event_emit() is declared in app-module's private app/private/event.h (PRIV_INCLUDE_DIRS,
// not exposed to this test target) - declared directly here, same as app_manager_test.cpp does
// for app_internal_loader_service_manifest.
extern "C" error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
TEST_CASE("app_event_subscribe/_poll deliver events in FIFO order") {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
@@ -451,6 +451,42 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is
app_manager_remove("test.app.top_b");
}
TEST_CASE("app_manager_start honors a custom AppManifest::stack.depth") {
ensure_fake_loader_registered();
AppManifest manifest { "test.app.stack.custom", "Stack Custom", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
manifest.stack.depth = 4096;
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t instance_id = 0;
REQUIRE_EQ(app_manager_start("test.app.stack.custom", &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED);
app_manager_remove("test.app.stack.custom");
}
TEST_CASE("app_manager_start still works when AppManifest::stack is left at its zero-value default") {
ensure_fake_loader_registered();
// stack.depth == 0 - app_scheduler_start() must fall back to its own default stack depth
// rather than fail or create a zero-sized stack.
AppManifest manifest { "test.app.stack.default", "Stack Default", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
REQUIRE_EQ(manifest.stack.depth, 0);
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t instance_id = 0;
REQUIRE_EQ(app_manager_start("test.app.stack.default", &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE);
CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED);
app_manager_remove("test.app.stack.default");
}
TEST_CASE("app_manager_get_topmost_app_id returns BUFFER_OVERFLOW for a too-small buffer, NOT_FOUND when nothing is active") {
ensure_fake_loader_registered();
@@ -4,6 +4,11 @@
#include <tactility/system_event.h>
// app_event_emit() is declared in app-module's private app/private/event.h (PRIV_INCLUDE_DIRS,
// not exposed to this test target) - declared directly here, same as app_manager_test.cpp does
// for app_internal_loader_service_manifest.
extern "C" error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);
// Regression coverage for the primary motivation behind TaskEventGroup: a task subscribed to
// both an app_event and a system_event must be able to block once and wake for either, without
// losing an event or regressing either subsystem's own delivery semantics (FIFO for app_event,