diff --git a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp index 4f96855fd..e8d4e7093 100644 --- a/Modules/app-esp32-module/source/app_esp32_loader_service.cpp +++ b/Modules/app-esp32-module/source/app_esp32_loader_service.cpp @@ -3,6 +3,7 @@ #include #endif +#include #include #include @@ -72,6 +73,27 @@ std::string resolve_elf_path(const std::string& path) { return path + "/elf/" + CONFIG_IDF_TARGET + ".elf"; } +constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = { + .elf_class = ELF_CLASS_32, + .data = ELF_DATA_2LSB, + .type = ELF_TYPE_DYN, +#if defined(__XTENSA__) + .machine = ELF_MACHINE_XTENSA, +#elif defined(__riscv) + .machine = ELF_MACHINE_RISCV, +#else +#error "Unsupported ESP32 architecture for ELF machine check" +#endif +}; + +// Validates an already-resolved binary path (see resolve_elf_path()) before it's handed to +// esp_elf_relocate(), which performs no header validation of its own: the extension check is a +// cheap string comparison, so the file is only opened as a last resort. +bool is_executable_file(const std::string& resolved_path) { + return resolved_path.ends_with(".elf") + && elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS); +} + error_t api_load(AppLocation location, AppRuntime* out_runtime) { if (location.type != APP_LOCATION_PATH) { LOG_E(TAG, "Out of memory"); @@ -88,6 +110,12 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { auto elf_path = resolve_elf_path(static_cast(location.location)); + if (!is_executable_file(elf_path)) { + LOG_E(TAG, "Not executable: %s", elf_path.c_str()); + delete runtime; + return ERROR_NOT_ALLOWED; + } + size_t size = 0; error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size); if (read_result != ERROR_NONE) { @@ -127,10 +155,20 @@ void api_unload(AppRuntime runtime_ptr) { delete runtime; } +bool api_is_executable(AppLocation location) { + if (location.type != APP_LOCATION_PATH) { + return false; + } + + auto elf_path = resolve_elf_path(static_cast(location.location)); + return is_executable_file(elf_path); +} + AppLoaderApi loader_api = { .load = api_load, .run = api_run, .unload = api_unload, + .is_executable = api_is_executable, }; void* create_service(const ServiceManifest*) { diff --git a/Modules/app-module/include/app/elf_check.h b/Modules/app-module/include/app/elf_check.h new file mode 100644 index 000000000..1c23ba0cc --- /dev/null +++ b/Modules/app-module/include/app/elf_check.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// is not guaranteed to exist under the ESP32 newlib toolchain +#define ELF_CLASS_32 1 +#define ELF_CLASS_64 2 +#define ELF_DATA_2LSB 1 +#define ELF_TYPE_DYN 3 +#define ELF_MACHINE_XTENSA 94 +#define ELF_MACHINE_RISCV 243 +#define ELF_MACHINE_X86_64 62 +#define ELF_MACHINE_AARCH64 183 + +/** What a loader needs an ELF file's header to say before it will try to load it. */ +struct ElfRequirements { + uint8_t elf_class; /**< ELF_CLASS_32 / ELF_CLASS_64 */ + uint8_t data; /**< ELF_DATA_2LSB */ + uint16_t type; /**< ELF_TYPE_DYN */ + uint16_t machine; /**< ELF_MACHINE_XTENSA / _RISCV / _X86_64 / _AARCH64 */ +}; + +/** + * Reads the first 20 bytes of @a path (e_ident, e_type, e_machine; identical offsets for + * ELF32 and ELF64) and checks the magic number and every field in @a requirements match. + * @return false if @a path can't be opened, is too short, or doesn't match + */ +bool elf_check_file(const char* path, const struct ElfRequirements* requirements); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/include/app/event.h b/Modules/app-module/include/app/event.h index e8dc3bb16..2cd793a2b 100644 --- a/Modules/app-module/include/app/event.h +++ b/Modules/app-module/include/app/event.h @@ -24,8 +24,8 @@ struct AppResultEventData { uint32_t launch_id; /** The child app instance's own AppMainFn/AppLoaderApi::run() return value. By convention: * 0 = Ok, 1 = Cancelled, 2 = Error. Apps that need to hand back more than this (e.g. picked - * text, a path) expose their own "get last result" getter instead - see e.g. - * tt::app::inputdialog::getLastText(). */ + * text, a path) write it to their own stdout instead, for the caller to read via an + * AppStream bound to it. See e.g. tt::app::inputdialog::start(). */ int32_t result; }; diff --git a/Modules/app-module/include/app/execute.h b/Modules/app-module/include/app/execute.h new file mode 100644 index 000000000..5bcf51be0 --- /dev/null +++ b/Modules/app-module/include/app/execute.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +/** + * This file contains functions to start and run apps. + * It differs from start.h by running apps directly from the specified location, + * instead of having to register them first via an AppManifest and the app manager. + */ + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Starts an app directly from @a location, without it having to be pre-registered via + * app_manager_add() first. + * Performs no checks of its own beyond what AppLoaderApi::load() itself rejects. + * @warning It's advised to validate @a location with app_is_executable() first + * @param[in] stack stack allocation config for the app's task; all-zero uses the scheduler's + * default depth/capability, same as an AppManifest that leaves AppManifest::stack zeroed + * @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type + * @retval ERROR_NONE on success + */ +error_t app_execute( + struct AppLocation location, + struct AppStackConfig stack, + int argc, + const char* const argv[], + AppInstanceId* out_app_instance_id +); + +/** + * Same as app_execute(), but as a modal child of @a parent_instance_id. + * See app_start_for_result()'s own doc for the result-delivery contract. + * @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type + * @retval ERROR_NONE on success + */ +error_t app_execute_for_result( + struct AppLocation location, + struct AppStackConfig stack, + int argc, + const char* const argv[], + AppInstanceId parent_instance_id, + AppInstanceId* out_app_instance_id +); + +/** + * Same as app_execute(), but installs @a bindings into the new instance's fd table before its + * task begins executing. See app_start_with_streams()'s own doc for stream ownership. + * @param[in] bindings see app_start_with_streams() + * @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type + * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range + * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim + * @retval ERROR_NONE on success + */ +error_t app_execute_with_streams( + struct AppLocation location, + struct AppStackConfig stack, + int argc, + const char* const argv[], + const struct AppStreamBinding* bindings, + size_t binding_count, + AppInstanceId* out_app_instance_id +); + +/** + * Combines app_execute_for_result() and app_execute_with_streams(). + * @param[in] bindings see app_start_with_streams() + * @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type + * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range + * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim + * @retval ERROR_NONE on success + */ +error_t app_execute_for_result_with_streams( + struct AppLocation location, + struct AppStackConfig stack, + int argc, + const char* const argv[], + const struct AppStreamBinding* bindings, + size_t binding_count, + AppInstanceId parent_instance_id, + AppInstanceId* out_app_instance_id +); + +/** + * Reports whether @a location is runnable on this target: the extension and header a loader + * requires (e.g. an ELF's class/data/type/machine), not whether it lives anywhere in particular. + * Any executable app is runnable from any path. Cheap enough to call while listing a directory. + * @return false if @a location can't be run, or if no AppLoaderApi is registered for its type + */ +bool app_is_executable(struct AppLocation location); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/include/app/loader.h b/Modules/app-module/include/app/loader.h index db2a12704..ec728fa03 100644 --- a/Modules/app-module/include/app/loader.h +++ b/Modules/app-module/include/app/loader.h @@ -3,6 +3,7 @@ #include #include +#include #include #include "location.h" @@ -18,15 +19,8 @@ extern "C" { * APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */ #define APP_LOADER_PATH_SERVICE_ID "app-loader-path" -/** - * Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this - * firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance, - * blocking for the app's whole lifetime - same contract as an external app's main(). Use - * app_scheduler_current_app_id() to identify this running instance (e.g. with - * app_event_subscribe()/window_manager_create()/etc.). The instance closes when this function - * returns - no separate call is needed. - * AppManifest::location.location holds this cast to void*. - */ +/** Entry point signature for an APP_LOCATION_MEMORY app. + * AppManifest::location.location holds this cast to void*. */ typedef int32_t (*AppMainFn)(int argc, char* argv[]); typedef void* AppRuntime; @@ -53,6 +47,11 @@ struct AppLoaderApi { /** Releases whatever load() allocated. Called after run() returns. */ void (*unload)(AppRuntime runtime); + + /** + * Reports whether this loader could load and run whatever @a location points at, without actually loading it. + */ + bool (*is_executable)(struct AppLocation location); }; #ifdef __cplusplus diff --git a/Modules/app-module/include/app/manager.h b/Modules/app-module/include/app/manager.h index 002629290..3b92bb162 100644 --- a/Modules/app-module/include/app/manager.h +++ b/Modules/app-module/include/app/manager.h @@ -44,51 +44,8 @@ error_t app_manager_find_manifest(const char* id, struct AppManifest* out_manife typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context); void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context); -/** - * Starts a new instance of the app registered under @a id. Every app instance gets its own - * dedicated task for its entire lifetime - starting an app never asks any other app to give up - * its task, and multiple instances (of the same or different apps) can be Active at once. - * @param[in] id the manifest id to start - * @param[out] out_app_instance_id the id of the new app instance - * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered - * @retval ERROR_NONE on success - */ -error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id); - -/** - * Same as app_manager_start(), but also passes @a argc/@a argv to the new instance's own main - * function (see app/loader.h's AppMainFn) - modelled on a C program's main(argc, argv). For - * regular (non-modal) navigations that need to pass data to the target app (e.g. "show details - * for this app id") without expecting a result back. - * @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so - * @a argv and the strings it points to may be freed/go out of scope immediately after this call - * returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s). - */ -error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id); - -/** - * Starts @a id as a modal child of @a parent_instance_id, for the purpose of receiving a - * result. The parent keeps running (window_manager's own multi-window stack handles burying its - * window while the child is shown). - * - * When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id - - * result is whatever the child's AppMainFn/AppLoaderApi::run() returned - unless - * @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for - * callers with no app_instance_id of their own). The parent is then responsible for calling - * app_manager_stop() on the child's instance id to fully reap it. Children that need to hand - * back more than an int32_t (e.g. picked text, a path) expose their own "get last result" - * getter for the parent to call after receiving the event - see e.g. - * tt::app::inputdialog::getLastText(). - * @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as - * app_manager_start_with_parameters()), so @a argv and the strings it points to may be - * freed/go out of scope immediately after this call returns. - * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered - * @retval ERROR_NONE on success - */ -error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id); - -/** One fd-to-stream binding for app_manager_start_with_streams(). Every field is passed through - * to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */ +/** One fd-to-stream binding for app_start_with_streams() (app/start.h). Every field is passed + * through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */ struct AppStreamBinding { int producer_fd; struct AppStream* stream; @@ -97,37 +54,6 @@ struct AppStreamBinding { struct TaskEventGroup* event_group; }; -/** - * Same as app_manager_start(), but installs @a bindings into the new instance's fd table before - * its task begins executing (e.g. a child's stdio, piped through parent-owned AppStreams; see - * app/stream.h). Writes the new instance's id into each bound stream's producer_id itself, since - * the caller cannot know it in advance. - * @param[in] bindings @a binding_count entries; each stream and buffer must stay alive (see - * app_stream_subscribe()) until unsubscribed or the child exits. - * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered - * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range - * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim - * @retval ERROR_NONE on success - */ -error_t app_manager_start_with_streams(const char* id, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id); - -/** - * Combines app_manager_start_for_result() and app_manager_start_with_streams(): starts @a id as - * a modal child of @a parent_instance_id (see app_manager_start_for_result()'s own doc for the - * result-delivery contract) with @a bindings installed into its fd table before its task begins - * executing (see app_manager_start_with_streams()'s own doc for stream ownership). For a child - * that needs to hand back more than an int32_t (e.g. a path) via its own stdout instead of the - * "get last result" getter pattern (see app_manager_start_for_result()) - see e.g. - * tt::app::fileselection::startForExistingFile(). - * @param[in] argv see app_manager_start_for_result(). - * @param[in] bindings see app_manager_start_with_streams(). - * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered - * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range - * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim - * @retval ERROR_NONE on success - */ -error_t app_manager_start_for_result_with_streams(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id); - /** * Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit * if it was running. @@ -143,7 +69,7 @@ AppInstanceState app_manager_get_state(AppInstanceId app_instance_id); /** * @param[out] out_app_instance_id set to the instance id of the topmost currently-Active app - * the most recently started of whichever instances are Active (a modal child launched via - * app_manager_start_for_result() stays Active alongside its parent while shown, so this + * app_start_for_result() (app/start.h) stays Active alongside its parent while shown, so this * correctly picks the child, not the parent, while a dialog is up). * @retval ERROR_NOT_FOUND no app is Active * @retval ERROR_NONE on success diff --git a/Modules/app-module/include/app/start.h b/Modules/app-module/include/app/start.h new file mode 100644 index 000000000..6d5cc3b32 --- /dev/null +++ b/Modules/app-module/include/app/start.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +/** + * This file contains functions to start and run apps that were registered to the app manager. + * It differs from execute.h which runs executables from a specific path. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Starts @a id, a manifest already registered via app_manager_add(), passing @a argc/@a argv to + * the new instance's own main function (see app/loader.h's AppMainFn), modelled on a C program's + * main(argc, argv). For regular (non-modal) navigations that need to pass data to the target app + * (e.g. "show details for this app id") without expecting a result back. + * @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so + * @a argv and the strings it points to may be freed/go out of scope immediately after this call + * returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s). + * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered + * @retval ERROR_NONE on success + */ +error_t app_start(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id); + +/** + * Starts @a id as a child of @a parent_instance_id, for the purpose of receiving a result. + * + * When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id. + * The result is whatever the child's AppMainFn/AppLoaderApi::run() returned, unless + * @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for + * callers with no app_instance_id of their own). The parent is then responsible for calling + * app_manager_stop() on the child's instance id to fully reap it. + * + * @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as + * app_start()), so @a argv and the strings it points to may be + * freed/go out of scope immediately after this call returns. + * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered + * @retval ERROR_NONE on success + */ +error_t app_start_for_result(const char* id, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id); + +/** + * Same as app_start(), but installs @a bindings into the new instance's fd table before + * its task begins executing (e.g. a child's stdio, piped through parent-owned AppStreams; see + * app/stream.h). Writes the new instance's id into each bound stream's producer_id itself, since + * the caller cannot know it in advance. + * @param[in] bindings @a binding_count entries; each stream and buffer must stay alive (see + * app_stream_subscribe()) until unsubscribed or the child exits. + * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered + * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range + * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim + * @retval ERROR_NONE on success + */ +error_t app_start_with_streams(const char* id, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id); + +/** + * Combines app_start_for_result() and app_start_with_streams(): starts @a id as + * a modal child of @a parent_instance_id (see app_start_for_result()'s own doc for the + * result-delivery contract) with @a bindings installed into its fd table before its task begins + * executing (see app_start_with_streams()'s own doc for stream ownership). + * For a child that needs to hand back more than an int32_t (e.g. a path) via its own stdout. + * @param[in] argv see app_start_for_result(). + * @param[in] bindings see app_start_with_streams(). + * @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered + * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range + * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim + * @retval ERROR_NONE on success + */ +error_t app_start_for_result_with_streams(const char* id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/include/app/stream.h b/Modules/app-module/include/app/stream.h index bf5127f00..0295f9405 100644 --- a/Modules/app-module/include/app/stream.h +++ b/Modules/app-module/include/app/stream.h @@ -37,7 +37,7 @@ struct AppStream { AppInstanceId producer_id; TaskHandle_t producer_task; /** fd this stream is installed at in producer_id's fd table; set by - * app_stream_subscribe()/app_manager_start_with_streams(), used by + * app_stream_subscribe()/app_start_with_streams(), used by * app_stream_unsubscribe() to find it again. */ int producer_fd; diff --git a/Modules/app-module/private/app/private/arguments.h b/Modules/app-module/private/app/private/arguments.h new file mode 100644 index 000000000..34126cb1d --- /dev/null +++ b/Modules/app-module/private/app/private/arguments.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +/** + * Deep-copies @a argv (@a argc <= 0 => NULL, matching "no parameters"). Caller passes the result + * to app_scheduler_start(), which takes ownership regardless of outcome. + * @return NULL if @a argc <= 0 (no parameters), or if allocation failed. For @a argc > 0, + * these are the only cases that produce NULL, so a caller can tell them apart by its own + * already-known @a argc: NULL back from a positive @a argc always means allocation failed. + * All partial allocations are freed before returning NULL, so failure never leaks memory. + */ +inline char** app_arguments_copy(int argc, const char* const argv[]) { + if (argc <= 0) { + return nullptr; + } + + auto* copy = new (std::nothrow) char*[argc + 1]; + if (copy == nullptr) { + return nullptr; + } + + int copied = 0; + for (; copied < argc; copied++) { + size_t length = strlen(argv[copied]); + copy[copied] = new (std::nothrow) char[length + 1]; + if (copy[copied] == nullptr) { + break; + } + memcpy(copy[copied], argv[copied], length + 1); + } + + if (copied < argc) { + for (int i = 0; i < copied; i++) { + delete[] copy[i]; + } + delete[] copy; + return nullptr; + } + + copy[argc] = nullptr; + return copy; +} + +/** + * Frees a deep-copied argv previously built by app_arguments_copy(): each individually + * heap-allocated string, then the array itself. Safe to call with count == 0 / values == nullptr + * (no-op). + */ +inline void app_arguments_free(int count, char** values) { + if (values == nullptr) { + return; + } + for (int i = 0; i < count; i++) { + delete[] values[i]; + } + delete[] values; +} diff --git a/Modules/app-module/private/app/private/ledger.h b/Modules/app-module/private/app/private/ledger.h index 52efa5aeb..eab9b3dcf 100644 --- a/Modules/app-module/private/app/private/ledger.h +++ b/Modules/app-module/private/app/private/ledger.h @@ -38,21 +38,22 @@ struct AppCompletionSignal { /** A registered/running app instance, as tracked internally by app-module. */ struct AppInstanceRecord { uint32_t id; + /** NULL for an instance started via app_execute() (app/execute.h; no manifest involved). */ 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. */ + /** 0 for a top-level launch (app_start()). Non-zero for a modal child launched via + * app_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; - /** This instance's fd table. Constructed by start_internal() before insertion into - * AppLedger::instances, torn down (every open fd closed) when the instance's task exits. */ + /** This instance's fd table. Constructed by app_manager_start_internal() before insertion + * into AppLedger::instances, torn down (every open fd closed) when the instance's task exits. */ AppFdTable fd_table {}; }; @@ -70,17 +71,3 @@ 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; -} diff --git a/Modules/app-module/private/app/private/manager_internal.h b/Modules/app-module/private/app/private/manager_internal.h new file mode 100644 index 000000000..19dcb9f0a --- /dev/null +++ b/Modules/app-module/private/app/private/manager_internal.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Shared core behind every app_start*() (app/manager.h) and app_execute*() + * (app/execute.h) entry point: deep-copies @a argv, allocates an instance id, installs + * @a bindings into the new instance's fd table before app_scheduler_start() is called, then + * starts it. @a manifest may be NULL for a location-based start with no manifest at all. + * @retval ERROR_INVALID_ARGUMENT @a binding_count is nonzero but @a bindings is NULL + * @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type + * @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range + * @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim + * @retval ERROR_NONE on success + */ +error_t app_manager_start_internal(const struct AppManifest* manifest, struct AppLocation location, struct AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/source/elf_check.cpp b/Modules/app-module/source/elf_check.cpp new file mode 100644 index 000000000..b08594da3 --- /dev/null +++ b/Modules/app-module/source/elf_check.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include + +namespace { + +constexpr size_t ELF_HEADER_PREFIX_SIZE = 20; // e_ident[16] + e_type(2) + e_machine(2) +constexpr uint8_t ELF_MAGIC[4] = { 0x7f, 'E', 'L', 'F' }; + +uint16_t read_le16(const uint8_t* bytes) { + return static_cast(bytes[0] | (bytes[1] << 8)); +} + +} // namespace + +bool elf_check_file(const char* path, const struct ElfRequirements* requirements) { + FILE* file = fopen(path, "rb"); + if (file == nullptr) { + return false; + } + + uint8_t header[ELF_HEADER_PREFIX_SIZE]; + size_t read = fread(header, 1, sizeof(header), file); + fclose(file); + + if (read != sizeof(header)) { + return false; + } + + if (memcmp(header, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) { + return false; + } + + uint8_t elf_class = header[4]; // e_ident[EI_CLASS] + uint8_t data = header[5]; // e_ident[EI_DATA] + uint16_t type = read_le16(header + 16); // e_type + uint16_t machine = read_le16(header + 18); // e_machine + + return elf_class == requirements->elf_class + && data == requirements->data + && type == requirements->type + && machine == requirements->machine; +} diff --git a/Modules/app-module/source/execute.cpp b/Modules/app-module/source/execute.cpp new file mode 100644 index 000000000..59a0a4ab2 --- /dev/null +++ b/Modules/app-module/source/execute.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include + +#include +#include + +namespace { + +// Same lookup as scheduler.cpp's own (private) find_loader_api() - duplicated rather than +// shared since it's a handful of lines and neither file depends on the other. +const char* loader_service_id_for(AppLocationType type) { + return (type == APP_LOCATION_MEMORY) ? APP_LOADER_MEMORY_SERVICE_ID : APP_LOADER_PATH_SERVICE_ID; +} + +const AppLoaderApi* find_loader_api(AppLocationType type) { + ServiceInstance* instance = service_manager_find_instance(loader_service_id_for(type)); + if (instance == nullptr) { + return nullptr; + } + return static_cast(service_instance_get_data(instance)); +} + +} // namespace + +extern "C" { + +error_t app_execute(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) { + return app_manager_start_internal(nullptr, location, stack, 0, argc, argv, nullptr, 0, out_app_instance_id); +} + +error_t app_execute_for_result(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) { + return app_manager_start_internal(nullptr, location, stack, parent_instance_id, argc, argv, nullptr, 0, out_app_instance_id); +} + +error_t app_execute_with_streams(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { + return app_manager_start_internal(nullptr, location, stack, 0, argc, argv, bindings, binding_count, out_app_instance_id); +} + +error_t app_execute_for_result_with_streams(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) { + return app_manager_start_internal(nullptr, location, stack, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id); +} + +bool app_is_executable(AppLocation location) { + const AppLoaderApi* loader = find_loader_api(location.type); + if (loader == nullptr || loader->is_executable == nullptr) { + return false; + } + return loader->is_executable(location); +} + +} // extern "C" diff --git a/Modules/app-module/source/internal_loader.cpp b/Modules/app-module/source/internal_loader.cpp index 70d0e179d..36b325875 100644 --- a/Modules/app-module/source/internal_loader.cpp +++ b/Modules/app-module/source/internal_loader.cpp @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include #include #include @@ -24,10 +23,15 @@ int32_t api_run(AppRuntime runtime, uint32_t /*app_instance_id*/, int argc, char void api_unload(AppRuntime /*unused*/) { } +bool api_is_executable(AppLocation location) { + return location.type == APP_LOCATION_MEMORY && location.location != nullptr; +} + AppLoaderApi memory_loader_api = { .load = api_load, .run = api_run, .unload = api_unload, + .is_executable = api_is_executable, }; void* create_service(const ServiceManifest*) { diff --git a/Modules/app-module/source/manager.cpp b/Modules/app-module/source/manager.cpp index 020b0d3f3..c76148996 100644 --- a/Modules/app-module/source/manager.cpp +++ b/Modules/app-module/source/manager.cpp @@ -1,9 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include #include #include +#include #include #include @@ -70,65 +72,37 @@ void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context) mutex_unlock(&ledger.mutex); } -namespace { - -// Deep-copies argv (argc <= 0 => NULL, matching "no parameters"). Caller passes the result to -// app_scheduler_start(), which takes ownership regardless of outcome. -char** copy_arguments(int argc, const char* const argv[]) { - if (argc <= 0) { - return nullptr; - } - auto* copy = new char*[argc + 1]; - for (int i = 0; i < argc; i++) { - size_t length = strlen(argv[i]); - copy[i] = new char[length + 1]; - memcpy(copy[i], argv[i], length + 1); +error_t app_manager_start_internal(const AppManifest* manifest, AppLocation location, AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv_in[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { + char** argv = app_arguments_copy(argc, argv_in); + if (argc > 0 && argv == nullptr) { + return ERROR_OUT_OF_MEMORY; } - copy[argc] = nullptr; - return copy; -} -// Takes ownership of argv (already a deep copy, or NULL/argc==0) regardless of outcome - -// app_scheduler_start() frees it on any failure path, and the spawned task frees it once its -// run() returns. @a bindings (@a binding_count entries, may be NULL/0) are subscribed into the -// new instance's fd table before app_scheduler_start() is called, so they're in place before its -// task begins executing (see app_manager_start_with_streams()). -error_t start_internal(const char* id, AppInstanceId parent_instance_id, int argc, char* argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { if (binding_count != 0 && bindings == nullptr) { - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_INVALID_ARGUMENT; } auto& ledger = app_ledger(); mutex_lock(&ledger.mutex); - auto manifest_iterator = ledger.manifests.find(id); - if (manifest_iterator == ledger.manifests.end()) { - mutex_unlock(&ledger.mutex); - app_ledger_free_arguments(argc, argv); - return ERROR_NOT_FOUND; - } - const AppManifest* manifest = manifest_iterator->second; - AppInstanceId target_id = ledger.next_instance_id++; AppInstanceRecord record { .id = target_id, .manifest = manifest, .state = APP_INSTANCE_STATE_STARTING, .task = nullptr }; record.parent_id = parent_instance_id; ledger.instances[target_id] = record; - // Constructed on the map-resident copy, not the local `record` about to go out of scope. - // AppFdTable::fds[] entries point into AppFdTable::slots[] by address (see fd_table.h), so - // constructing before the copy above would leave them pointing at stack storage. + // Construct on the map-resident copy, not `record`: fds[] point into slots[] by address + // (fd_table.h), so constructing on the stack-local record would leave them dangling. app_fd_table_construct(&ledger.instances[target_id].fd_table); mutex_unlock(&ledger.mutex); - LOG_I(TAG, "[instance %d] starting %s with parent %d", target_id, manifest->id, parent_instance_id); + LOG_I(TAG, "[instance %d] starting %s with parent %d", target_id, manifest != nullptr ? manifest->id : "", parent_instance_id); for (size_t i = 0; i < binding_count; i++) { error_t bind_result = app_stream_subscribe(bindings[i].stream, bindings[i].buffer, bindings[i].buffer_capacity, bindings[i].event_group, target_id, bindings[i].producer_fd); if (bind_result != ERROR_NONE) { LOG_E(TAG, "[instance %d] Failed to bind stream at fd %d: %s", target_id, bindings[i].producer_fd, error_to_string(bind_result)); - // Undo bindings[0..i): app_fd_table_teardown() below only closes each stream. It - // doesn't release the event bits app_stream_subscribe() claimed or destruct - // stream->internal.mutex; only app_stream_unsubscribe() does that. + // Undo bindings[0..i): teardown() below only closes the fd, not the event bits + // or mutex app_stream_subscribe() claimed; only app_stream_unsubscribe() does. for (size_t j = 0; j < i; j++) { app_stream_unsubscribe(bindings[j].stream); } @@ -136,15 +110,13 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg app_fd_table_teardown(&ledger.instances[target_id].fd_table); ledger.instances.erase(target_id); mutex_unlock(&ledger.mutex); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return bind_result; } } - error_t error = app_scheduler_start(target_id, manifest->location, manifest->stack, argc, argv); + error_t error = app_scheduler_start(target_id, location, stack, argc, argv); if (error != ERROR_NONE) { - // Every binding succeeded before app_scheduler_start() failed. Unsubscribe all of them, - // same reasoning as the bind-failure path above. for (size_t j = 0; j < binding_count; j++) { app_stream_unsubscribe(bindings[j].stream); } @@ -160,28 +132,6 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg return ERROR_NONE; } -} // namespace - -error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id) { - return start_internal(id, 0, 0, nullptr, nullptr, 0, out_app_instance_id); -} - -error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) { - return start_internal(id, 0, argc, copy_arguments(argc, argv), nullptr, 0, out_app_instance_id); -} - -error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) { - return start_internal(id, parent_instance_id, argc, copy_arguments(argc, argv), nullptr, 0, out_app_instance_id); -} - -error_t app_manager_start_with_streams(const char* id, const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { - return start_internal(id, 0, 0, nullptr, bindings, binding_count, out_app_instance_id); -} - -error_t app_manager_start_for_result_with_streams(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { - return start_internal(id, parent_instance_id, argc, copy_arguments(argc, argv), bindings, binding_count, out_app_instance_id); -} - error_t app_manager_stop(AppInstanceId app_instance_id) { return app_scheduler_stop(app_instance_id, pdMS_TO_TICKS(2000)); } @@ -200,8 +150,7 @@ error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id) mutex_lock(&ledger.mutex); AppInstanceId topmost_id = 0; for (auto& [instance_id, record] : ledger.instances) { - // Instance ids are handed out in increasing order (AppLedger::next_instance_id), so - // the highest Active id is also the most recently started one. + // Ids increase monotonically, so the highest Active id is the most recent. if (record.state == APP_INSTANCE_STATE_ACTIVE && instance_id > topmost_id) { topmost_id = instance_id; } @@ -230,7 +179,8 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) { auto& ledger = app_ledger(); mutex_lock(&ledger.mutex); auto iterator = ledger.instances.find(topmost_id); - const char* app_id = (iterator != ledger.instances.end()) ? iterator->second.manifest->id : nullptr; + const AppManifest* manifest = (iterator != ledger.instances.end()) ? iterator->second.manifest : nullptr; + const char* app_id = manifest != nullptr ? manifest->id : nullptr; mutex_unlock(&ledger.mutex); if (app_id == nullptr) { @@ -251,10 +201,8 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) { 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()). +// non-owning pointer to. Separate from app_install.cpp's registry: scanning only +// adds/removes registrations, never touches disk or running instances. struct ScannedAppManifest { std::string id; std::string name; @@ -301,15 +249,15 @@ void app_manager_install_path_scan(void) { app_fs_list_direct_subdirectories(root, found_app_dirs); } - // Snapshot of what's already registered, taken once so the rest of this scan can run without holding registry.mutex + // Snapshot once so the rest of the scan doesn't hold registry.mutex. mutex_lock(®istry.mutex); - std::unordered_map known_paths; // id -> path + std::unordered_map known_paths; for (const auto& [id, record] : registry.scanned) { known_paths.emplace(id, record->path); } mutex_unlock(®istry.mutex); - // Stat each manifest and parse it entirely without registry.mutex held (due to filesystem IO being slow) + // Parses without registry.mutex held; filesystem IO is slow. std::vector> new_records; for (const auto& app_dir : found_app_dirs) { auto manifest_path = app_dir + "/manifest.properties"; @@ -324,7 +272,7 @@ void app_manager_install_path_scan(void) { } if (known_paths.contains(metadata.app_id)) { - continue; // already registered by an earlier scan + continue; } auto record = std::make_unique(); @@ -342,7 +290,6 @@ void app_manager_install_path_scan(void) { new_records.push_back(std::move(record)); } - // Anything a previous scan registered whose directory has since disappeared gets unregistered below. std::vector missing_ids; for (const auto& [id, path] : known_paths) { if (!app_fs_is_directory(path)) { @@ -350,11 +297,9 @@ void app_manager_install_path_scan(void) { } } - // app_manager_add()/app_manager_remove() take app-module's own ledger mutex internally - - // calling them while holding registry.mutex would establish a registry.mutex -> ledger- - // mutex lock order that any future opposite-order path would deadlock against, so these - // also run with registry.mutex released. registry.mutex is taken only afterward, briefly, - // to publish the results (plain in-memory map updates, no I/O or other locks involved). + // app_manager_add()/remove() take the ledger mutex internally, so calling them under + // registry.mutex would fix a lock order an opposite-order caller could deadlock against. + // registry.mutex is retaken afterward only to publish the in-memory results. for (const auto& id : missing_ids) { app_manager_remove(id.c_str()); } @@ -391,10 +336,8 @@ error_t app_manager_install_path_uninstall(const char* app_id) { auto path = iterator->second->path; mutex_unlock(®istry.mutex); - // Stop every running instance that retains this manifest pointer, mirroring - // stop_all_instances_of() in app_install.cpp. Collect under ledger.mutex, - // then call app_manager_stop() outside it (that call bound-joins the - // instance's thread, which itself takes ledger.mutex in its thread_main). + // Mirrors stop_all_instances_of() in app_install.cpp. Collect under ledger.mutex, stop + // outside it: app_manager_stop() bound-joins the thread, which itself takes ledger.mutex. std::vector instance_ids; auto& ledger = app_ledger(); mutex_lock(&ledger.mutex); @@ -409,14 +352,12 @@ error_t app_manager_install_path_uninstall(const char* app_id) { app_manager_stop(id); } - // app_manager_remove takes ledger.mutex internally - call outside both - // registry.mutex and ledger.mutex to match the lock ordering in - // app_manager_install_path_scan(). + // app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching + // the lock order in app_manager_install_path_scan(). app_manager_remove(app_id); - // Every instance has stopped and the manifest is unregistered — safe to - // delete the on-disk directory. Delete before erasing the scan record so - // that a failed deletion leaves the entry discoverable for a retry. + // Delete before erasing the scan record, so a failed deletion still leaves the + // entry discoverable for a retry. if (!app_fs_delete_recursively(path)) { return ERROR_RESOURCE; } diff --git a/Modules/app-module/source/module.cpp b/Modules/app-module/source/module.cpp index 9c3fb2f98..fcdf041a3 100644 --- a/Modules/app-module/source/module.cpp +++ b/Modules/app-module/source/module.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -7,11 +8,11 @@ #include #include #include +#include #include #include -#include #include #include @@ -25,6 +26,12 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id), DEFINE_MODULE_SYMBOL(app_event_unsubscribe), DEFINE_MODULE_SYMBOL(app_event_poll), + // app/execute + DEFINE_MODULE_SYMBOL(app_execute), + DEFINE_MODULE_SYMBOL(app_execute_for_result), + DEFINE_MODULE_SYMBOL(app_execute_with_streams), + DEFINE_MODULE_SYMBOL(app_execute_for_result_with_streams), + DEFINE_MODULE_SYMBOL(app_is_executable), // app/install DEFINE_MODULE_SYMBOL(app_get_install_path), DEFINE_MODULE_SYMBOL(app_install), @@ -34,11 +41,6 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_io_write), DEFINE_MODULE_SYMBOL(app_io_close), // 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_start_with_streams), - DEFINE_MODULE_SYMBOL(app_manager_start_for_result_with_streams), DEFINE_MODULE_SYMBOL(app_manager_stop), DEFINE_MODULE_SYMBOL(app_manager_get_state), DEFINE_MODULE_SYMBOL(app_manager_find_manifest), @@ -50,6 +52,11 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(app_manager_install_path_add), DEFINE_MODULE_SYMBOL(app_manager_install_path_scan), DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall), + // app/start + DEFINE_MODULE_SYMBOL(app_start), + DEFINE_MODULE_SYMBOL(app_start_for_result), + DEFINE_MODULE_SYMBOL(app_start_with_streams), + DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams), // app/manifest DEFINE_MODULE_SYMBOL(app_id_is_valid), // app/metadata diff --git a/Modules/app-module/source/scheduler.cpp b/Modules/app-module/source/scheduler.cpp index 6bbc3fc7c..5155657d6 100644 --- a/Modules/app-module/source/scheduler.cpp +++ b/Modules/app-module/source/scheduler.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include #include #include @@ -101,7 +102,7 @@ void set_task(AppInstanceId app_instance_id, TaskHandle_t task) { auto iterator = ledger.instances.find(app_instance_id); if (iterator != ledger.instances.end()) { iterator->second.task = task; - // Streams bound before this instance's task existed (app_manager_start_with_streams()) + // Streams bound before this instance's task existed (app_start_with_streams()) // only got producer_task filled in as NULL at subscribe time. Backfill it now. AppFdTable& fd_table = iterator->second.fd_table; for (auto& slot : fd_table.slots) { @@ -168,7 +169,7 @@ const AppLoaderApi* find_loader_api(AppLocationType type) { return static_cast(service_instance_get_data(instance)); } -// If this instance was launched via app_manager_start_for_result(), delivers @a result (its +// If this instance was launched via app_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(AppInstanceId app_instance_id, int32_t result) { @@ -223,7 +224,7 @@ void app_task_main(void* context) { // response to APP_EVENT_CLOSE. set_state(ctx->app_instance_id, APP_INSTANCE_STATE_STOPPED); - app_ledger_free_arguments(ctx->argc, ctx->argv); + app_arguments_free(ctx->argc, ctx->argv); AppInstanceId app_instance_id = ctx->app_instance_id; AppCompletionSignal* completion = ctx->completion; @@ -274,7 +275,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, const AppLoaderApi* loader = find_loader_api(location.type); if (loader == nullptr) { LOG_E(TAG, "[instance %lu] No app loader is registered (service '%s' not found)", app_instance_id, loader_service_id_for(location.type)); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_NOT_FOUND; } @@ -282,7 +283,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, error_t load_result = loader->load(location, &runtime); if (load_result != ERROR_NONE) { LOG_E(TAG, "[instance %lu] Failed to load app: %s", app_instance_id, error_to_string(load_result)); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return load_result; } @@ -290,7 +291,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, if (completion == nullptr) { LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id); loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_OUT_OF_MEMORY; } completion->semaphore = xSemaphoreCreateBinary(); @@ -298,7 +299,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, LOG_E(TAG, "[instance %lu] Failed to allocate app", app_instance_id); delete completion; loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_OUT_OF_MEMORY; } @@ -309,7 +310,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, vSemaphoreDelete(completion->semaphore); delete completion; loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_INVALID_ARGUMENT; } @@ -335,7 +336,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, vSemaphoreDelete(completion->semaphore); delete completion; loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_OUT_OF_MEMORY; } @@ -346,7 +347,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, vSemaphoreDelete(completion->semaphore); delete completion; loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_OUT_OF_MEMORY; } #else @@ -374,7 +375,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, vSemaphoreDelete(completion->semaphore); delete completion; loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_OUT_OF_MEMORY; } @@ -401,7 +402,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, vSemaphoreDelete(completion->semaphore); delete completion; loader->unload(runtime); - app_ledger_free_arguments(argc, argv); + app_arguments_free(argc, argv); return ERROR_OUT_OF_MEMORY; } vTaskSuspend(task_handle); diff --git a/Modules/app-module/source/start.cpp b/Modules/app-module/source/start.cpp new file mode 100644 index 000000000..f9da33028 --- /dev/null +++ b/Modules/app-module/source/start.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include + +#include + +namespace { + +// Looks @a id up in the manifest registry, then delegates to app_manager_start_internal(). The +// only path that requires a registered manifest; app_execute*() (app/execute.h) bypasses this +// entirely. +error_t start_internal_by_id(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { + auto& ledger = app_ledger(); + + mutex_lock(&ledger.mutex); + auto manifest_iterator = ledger.manifests.find(id); + if (manifest_iterator == ledger.manifests.end()) { + mutex_unlock(&ledger.mutex); + return ERROR_NOT_FOUND; + } + const AppManifest* manifest = manifest_iterator->second; + mutex_unlock(&ledger.mutex); + + return app_manager_start_internal(manifest, manifest->location, manifest->stack, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id); +} + +} // namespace + +extern "C" { + +error_t app_start(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) { + return start_internal_by_id(id, 0, argc, argv, nullptr, 0, out_app_instance_id); +} + +error_t app_start_for_result(const char* id, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) { + return start_internal_by_id(id, parent_instance_id, argc, argv, nullptr, 0, out_app_instance_id); +} + +error_t app_start_with_streams(const char* id, const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) { + return start_internal_by_id(id, 0, 0, nullptr, bindings, binding_count, out_app_instance_id); +} + +error_t app_start_for_result_with_streams(const char* id, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id) { + return start_internal_by_id(id, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id); +} + +} // extern "C" diff --git a/Modules/app-module/tests/CMakeLists.txt b/Modules/app-module/tests/CMakeLists.txt index de1c477e0..d10cf759a 100644 --- a/Modules/app-module/tests/CMakeLists.txt +++ b/Modules/app-module/tests/CMakeLists.txt @@ -12,7 +12,7 @@ if (NOT APPLE) target_sources(AppModuleTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../Tactility/Source/AppStdioWrap.cpp) endif () -target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC}) +target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC} ${CMAKE_CURRENT_LIST_DIR}/../private) add_test(NAME AppModuleTests COMMAND AppModuleTests) diff --git a/Modules/app-module/tests/source/arguments_test.cpp b/Modules/app-module/tests/source/arguments_test.cpp new file mode 100644 index 000000000..2f417fe85 --- /dev/null +++ b/Modules/app-module/tests/source/arguments_test.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "doctest.h" + +#include + +#include + +TEST_CASE("app_arguments_copy returns NULL for argc <= 0") { + const char* const argv[] = { "a" }; + CHECK_EQ(app_arguments_copy(0, argv), nullptr); + CHECK_EQ(app_arguments_copy(-1, argv), nullptr); + CHECK_EQ(app_arguments_copy(0, nullptr), nullptr); +} + +TEST_CASE("app_arguments_copy deep-copies argv") { + const char* const argv[] = { "one", "two", "three" }; + char** copy = app_arguments_copy(3, argv); + REQUIRE_NE(copy, nullptr); + + CHECK_NE(static_cast(copy[0]), static_cast(argv[0])); + CHECK_EQ(std::strcmp(copy[0], "one"), 0); + CHECK_EQ(std::strcmp(copy[1], "two"), 0); + CHECK_EQ(std::strcmp(copy[2], "three"), 0); + CHECK_EQ(copy[3], nullptr); + + app_arguments_free(3, copy); +} + +TEST_CASE("app_arguments_copy handles an empty string argument") { + const char* const argv[] = { "" }; + char** copy = app_arguments_copy(1, argv); + REQUIRE_NE(copy, nullptr); + CHECK_EQ(std::strcmp(copy[0], ""), 0); + CHECK_EQ(copy[1], nullptr); + app_arguments_free(1, copy); +} + +TEST_CASE("app_arguments_free is a no-op for count 0 / null values") { + app_arguments_free(0, nullptr); +} diff --git a/Modules/app-module/tests/source/execute_test.cpp b/Modules/app-module/tests/source/execute_test.cpp new file mode 100644 index 000000000..11a4734a3 --- /dev/null +++ b/Modules/app-module/tests/source/execute_test.cpp @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "doctest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +extern ServiceManifest app_internal_loader_service_manifest; + +namespace { + +// See manager_test.cpp's own copy of this helper for why this checks the registry directly +// rather than a per-translation-unit static bool. +void ensure_memory_loader_registered() { + if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) { + service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true); + } +} + +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; +} + +AppInstanceId topmost_instance_id() { + AppInstanceId id = 0; + return app_manager_get_topmost_instance_id(&id) == ERROR_NONE ? id : 0; +} + +// Every APP_LOCATION_PATH-based test below would need its own fake path loader, registered +// under the same singleton APP_LOADER_PATH_SERVICE_ID that manager_test.cpp's own fake loader +// already claims - two independent-but-competing registrations in the same test binary is a real +// race (whichever file's static registrar runs first wins process-wide; the other file then runs +// silently against a loader it didn't write). Using APP_LOCATION_MEMORY + the real, already-safe +// app_internal_loader_service_manifest (registered via the find-instance-check idiom above, safe +// under this exact contention already used elsewhere in this test suite) sidesteps the problem +// entirely: no second competing registration exists. + +// Subscribes until APP_EVENT_CLOSE like a real app instance; if launched with a single argv +// entry, returns it parsed as int immediately instead (the app_execute_for_result() shortcut, +// mirroring a modal dialog's result). +int32_t location_app_main(int argc, char* argv[]) { + if (argc == 1) { + return static_cast(strtol(argv[0], nullptr, 10)); + } + + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + AppEventSubscription sub {}; + app_event_subscribe(&sub, &event_group); + + while (true) { + if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) { + break; // safety net so a bug here can't hang the test suite + } + bool done = false; + AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + done = true; + break; + } + } + if (done) { + break; + } + } + + app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); + return 0; +} + +// Writes a fixed string to its own stdout then returns 7 as its result, for the +// app_execute_with_streams()/_for_result_with_streams() tests: proves a binding installed +// before the task starts actually reaches the app's own app_io_write() calls. +int32_t stream_writer_app_main(int, char*[]) { + const char message[] = "loc"; + size_t sent = 0; + while (sent < sizeof(message) - 1) { + ssize_t written = app_io_write(STDOUT_FILENO, message + sent, sizeof(message) - 1 - sent); + if (written < 0) { + break; + } + sent += static_cast(written); + } + return 7; +} + +// Writes argv[0] to its own stdout, for the app_execute_with_streams() argv-delivery test. +int32_t argv_echo_app_main(int argc, char* argv[]) { + if (argc < 1) { + return -1; + } + const char* message = argv[0]; + size_t length = strlen(message); + size_t sent = 0; + while (sent < length) { + ssize_t written = app_io_write(STDOUT_FILENO, message + sent, length - sent); + if (written < 0) { + break; + } + sent += static_cast(written); + } + return 0; +} + +} // namespace + +TEST_CASE("app_execute runs a location with no manifest at all, and reports no topmost app id for it") { + ensure_memory_loader_registered(); + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) }; + uint32_t instance_id = 0; + REQUIRE_EQ(app_execute(location, AppStackConfig {}, 0, nullptr, &instance_id), ERROR_NONE); + CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + + CHECK_EQ(topmost_instance_id(), instance_id); + char buffer[64]; + // No manifest to report an id from - same NOT_FOUND a caller already sees for "nothing active". + CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_NOT_FOUND); + + CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE); + CHECK_EQ(app_manager_get_state(instance_id), APP_INSTANCE_STATE_STOPPED); +} + +TEST_CASE("app_execute doesn't disturb app_manager_get_topmost_app_id for a normal manifest-backed app started afterward") { + ensure_memory_loader_registered(); + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) }; + uint32_t unregistered_id = 0; + REQUIRE_EQ(app_execute(location, AppStackConfig {}, 0, nullptr, &unregistered_id), ERROR_NONE); + CHECK(wait_for_state(unregistered_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + + AppManifest manifest { "test.app.execute.after", "After", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) } }; + REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); + uint32_t registered_id = 0; + REQUIRE_EQ(app_start("test.app.execute.after", 0, nullptr, ®istered_id), ERROR_NONE); + CHECK(wait_for_state(registered_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + + char buffer[64]; + CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::string(buffer), "test.app.execute.after"); + + app_manager_stop(unregistered_id); + app_manager_stop(registered_id); + app_manager_remove("test.app.execute.after"); +} + +TEST_CASE("app_execute_for_result delivers APP_EVENT_RESULT to the parent, with no manifest for the child either") { + ensure_memory_loader_registered(); + + AppManifest parent_manifest { "test.app.execute.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) } }; + REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE); + + uint32_t parent_id = 0; + REQUIRE_EQ(app_start("test.app.execute.parent", 0, nullptr, &parent_id), ERROR_NONE); + CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + + TaskEventGroup parent_event_group {}; + task_event_group_construct(&parent_event_group); + + AppEventSubscription parent_sub {}; + REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) }; + const char* argv[] = { "42" }; // location_app_main's single-arg shortcut - returns 42 immediately + uint32_t child_id = 0; + REQUIRE_EQ(app_execute_for_result(location, AppStackConfig {}, 1, argv, parent_id, &child_id), ERROR_NONE); + + REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + AppEvent event {}; + REQUIRE_EQ(app_event_poll(&parent_sub, &event), 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); + task_event_group_destruct(&parent_event_group); + app_manager_stop(child_id); + app_manager_stop(parent_id); + app_manager_remove("test.app.execute.parent"); +} + +TEST_CASE("app_execute_with_streams pipes a manifest-less child's app_io_write() calls into a parent-owned AppStream") { + ensure_memory_loader_registered(); + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(stream_writer_app_main) }; + + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + uint8_t storage[64]; + AppStream child_stdout {}; + AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group }; + + AppInstanceId child_id = 0; + REQUIRE_EQ(app_execute_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, &child_id), ERROR_NONE); + + std::vector received; + while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { + uint8_t chunk[16]; + size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk)); + if (n == 0) { + break; // EOF + } + received.insert(received.end(), chunk, chunk + n); + } + + REQUIRE_EQ(received.size(), 3u); + CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0); + + REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); + app_stream_unsubscribe(&child_stdout); + task_event_group_destruct(&event_group); +} + +TEST_CASE("app_execute_with_streams passes argv through to the started app") { + ensure_memory_loader_registered(); + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(argv_echo_app_main) }; + + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + uint8_t storage[64]; + AppStream child_stdout {}; + AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group }; + + const char* argv[] = { "hello" }; + AppInstanceId child_id = 0; + REQUIRE_EQ(app_execute_with_streams(location, AppStackConfig {}, 1, argv, &binding, 1, &child_id), ERROR_NONE); + + std::vector received; + while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { + uint8_t chunk[16]; + size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk)); + if (n == 0) { + break; // EOF + } + received.insert(received.end(), chunk, chunk + n); + } + + REQUIRE_EQ(received.size(), 5u); + CHECK_EQ(std::memcmp(received.data(), "hello", 5), 0); + + REQUIRE(wait_for_state(child_id, APP_INSTANCE_STATE_STOPPED, 1000)); + app_stream_unsubscribe(&child_stdout); + task_event_group_destruct(&event_group); +} + +TEST_CASE("app_execute_for_result_with_streams delivers both the stream data and the APP_EVENT_RESULT") { + ensure_memory_loader_registered(); + + AppManifest parent_manifest { "test.app.execute.parent_streams", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(location_app_main) } }; + REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE); + + uint32_t parent_id = 0; + REQUIRE_EQ(app_start("test.app.execute.parent_streams", 0, nullptr, &parent_id), ERROR_NONE); + CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + + TaskEventGroup parent_event_group {}; + task_event_group_construct(&parent_event_group); + AppEventSubscription parent_sub {}; + REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); + + uint8_t storage[64]; + AppStream child_stdout {}; + AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &parent_event_group }; + + AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast(stream_writer_app_main) }; + uint32_t child_id = 0; + REQUIRE_EQ(app_execute_for_result_with_streams(location, AppStackConfig {}, 0, nullptr, &binding, 1, parent_id, &child_id), ERROR_NONE); + + std::vector received; + while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { + uint8_t chunk[16]; + size_t n = app_stream_read(&child_stdout, chunk, sizeof(chunk)); + if (n == 0) { + break; // EOF + } + received.insert(received.end(), chunk, chunk + n); + } + REQUIRE_EQ(received.size(), 3u); + CHECK_EQ(std::memcmp(received.data(), "loc", 3), 0); + + REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + AppEvent event {}; + REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE); + CHECK_EQ(event.type, APP_EVENT_RESULT); + CHECK_EQ(event.result.launch_id, child_id); + CHECK_EQ(event.result.result, 7); + + app_stream_unsubscribe(&child_stdout); + app_event_unsubscribe(&parent_sub); + task_event_group_destruct(&parent_event_group); + app_manager_stop(child_id); + app_manager_stop(parent_id); + app_manager_remove("test.app.execute.parent_streams"); +} diff --git a/Modules/app-module/tests/source/io_test.cpp b/Modules/app-module/tests/source/io_test.cpp index aa7b37de2..400bd8d43 100644 --- a/Modules/app-module/tests/source/io_test.cpp +++ b/Modules/app-module/tests/source/io_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -135,7 +136,7 @@ TEST_CASE("an app's stdio fds default to the null device: write succeeds and dis REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; - REQUIRE_EQ(app_manager_start("test.io.unbound", &instance_id), ERROR_NONE); + REQUIRE_EQ(app_start("test.io.unbound", 0, nullptr, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000)); CHECK_EQ(g_stdio_write_result.load(std::memory_order_acquire), 1); @@ -144,7 +145,7 @@ TEST_CASE("an app's stdio fds default to the null device: write succeeds and dis app_manager_remove("test.io.unbound"); } -TEST_CASE("app_manager_start_with_streams pipes a child's app_io_write() calls into a parent-owned AppStream, EOF at exit") { +TEST_CASE("app_start_with_streams pipes a child's app_io_write() calls into a parent-owned AppStream, EOF at exit") { ensure_memory_loader_registered(); AppManifest manifest { "test.io.writer", "Writer", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(stdout_writer_app_main) } }; @@ -158,7 +159,7 @@ TEST_CASE("app_manager_start_with_streams pipes a child's app_io_write() calls i AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group }; AppInstanceId child_id = 0; - REQUIRE_EQ(app_manager_start_with_streams("test.io.writer", &binding, 1, &child_id), ERROR_NONE); + REQUIRE_EQ(app_start_with_streams("test.io.writer", &binding, 1, &child_id), ERROR_NONE); std::vector received; while (app_stream_await(&child_stdout, APP_FILE_WAIT_READABLE, pdMS_TO_TICKS(1000)) == ERROR_NONE) { @@ -195,7 +196,7 @@ TEST_CASE("a write blocked on a full stream wakes with an error once the consume AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group }; AppInstanceId child_id = 0; - REQUIRE_EQ(app_manager_start_with_streams("test.io.blocked", &binding, 1, &child_id), ERROR_NONE); + REQUIRE_EQ(app_start_with_streams("test.io.blocked", &binding, 1, &child_id), ERROR_NONE); // Never drained: the child fills the 4-byte buffer and blocks awaiting space for the rest. delay_millis(200); @@ -231,7 +232,7 @@ TEST_CASE("app_stream_unsubscribe is safe to call while a write is actively bloc AppStreamBinding binding { STDOUT_FILENO, &child_stdout, storage, sizeof(storage), &event_group }; AppInstanceId child_id = 0; - REQUIRE_EQ(app_manager_start_with_streams("test.io.unsub_race", &binding, 1, &child_id), ERROR_NONE); + REQUIRE_EQ(app_start_with_streams("test.io.unsub_race", &binding, 1, &child_id), ERROR_NONE); // Give the child time to fill the 4-byte buffer and block inside app_io_write(), already // dispatched through app_fd_table_get_and_retain() and currently waiting in @@ -262,7 +263,7 @@ TEST_CASE("app_io_read/write/close pass through a real file fd app-module never REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; - REQUIRE_EQ(app_manager_start("test.io.real_file", &instance_id), ERROR_NONE); + REQUIRE_EQ(app_start("test.io.real_file", 0, nullptr, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000)); CHECK_EQ(g_real_file_write_result.load(std::memory_order_acquire), 2); @@ -283,7 +284,7 @@ TEST_CASE("closing an already-closed app fd reports EBADF instead of falling thr REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; - REQUIRE_EQ(app_manager_start("test.io.double_close", &instance_id), ERROR_NONE); + REQUIRE_EQ(app_start("test.io.double_close", 0, nullptr, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 1000)); CHECK_EQ(g_double_close_first_result.load(std::memory_order_acquire), 0); diff --git a/Modules/app-module/tests/source/manager_test.cpp b/Modules/app-module/tests/source/manager_test.cpp index 9dc8fca11..28f92fd30 100644 --- a/Modules/app-module/tests/source/manager_test.cpp +++ b/Modules/app-module/tests/source/manager_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -53,7 +54,7 @@ void stash_received_arguments(int argc, char* argv[]) { // 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 +// parameter (app_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[]) { @@ -118,11 +119,13 @@ ServiceManifest fake_loader_manifest = { .on_stop = nullptr, }; +// Checks the registry directly rather than a per-translation-unit static bool, matching +// ensure_memory_loader_registered() below - this is the only file that registers a fake path +// loader, but a second one would silently win the race otherwise (see execute_test.cpp, which +// deliberately avoids needing one at all for exactly this reason). 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; + if (service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID) == nullptr) { + service_manager_add(&fake_loader_manifest, /*auto_start=*/true); } } @@ -175,14 +178,14 @@ bool wait_for_arguments_stashed(uint32_t timeout_ms) { } // namespace -TEST_CASE("app_manager_start activates an app instance, app_manager_stop terminates it") { +TEST_CASE("app_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); + REQUIRE_EQ(app_start("test.app.a", 0, nullptr, &instance_id), ERROR_NONE); CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE); @@ -191,7 +194,7 @@ TEST_CASE("app_manager_start activates an app instance, app_manager_stop termina app_manager_remove("test.app.a"); } -TEST_CASE("app_manager_start never touches another already-running app - every instance gets its own task") { +TEST_CASE("app_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 } }; @@ -200,11 +203,11 @@ TEST_CASE("app_manager_start never touches another already-running app - every i 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); + REQUIRE_EQ(app_start("test.app.b", 0, nullptr, &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); + REQUIRE_EQ(app_start("test.app.c", 0, nullptr, &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. @@ -216,18 +219,18 @@ TEST_CASE("app_manager_start never touches another already-running app - every i app_manager_remove("test.app.c"); } -TEST_CASE("app_manager_start always creates a fresh instance, even for the same manifest id twice") { +TEST_CASE("app_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); + REQUIRE_EQ(app_start("test.app.twice", 0, nullptr, &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); + REQUIRE_EQ(app_start("test.app.twice", 0, nullptr, &id_second), ERROR_NONE); CHECK(wait_for_state(id_second, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_NE(id_first, id_second); @@ -242,7 +245,7 @@ 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") { +TEST_CASE("app_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 } }; @@ -256,7 +259,7 @@ TEST_CASE("app_manager_start_with_parameters deep-copies argv before the app ins 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); + REQUIRE_EQ(app_start("test.app.args", 2, argv, &instance_id), ERROR_NONE); } CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); REQUIRE(wait_for_arguments_stashed(1000)); @@ -301,12 +304,12 @@ TEST_CASE("app_manager_for_each_manifest visits every registered manifest, inclu CHECK(std::ranges::find(seen_ids, "test.app.foreach.x") == seen_ids.end()); } -TEST_CASE("app_manager_start fails for an unregistered manifest id") { +TEST_CASE("app_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); + CHECK_EQ(app_start("test.app.nonexistent", 0, nullptr, &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") { +TEST_CASE("app_start runs an APP_LOCATION_MEMORY app via its function pointer, through the real internal loader") { ensure_memory_loader_registered(); AppManifest manifest { @@ -318,7 +321,7 @@ TEST_CASE("app_manager_start runs an APP_LOCATION_MEMORY app via its function po 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); + REQUIRE_EQ(app_start("test.app.memory", 0, nullptr, &instance_id), ERROR_NONE); CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE); @@ -327,7 +330,7 @@ TEST_CASE("app_manager_start runs an APP_LOCATION_MEMORY app via its function po app_manager_remove("test.app.memory"); } -TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, which stays Active throughout") { +TEST_CASE("app_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 } }; @@ -336,7 +339,7 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, 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); + REQUIRE_EQ(app_start("test.app.parent", 0, nullptr, &parent_id), ERROR_NONE); CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); TaskEventGroup parent_event_group {}; @@ -347,7 +350,7 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, 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); + REQUIRE_EQ(app_start_for_result("test.app.child", 1, argv, parent_id, &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); @@ -367,7 +370,7 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, app_manager_remove("test.app.child"); } -TEST_CASE("app_manager_start_for_result delivers the child's own return value as the result") { +TEST_CASE("app_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 } }; @@ -376,7 +379,7 @@ TEST_CASE("app_manager_start_for_result delivers the child's own return value as 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); + REQUIRE_EQ(app_start("test.app.parent2", 0, nullptr, &parent_id), ERROR_NONE); CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); TaskEventGroup parent_event_group {}; @@ -388,7 +391,7 @@ TEST_CASE("app_manager_start_for_result delivers the child's own return value as 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); + REQUIRE_EQ(app_start_for_result("test.app.child2", 0, nullptr, parent_id, &child_id), ERROR_NONE); CHECK(wait_for_state(child_id, APP_INSTANCE_STATE_ACTIVE, 1000)); app_manager_stop(child_id); // force-close @@ -418,14 +421,14 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is 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); + REQUIRE_EQ(app_start("test.app.top_a", 0, nullptr, &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); + REQUIRE_EQ(app_start("test.app.top_b", 0, nullptr, &id_b), ERROR_NONE); CHECK(wait_for_state(id_b, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_EQ(topmost_instance_id(), id_b); @@ -438,7 +441,7 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is // 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); + REQUIRE_EQ(app_start_for_result("test.app.top_a", 0, nullptr, id_b, &id_c), ERROR_NONE); CHECK(wait_for_state(id_c, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_EQ(topmost_instance_id(), id_c); @@ -451,7 +454,7 @@ TEST_CASE("app_manager_get_topmost_instance_id returns NOT_FOUND when nothing is app_manager_remove("test.app.top_b"); } -TEST_CASE("app_manager_start honors a custom AppManifest::stack.depth") { +TEST_CASE("app_start honors a custom AppManifest::stack.depth") { ensure_fake_loader_registered(); AppManifest manifest { "test.app.stack.custom", "Stack Custom", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } }; @@ -459,7 +462,7 @@ TEST_CASE("app_manager_start honors a custom AppManifest::stack.depth") { REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); uint32_t instance_id = 0; - REQUIRE_EQ(app_manager_start("test.app.stack.custom", &instance_id), ERROR_NONE); + REQUIRE_EQ(app_start("test.app.stack.custom", 0, nullptr, &instance_id), ERROR_NONE); CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE); @@ -468,7 +471,7 @@ TEST_CASE("app_manager_start honors a custom AppManifest::stack.depth") { app_manager_remove("test.app.stack.custom"); } -TEST_CASE("app_manager_start still works when AppManifest::stack is left at its zero-value default") { +TEST_CASE("app_start still works when AppManifest::stack is left at its zero-value default") { ensure_fake_loader_registered(); // stack.depth == 0 - app_scheduler_start() must fall back to its own default stack depth @@ -478,7 +481,7 @@ TEST_CASE("app_manager_start still works when AppManifest::stack is left at its REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); uint32_t instance_id = 0; - REQUIRE_EQ(app_manager_start("test.app.stack.default", &instance_id), ERROR_NONE); + REQUIRE_EQ(app_start("test.app.stack.default", 0, nullptr, &instance_id), ERROR_NONE); CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); CHECK_EQ(app_manager_stop(instance_id), ERROR_NONE); @@ -498,7 +501,7 @@ TEST_CASE("app_manager_get_topmost_app_id returns BUFFER_OVERFLOW for a too-smal REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); uint32_t id = 0; - REQUIRE_EQ(app_manager_start("test.app.top_overflow", &id), ERROR_NONE); + REQUIRE_EQ(app_start("test.app.top_overflow", 0, nullptr, &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. diff --git a/Modules/app-module/tests/source/stream_test.cpp b/Modules/app-module/tests/source/stream_test.cpp index f11dc1ed6..9d2cd4e0d 100644 --- a/Modules/app-module/tests/source/stream_test.cpp +++ b/Modules/app-module/tests/source/stream_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -75,7 +76,7 @@ AppInstanceId start_idle_app(const char* id) { AppManifest manifest { id, id, APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast(idle_app_main) } }; REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); AppInstanceId instance_id = 0; - REQUIRE_EQ(app_manager_start(id, &instance_id), ERROR_NONE); + REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE); REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); return instance_id; } diff --git a/Modules/app-posix-module/source/app_posix_loader_service.cpp b/Modules/app-posix-module/source/app_posix_loader_service.cpp index 9103a821a..0d66cd78d 100644 --- a/Modules/app-posix-module/source/app_posix_loader_service.cpp +++ b/Modules/app-posix-module/source/app_posix_loader_service.cpp @@ -1,4 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 +#include #include #include @@ -27,6 +28,36 @@ bool is_regular_file(const std::string& path) { return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode); } +#ifndef __APPLE__ +constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = { + .elf_class = ELF_CLASS_64, + .data = ELF_DATA_2LSB, + .type = ELF_TYPE_DYN, +#if defined(__x86_64__) + .machine = ELF_MACHINE_X86_64, +#elif defined(__aarch64__) + .machine = ELF_MACHINE_AARCH64, +#else +#error "Unsupported POSIX architecture for ELF machine check" +#endif +}; +#endif + +// Validates an already-resolved binary path (see resolve_app_path()) before it's handed to +// dlopen(): the extension check is a cheap string comparison, so the file is only opened as a +// last resort. +bool is_executable_file(const std::string& resolved_path) { + if (!resolved_path.ends_with(".so")) { + return false; + } +#ifdef __APPLE__ + // The simulator's app binaries are Mach-O on macOS, not ELF, so there is no header to check. + return is_regular_file(resolved_path); +#else + return elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS); +#endif +} + // location.location can be either an app's install directory or the .so file directly; the // former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so, // mirroring app_esp32_loader_service.cpp's resolve_elf_path(). @@ -56,6 +87,11 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { return error; } + if (!is_executable_file(app_path)) { + LOG_E(TAG, "Not executable: %s", app_path.c_str()); + return ERROR_NOT_ALLOWED; + } + LOG_I(TAG, "Loading %s", app_path.c_str()); // RTLD_NOW: a missing symbol fails here, not mid-run(). RTLD_LOCAL: this app's own exported @@ -100,10 +136,24 @@ void api_unload(AppRuntime runtime_ptr) { delete runtime; } +bool api_is_executable(AppLocation location) { + if (location.type != APP_LOCATION_PATH) { + return false; + } + + std::string app_path; + if (resolve_app_path(static_cast(location.location), app_path) != ERROR_NONE) { + return false; + } + + return is_executable_file(app_path); +} + AppLoaderApi loader_api = { .load = api_load, .run = api_run, .unload = api_unload, + .is_executable = api_is_executable, }; void* create_service(const ServiceManifest*) { diff --git a/Modules/app-posix-module/tests/CMakeLists.txt b/Modules/app-posix-module/tests/CMakeLists.txt index 11d2d6654..41f59cb0e 100644 --- a/Modules/app-posix-module/tests/CMakeLists.txt +++ b/Modules/app-posix-module/tests/CMakeLists.txt @@ -11,6 +11,10 @@ add_library(app_posix_module_test_fixture SHARED EXCLUDE_FROM_ALL ${CMAKE_CURREN target_include_directories(app_posix_module_test_fixture PRIVATE ${CMAKE_SOURCE_DIR}/Modules/app-module/include) set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPENDENT_CODE ON) +# A file with a ".so" extension but no ELF header, for is_executable() rejection tests. +set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so") +file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file") + file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp) add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES}) add_dependencies(AppPosixModuleTests app_posix_module_test_fixture) @@ -18,6 +22,7 @@ add_dependencies(AppPosixModuleTests app_posix_module_test_fixture) target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC}) target_compile_definitions(AppPosixModuleTests PRIVATE FIXTURE_APP_PATH="$" + FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}" ) add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests) diff --git a/Modules/app-posix-module/tests/source/loader_test.cpp b/Modules/app-posix-module/tests/source/loader_test.cpp index 03e85dc76..9ab8953a9 100644 --- a/Modules/app-posix-module/tests/source/loader_test.cpp +++ b/Modules/app-posix-module/tests/source/loader_test.cpp @@ -2,8 +2,10 @@ #include "doctest.h" #include +#include #include #include +#include #include #include @@ -11,6 +13,7 @@ #include #include +#include extern ServiceManifest loader_service_manifest; // app-posix-module's own extern ServiceManifest app_internal_loader_service_manifest; // app-module's real memory loader @@ -29,6 +32,13 @@ void ensure_memory_loader_registered() { } } +std::string directory_of(const std::string& path) { + auto slash = path.find_last_of('/'); + return slash == std::string::npos ? "." : path.substr(0, slash); +} + +const std::string FIXTURE_DIR = directory_of(FIXTURE_APP_PATH); + bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) { uint32_t waited = 0; while (waited < timeout_ms) { @@ -41,6 +51,11 @@ bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ return app_manager_get_state(id) == target; } +bool is_executable_path(const char* path) { + AppLocation location { APP_LOCATION_PATH, const_cast(path) }; + return app_is_executable(location); +} + std::atomic g_fixture_result { -1 }; std::atomic g_fixture_result_received { false }; @@ -62,7 +77,7 @@ int32_t parent_app_main(int, char*[]) { app_manager_add(&fixture_manifest); AppInstanceId fixture_id = 0; - app_manager_start_for_result("test.posix.fixture", self_id, 0, nullptr, &fixture_id); + app_start_for_result("test.posix.fixture", 0, nullptr, self_id, &fixture_id); while (true) { if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) { @@ -100,7 +115,7 @@ TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE); AppInstanceId parent_id = 0; - REQUIRE_EQ(app_manager_start("test.posix.parent", &parent_id), ERROR_NONE); + REQUIRE_EQ(app_start("test.posix.parent", 0, nullptr, &parent_id), ERROR_NONE); REQUIRE(wait_for_state(parent_id, APP_INSTANCE_STATE_STOPPED, 3000)); CHECK(g_fixture_result_received.load(std::memory_order_acquire)); @@ -111,3 +126,28 @@ TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its app_manager_remove("test.posix.parent"); } + +TEST_CASE("app_is_executable() accepts a real .so") { + ensure_path_loader_registered(); + + CHECK(is_executable_path(FIXTURE_APP_PATH)); +} + +TEST_CASE("app_is_executable() rejects a .so-named file with no ELF header") { + ensure_path_loader_registered(); + + CHECK_FALSE(is_executable_path(FIXTURE_NON_ELF_PATH)); +} + +TEST_CASE("app_is_executable() rejects a nonexistent path") { + ensure_path_loader_registered(); + + CHECK_FALSE(is_executable_path((FIXTURE_DIR + "/does-not-exist.so").c_str())); +} + +TEST_CASE("app_is_executable() rejects an install-directory-shaped path missing its per-arch .so") { + ensure_path_loader_registered(); + + // FIXTURE_DIR itself has no elf/posix-.so under it, so resolution fails. + CHECK_FALSE(is_executable_path(FIXTURE_DIR.c_str())); +} diff --git a/Tactility/Include/Tactility/app/fileselection/FileSelection.h b/Tactility/Include/Tactility/app/fileselection/FileSelection.h index 2111b3604..ae962d235 100644 --- a/Tactility/Include/Tactility/app/fileselection/FileSelection.h +++ b/Tactility/Include/Tactility/app/fileselection/FileSelection.h @@ -10,7 +10,7 @@ namespace tt::app::fileselection { /** * Show a file selection dialog that allows the user to select an existing file, as a modal - * child of @a callerAppInstanceId (see app_manager_start_for_result_with_streams()). Result + * child of @a callerAppInstanceId (see app_start_for_result_with_streams()). Result * (0 = Ok, 1 = Cancelled) is delivered back via APP_EVENT_RESULT once this app's thread exits. * On result == 0, read the picked path with app_stream_read(&stream, ...) then * app_stream_unsubscribe(&stream); on any other result, just app_stream_unsubscribe(&stream). diff --git a/Tactility/Include/Tactility/app/inputdialog/InputDialog.h b/Tactility/Include/Tactility/app/inputdialog/InputDialog.h index 28fc089a4..ee42c7556 100644 --- a/Tactility/Include/Tactility/app/inputdialog/InputDialog.h +++ b/Tactility/Include/Tactility/app/inputdialog/InputDialog.h @@ -1,8 +1,12 @@ #pragma once +#include #include #include +#include +#include + /** * Show a dialog with a title, a message and a text field. */ @@ -10,19 +14,21 @@ namespace tt::app::inputdialog { /** * Show a dialog with the provided title, message and prefilled text, as a modal child of - * @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the result - * as an APP_EVENT_RESULT in its own event loop: 0 = OK (call getLastText() for the entered - * text), 1 = Cancelled or dismissed without a press. The caller is responsible for calling - * app_manager_stop() on the returned instance id once it has handled the result. + * @a callerAppInstanceId (see app_start_for_result_with_streams()). Result (0 = OK, 1 = + * Cancelled or dismissed without a press) is delivered back via APP_EVENT_RESULT once this app's + * thread exits. On result == 0, read the entered text with app_stream_read(&stream, ...) then + * app_stream_unsubscribe(&stream); on any other result, just app_stream_unsubscribe(&stream). + * The caller must call app_manager_stop() on the returned instance id once that event arrives, + * to fully reap this instance. + * @param[in,out] stream caller-owned storage bound to the started app's stdout; must stay valid + * until app_stream_unsubscribe() is called on it (see above). + * @param[in] buffer caller-owned backing storage for @a stream's ring buffer; must stay valid + * for the same duration as @a stream. + * @param[in] bufferCapacity size of @a buffer in bytes. + * @param[in] eventGroup the caller's own event group, reused for the stream's readiness bits + * (see app_stream_subscribe()). The caller isn't required to actually wait on them itself. * @return the new dialog's app instance id */ -uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled = ""); - -/** - * @return the text entered the last time any InputDialog instance was closed with OK. Only one - * dialog is expected to be open at a time - call this right after receiving its - * APP_EVENT_RESULT with result == 0. - */ -std::string getLastText(); +uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup); } diff --git a/Tactility/Include/Tactility/app/wifimanage/WifiManage.h b/Tactility/Include/Tactility/app/wifimanage/WifiManage.h index 2c187bb1d..bbdafa586 100644 --- a/Tactility/Include/Tactility/app/wifimanage/WifiManage.h +++ b/Tactility/Include/Tactility/app/wifimanage/WifiManage.h @@ -5,7 +5,7 @@ namespace tt::app::wifimanage { /** - * Starts as a modal child of @a callerAppInstanceId (see app_manager_start_for_result()) - an + * Starts as a modal child of @a callerAppInstanceId (see app_start_for_result()) - an * APP_EVENT_RESULT is delivered back once the user closes this screen (default Cancelled/no * bundle if never explicitly set - callers that just want a "the wifi step is done" signal, like * Setup, can ignore the actual result value). diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/files/View.h index ab3fa7f81..51c34a41b 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/files/View.h @@ -2,6 +2,9 @@ #include "./State.h" +#include +#include + #include #include #include @@ -10,8 +13,12 @@ namespace tt::app::files { class View final { std::shared_ptr state; + TaskEventGroup* eventGroup = nullptr; uint32_t appInstanceId = 0; + AppStream inputDialogStream {}; + uint8_t inputDialogBuffer[256] {}; + size_t current_start_index = 0; size_t last_loaded_index = 0; const size_t MAX_BATCH = 50; @@ -30,14 +37,16 @@ class View final { void showActionsForDirectory(); void showActionsForFile(); void showActionsForMountPoint(); + void addCommonFileActions(); void viewFile(const std::string&path, const std::string&filename); + void runFile(const std::string& file_path); void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry); void onNavigate(); public: - explicit View(const std::shared_ptr& state) : state(state) {} + View(const std::shared_ptr& state, TaskEventGroup* eventGroup) : state(state), eventGroup(eventGroup) {} void init(uint32_t appInstanceId, lv_obj_t* parent); void update(size_t start_index = 0); @@ -54,6 +63,7 @@ class View final { void onCutPressed(); void onPastePressed(); void onEjectPressed(); + void onRunPressed(); void onDirEntryListScrollBegin(); void onResult(uint32_t launchId, int32_t result); void deinit(); diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index d2e30b1f8..cd9f7dec1 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -571,7 +572,7 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) { // It's a new-model (app-module + window-manager) app now, replacing the old app::start(). app_manager_add(&app::boot::manifest); uint32_t boot_instance_id = 0; - app_manager_start(app::boot::manifest.id, &boot_instance_id); + app_start(app::boot::manifest.id, 0, nullptr, &boot_instance_id); LOG_I(TAG, "Main dispatcher ready"); while (true) { diff --git a/Tactility/Source/app/alertdialog/AlertDialog.cpp b/Tactility/Source/app/alertdialog/AlertDialog.cpp index 0ff79276e..4d7b9aa09 100644 --- a/Tactility/Source/app/alertdialog/AlertDialog.cpp +++ b/Tactility/Source/app/alertdialog/AlertDialog.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -132,7 +133,7 @@ int32_t appMain(int argc, char* argv[]) { namespace { -// Builds argv = [title, message, buttonLabels...] for app_manager_start_for_result(). +// Builds argv = [title, message, buttonLabels...] for app_start_for_result(). std::vector buildArgv(const std::string& title, const std::string& message, const std::vector& buttonLabels) { std::vector argv { title.c_str(), message.c_str() }; for (const auto& label: buttonLabels) { @@ -146,7 +147,7 @@ std::vector buildArgv(const std::string& title, const std::string& uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::vector& buttonLabels) { auto argv = buildArgv(title, message, buttonLabels); uint32_t instanceId = 0; - app_manager_start_for_result(manifest.id, callerAppInstanceId, static_cast(argv.size()), argv.data(), &instanceId); + app_start_for_result(manifest.id, static_cast(argv.size()), argv.data(), callerAppInstanceId, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/appdetails/AppDetails.cpp b/Tactility/Source/app/appdetails/AppDetails.cpp index d716511b6..4afd45d21 100644 --- a/Tactility/Source/app/appdetails/AppDetails.cpp +++ b/Tactility/Source/app/appdetails/AppDetails.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -158,7 +159,7 @@ int32_t appMain(int argc, char* argv[]) { void start(const std::string& appId) { const char* argv[] = { appId.c_str() }; uint32_t instanceId = 0; - app_manager_start_with_parameters(manifest.id, 1, argv, &instanceId); + app_start(manifest.id, 1, argv, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp index bf21cb6c8..4d879d7de 100644 --- a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp +++ b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -351,7 +352,7 @@ void start(const apphub::AppHubEntry& entry) { argv.push_back(platform.c_str()); } uint32_t instanceId = 0; - app_manager_start_for_result(manifest.id, /*parent_instance_id=*/0, static_cast(argv.size()), argv.data(), &instanceId); + app_start_for_result(manifest.id, static_cast(argv.size()), argv.data(), /*parent_instance_id=*/0, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/applist/AppList.cpp b/Tactility/Source/app/applist/AppList.cpp index c7b0effd8..6ef4e6367 100644 --- a/Tactility/Source/app/applist/AppList.cpp +++ b/Tactility/Source/app/applist/AppList.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -28,7 +29,7 @@ void onAppPressed(lv_event_t* e) { // Fire-and-forget top-level navigation, same as Launcher's own app-launch buttons. const auto* manifest = static_cast(lv_event_get_user_data(e)); uint32_t instanceId = 0; - app_manager_start(manifest->id, &instanceId); + app_start(manifest->id, 0, nullptr, &instanceId); } void onBackPressed(lv_event_t* event) { diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index aa1b24f33..983302dd0 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -251,7 +252,7 @@ void startNextApp() { auto launcher_app_id = getLauncherAppId(); uint32_t launcher_instance_id = 0; - app_manager_start(launcher_app_id.c_str(), &launcher_instance_id); + app_start(launcher_app_id.c_str(), 0, nullptr, &launcher_instance_id); } void runBootSequence(TickType_t startTime) { @@ -318,7 +319,7 @@ int32_t appMain(int argc, char* argv[]) { runBootSequence(start_time); - // Waits until app_manager_start(launcher) (or a permanent stop) tells us to give up - + // Waits until app_start(launcher) (or a permanent stop) tells us to give up - // startNextApp() above is what triggers that, via app-module's "save the previously active // app" policy, unless sdCardMissing halted before it. while (true) { diff --git a/Tactility/Source/app/btmanage/BtManage.cpp b/Tactility/Source/app/btmanage/BtManage.cpp index 112aac051..8b1a327a4 100644 --- a/Tactility/Source/app/btmanage/BtManage.cpp +++ b/Tactility/Source/app/btmanage/BtManage.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -236,7 +237,7 @@ int32_t appMain(int argc, char* argv[]) { uint32_t start() { uint32_t instanceId = 0; - app_manager_start(manifest.id, &instanceId); + app_start(manifest.id, 0, nullptr, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp index d67fbec2a..c339332cd 100644 --- a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp +++ b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -262,7 +263,7 @@ int32_t appMain(int argc, char* argv[]) { void start(const std::string& addrHex) { const char* argv[] = { addrHex.c_str() }; uint32_t instanceId = 0; - app_manager_start_with_parameters(manifest.id, 1, argv, &instanceId); + app_start(manifest.id, 1, argv, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp index e20ec93ab..fdcd7ab59 100644 --- a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp +++ b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -272,7 +273,7 @@ int32_t appMain(int argc, char* argv[]) { void start() { uint32_t instanceId = 0; - app_manager_start(manifest.id, &instanceId); + app_start(manifest.id, 0, nullptr, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index b4fae6797..f27c24871 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -184,7 +185,7 @@ int32_t appMain(int argc, char* argv[]) { if (lvgl_is_running()) { lvgl_lock(); // Widgets only exist while this window is topmost - skip otherwise. Another app - // (started non-modally, e.g. via app_manager_start()) can bury this window without + // (started non-modally, e.g. via app_start()) can bury this window without // stopping this instance or notifying it; window_manager deletes a buried window's // widgets, so touching ctx->statusLabel here would use-after-free it. if (window_manager_get_state(window) == WINDOW_STATE_GRANTED) { diff --git a/Tactility/Source/app/files/FilesApp.cpp b/Tactility/Source/app/files/FilesApp.cpp index c08d48423..16b5dd7e4 100644 --- a/Tactility/Source/app/files/FilesApp.cpp +++ b/Tactility/Source/app/files/FilesApp.cpp @@ -31,12 +31,13 @@ void createWidgets(lv_obj_t* parent, void* userData) { int32_t appMain(int argc, char* argv[]) { uint32_t appInstanceId = app_scheduler_current_app_id(); auto state = std::make_shared(); - View view(state); - CreateContext createContext { &view, appInstanceId }; TaskEventGroup event_group {}; task_event_group_construct(&event_group); + View view(state, &event_group); + CreateContext createContext { &view, appInstanceId }; + AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 7512be500..c211a28d7 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -1,5 +1,7 @@ -#include #include +#include +#include +#include #include #include @@ -102,10 +104,20 @@ static void onPastePressedCallback(lv_event_t* event) { view->onPastePressed(); } +static void onRunPressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onRunPressed(); +} + // endregion // region File helpers +static bool isExecutablePath(const std::string& path) { + AppLocation location { APP_LOCATION_PATH, const_cast(path.c_str()) }; + return app_is_executable(location); +} + static bool copyFileContents(const std::string& src, const std::string& dst) { FILE* in = fopen(src.c_str(), "rb"); if (in == nullptr) { @@ -192,6 +204,8 @@ void View::viewFile(const std::string& path, const std::string& filename) { // Remove forward slash, because we need a relative path notes::start(file_path.substr(1)); } + } else if (isExecutablePath(file_path)) { + runFile(file_path); } else { LOG_W(TAG, "Opening files of this type is not supported"); } @@ -199,6 +213,23 @@ void View::viewFile(const std::string& path, const std::string& filename) { onNavigate(); } +void View::runFile(const std::string& file_path) { + LOG_I(TAG, "Running %s", file_path.c_str()); + + if (!isExecutablePath(file_path)) { + LOG_W(TAG, "Not executable: %s", file_path.c_str()); + alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\"."); + return; + } + + AppLocation location { APP_LOCATION_PATH, const_cast(file_path.c_str()) }; + AppInstanceId instance_id = 0; + if (app_execute(location, AppStackConfig {}, 0, nullptr, &instance_id) != ERROR_NONE) { + LOG_W(TAG, "Failed to run %s", file_path.c_str()); + alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\"."); + } +} + bool View::resolveDirentFromListIndex(int32_t list_index, dirent& out_entry) { const bool is_root = (state->getCurrentPath() == "/"); const bool has_back = (!is_root && current_start_index > 0); @@ -287,6 +318,8 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { symbol = LV_SYMBOL_IMAGE; } else if (dir_entry.d_type == file::TT_DT_LNK) { symbol = LV_SYMBOL_LOOP; + } else if (isExecutablePath(file::getChildPath(state->getCurrentPath(), dir_entry.d_name))) { + symbol = LV_SYMBOL_PLAY; } else { symbol = LV_SYMBOL_FILE; } @@ -346,7 +379,7 @@ void View::onRenamePressed() { std::string entry_name = state->getSelectedChildEntry(); LOG_I(TAG, "Pending rename %s", entry_name.c_str()); state->setPendingAction(State::ActionRename); - inputdialog::start(appInstanceId, "Rename", "", entry_name); + inputdialog::start(appInstanceId, "Rename", "", entry_name, inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup); } void View::onDeletePressed() { @@ -361,18 +394,16 @@ void View::onDeletePressed() { void View::onNewFilePressed() { LOG_I(TAG, "Creating new file"); state->setPendingAction(State::ActionCreateFile); - inputdialog::start(appInstanceId, "New File", "Enter filename:", ""); + inputdialog::start(appInstanceId, "New File", "Enter filename:", "", inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup); } void View::onNewFolderPressed() { LOG_I(TAG, "Creating new folder"); state->setPendingAction(State::ActionCreateFolder); - inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", ""); + inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", "", inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup); } -void View::showActions() { - lv_obj_clean(action_list); - +void View::addCommonFileActions() { auto* copy_button = lv_list_add_button(action_list, LV_SYMBOL_COPY, "Copy"); lv_obj_add_event_cb(copy_button, onCopyPressedCallback, LV_EVENT_SHORT_CLICKED, this); auto* cut_button = lv_list_add_button(action_list, LV_SYMBOL_CUT, "Cut"); @@ -381,12 +412,27 @@ void View::showActions() { lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this); auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete"); lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this); +} +void View::showActions() { + lv_obj_clean(action_list); + addCommonFileActions(); lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN); } void View::showActionsForDirectory() { showActions(); } -void View::showActionsForFile() { showActions(); } + +void View::showActionsForFile() { + lv_obj_clean(action_list); + + if (isExecutablePath(state->getSelectedChildPath())) { + auto* run_button = lv_list_add_button(action_list, LV_SYMBOL_PLAY, "Run"); + lv_obj_add_event_cb(run_button, onRunPressedCallback, LV_EVENT_SHORT_CLICKED, this); + } + + addCommonFileActions(); + lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN); +} void View::showActionsForMountPoint() { lv_obj_clean(action_list); @@ -397,6 +443,12 @@ void View::showActionsForMountPoint() { lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN); } +void View::onRunPressed() { + std::string file_path = state->getSelectedChildPath(); + onNavigate(); + runFile(file_path); +} + void View::onEjectPressed() { std::string mount_path = state->getSelectedChildPath(); LOG_I(TAG, "Ejecting %s", mount_path.c_str()); @@ -546,10 +598,21 @@ void View::onResult(uint32_t launchId, int32_t result) { std::string filepath = state->getSelectedChildPath(); LOG_I(TAG, "Result for %s", filepath.c_str()); - // Text-entry result (rename/new file/new folder); empty for Cancel, or for a dialog that - // doesn't produce text (delete/paste confirmations) - those switch cases below only look at - // `result`, not this. - std::string resultText = (result == 0) ? inputdialog::getLastText() : std::string(); + // Text-entry result (rename/new file/new folder), read from the AppStream bound to that + // dialog's stdout. Empty for Cancel. Other pending actions (delete/paste confirmations) never + // bound this stream; their switch cases below only look at `result`, not `resultText`. + bool isTextEntryAction = state->getPendingAction() == State::ActionRename || + state->getPendingAction() == State::ActionCreateFile || + state->getPendingAction() == State::ActionCreateFolder; + std::string resultText; + if (isTextEntryAction) { + if (result == 0) { + char buffer[sizeof(inputDialogBuffer)]; + size_t length = app_stream_read(&inputDialogStream, buffer, sizeof(buffer)); + resultText = std::string(buffer, length); + } + app_stream_unsubscribe(&inputDialogStream); + } switch (state->getPendingAction()) { case State::ActionDelete: { diff --git a/Tactility/Source/app/fileselection/FileSelection.cpp b/Tactility/Source/app/fileselection/FileSelection.cpp index dd28b5236..3e98e2c2f 100644 --- a/Tactility/Source/app/fileselection/FileSelection.cpp +++ b/Tactility/Source/app/fileselection/FileSelection.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -106,7 +107,7 @@ uint32_t startWithMode(const char* modeArg, uint32_t callerAppInstanceId, AppStr .event_group = eventGroup, }; uint32_t instanceId = 0; - app_manager_start_for_result_with_streams(manifest.id, callerAppInstanceId, 1, argv, &binding, 1, &instanceId); + app_start_for_result_with_streams(manifest.id, 1, argv, &binding, 1, callerAppInstanceId, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index d24624cc5..f3798cd2d 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -73,7 +74,7 @@ void onAddGpsPressed(lv_event_t* event) { // this app; rebuildDeviceList() runs fresh whenever this app is resumed regardless). (void)ctx; uint32_t instanceId = 0; - app_manager_start(addgps::manifest.id, &instanceId); + app_start(addgps::manifest.id, 0, nullptr, &instanceId); } void onDeviceButtonPressed(lv_event_t* event) { diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index 0f49507ea..87fc23db9 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -430,7 +431,7 @@ extern const ::AppManifest manifest = { uint32_t start() { uint32_t instanceId = 0; - app_manager_start(manifest.id, &instanceId); + app_start(manifest.id, 0, nullptr, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/imageviewer/ImageViewer.cpp b/Tactility/Source/app/imageviewer/ImageViewer.cpp index a26bbbd03..ac64c298a 100644 --- a/Tactility/Source/app/imageviewer/ImageViewer.cpp +++ b/Tactility/Source/app/imageviewer/ImageViewer.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -123,7 +124,7 @@ int32_t appMain(int argc, char* argv[]) { void start(const std::string& file) { const char* argv[] = { file.c_str() }; uint32_t instanceId = 0; - app_manager_start_with_parameters(manifest.id, 1, argv, &instanceId); + app_start(manifest.id, 1, argv, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/inputdialog/InputDialog.cpp b/Tactility/Source/app/inputdialog/InputDialog.cpp index 4e3e57248..13ea866d1 100644 --- a/Tactility/Source/app/inputdialog/InputDialog.cpp +++ b/Tactility/Source/app/inputdialog/InputDialog.cpp @@ -2,8 +2,10 @@ #include #include +#include #include #include +#include #include @@ -13,6 +15,8 @@ #include +#include + namespace tt::app::inputdialog { constexpr auto* TAG = "InputDialog"; @@ -30,7 +34,8 @@ struct Context { // The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this // is a plain (non-atomic) field safely shared between the LVGL thread (writer, before // emitting APP_EVENT_CLOSE) and this dialog's own thread (reader, after waking from it). - int32_t result = 1; // Cancelled - safety-net default if closed without pressing a button + int32_t resultCode = 1; // Cancelled - safety-net default if closed without pressing a button + std::string resultText; }; struct ButtonContext { @@ -39,12 +44,6 @@ struct ButtonContext { lv_obj_t* textarea; }; -// The last text entered via OK. Static rather than per-instance: simple, and in practice only -// one InputDialog is ever open at a time. Written on the LVGL thread (onButtonPressed(), before -// emitting APP_EVENT_CLOSE); read by the parent via getLastText() after receiving that event - -// safe without a lock for the same reason Context::result is (see AlertDialog.cpp). -std::string lastText; - void onButtonDeleted(lv_event_t* e) { delete static_cast(lv_event_get_user_data(e)); } @@ -53,11 +52,11 @@ void onButtonPressed(lv_event_t* e) { auto* btnCtx = static_cast(lv_event_get_user_data(e)); if (btnCtx->textarea != nullptr) { LOG_I(TAG, "OK pressed"); - lastText = lv_textarea_get_text(btnCtx->textarea); - btnCtx->ctx->result = 0; + btnCtx->ctx->resultText = lv_textarea_get_text(btnCtx->textarea); + btnCtx->ctx->resultCode = 0; } else { LOG_I(TAG, "Cancel pressed"); - btnCtx->ctx->result = 1; + btnCtx->ctx->resultCode = 1; } app_event_emit_close(btnCtx->ctx->appInstanceId); } @@ -137,22 +136,30 @@ int32_t appMain(int argc, char* argv[]) { check(app_event_unsubscribe(&sub) == ERROR_NONE); task_event_group_destruct(&event_group); - return ctx.result; + if (ctx.resultCode == 0) { + // The caller captures this via an AppStream bound to our stdout (see start()); see + // AppStdioWrap.cpp for how printf() itself gets routed there on POSIX. + printf("%s", ctx.resultText.c_str()); + } + return ctx.resultCode; } } // namespace -uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled) { +uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) { const char* argv[] = { title.c_str(), message.c_str(), prefilled.c_str() }; + AppStreamBinding binding = { + .producer_fd = STDOUT_FILENO, + .stream = &stream, + .buffer = buffer, + .buffer_capacity = bufferCapacity, + .event_group = eventGroup, + }; uint32_t instanceId = 0; - app_manager_start_for_result(manifest.id, callerAppInstanceId, 3, argv, &instanceId); + app_start_for_result_with_streams(manifest.id, 3, argv, &binding, 1, callerAppInstanceId, &instanceId); return instanceId; } -std::string getLastText() { - return lastText; -} - extern const ::AppManifest manifest = { .id = "tactility.inputdialog", .name = "Input Dialog", diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index f899a5e42..63a9d9865 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -45,7 +46,7 @@ int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) { void onAppPressed(lv_event_t* e) { auto* appId = static_cast(lv_event_get_user_data(e)); uint32_t instance_id = 0; - app_manager_start(appId, &instance_id); + app_start(appId, 0, nullptr, &instance_id); } lv_obj_t* createAppButton(lv_obj_t* parent, UiDensity uiDensity, const char* imageFile, const char* appId, int32_t itemMargin, bool isLandscape) { @@ -214,7 +215,7 @@ void runAutoStart() { ) { LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID); uint32_t app_launch_id; - app_manager_start(CONFIG_TT_AUTO_START_APP_ID, &app_launch_id); + app_start(CONFIG_TT_AUTO_START_APP_ID, 0, nullptr, &app_launch_id); } else if ( // Auto-start due to user configuration settings::loadBootSettings(boot_properties) && @@ -223,7 +224,7 @@ void runAutoStart() { ) { LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str()); uint32_t app_launch_id; - app_manager_start(boot_properties.autoStartAppId.c_str(), &app_launch_id); + app_start(boot_properties.autoStartAppId.c_str(), 0, nullptr, &app_launch_id); } else { // No auto-start, consider running system setup if (!setup::isCompleted()) { @@ -282,7 +283,7 @@ extern const ::AppManifest manifest = { // used by the old, unconverted CrashDiagnostics app to return to the launcher after a crash). uint32_t start() { uint32_t instance_id = 0; - app_manager_start(manifest.id, &instance_id); + app_start(manifest.id, 0, nullptr, &instance_id); return instance_id; } diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index 381d618aa..24c1fc657 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -268,7 +269,7 @@ int32_t appMain(int argc, char* argv[]) { void start(const std::string& filePath) { const char* argv[] = { filePath.c_str() }; uint32_t instanceId = 0; - app_manager_start_with_parameters(manifest.id, 1, argv, &instanceId); + app_start(manifest.id, 1, argv, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index 96442721e..5f9927b1b 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -133,7 +134,7 @@ int32_t appMain(int argc, char* argv[]) { namespace { -// Builds argv = [title, items...] for app_manager_start_for_result(). +// Builds argv = [title, items...] for app_start_for_result(). std::vector buildArgv(const std::string& title, const std::vector& items) { std::vector argv { title.c_str() }; for (const auto& item: items) { @@ -147,7 +148,7 @@ std::vector buildArgv(const std::string& title, const std::vector& items) { auto argv = buildArgv(title, items); AppInstanceId instanceId = 0; - app_manager_start_for_result(manifest.id, callerAppInstanceId, static_cast(argv.size()), argv.data(), &instanceId); + app_start_for_result(manifest.id, static_cast(argv.size()), argv.data(), callerAppInstanceId, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp index 8b05f4ccc..f623618f0 100644 --- a/Tactility/Source/app/settings/Settings.cpp +++ b/Tactility/Source/app/settings/Settings.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -28,7 +29,7 @@ void onAppPressed(lv_event_t* e) { // Fire-and-forget top-level navigation, same as AppList's own app-launch buttons. const auto* manifest = static_cast(lv_event_get_user_data(e)); uint32_t instanceId = 0; - app_manager_start(manifest->id, &instanceId); + app_start(manifest->id, 0, nullptr, &instanceId); } void onBackPressed(lv_event_t* event) { diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp index b06e2831b..42047b9b2 100644 --- a/Tactility/Source/app/setup/Setup.cpp +++ b/Tactility/Source/app/setup/Setup.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -269,7 +270,7 @@ int32_t appMain(int argc, char* argv[]) { void start() { uint32_t instanceId = 0; - app_manager_start(manifest.id, &instanceId); + app_start(manifest.id, 0, nullptr, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index 8a5da22d1..c52c486b5 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -206,7 +207,7 @@ int32_t appMain(int argc, char* argv[]) { uint32_t start() { uint32_t instanceId = 0; - app_manager_start(manifest.id, &instanceId); + app_start(manifest.id, 0, nullptr, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/timezone/TimeZone.cpp b/Tactility/Source/app/timezone/TimeZone.cpp index 9ef2f9f37..f10850667 100644 --- a/Tactility/Source/app/timezone/TimeZone.cpp +++ b/Tactility/Source/app/timezone/TimeZone.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -273,7 +274,7 @@ int32_t appMain(int argc, char* argv[]) { uint32_t start(uint32_t callerAppInstanceId, bool saveTimeZone) { const char* argv[] = { saveTimeZone ? "1" : "0" }; uint32_t instanceId = 0; - app_manager_start_for_result(manifest.id, callerAppInstanceId, 1, argv, &instanceId); + app_start_for_result(manifest.id, 1, argv, callerAppInstanceId, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/touchcalibration/TouchCalibration.cpp b/Tactility/Source/app/touchcalibration/TouchCalibration.cpp index 96867584d..7db51338f 100644 --- a/Tactility/Source/app/touchcalibration/TouchCalibration.cpp +++ b/Tactility/Source/app/touchcalibration/TouchCalibration.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -285,7 +286,7 @@ int32_t appMain(int argc, char* argv[]) { uint32_t start(uint32_t callerAppInstanceId) { uint32_t instanceId = 0; - app_manager_start_for_result(manifest.id, callerAppInstanceId, 0, nullptr, &instanceId); + app_start_for_result(manifest.id, 0, nullptr, callerAppInstanceId, &instanceId); return instanceId; } diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index cf4b697f7..581b91066 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -317,7 +318,7 @@ int32_t appMain(int argc, char* argv[]) { void start(const std::string& ssid) { const char* argv[] = { ssid.c_str() }; uint32_t instanceId = 0; - app_manager_start_with_parameters(manifest.id, 1, argv, &instanceId); + app_start(manifest.id, 1, argv, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index 08ccf333f..3cc324605 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -377,7 +378,7 @@ int32_t appMain(int argc, char* argv[]) { void start(const std::string& ssid, const std::string& password) { const char* argv[] = { ssid.c_str(), password.c_str() }; uint32_t instanceId = 0; - app_manager_start_with_parameters(manifest.id, 2, argv, &instanceId); + app_start(manifest.id, 2, argv, &instanceId); } extern const ::AppManifest manifest = { diff --git a/Tactility/Source/app/wifimanage/WifiManage.cpp b/Tactility/Source/app/wifimanage/WifiManage.cpp index 935f8b400..fdc97442b 100644 --- a/Tactility/Source/app/wifimanage/WifiManage.cpp +++ b/Tactility/Source/app/wifimanage/WifiManage.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -240,7 +241,7 @@ int32_t appMain(int argc, char* argv[]) { uint32_t start(uint32_t callerAppInstanceId) { uint32_t instanceId = 0; - app_manager_start_for_result(manifest.id, callerAppInstanceId, 0, nullptr, &instanceId); + app_start_for_result(manifest.id, 0, nullptr, callerAppInstanceId, &instanceId); return instanceId; } diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp index 833fcf714..232c0687e 100644 --- a/Tactility/Source/service/development/DevelopmentService.cpp +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -113,7 +114,7 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { } } - app_manager_start(id_key_pos->second.c_str(), &instance_id); + app_start(id_key_pos->second.c_str(), 0, nullptr, &instance_id); LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str()); httpd_resp_send(request, nullptr, 0); diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index e3af7ca3d..529d0e404 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -2,6 +2,8 @@ #include #include + +#include #include #include #include @@ -1270,7 +1272,7 @@ esp_err_t WebServerService::handleApiAppsRun(httpd_req_t* request) { // Every app instance gets its own task now, so there's no "stop the existing one first" - // this just starts a fresh instance alongside whatever's already running. AppInstanceId instance_id = 0; - app_manager_start(appId.c_str(), &instance_id); + app_start(appId.c_str(), 0, nullptr, &instance_id); LOG_I(TAG, "[200] /api/apps/run %s", appId.c_str()); httpd_resp_sendstr(request, "ok"); diff --git a/Tests/SdkIntegration/main/CMakeLists.txt b/Tests/SdkIntegration/main/CMakeLists.txt index 075f19a11..54bb59414 100644 --- a/Tests/SdkIntegration/main/CMakeLists.txt +++ b/Tests/SdkIntegration/main/CMakeLists.txt @@ -1,7 +1,4 @@ -if (NOT DEFINED TACTILITY_SDK_PATH) - get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_LIST_DIR}/..") -endif () -include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") file(GLOB_RECURSE SOURCE_FILES Source/*.c) tactility_component_register(SRCS ${SOURCE_FILES} INCLUDE_DIRS include) \ No newline at end of file diff --git a/Tests/SdkIntegration/tactility.py b/Tests/SdkIntegration/tactility.py index f57d2fef6..91d6c2f37 100644 --- a/Tests/SdkIntegration/tactility.py +++ b/Tests/SdkIntegration/tactility.py @@ -12,7 +12,7 @@ from urllib.parse import urlparse ttbuild_path = ".tactility" -ttbuild_version = "5.0.0" +ttbuild_version = "5.0.1" ttbuild_cdn = "https://cdn.tactilityproject.org" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666 @@ -140,9 +140,13 @@ def get_sdk_dir(version, platform): sdk_dir = os.path.join(sdk_parent_dir, "TactilitySDK") if not os.path.isdir(sdk_dir): exit_with_error(f"Local SDK folder not found for platform {platform}: {sdk_dir}") - return sdk_dir + return os.path.abspath(sdk_dir) else: - return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") + # Must be absolute: this is exported as $TACTILITY_SDK_PATH and included by each app's + # main/CMakeLists.txt, which ESP-IDF also re-processes in a separate `cmake -P` subprocess + # (tools/cmake/scripts/component_get_requirements.cmake) with its own working directory - + # a relative path here resolves against whatever CWD that subprocess happens to have. + return os.path.abspath(os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK")) def validate_local_sdks(platforms, version): if not use_local_sdk: