Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
// Minimal filesystem helpers shared by app-module internals that need to look at on-disk app
// directories (app_install.cpp, manager.cpp's install-path scan) - app-module may not depend
// upward on Tactility::file, so this is a small local re-implementation (see
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading).
#include <tactility/filesystem/file_mutex.h>
#include <cstring>
#include <dirent.h>
#include <string>
#include <sys/stat.h>
#include <vector>
inline bool app_fs_is_directory(const std::string& path) {
struct stat result {};
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
auto is_dir = stat(path.c_str(), &result) == 0 && S_ISDIR(result.st_mode);
file_mutex_unlock(&file_mutex);
return is_dir;
}
inline bool app_fs_is_file(const std::string& path) {
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
struct stat result {};
auto retval = stat(path.c_str(), &result) == 0 && S_ISREG(result.st_mode);
file_mutex_unlock(&file_mutex);
return retval;
}
// Appends the full path of every direct subdirectory of @a path to @a out.
// No-op (not an error) if @a path can't be opened.
inline void app_fs_list_direct_subdirectories(const std::string& path, std::vector<std::string>& out) {
// Collect child names while the directory lock is held, then release it before classifying
// each one with app_fs_is_directory() - that function looks up and locks a FileMutex too,
// and file_mutex_get() resolves a child path to the same registered mutex as its parent
// mount. Calling it while still holding the directory's own lock would be a nested
// acquisition of that same (possibly non-recursive) mutex, and could self-deadlock.
std::vector<std::string> children;
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
file_mutex_unlock(&file_mutex);
return;
}
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
continue;
}
children.push_back(path + "/" + entry->d_name);
}
closedir(dir);
file_mutex_unlock(&file_mutex);
for (const auto& child_path : children) {
if (app_fs_is_directory(child_path)) {
out.push_back(child_path);
}
}
}
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <app/manifest.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
#include <tactility/freertos/task.h>
#include <stdint.h>
#include <string>
#include <unordered_map>
/**
* A dedicated completion signal(1) for one app instance's task, given as the
* literal last action app_task_main() takes before vTaskDelete().
* Heap-allocated with its own refcount (protected by app_ledger().mutex, not atomic)
* rather than owned by the ledger entry, since app_task_main() always erases that entry -
* and may run its exit path entirely - before app_scheduler_stop() ever looks for it:
* Whichever side(2) finishes with it last is the one that deletes `semaphore` and frees this struct.
*
* (1) Not the task's shared default FreeRTOS notification, which app_event.cpp's
* AppEventSubscription also uses - an unrelated event delivered to the same task could
* otherwise unblock a waiter early.
* (2) The exiting task, or a concurrent app_scheduler_stop() that found the entry in time and is waiting on `semaphore`
*/
struct AppCompletionSignal {
SemaphoreHandle_t semaphore;
/** Starts at 1, owned by app_task_main() until its own exit. app_scheduler_stop() takes an
* additional reference for as long as it's waiting on `semaphore`, if it finds the instance
* still running. Reaching 0 means deletion. */
int refcount = 1;
};
/** A registered/running app instance, as tracked internally by app-module. */
struct AppInstanceRecord {
uint32_t id;
const AppManifest* manifest;
AppInstanceState state;
/** The FreeRTOS task currently executing AppLoaderApi::run() for this instance; NULL when not running. */
TaskHandle_t task;
/** 0 for a top-level launch (app_manager_start()). Non-zero for a modal child launched via
* app_manager_start_for_result() - the instance that receives this child's APP_EVENT_RESULT. */
uint32_t parent_id = 0;
/** This instance's completion signal - see AppCompletionSignal. Set once by
* app_scheduler_start(), never reassigned. */
AppCompletionSignal* completion = nullptr;
};
struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests;
std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1;
Mutex mutex {};
AppLedger() { mutex_construct(&mutex); }
~AppLedger() { mutex_destruct(&mutex); }
};
inline AppLedger& app_ledger() {
static AppLedger ledger;
return ledger;
}
/**
* Frees a deep-copied argv previously built by app_manager_start_with_parameters()/app_manager_start_for_result():
* each individually heap-allocated string, then the array itself. Safe to call with count == 0 values == nullptr (no-op).
*/
inline void app_ledger_free_arguments(int count, char** values) {
if (values == nullptr) {
return;
}
for (int i = 0; i < count; i++) {
delete[] values[i];
}
delete[] values;
}
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/metadata.h>
#include <map>
#include <string>
/** Shared helpers + per-format parsers for app_metadata_parse() (source/app_metadata_parsing.cpp)
* - split out like the old tt::app manifest parser (AppManifestParsing/V1/V2.cpp) that this is
* modelled on, one file per format plus a shared dispatcher. */
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);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
/** 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);
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
/**
* Owns per-app task lifecycle on behalf of app_manager_*(). AppLoaderApi implementations
* stay task-agnostic; all of xTaskCreate()/vTaskDelete() happens here, as a plain FreeRTOS task
* (not TactilityKernel's Thread wrapper). Every app instance gets its own dedicated task for its
* entire lifetime - no task is ever reused for a different instance.
*/
#ifdef __cplusplus
extern "C" {
#endif
/**
* Loads and starts an app instance: spawns a dedicated task that calls
* 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] 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[]);
/**
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its
* task to exit, and removes it from the ledger.
*/
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout);
// app_scheduler_current_app_id() is public - see app/scheduler.h.
#ifdef __cplusplus
}
#endif