Run executables directly (#645)

- Apps can be launched directly from file paths (including Files app support)
- Added executable detection to identify unsupported or invalid binaries before launch.
- App startup is now streamlined through separate registered-app and direct-execution interfaces.
- Existing app launch points were migrated to the updated startup APIs.
This commit is contained in:
Ken Van Hoeylandt
2026-09-04 21:23:08 +02:00
committed by GitHub
parent 643cbc3806
commit a0b2ee7ebc
63 changed files with 1276 additions and 347 deletions
@@ -3,6 +3,7 @@
#include <sdkconfig.h>
#endif
#include <app/elf_check.h>
#include <app/loader.h>
#include <app/location.h>
@@ -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<const char*>(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<const char*>(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*) {
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// <elf.h> 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
+2 -2
View File
@@ -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;
};
+99
View File
@@ -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 <app/manager.h>
#include <stdbool.h>
#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
+8 -9
View File
@@ -3,6 +3,7 @@
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
#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
+3 -77
View File
@@ -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
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manager.h>
/**
* 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
+1 -1
View File
@@ -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;
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cstring>
#include <new>
/**
* 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;
}
@@ -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;
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manager.h>
#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
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/elf_check.h>
#include <cstdio>
#include <cstring>
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<uint16_t>(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;
}
+55
View File
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/execute.h>
#include <app/loader.h>
#include <app/location.h>
#include <app/private/manager_internal.h>
#include <service/instance.h>
#include <service/manager.h>
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<const AppLoaderApi*>(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"
@@ -1,6 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/loader.h>
#include <app/manifest.h>
#include <service/instance.h>
#include <service/manager.h>
@@ -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*) {
+32 -91
View File
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/arguments.h>
#include <app/private/fd_table.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/manager_internal.h>
#include <app/private/scheduler.h>
#include <tactility/concurrent/mutex.h>
@@ -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;
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;
}
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);
}
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 : "<unregistered>", 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(&registry.mutex);
std::unordered_map<std::string, std::string> known_paths; // id -> path
std::unordered_map<std::string, std::string> known_paths;
for (const auto& [id, record] : registry.scanned) {
known_paths.emplace(id, record->path);
}
mutex_unlock(&registry.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<std::unique_ptr<ScannedAppManifest>> 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<ScannedAppManifest>();
@@ -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<std::string> 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(&registry.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<uint32_t> 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;
}
+13 -6
View File
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <app/execute.h>
#include <app/install.h>
#include <app/io.h>
#include <app/manager.h>
@@ -7,11 +8,11 @@
#include <app/metadata.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <service/manager.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/module.h>
@@ -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
+13 -12
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/instance.h>
#include <app/loader.h>
#include <app/private/arguments.h>
#include <app/private/event.h>
#include <app/private/fd_table.h>
#include <app/private/ledger.h>
@@ -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<const AppLoaderApi*>(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);
+49
View File
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/start.h>
#include <app/private/ledger.h>
#include <app/private/manager_internal.h>
#include <tactility/concurrent/mutex.h>
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"
+1 -1
View File
@@ -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)
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
#include "doctest.h"
#include <app/private/arguments.h>
#include <cstring>
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<const void*>(copy[0]), static_cast<const void*>(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);
}
@@ -0,0 +1,323 @@
// SPDX-License-Identifier: Apache-2.0
#include "doctest.h"
#include <app/event.h>
#include <app/execute.h>
#include <app/io.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <app/stream.h>
#include <service/manager.h>
#include <tactility/delay.h>
#include <tactility/freertos/task.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
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<int32_t>(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<size_t>(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<size_t>(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<void*>(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<void*>(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<void*>(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, &registered_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<void*>(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<void*>(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<void*>(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<uint8_t> 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<void*>(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<uint8_t> 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<void*>(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<void*>(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<uint8_t> 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");
}
+8 -7
View File
@@ -4,6 +4,7 @@
#include <app/io.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <app/stream.h>
@@ -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<void*>(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<uint8_t> 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);
@@ -3,6 +3,7 @@
#include <app/event.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <service/manager.h>
@@ -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.
@@ -4,6 +4,7 @@
#include <app/event.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <app/stream.h>
@@ -75,7 +76,7 @@ AppInstanceId start_idle_app(const char* id) {
AppManifest manifest { id, id, APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(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;
}
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/elf_check.h>
#include <app/loader.h>
#include <app/location.h>
@@ -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<const char*>(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*) {
@@ -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="$<TARGET_FILE:app_posix_module_test_fixture>"
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
)
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
@@ -2,8 +2,10 @@
#include "doctest.h"
#include <app/event.h>
#include <app/execute.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <service/manager.h>
@@ -11,6 +13,7 @@
#include <tactility/delay.h>
#include <atomic>
#include <string>
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<char*>(path) };
return app_is_executable(location);
}
std::atomic<int32_t> g_fixture_result { -1 };
std::atomic<bool> 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-<arch>.so under it, so resolution fails.
CHECK_FALSE(is_executable_path(FIXTURE_DIR.c_str()));
}