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:
committed by
GitHub
parent
643cbc3806
commit
a0b2ee7ebc
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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*) {
|
||||
|
||||
@@ -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(®istry.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(®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<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(®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<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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
@@ -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, ®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<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");
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user