Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Modules/app-esp32-module/source/app_esp32_loader_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <sdkconfig.h>
#endif

#include <app/elf_check.h>
#include <app/loader.h>
#include <app/location.h>

Expand Down Expand Up @@ -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.
Comment thread
KenVanHoeylandt marked this conversation as resolved.
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");
Expand All @@ -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) {
Expand Down Expand Up @@ -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*) {
Expand Down
38 changes: 38 additions & 0 deletions Modules/app-module/include/app/elf_check.h
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions Modules/app-module/include/app/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
99 changes: 99 additions & 0 deletions Modules/app-module/include/app/execute.h
Original file line number Diff line number Diff line change
@@ -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
17 changes: 8 additions & 9 deletions Modules/app-module/include/app/loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
#include "location.h"

Expand All @@ -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;
Expand All @@ -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
Expand Down
80 changes: 3 additions & 77 deletions Modules/app-module/include/app/manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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
Expand Down
Loading
Loading