mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-18 16:05:05 +00:00
Update
This commit is contained in:
parent
f61a30726f
commit
c628668e75
@ -2,12 +2,12 @@ idf_component_register(
|
||||
INCLUDE_DIRS
|
||||
"Libraries/TactilityC/include"
|
||||
"Libraries/TactilityKernel/include"
|
||||
"Libraries/TactilityFreeRtos/include"
|
||||
"Libraries/TactilityFreeRtos/Include"
|
||||
"Libraries/lvgl/include"
|
||||
"Libraries/minmea/include"
|
||||
"Modules/lvgl-module/include"
|
||||
# DRIVER_INCLUDE_DIRS_PLACEHOLDER
|
||||
REQUIRES esp_timer
|
||||
REQUIRES esp_timer app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module
|
||||
)
|
||||
|
||||
# Regular and core features
|
||||
|
||||
@ -18,13 +18,19 @@ macro(tactility_project project_name)
|
||||
endif()
|
||||
|
||||
set(EXTRA_COMPONENT_DIRS
|
||||
"Libraries/TactilityFreeRtos"
|
||||
"Modules"
|
||||
"Drivers"
|
||||
"${TACTILITY_SDK_PATH}/Libraries/TactilityFreeRtos"
|
||||
"${TACTILITY_SDK_PATH}/Modules"
|
||||
"${TACTILITY_SDK_PATH}/Drivers"
|
||||
)
|
||||
|
||||
set(COMPONENTS
|
||||
TactilityFreeRtos
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
service-module
|
||||
# DRIVER_COMPONENTS_PLACEHOLDER
|
||||
)
|
||||
|
||||
|
||||
@ -167,7 +167,7 @@ def main():
|
||||
{'src': 'TactilityC/CMakeLists.txt', 'dst': 'Libraries/TactilityC/'},
|
||||
{'src': 'TactilityC/LICENSE*.*', 'dst': 'Libraries/TactilityC/'},
|
||||
# TactilityFreeRtos
|
||||
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/include/'},
|
||||
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/Include/'},
|
||||
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||
{'src': 'TactilityFreeRtos/LICENSE*.*', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||
# TactilityKernel
|
||||
@ -197,9 +197,11 @@ def main():
|
||||
map_copy(mappings, target_path)
|
||||
|
||||
# Modules
|
||||
add_module(target_path, "lvgl-module")
|
||||
add_module(target_path, "app-module")
|
||||
add_module(target_path, "crypt-module")
|
||||
add_module(target_path, "gps-module")
|
||||
add_module(target_path, "lvgl-module")
|
||||
add_module(target_path, "lvgl-window-manager-module")
|
||||
add_module(target_path, "service-module")
|
||||
|
||||
# Drivers - only ones actually built for this target (chip-restricted drivers like
|
||||
|
||||
@ -99,6 +99,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
add_subdirectory(Modules/crypt-module)
|
||||
add_subdirectory(Modules/gps-module)
|
||||
add_subdirectory(Modules/service-module)
|
||||
add_subdirectory(Modules/app-module)
|
||||
add_subdirectory(Modules/lvgl-window-manager-module)
|
||||
add_subdirectory(Drivers/gps-generic-module)
|
||||
add_subdirectory(Drivers/gps-meshtastic-module)
|
||||
|
||||
|
||||
@ -23,3 +23,8 @@ cdn.infoMessage=To put the device into bootloader mode: <br/>1. Press the trackb
|
||||
lvgl.colorDepth=16
|
||||
|
||||
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
|
||||
|
||||
# Fix error "PSRAM space not enough for the Flash instructions" on boot:
|
||||
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
|
||||
sdkconfig.CONFIG_SPIRAM_RODATA=n
|
||||
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
|
||||
|
||||
@ -12,13 +12,25 @@
|
||||
|
||||
## Higher Priority
|
||||
|
||||
- Move "# Fix error "PSRAM space not enough for the Flash instructions" on boot:" fix from T-Deck and others to device.py
|
||||
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
|
||||
- Put task stacks in PSRAM when possible.
|
||||
- Wrap file operations like fopen/fclose with file_mutex
|
||||
- Add bold fonts for e-ink readability improvement
|
||||
- Split up Claude instructions: https://code.claude.com/docs/en/memory#import-additional-files
|
||||
and add https://github.com/multica-ai/andrej-karpathy-skills/blob/main/CLAUDE.md
|
||||
- Move test projects to their relevant subproject
|
||||
- tt_alertdialog start() etc is broken as it can't fetch the app instance id. Fetch automatically via thread context?
|
||||
- Migrate Tactility/Paths.cpp functions to TactilityKernel
|
||||
- app_manager_find_manifest() should make a copy, not return a pointer.
|
||||
- Httpd.cpp: warn if running on same CPU core (or task) as UI/LVGL/window manager.
|
||||
- Improve Setup: Show "Step done" screen
|
||||
- Improve Setup: Add keyboard/keypad navigation explanation
|
||||
- display.h API: get_backlight does not change ref counting, but it should
|
||||
- bluetooth: various getters for child devices do not change ref counting, but they should
|
||||
- Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed()
|
||||
- Remove and migrate `Include/Tactility/kernel/Kernel.h` into `tactility/delay.h`
|
||||
- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module.
|
||||
- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual modulej.
|
||||
- LilyGO T-Dongle S3: 1 button control, stop auto-launching web server
|
||||
- Core2: support power off via software
|
||||
- Create `#define` for empty module (for modules that fully rely on device.properties and don't define drivers or have start/stop logic)
|
||||
@ -40,6 +52,7 @@
|
||||
|
||||
## Medium Priority
|
||||
|
||||
- Consider using https://github.com/Graphify-Labs/graphify
|
||||
- Implement a LED kernel driver (single colour and RGB, plain GPIO and PWM)
|
||||
- Make USB host driver disabled by default, so it doesn't consume memory
|
||||
- Filtering for apps in App Hub:
|
||||
|
||||
@ -1,24 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "../../../TactilityKernel/include/tactility/error.h"
|
||||
#include "../../../TactilityKernel/include/tactility/filesystem/file_mutex.h"
|
||||
#include "../../app-module/include/app/loader.h"
|
||||
#include "../../app-module/include/app/location.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <app/loader.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/location.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/filesystem/file_mutex.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <esp_elf.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
constexpr auto* TAG = "app_esp32_loader";
|
||||
|
||||
namespace {
|
||||
|
||||
/** load()-allocated state, passed back through run()/unload(). */
|
||||
@ -34,6 +37,7 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
|
||||
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (file == nullptr) {
|
||||
LOG_E(TAG, "Failed to open %s", path);
|
||||
file_mutex_unlock(&mutex);
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
@ -68,19 +72,35 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
auto* runtime = new (std::nothrow) Esp32AppRuntime();
|
||||
if (runtime == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
// location.location can be either an app's install directory or the .elf file directly; the
|
||||
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
|
||||
std::string resolve_elf_path(const std::string& path) {
|
||||
if (path.ends_with(".elf")) {
|
||||
return path;
|
||||
}
|
||||
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
LOG_E(TAG, "Out of memory");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Loading %s", static_cast<const char*>(location.location));
|
||||
|
||||
auto* runtime = new (std::nothrow) Esp32AppRuntime();
|
||||
if (runtime == nullptr) {
|
||||
LOG_E(TAG, "Out of memory");
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
|
||||
|
||||
size_t size = 0;
|
||||
error_t read_result = read_file(static_cast<const char*>(location.location), &runtime->file_data, &size);
|
||||
error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size);
|
||||
if (read_result != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to read file");
|
||||
delete runtime;
|
||||
return read_result;
|
||||
}
|
||||
@ -88,13 +108,15 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
if (esp_elf_init(&runtime->elf) != ESP_OK) {
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
LOG_E(TAG, "Failed to init elf");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_elf_relocate(&runtime->elf, runtime->file_data) != 0) {
|
||||
esp_elf_deinit(&runtime->elf);
|
||||
// esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
LOG_E(TAG, "Failed to map elf");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
@ -104,9 +126,6 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
|
||||
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
|
||||
auto* runtime = static_cast<Esp32AppRuntime*>(runtime_ptr);
|
||||
// A side-loaded ELF's own main() only ever gets a real argc/argv from esp_elf_request()'s
|
||||
// fixed signature - there's no slot for app_instance_id there, and side-loaded apps don't
|
||||
// need one yet.
|
||||
return esp_elf_request(&runtime->elf, 0, argc, argv);
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
|
||||
typedef uint32_t AppInstanceId;
|
||||
|
||||
/** Lifecycle state of a running (or previously running) app instance. Every app instance owns
|
||||
* its own task for its entire lifetime - there is no "saved, task given up" state. */
|
||||
typedef enum {
|
||||
|
||||
@ -13,9 +13,6 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
|
||||
typedef uint32_t AppInstanceId;
|
||||
|
||||
/**
|
||||
* Register an app manifest.
|
||||
* @retval ERROR_INVALID_ARGUMENT a manifest with the same id is already registered
|
||||
@ -130,6 +127,26 @@ error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id);
|
||||
*/
|
||||
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
|
||||
|
||||
/**
|
||||
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
|
||||
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
|
||||
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
|
||||
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
|
||||
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
|
||||
* call app_manager_install_path_scan() to do that.
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_install_path_add(const char* path);
|
||||
|
||||
/**
|
||||
* Scans every path registered via app_manager_install_path_add(): registers
|
||||
* (app_manager_add()) any direct subdirectory with a valid manifest.properties that isn't
|
||||
* already registered, and unregisters (app_manager_remove() only - does not stop it if running,
|
||||
* does not delete anything) any manifest a previous scan registered whose directory has since
|
||||
* disappeared. Safe to call repeatedly (e.g. after an SD card is mounted/unmounted).
|
||||
*/
|
||||
void app_manager_install_path_scan(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -39,8 +39,8 @@ struct AppMetadata {
|
||||
*/
|
||||
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
|
||||
|
||||
/** The technical version (must be incremented with new releases of the app */
|
||||
uint64_t app_version_code = 0;
|
||||
/** The technical version (must be incremented with new releases of the app) */
|
||||
uint64_t app_version_code;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
21
Modules/app-module/include/app/scheduler.h
Normal file
21
Modules/app-module/include/app/scheduler.h
Normal file
@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @return the app_instance_id of whichever app instance's task is calling this (every app
|
||||
* instance's task stashes it in its own thread-local storage when it starts), or 0 if called
|
||||
* from a task that isn't a running app instance. An app's own main() typically calls this once,
|
||||
* near the top, to learn its own instance id - see e.g. app_event_subscribe()/
|
||||
* window_manager_create(), both of which need it.
|
||||
*/
|
||||
AppInstanceId app_scheduler_current_app_id(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
63
Modules/app-module/private/app/private/app_fs.h
Normal file
63
Modules/app-module/private/app/private/app_fs.h
Normal file
@ -0,0 +1,63 @@
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
auto child_path = path + "/" + entry->d_name;
|
||||
if (app_fs_is_directory(child_path)) {
|
||||
out.push_back(child_path);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
file_mutex_unlock(&file_mutex);
|
||||
}
|
||||
@ -5,7 +5,8 @@
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@ -16,8 +17,9 @@ struct AppInstanceRecord {
|
||||
uint32_t id;
|
||||
const AppManifest* manifest;
|
||||
AppInstanceState state;
|
||||
/** The kernel thread currently executing AppLoaderApi::run() for this instance; NULL when not running. */
|
||||
Thread* thread;
|
||||
/** 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. */
|
||||
@ -40,7 +42,7 @@ inline AppLedger& app_ledger() {
|
||||
}
|
||||
|
||||
/** Frees a deep-copied argv previously built by app_manager_start_with_parameters()/
|
||||
* app_manager_start_for_result() (see app_scheduler.cpp's ThreadContext::argv) - each
|
||||
* app_manager_start_for_result() (see app_scheduler.cpp's TaskContext::argv) - 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) {
|
||||
|
||||
@ -4,16 +4,14 @@
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Owns per-app task lifecycle on behalf of app_manager_*(). AppLoaderApi implementations
|
||||
* stay task-agnostic; all of thread_alloc_full()/thread_start()/thread_join() happen here.
|
||||
* Every app instance gets its own dedicated task for its entire lifetime - no task is ever
|
||||
* reused for a different instance.
|
||||
* 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
|
||||
@ -30,13 +28,15 @@ extern "C" {
|
||||
* 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(uint32_t app_instance_id, struct AppLocation location, int argc, char* argv[]);
|
||||
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(uint32_t app_instance_id, TickType_t join_timeout);
|
||||
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
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <app/manager.h>
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_fs.h>
|
||||
#include <app/private/app_ledger.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
@ -37,19 +38,9 @@ std::string last_path_segment(const std::string& path) {
|
||||
return index == std::string::npos ? path : path.substr(index + 1);
|
||||
}
|
||||
|
||||
bool is_directory(const std::string& path) {
|
||||
struct stat result {};
|
||||
return stat(path.c_str(), &result) == 0 && S_ISDIR(result.st_mode);
|
||||
}
|
||||
|
||||
bool is_file(const std::string& path) {
|
||||
struct stat result {};
|
||||
return stat(path.c_str(), &result) == 0 && S_ISREG(result.st_mode);
|
||||
}
|
||||
|
||||
// mkdir -p.
|
||||
bool ensure_directory(const std::string& path) {
|
||||
if (path.empty() || is_directory(path)) {
|
||||
if (path.empty() || app_fs_is_directory(path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -62,7 +53,7 @@ bool ensure_directory(const std::string& path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_directory(path);
|
||||
return app_fs_is_directory(path);
|
||||
}
|
||||
|
||||
bool ensure_directory_recursive(const std::string& path) {
|
||||
@ -75,19 +66,27 @@ bool ensure_directory_recursive(const std::string& path) {
|
||||
}
|
||||
|
||||
bool delete_recursively(const std::string& path) {
|
||||
LOG_D(TAG, "Deleting %s...", path.c_str());
|
||||
if (path.empty() || path == "/" || path == "." || path == "..") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_directory(path)) {
|
||||
if (app_fs_is_directory(path)) {
|
||||
LOG_D(TAG, "Deleting dir %s", path.c_str());
|
||||
|
||||
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) {
|
||||
LOG_E(TAG, "Failed to scan directory %s", path.c_str());
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
struct dirent* entry;
|
||||
dirent* entry;
|
||||
while (success && (entry = readdir(dir)) != nullptr) {
|
||||
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
@ -97,18 +96,17 @@ bool delete_recursively(const std::string& path) {
|
||||
closedir(dir);
|
||||
|
||||
if (!success) {
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
FileMutex mutex {};
|
||||
file_mutex_get(&mutex, path.c_str());
|
||||
file_mutex_lock(&mutex);
|
||||
bool result = rmdir(path.c_str()) == 0;
|
||||
file_mutex_unlock(&mutex);
|
||||
file_mutex_unlock(&file_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (is_file(path)) {
|
||||
if (app_fs_is_file(path)) {
|
||||
LOG_D(TAG, "Deleting file %s", path.c_str());
|
||||
FileMutex mutex {};
|
||||
file_mutex_get(&mutex, path.c_str());
|
||||
file_mutex_lock(&mutex);
|
||||
@ -117,7 +115,7 @@ bool delete_recursively(const std::string& path) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Doesn't exist - nothing to do.
|
||||
LOG_D(TAG, "Deleting done");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -213,6 +211,35 @@ InstallRegistry& install_registry() {
|
||||
return registry;
|
||||
}
|
||||
|
||||
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
|
||||
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
|
||||
// @warning Caller must hold install_registry().mutex, and must have already ensured
|
||||
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
|
||||
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
|
||||
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
|
||||
auto& registry = install_registry();
|
||||
|
||||
auto record = std::make_unique<InstalledAppRecord>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = app_dir_path;
|
||||
record->manifest = AppManifest {
|
||||
.id = record->id.c_str(),
|
||||
.name = record->name.c_str(),
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
error_t add_result = app_manager_add(&record->manifest);
|
||||
if (add_result != ERROR_NONE) {
|
||||
return add_result;
|
||||
}
|
||||
|
||||
registry.apps[record->id] = std::move(record);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
|
||||
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
|
||||
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
|
||||
@ -312,7 +339,7 @@ error_t app_install(const char* source_path) {
|
||||
}
|
||||
|
||||
auto manifest_path = staging_path + "/manifest.properties";
|
||||
if (!is_file(manifest_path)) {
|
||||
if (!app_fs_is_file(manifest_path)) {
|
||||
LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
|
||||
delete_recursively(staging_path);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
@ -320,7 +347,7 @@ error_t app_install(const char* source_path) {
|
||||
|
||||
AppMetadata metadata {};
|
||||
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Invalid manifest");
|
||||
LOG_E(TAG, "Install failed: invalid manifest");
|
||||
delete_recursively(staging_path);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
@ -329,8 +356,18 @@ error_t app_install(const char* source_path) {
|
||||
mutex_lock(®istry.mutex);
|
||||
|
||||
// Replace any previous install of this app id (mirrors the old install()'s "already
|
||||
// running/present" handling).
|
||||
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
|
||||
// registry - the same app id may instead be registered by app_manager_install_path_scan()
|
||||
// (manager.cpp's separate registry, scanning this same directory tree), which
|
||||
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
|
||||
// too, or app_manager_add() below rejects the re-add as a duplicate.
|
||||
uninstall_locked(metadata.app_id);
|
||||
if (app_manager_remove(metadata.app_id) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Install failed: failed to remove existing installation");
|
||||
mutex_unlock(®istry.mutex);
|
||||
delete_recursively(staging_path);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto final_path = app_parent_path + "/" + metadata.app_id;
|
||||
delete_recursively(final_path);
|
||||
@ -346,30 +383,12 @@ error_t app_install(const char* source_path) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
auto record = std::make_unique<InstalledAppRecord>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = final_path;
|
||||
record->manifest = AppManifest {
|
||||
.id = record->id.c_str(),
|
||||
.name = record->name.c_str(),
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
error_t add_result = app_manager_add(&record->manifest);
|
||||
if (add_result != ERROR_NONE) {
|
||||
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
|
||||
// already removed any previous registration for this exact id.
|
||||
mutex_unlock(®istry.mutex);
|
||||
return add_result;
|
||||
}
|
||||
|
||||
registry.apps[record->id] = std::move(record);
|
||||
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
|
||||
// already removed any previous registration for this exact id.
|
||||
error_t add_result = register_installed_app_locked(final_path, metadata);
|
||||
mutex_unlock(®istry.mutex);
|
||||
|
||||
return ERROR_NONE;
|
||||
return add_result;
|
||||
}
|
||||
|
||||
error_t app_uninstall(const char* app_id) {
|
||||
|
||||
@ -57,15 +57,16 @@ bool load_properties(const std::string& path, std::map<std::string, std::string>
|
||||
bool got_first_line = false;
|
||||
while (std::getline(file, line)) {
|
||||
auto trimmed_line = trim(line);
|
||||
if (!got_first_line) {
|
||||
out_first_line = trimmed_line;
|
||||
got_first_line = true;
|
||||
}
|
||||
|
||||
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!got_first_line) {
|
||||
out_first_line = trimmed_line;
|
||||
got_first_line = true;
|
||||
}
|
||||
|
||||
if (trimmed_line.starts_with("[")) {
|
||||
section_prefix = trimmed_line;
|
||||
continue;
|
||||
@ -123,7 +124,8 @@ bool app_metadata_is_valid_version_name(const std::string& version) {
|
||||
}
|
||||
|
||||
bool app_metadata_is_valid_version_code(const std::string& version) {
|
||||
return !version.empty() && validate_string(version, [](char c) {
|
||||
// 20 digits is the maximum decimal width of uint64_t.
|
||||
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
|
||||
#include <charconv>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "app_metadata_v1";
|
||||
@ -77,9 +78,14 @@ bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
out_metadata.app_version_code = std::stoull(version_code_string);
|
||||
|
||||
// [target]
|
||||
uint64_t version_code = 0;
|
||||
const auto* first = version_code_string.data();
|
||||
const auto* last = first + version_code_string.size();
|
||||
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
|
||||
LOG_E(TAG, "App version code out of range");
|
||||
return false;
|
||||
}
|
||||
out_metadata.app_version_code = version_code; // [target]
|
||||
|
||||
std::string target_sdk;
|
||||
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
|
||||
#include <charconv>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "app_metadata_v2";
|
||||
@ -77,7 +78,14 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
|
||||
return false;
|
||||
}
|
||||
|
||||
out_metadata.app_version_code = std::stoull(version_code_string);
|
||||
uint64_t version_code = 0;
|
||||
const auto* first = version_code_string.data();
|
||||
const auto* last = first + version_code_string.size();
|
||||
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
|
||||
LOG_E(TAG, "App version code out of range");
|
||||
return false;
|
||||
}
|
||||
out_metadata.app_version_code = version_code; // [target]
|
||||
|
||||
// target
|
||||
|
||||
|
||||
@ -1,31 +1,45 @@
|
||||
// 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/scheduler.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <new>
|
||||
|
||||
#define TAG "app_scheduler"
|
||||
constexpr auto* TAG = "app_scheduler";
|
||||
|
||||
// Slot 0 is reserved by ESP-IDF's pthread API (see TactilityKernel's Thread wrapper for the
|
||||
// same convention/comment) - app tasks use slot 1 to stash their own app_instance_id, so any
|
||||
// code running on an app's own task can retrieve it via app_scheduler_current_app_id() without
|
||||
// needing it threaded through as a parameter.
|
||||
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;
|
||||
|
||||
namespace {
|
||||
|
||||
struct ThreadContext {
|
||||
struct TaskContext {
|
||||
const AppLoaderApi* loader;
|
||||
void* runtime;
|
||||
uint32_t app_instance_id;
|
||||
AppInstanceId app_instance_id;
|
||||
int argc;
|
||||
char** argv;
|
||||
};
|
||||
|
||||
void set_state(uint32_t app_instance_id, AppInstanceState state) {
|
||||
void set_state(AppInstanceId app_instance_id, AppInstanceState state) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
@ -35,21 +49,21 @@ void set_state(uint32_t app_instance_id, AppInstanceState state) {
|
||||
mutex_unlock(&ledger.mutex);
|
||||
}
|
||||
|
||||
Thread* get_thread(uint32_t app_instance_id) {
|
||||
TaskHandle_t get_task(AppInstanceId app_instance_id) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
Thread* thread = (iterator != ledger.instances.end()) ? iterator->second.thread : nullptr;
|
||||
TaskHandle_t task = (iterator != ledger.instances.end()) ? iterator->second.task : nullptr;
|
||||
mutex_unlock(&ledger.mutex);
|
||||
return thread;
|
||||
return task;
|
||||
}
|
||||
|
||||
void set_thread(uint32_t app_instance_id, Thread* thread) {
|
||||
void set_task(AppInstanceId app_instance_id, TaskHandle_t task) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
if (iterator != ledger.instances.end()) {
|
||||
iterator->second.thread = thread;
|
||||
iterator->second.task = task;
|
||||
}
|
||||
mutex_unlock(&ledger.mutex);
|
||||
}
|
||||
@ -69,10 +83,10 @@ const AppLoaderApi* find_loader_api(AppLocationType type) {
|
||||
// If this instance was launched via app_manager_start_for_result(), delivers @a result (its
|
||||
// own AppMainFn/AppLoaderApi::run() return value) to its parent. No-op for a top-level instance
|
||||
// (parent_id == 0).
|
||||
void deliver_result_to_parent_if_any(uint32_t app_instance_id, int32_t result) {
|
||||
void deliver_result_to_parent_if_any(AppInstanceId app_instance_id, int32_t result) {
|
||||
auto& ledger = app_ledger();
|
||||
|
||||
uint32_t parent_id;
|
||||
AppInstanceId parent_id;
|
||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = {} };
|
||||
|
||||
mutex_lock(&ledger.mutex);
|
||||
@ -91,13 +105,20 @@ void deliver_result_to_parent_if_any(uint32_t app_instance_id, int32_t result) {
|
||||
}
|
||||
}
|
||||
|
||||
int32_t thread_main(void* context) {
|
||||
auto* ctx = static_cast<ThreadContext*>(context);
|
||||
void app_task_main(void* context) {
|
||||
auto* ctx = static_cast<TaskContext*>(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);
|
||||
|
||||
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, ctx->argc, ctx->argv);
|
||||
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, nullptr);
|
||||
|
||||
ctx->loader->unload(ctx->runtime);
|
||||
|
||||
deliver_result_to_parent_if_any(ctx->app_instance_id, result);
|
||||
@ -108,15 +129,26 @@ int32_t thread_main(void* context) {
|
||||
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_STOPPED);
|
||||
|
||||
app_ledger_free_arguments(ctx->argc, ctx->argv);
|
||||
|
||||
AppInstanceId app_instance_id = ctx->app_instance_id;
|
||||
delete ctx;
|
||||
return result;
|
||||
|
||||
LOG_I(TAG, "Thread for %d finished", app_instance_id);
|
||||
|
||||
// Erase the ledger entry before self-deleting
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
ledger.instances.erase(app_instance_id);
|
||||
mutex_unlock(&ledger.mutex);
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_scheduler_start(uint32_t app_instance_id, AppLocation location, int argc, char* argv[]) {
|
||||
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, 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));
|
||||
@ -127,54 +159,54 @@ error_t app_scheduler_start(uint32_t app_instance_id, AppLocation location, int
|
||||
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));
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return load_result;
|
||||
}
|
||||
|
||||
auto* context = new (std::nothrow) ThreadContext { loader, runtime, app_instance_id, argc, argv };
|
||||
auto* context = new (std::nothrow) TaskContext { loader, runtime, app_instance_id, argc, argv };
|
||||
if (context == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate app");
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// -1 (no affinity) matches the FreeRTOS POSIX/simulator port; ESP-IDF's tskNO_AFFINITY is
|
||||
// a numerically equivalent SMP-only constant not available in the plain FreeRTOS-Kernel port.
|
||||
Thread* thread = thread_alloc_full("app", 8192, thread_main, context, -1);
|
||||
if (thread == nullptr) {
|
||||
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.
|
||||
BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, APP_TASK_PRIORITY, &task_handle);
|
||||
if (create_result != pdPASS) {
|
||||
delete context;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
set_thread(app_instance_id, thread);
|
||||
|
||||
error_t start_result = thread_start(thread);
|
||||
if (start_result != ERROR_NONE) {
|
||||
set_thread(app_instance_id, nullptr);
|
||||
thread_free(thread);
|
||||
delete context;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return start_result;
|
||||
}
|
||||
set_task(app_instance_id, task_handle);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout) {
|
||||
Thread* thread = get_thread(app_instance_id);
|
||||
if (thread != nullptr) {
|
||||
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout) {
|
||||
TaskHandle_t task = get_task(app_instance_id);
|
||||
if (task != nullptr) {
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(app_instance_id, &event);
|
||||
|
||||
if (thread_join(thread, join_timeout, pdMS_TO_TICKS(10)) != ERROR_NONE) {
|
||||
LOG_W(TAG, "App instance %u did not stop in time", app_instance_id);
|
||||
return ERROR_TIMEOUT;
|
||||
// Poll for the task to clear its own ledger entry (see app_task_main()) - plain
|
||||
// FreeRTOS has no built-in task-join primitive.
|
||||
TickType_t start_ticks = get_ticks();
|
||||
while (get_task(app_instance_id) != nullptr) {
|
||||
delay_ticks(pdMS_TO_TICKS(10));
|
||||
if (get_ticks() - start_ticks > join_timeout) {
|
||||
LOG_W(TAG, "App instance %u did not stop in time", app_instance_id);
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
}
|
||||
thread_free(thread);
|
||||
set_thread(app_instance_id, nullptr);
|
||||
}
|
||||
|
||||
set_state(app_instance_id, APP_INSTANCE_STATE_STOPPED);
|
||||
@ -187,4 +219,9 @@ error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
AppInstanceId app_scheduler_current_app_id(void) {
|
||||
void* value = pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX);
|
||||
return reinterpret_cast<uintptr_t>(value);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
// 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 <tactility/concurrent/mutex.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#define TAG "app_manager"
|
||||
|
||||
@ -197,3 +205,113 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
namespace {
|
||||
|
||||
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
|
||||
// non-owning pointer to (see app_manager_add()'s contract), for manifests registered by
|
||||
// app_manager_install_path_scan() specifically - separate from app_install.cpp's own registry,
|
||||
// since scanning only ever adds/removes manifest registrations and never touches files on disk
|
||||
// or running instances (unlike app_install()/app_uninstall()).
|
||||
struct ScannedAppManifest {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string path;
|
||||
AppManifest manifest {};
|
||||
};
|
||||
|
||||
struct InstallPathRegistry {
|
||||
std::vector<std::string> paths;
|
||||
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
|
||||
Mutex mutex {};
|
||||
|
||||
InstallPathRegistry() { mutex_construct(&mutex); }
|
||||
};
|
||||
|
||||
InstallPathRegistry& install_path_registry() {
|
||||
static InstallPathRegistry registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_manager_install_path_add(const char* path) {
|
||||
auto& registry = install_path_registry();
|
||||
mutex_lock(®istry.mutex);
|
||||
if (std::ranges::find(registry.paths, path) == registry.paths.end()) {
|
||||
registry.paths.emplace_back(path);
|
||||
}
|
||||
mutex_unlock(®istry.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void app_manager_install_path_scan(void) {
|
||||
auto& registry = install_path_registry();
|
||||
|
||||
mutex_lock(®istry.mutex);
|
||||
auto paths_copy = registry.paths;
|
||||
mutex_unlock(®istry.mutex);
|
||||
|
||||
std::vector<std::string> found_app_dirs;
|
||||
for (const auto& root : paths_copy) {
|
||||
app_fs_list_direct_subdirectories(root, found_app_dirs);
|
||||
}
|
||||
|
||||
mutex_lock(®istry.mutex);
|
||||
|
||||
for (const auto& app_dir : found_app_dirs) {
|
||||
auto manifest_path = app_dir + "/manifest.properties";
|
||||
if (!app_fs_is_file(manifest_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AppMetadata metadata {};
|
||||
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (registry.scanned.contains(metadata.app_id)) {
|
||||
continue; // already registered by an earlier scan
|
||||
}
|
||||
|
||||
auto record = std::make_unique<ScannedAppManifest>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = app_dir;
|
||||
record->manifest = AppManifest {
|
||||
.id = record->id.c_str(),
|
||||
.name = record->name.c_str(),
|
||||
.category = APP_CATEGORY_USER,
|
||||
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
if (app_manager_add(&record->manifest) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
registry.scanned[record->id] = std::move(record);
|
||||
}
|
||||
|
||||
// Anything a previous scan registered whose directory has since disappeared (e.g. an SD
|
||||
// card was removed) just gets unregistered - no file deletion, no touching running
|
||||
// instances, that's app_install()/app_uninstall()'s job, not scanning's.
|
||||
std::vector<std::string> missing_ids;
|
||||
for (const auto& [id, record] : registry.scanned) {
|
||||
if (!app_fs_is_directory(record->path)) {
|
||||
missing_ids.push_back(id);
|
||||
}
|
||||
}
|
||||
for (const auto& id : missing_ids) {
|
||||
app_manager_remove(id.c_str());
|
||||
registry.scanned.erase(id);
|
||||
}
|
||||
|
||||
mutex_unlock(®istry.mutex);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
return service_manager_remove(app_internal_loader_service_manifest.id);
|
||||
}
|
||||
|
||||
Module app_module = {
|
||||
.name = "app",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
54
Modules/app-module/source/symbols.cpp
Normal file
54
Modules/app-module/source/symbols.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/module.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
const ModuleSymbol app_module_symbols[] = {
|
||||
// app/scheduler
|
||||
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
|
||||
// app/event
|
||||
DEFINE_MODULE_SYMBOL(app_event_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_emit),
|
||||
DEFINE_MODULE_SYMBOL(app_event_await),
|
||||
// app/manager
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_with_parameters),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_for_result),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_stop),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_finish),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_find_manifest),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
|
||||
// terminator
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
return service_manager_remove(app_internal_loader_service_manifest.id);
|
||||
}
|
||||
|
||||
Module app_module = {
|
||||
.name = "app",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = app_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@ -4,7 +4,7 @@ include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(lvgl-window-manager
|
||||
tactility_add_module(lvgl-window-manager-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel lvgl-module
|
||||
@ -7,12 +7,21 @@
|
||||
|
||||
extern "C" {
|
||||
|
||||
const ModuleSymbol lvgl_window_manager_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(window_manager_create),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_remove),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
|
||||
// terminator
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
Module lvgl_window_manager_module = {
|
||||
.name = "lvgl-window-manager",
|
||||
.start = window_manager_start,
|
||||
.stop = window_manager_stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.symbols = lvgl_window_manager_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
@ -18,8 +18,17 @@ struct WindowRecord {
|
||||
};
|
||||
|
||||
struct WindowManagerState {
|
||||
/** Mutex for read/write operations. Shortly held. */
|
||||
Mutex mutex {};
|
||||
|
||||
/** Serializes the full start()/stop() transition (including the LVGL work done with
|
||||
* `mutex` released) so two concurrent starts can't both pass the `started` check and each
|
||||
* create their own root widget, and a concurrent stop can't run while a start is still
|
||||
* mid-flight. Never held across a create_widgets()/screen_init() callback - those only
|
||||
* reach window_manager_create()/remove(), not start()/stop() - so there's no lock-order
|
||||
* risk with `mutex` or the LVGL lock. */
|
||||
Mutex lifecycle_mutex {};
|
||||
|
||||
bool started = false;
|
||||
WindowManagerScreenInitFn screen_init = nullptr;
|
||||
|
||||
@ -38,7 +47,10 @@ struct WindowManagerState {
|
||||
/** Task blocked in window_manager_await_state_change(), if any. */
|
||||
TaskHandle_t waiting_task = nullptr;
|
||||
|
||||
WindowManagerState() { mutex_construct(&mutex); }
|
||||
WindowManagerState() {
|
||||
mutex_construct(&mutex);
|
||||
mutex_construct(&lifecycle_mutex);
|
||||
}
|
||||
};
|
||||
|
||||
WindowManagerState& state() {
|
||||
@ -86,9 +98,16 @@ void window_manager_configure(WindowManagerScreenInitFn screen_init) {
|
||||
error_t window_manager_start(void) {
|
||||
auto& s = state();
|
||||
|
||||
// Held for the whole transition (including the LVGL work below, done with `mutex`
|
||||
// released) - blocks a concurrent start() from also passing the `started` check and
|
||||
// building its own root widget, and blocks a concurrent stop() from running while this
|
||||
// start is still mid-flight.
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
WindowManagerScreenInitFn screen_init = s.screen_init;
|
||||
@ -114,6 +133,7 @@ error_t window_manager_start(void) {
|
||||
lvgl_unlock();
|
||||
|
||||
if (real_widget == nullptr) {
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
@ -123,15 +143,21 @@ error_t window_manager_start(void) {
|
||||
s.started = true;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t window_manager_stop(void) {
|
||||
auto& s = state();
|
||||
|
||||
// See window_manager_start() - blocks until any in-flight start() has fully completed (or
|
||||
// failed) before this stop can observe/tear down state.
|
||||
mutex_lock(&s.lifecycle_mutex);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
lv_obj_t* widget = s.real_root_widget;
|
||||
@ -151,6 +177,7 @@ error_t window_manager_stop(void) {
|
||||
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
|
||||
delete_widget(widget);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
@ -212,6 +239,7 @@ void window_manager_remove(WindowId id) {
|
||||
void* next_user_data = nullptr;
|
||||
WindowId next_id = 0;
|
||||
bool has_next = false;
|
||||
TaskHandle_t waiter = nullptr;
|
||||
|
||||
if (was_topmost) {
|
||||
old_widget = s.top_widget;
|
||||
@ -222,10 +250,12 @@ void window_manager_remove(WindowId id) {
|
||||
next_id = s.windows.back().id;
|
||||
has_next = true;
|
||||
}
|
||||
// Only the topmost window's state can actually change here - a waiter blocked in
|
||||
// window_manager_await_state_change() is always waiting on the current top window (see
|
||||
// that function), so removing a buried window never affects what it's waiting for.
|
||||
waiter = s.waiting_task;
|
||||
s.waiting_task = nullptr;
|
||||
}
|
||||
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.waiting_task = nullptr;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
@ -273,6 +303,21 @@ WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, timeout);
|
||||
|
||||
/* Deregister ourselves if a create()/remove() hasn't already claimed us (the ordinary, intended wakeup)
|
||||
* Otherwise a later create()/remove() could notify a task that's no longer waiting here:
|
||||
* a use-after-exit on the handle if this task is gone, or a stale wakeup the next time it waits. */
|
||||
mutex_lock(&s.mutex);
|
||||
if (s.waiting_task == xTaskGetCurrentTaskHandle()) {
|
||||
s.waiting_task = nullptr;
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
/* create()/remove() read+clear `waiting_task` under the lock but call xTaskNotifyGive()
|
||||
* after releasing it, so a notification can still land on us right around the timeout
|
||||
* boundary regardless of which branch above ran. Drain it now (non-blocking) so it doesn't
|
||||
* linger and cause a spurious immediate return the next time this task awaits. */
|
||||
ulTaskNotifyTake(pdTRUE, 0);
|
||||
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -143,7 +144,11 @@ struct SystemEventSubscription {
|
||||
/** Event type to subscribe to; set by the caller before system_event_subscribe(). */
|
||||
enum SystemEventType type;
|
||||
|
||||
TaskHandle_t task;
|
||||
/** Own wakeup signal, not the subscribing task's shared default notification value - a
|
||||
* task with more than one poll subscription would otherwise have events for one
|
||||
* subscription wake (and consume the notification meant for) system_event_await() calls
|
||||
* on another. */
|
||||
SemaphoreHandle_t semaphore;
|
||||
|
||||
uint64_t timestamp;
|
||||
uint8_t data[SYSTEM_EVENT_MAX_DATA_SIZE];
|
||||
@ -160,7 +165,10 @@ struct SystemEventSubscription {
|
||||
* @warning Does not work in ISR context.
|
||||
* @param[in,out] sub subscription to register; caller sets @a sub->type beforehand, owns the
|
||||
* storage, and must keep it alive (and stationary) until unsubscribed
|
||||
* @return ERROR_NONE on success
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_OUT_OF_MEMORY failed to allocate the subscription's wakeup semaphore; @a sub
|
||||
* was not registered
|
||||
* @retval ERROR_INVALID_STATE @a sub is already registered
|
||||
*/
|
||||
error_t system_event_subscribe(struct SystemEventSubscription* sub);
|
||||
|
||||
|
||||
@ -67,9 +67,9 @@ error_t system_event_callback_remove(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Copies `data` into every current poll subscriber of `type` and wakes its waiting task.
|
||||
// Copies `data` into every current poll subscriber of `type` and signals its wakeup semaphore.
|
||||
// Held entirely under the lock: unlike the callback path, this never invokes caller code
|
||||
// (just a memcpy and an xTaskNotifyGive), so there is nothing that could reenter and deadlock.
|
||||
// (just a memcpy and a semaphore give), so there is nothing that could reenter and deadlock.
|
||||
static void notify_poll_subscribers(
|
||||
SystemEventType type,
|
||||
uint64_t timestamp,
|
||||
@ -81,12 +81,13 @@ static void notify_poll_subscribers(
|
||||
for (SystemEventSubscription* sub = poll_subscriptions; sub != nullptr; sub = sub->next) {
|
||||
if (sub->type == type) {
|
||||
sub->timestamp = timestamp;
|
||||
if (data_len > 0) {
|
||||
std::memcpy(sub->data, data, std::min(data_len, static_cast<size_t>(SYSTEM_EVENT_MAX_DATA_SIZE)));
|
||||
const size_t copied_len = std::min(data_len, SYSTEM_EVENT_MAX_DATA_SIZE);
|
||||
if (copied_len > 0) {
|
||||
std::memcpy(sub->data, data, copied_len);
|
||||
}
|
||||
sub->data_len = data_len;
|
||||
sub->data_len = copied_len;
|
||||
sub->sequence++;
|
||||
xTaskNotifyGive(sub->task);
|
||||
xSemaphoreGive(sub->semaphore);
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,14 +163,31 @@ error_t system_event_emit(
|
||||
}
|
||||
|
||||
error_t system_event_subscribe(SystemEventSubscription* sub) {
|
||||
sub->task = xTaskGetCurrentTaskHandle();
|
||||
SemaphoreHandle_t semaphore = xSemaphoreCreateBinary();
|
||||
if (semaphore == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
mutex_lock(&poll_subscriptions_mutex.handle);
|
||||
|
||||
// Check-and-insert in one critical section: registering the same `sub` twice would link
|
||||
// it into a list that already contains it, creating a cycle that notify_poll_subscribers()
|
||||
// would then traverse forever while holding this same mutex.
|
||||
for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->next) {
|
||||
if (existing == sub) {
|
||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||
vSemaphoreDelete(semaphore);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
}
|
||||
|
||||
sub->semaphore = semaphore;
|
||||
sub->sequence = 0;
|
||||
sub->consumed_sequence = 0;
|
||||
sub->data_len = 0;
|
||||
|
||||
mutex_lock(&poll_subscriptions_mutex.handle);
|
||||
sub->next = poll_subscriptions;
|
||||
poll_subscriptions = sub;
|
||||
|
||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||
|
||||
return ERROR_NONE;
|
||||
@ -188,6 +206,13 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) {
|
||||
}
|
||||
mutex_unlock(&poll_subscriptions_mutex.handle);
|
||||
|
||||
if (result == ERROR_NONE) {
|
||||
// Unlinked first, so notify_poll_subscribers() can no longer reach this semaphore
|
||||
// before it's deleted.
|
||||
vSemaphoreDelete(sub->semaphore);
|
||||
sub->semaphore = nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -195,7 +220,7 @@ error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) {
|
||||
uint32_t old_sequence = sub->sequence;
|
||||
|
||||
while (sub->sequence == old_sequence) {
|
||||
if (ulTaskNotifyTake(pdTRUE, timeout) == 0) {
|
||||
if (xSemaphoreTake(sub->semaphore, timeout) == pdFALSE) {
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ add_subdirectory(TactilityFreeRtos)
|
||||
add_subdirectory(TactilityKernel)
|
||||
add_subdirectory(Tactility)
|
||||
add_subdirectory(crypt-module)
|
||||
add_subdirectory(app-module)
|
||||
|
||||
add_custom_target(build-tests)
|
||||
add_dependencies(build-tests ServiceModuleTests)
|
||||
@ -15,3 +16,4 @@ add_dependencies(build-tests TactilityFreeRtosTests)
|
||||
add_dependencies(build-tests TactilityTests)
|
||||
add_dependencies(build-tests TactilityKernelTests)
|
||||
add_dependencies(build-tests CryptModuleTests)
|
||||
add_dependencies(build-tests AppModuleTests)
|
||||
|
||||
@ -15,6 +15,8 @@ target_link_libraries(TactilityTests PRIVATE
|
||||
TactilityKernel
|
||||
platform-posix
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
gps-generic-module
|
||||
|
||||
19
Tests/app-module/CMakeLists.txt
Normal file
19
Tests/app-module/CMakeLists.txt
Normal file
@ -0,0 +1,19 @@
|
||||
project(AppModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES ${PROJECT_SOURCE_DIR}/Source/*.cpp)
|
||||
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
|
||||
target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC})
|
||||
|
||||
add_test(NAME AppModuleTests COMMAND AppModuleTests)
|
||||
|
||||
target_link_libraries(AppModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
app-module
|
||||
service-module
|
||||
platform-posix
|
||||
freertos_kernel
|
||||
)
|
||||
137
Tests/app-module/Source/AppEventTest.cpp
Normal file
137
Tests/app-module/Source/AppEventTest.cpp
Normal file
@ -0,0 +1,137 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
TEST_CASE("app_event_subscribe/_await deliver events in FIFO order") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 1;
|
||||
CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
for (uint32_t i = 0; i < 3; i++) {
|
||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
||||
CHECK_EQ(app_event_emit(1, &event), ERROR_NONE);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < 3; i++) {
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
||||
CHECK_EQ(out.type, APP_EVENT_RESULT);
|
||||
CHECK_EQ(out.result.launch_id, i);
|
||||
}
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_emit only delivers to subscriptions for that app_instance_id") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 10;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
CHECK_EQ(app_event_emit(11, &event), ERROR_NOT_FOUND);
|
||||
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once a subscription's queue is full") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 20;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
||||
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } };
|
||||
CHECK_EQ(app_event_emit(20, &event), ERROR_NONE);
|
||||
}
|
||||
|
||||
// Queue is now full; this one should be dropped.
|
||||
AppEvent overflow_event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = 999, .result = 0 } };
|
||||
CHECK_EQ(app_event_emit(20, &overflow_event), ERROR_RESOURCE);
|
||||
|
||||
// The already-queued events survive, in order, and the dropped one never arrives.
|
||||
for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) {
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
||||
CHECK_EQ(out.result.launch_id, i);
|
||||
}
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_unsubscribe stops further delivery") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 30;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NONE);
|
||||
CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NOT_FOUND);
|
||||
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
CHECK_EQ(app_event_emit(30, &event), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_await times out when no event has arrived") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 40;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_emit stamps the event with the current boot-relative time") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 50;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
auto before = static_cast<uint64_t>(get_micros_since_boot());
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(50, &event);
|
||||
auto after = static_cast<uint64_t>(get_micros_since_boot());
|
||||
|
||||
AppEvent out {};
|
||||
REQUIRE_EQ(app_event_await(&sub, &out, 0), ERROR_NONE);
|
||||
CHECK_GE(out.timestamp, before);
|
||||
CHECK_LE(out.timestamp, after);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
|
||||
TEST_CASE("app_event_await wakes when the event is emitted from another task") {
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = 60;
|
||||
CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE);
|
||||
|
||||
auto* thread = thread_alloc_full(
|
||||
"app-event-emitter",
|
||||
4096,
|
||||
[](void*) -> int32_t {
|
||||
delay_millis(20);
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(60, &event);
|
||||
return 0;
|
||||
},
|
||||
nullptr,
|
||||
-1
|
||||
);
|
||||
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||
|
||||
AppEvent out {};
|
||||
CHECK_EQ(app_event_await(&sub, &out, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(out.type, APP_EVENT_CLOSE);
|
||||
|
||||
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), 1), ERROR_NONE);
|
||||
thread_free(thread);
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
}
|
||||
431
Tests/app-module/Source/AppManagerTest.cpp
Normal file
431
Tests/app-module/Source/AppManagerTest.cpp
Normal file
@ -0,0 +1,431 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/manager.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
error_t fake_load(AppLocation, void** out_runtime) {
|
||||
*out_runtime = nullptr;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// Stashed by fake_run() on every call, for tests that need to verify exactly what argc/argv it
|
||||
// received (e.g. that app-module deep-copied the caller's argv) without a getter to query it
|
||||
// through.
|
||||
int last_received_argc = -1;
|
||||
std::vector<std::string> last_received_argv;
|
||||
|
||||
void stash_received_arguments(int argc, char* argv[]) {
|
||||
last_received_argc = argc;
|
||||
last_received_argv.clear();
|
||||
for (int i = 0; i < argc; i++) {
|
||||
last_received_argv.emplace_back(argv[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// A minimal stand-in for a real app's main(): subscribes to its own app_event stream and exits
|
||||
// as soon as it's asked to close - exactly the contract every app instance (with its own
|
||||
// dedicated task for its whole lifetime) is expected to follow. If launched with a single
|
||||
// parameter (app_manager_start_for_result()), acts as a modal dialog instead: returns the
|
||||
// requested result (argv[0], parsed as an int) immediately (the app's own return value IS the
|
||||
// delivered APP_EVENT_RESULT.result - see app_scheduler.cpp's thread_main()).
|
||||
int32_t fake_run(void*, uint32_t app_instance_id, int argc, char* argv[]) {
|
||||
stash_received_arguments(argc, argv);
|
||||
|
||||
if (argc == 1) {
|
||||
// Single-arg shortcut used by the start_for_result() result-delivery tests: returns
|
||||
// immediately with argv[0] parsed as the result code, instead of running the normal
|
||||
// event loop below. Tests that pass other argc (0, or >1 to check deep-copy) fall
|
||||
// through and run the loop as usual.
|
||||
return static_cast<int32_t>(strtol(argv[0], nullptr, 10));
|
||||
}
|
||||
|
||||
AppEventSubscription sub {};
|
||||
sub.app_instance_id = app_instance_id;
|
||||
app_event_subscribe(&sub);
|
||||
|
||||
while (true) {
|
||||
AppEvent event {};
|
||||
if (app_event_await(&sub, &event, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
|
||||
break; // safety net so a bug here can't hang the test suite
|
||||
}
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
app_manager_finish(app_instance_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
app_event_unsubscribe(&sub);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void fake_unload(void*) {
|
||||
}
|
||||
|
||||
AppLoaderApi fake_loader_api = {
|
||||
.load = fake_load,
|
||||
.run = fake_run,
|
||||
.unload = fake_unload,
|
||||
};
|
||||
|
||||
void* create_loader_service(const ServiceManifest*) {
|
||||
return &fake_loader_api;
|
||||
}
|
||||
|
||||
void destroy_loader_service(const ServiceManifest*, void*) {
|
||||
}
|
||||
|
||||
ServiceManifest fake_loader_manifest = {
|
||||
.id = APP_LOADER_PATH_SERVICE_ID,
|
||||
.create_service = create_loader_service,
|
||||
.destroy_service = destroy_loader_service,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
};
|
||||
|
||||
void ensure_fake_loader_registered() {
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
CHECK_EQ(service_manager_add(&fake_loader_manifest, /*auto_start=*/true), ERROR_NONE);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// app-module's real APP_LOCATION_MEMORY loader (source/app_internal_loader.cpp) - not a fake,
|
||||
// since it has no platform dependency and is exactly what a statically-linked app would go
|
||||
// through.
|
||||
void ensure_memory_loader_registered() {
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
CHECK_EQ(service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true), ERROR_NONE);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Same subscribe-until-close contract as fake_run() above, but called directly as an AppMainFn -
|
||||
// this is what a real internal app's entry point looks like.
|
||||
int32_t fake_app_main(uint32_t app_instance_id, int argc, char* argv[]) {
|
||||
return fake_run(nullptr, app_instance_id, argc, argv);
|
||||
}
|
||||
|
||||
// Wraps app_manager_get_topmost_instance_id() for terse assertions: 0 if no app is Active.
|
||||
AppInstanceId topmost_instance_id() {
|
||||
AppInstanceId id = 0;
|
||||
return app_manager_get_topmost_instance_id(&id) == ERROR_NONE ? id : 0;
|
||||
}
|
||||
|
||||
bool wait_for_state(uint32_t instance_id, AppInstanceState target, uint32_t timeout_ms) {
|
||||
uint32_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
if (app_manager_get_state(instance_id) == target) {
|
||||
return true;
|
||||
}
|
||||
delay_millis(10);
|
||||
waited += 10;
|
||||
}
|
||||
return app_manager_get_state(instance_id) == target;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("app_manager_start activates an app instance, app_manager_stop terminates it") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.a", "Test App A", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.a", &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.a");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start never touches another already-running app - every instance gets its own task") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest_b { "test.app.b", "Test App B", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest manifest_c { "test.app.c", "Test App C", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest_b), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&manifest_c), ERROR_NONE);
|
||||
|
||||
uint32_t id_b = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.b", &id_b), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
uint32_t id_c = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.c", &id_c), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
// b is untouched by c starting - both stay Active at once, each with its own task.
|
||||
CHECK_EQ(app_manager_get_state(id_b), APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
app_manager_stop(id_b);
|
||||
app_manager_stop(id_c);
|
||||
app_manager_remove("test.app.b");
|
||||
app_manager_remove("test.app.c");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start always creates a fresh instance, even for the same manifest id twice") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.twice", "Test App Twice", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t id_first = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.twice", &id_first), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_first, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
uint32_t id_second = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.twice", &id_second), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_second, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
CHECK_NE(id_first, id_second);
|
||||
CHECK_EQ(app_manager_get_state(id_first), APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
app_manager_stop(id_first);
|
||||
app_manager_stop(id_second);
|
||||
app_manager_remove("test.app.twice");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_get_state returns STOPPED for an unknown instance id") {
|
||||
CHECK_EQ(app_manager_get_state(999999), APP_INSTANCE_STATE_STOPPED);
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start_with_parameters deep-copies argv before the app instance receives it") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest manifest { "test.app.args", "Test App Args", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
{
|
||||
// Caller's argv is stack-local and goes out of scope immediately after this block -
|
||||
// proves app-module made its own copy rather than aliasing the caller's strings.
|
||||
std::string ssid = "MyNetwork";
|
||||
std::string password = "hunter2";
|
||||
const char* argv[] = { ssid.c_str(), password.c_str() };
|
||||
REQUIRE_EQ(app_manager_start_with_parameters("test.app.args", 2, argv, &instance_id), ERROR_NONE);
|
||||
}
|
||||
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
REQUIRE_EQ(last_received_argc, 2);
|
||||
REQUIRE_EQ(last_received_argv.size(), 2u);
|
||||
CHECK_EQ(last_received_argv[0], "MyNetwork");
|
||||
CHECK_EQ(last_received_argv[1], "hunter2");
|
||||
|
||||
app_manager_stop(instance_id);
|
||||
app_manager_remove("test.app.args");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_add rejects a duplicate id") {
|
||||
AppManifest manifest { "test.app.dup", "Test App Dup", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
CHECK_EQ(app_manager_add(&manifest), ERROR_INVALID_ARGUMENT);
|
||||
app_manager_remove("test.app.dup");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_for_each_manifest visits every registered manifest, including newly added ones") {
|
||||
AppManifest manifest_x { "test.app.foreach.x", "X", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest manifest_y { "test.app.foreach.y", "Y", APP_CATEGORY_SETTINGS, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest_x), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&manifest_y), ERROR_NONE);
|
||||
|
||||
std::vector<std::string> seen_ids;
|
||||
app_manager_for_each_manifest([](const AppManifest* manifest, void* context) {
|
||||
static_cast<std::vector<std::string>*>(context)->emplace_back(manifest->id);
|
||||
}, &seen_ids);
|
||||
|
||||
CHECK(std::ranges::find(seen_ids, "test.app.foreach.x") != seen_ids.end());
|
||||
CHECK(std::ranges::find(seen_ids, "test.app.foreach.y") != seen_ids.end());
|
||||
|
||||
app_manager_remove("test.app.foreach.x");
|
||||
app_manager_remove("test.app.foreach.y");
|
||||
|
||||
seen_ids.clear();
|
||||
app_manager_for_each_manifest([](const AppManifest* manifest, void* context) {
|
||||
static_cast<std::vector<std::string>*>(context)->emplace_back(manifest->id);
|
||||
}, &seen_ids);
|
||||
CHECK(std::ranges::find(seen_ids, "test.app.foreach.x") == seen_ids.end());
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start fails for an unregistered manifest id") {
|
||||
uint32_t instance_id = 0;
|
||||
CHECK_EQ(app_manager_start("test.app.nonexistent", &instance_id), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start runs an APP_LOCATION_MEMORY app via its function pointer, through the real internal loader") {
|
||||
ensure_memory_loader_registered();
|
||||
|
||||
AppManifest manifest {
|
||||
"test.app.memory",
|
||||
"Test App Memory",
|
||||
APP_CATEGORY_USER,
|
||||
{ APP_LOCATION_MEMORY, reinterpret_cast<void*>(fake_app_main) }
|
||||
};
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t instance_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.memory", &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.memory");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, which stays Active throughout") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest parent_manifest { "test.app.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest child_manifest { "test.app.child", "Child", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&child_manifest), ERROR_NONE);
|
||||
|
||||
uint32_t parent_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.parent", &parent_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
AppEventSubscription parent_sub {};
|
||||
parent_sub.app_instance_id = parent_id;
|
||||
REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE);
|
||||
|
||||
const char* argv[] = { "42" };
|
||||
uint32_t child_id = 0;
|
||||
REQUIRE_EQ(app_manager_start_for_result("test.app.child", parent_id, 1, argv, &child_id), ERROR_NONE);
|
||||
|
||||
// Launching a modal child never touches the parent's own task/state.
|
||||
CHECK_EQ(app_manager_get_state(parent_id), APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
AppEvent event {};
|
||||
REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||
CHECK_EQ(event.result.launch_id, child_id);
|
||||
CHECK_EQ(event.result.result, 42);
|
||||
|
||||
app_event_unsubscribe(&parent_sub);
|
||||
app_manager_stop(child_id);
|
||||
app_manager_stop(parent_id);
|
||||
app_manager_remove("test.app.parent");
|
||||
app_manager_remove("test.app.child");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_start_for_result delivers the child's own return value as the result") {
|
||||
ensure_fake_loader_registered();
|
||||
|
||||
AppManifest parent_manifest { "test.app.parent2", "Parent2", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest child_manifest { "test.app.child2", "Child2", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&child_manifest), ERROR_NONE);
|
||||
|
||||
uint32_t parent_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.parent2", &parent_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
AppEventSubscription parent_sub {};
|
||||
parent_sub.app_instance_id = parent_id;
|
||||
REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE);
|
||||
|
||||
uint32_t child_id = 0;
|
||||
// No parameters - fake_run falls through to its normal CLOSE loop instead of acting as a
|
||||
// dialog.
|
||||
REQUIRE_EQ(app_manager_start_for_result("test.app.child2", parent_id, 0, nullptr, &child_id), ERROR_NONE);
|
||||
CHECK(wait_for_state(child_id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
app_manager_stop(child_id); // force-close
|
||||
|
||||
AppEvent event {};
|
||||
REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE);
|
||||
CHECK_EQ(event.type, APP_EVENT_RESULT);
|
||||
CHECK_EQ(event.result.launch_id, child_id);
|
||||
CHECK_EQ(event.result.result, 0); // fake_run's CLOSE loop always returns 0
|
||||
|
||||
app_event_unsubscribe(&parent_sub);
|
||||
app_manager_stop(parent_id);
|
||||
app_manager_remove("test.app.parent2");
|
||||
app_manager_remove("test.app.child2");
|
||||
}
|
||||
|
||||
TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is active, then tracks who's on top") {
|
||||
ensure_fake_loader_registered();
|
||||
AppInstanceId id = 999999;
|
||||
CHECK_EQ(app_manager_get_topmost_instance_id(&id), ERROR_NOT_FOUND);
|
||||
|
||||
AppManifest manifest_a { "test.app.top_a", "A", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
AppManifest manifest_b { "test.app.top_b", "B", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest_a), ERROR_NONE);
|
||||
REQUIRE_EQ(app_manager_add(&manifest_b), ERROR_NONE);
|
||||
|
||||
uint32_t id_a = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.top_a", &id_a), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_a, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
CHECK_EQ(topmost_instance_id(), id_a);
|
||||
|
||||
// a stays Active - b just has a higher (more recently allocated) instance id, so it becomes
|
||||
// topmost without a superseding/saving.
|
||||
uint32_t id_b = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.top_b", &id_b), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
CHECK_EQ(topmost_instance_id(), id_b);
|
||||
|
||||
char app_id_buffer[64];
|
||||
REQUIRE_EQ(app_manager_get_topmost_app_id(app_id_buffer, sizeof(app_id_buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::string(app_id_buffer), "test.app.top_b");
|
||||
|
||||
// A modal child stays Active alongside its parent while shown - the child (started more
|
||||
// recently) must be reported as topmost, not the parent. No parameters, so fake_run() takes
|
||||
// its persistent CLOSE loop branch instead of instantly resolving like a real dialog would -
|
||||
// needed here so there's a reliable window to observe it as topmost.
|
||||
uint32_t id_c = 0;
|
||||
REQUIRE_EQ(app_manager_start_for_result("test.app.top_a", id_b, 0, nullptr, &id_c), ERROR_NONE);
|
||||
CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
CHECK_EQ(topmost_instance_id(), id_c);
|
||||
|
||||
app_manager_stop(id_c);
|
||||
CHECK_EQ(topmost_instance_id(), id_b);
|
||||
|
||||
app_manager_stop(id_a);
|
||||
app_manager_stop(id_b);
|
||||
app_manager_remove("test.app.top_a");
|
||||
app_manager_remove("test.app.top_b");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
char buffer[4];
|
||||
CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_NOT_FOUND);
|
||||
CHECK_EQ(app_manager_get_topmost_app_id(buffer, 0), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
AppManifest manifest { "test.app.top_overflow", "Overflow", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
|
||||
uint32_t id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.app.top_overflow", &id), ERROR_NONE);
|
||||
CHECK(wait_for_state(id, APP_INSTANCE_STATE_ACTIVE, 1000));
|
||||
|
||||
// "test.app.top_overflow" doesn't fit in a 4-byte buffer.
|
||||
CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
|
||||
|
||||
app_manager_stop(id);
|
||||
app_manager_remove("test.app.top_overflow");
|
||||
}
|
||||
51
Tests/app-module/Source/Main.cpp
Normal file
51
Tests/app-module/Source/Main.cpp
Normal file
@ -0,0 +1,51 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT
|
||||
#include "doctest.h"
|
||||
#include <cassert>
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char** argv;
|
||||
int result;
|
||||
} TestTaskData;
|
||||
|
||||
void test_task(void* parameter) {
|
||||
auto* data = (TestTaskData*)parameter;
|
||||
|
||||
doctest::Context context;
|
||||
|
||||
context.applyCommandLine(data->argc, data->argv);
|
||||
|
||||
// overrides
|
||||
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
|
||||
|
||||
data->result = context.run();
|
||||
|
||||
vTaskEndScheduler();
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.result = 0
|
||||
};
|
||||
|
||||
BaseType_t task_result = xTaskCreate(
|
||||
test_task,
|
||||
"test_task",
|
||||
8192,
|
||||
&data,
|
||||
1,
|
||||
nullptr
|
||||
);
|
||||
assert(task_result == pdPASS);
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
return data.result;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user