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:
committed by
GitHub
parent
c656ee9ffd
commit
92ca046681
@@ -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
|
||||
|
||||
@@ -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
|
||||
+3
-1
@@ -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));
|
||||
+2
-1
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 == '.';
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+150
-16
@@ -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 = {} };
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user