Refactor execution logic

This commit is contained in:
Ken Van Hoeylandt 2026-09-03 23:17:03 +02:00
parent e0f3c426db
commit a58638ced2
8 changed files with 382 additions and 124 deletions

View File

@ -1,8 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <stdbool.h>
#include <tactility/error.h>
@ -41,17 +39,6 @@ bool app_exec_path_allowed(const char* file_path);
*/
bool app_exec_is_executable_path(const char* path);
/**
* Runs @a path directly, without it having to be pre-registered as an app (e.g. via
* app_manager_add() or app_install()), for a file a user picked in a file browser. Internally
* registers (app_manager_add(), APP_MANIFEST_FLAG_HIDDEN so it never shows up in app-browsing
* UIs) a manifest for @a path the first time it's run, reusing that same registration on
* subsequent calls for the same path, then starts it (app_manager_start_with_parameters()).
* @retval ERROR_NOT_ALLOWED app_exec_is_executable_path(path) is false
* @retval ERROR_NONE on success
*/
error_t app_exec_run_path(const char* path, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
#ifdef __cplusplus
}
#endif

View File

@ -128,6 +128,53 @@ error_t app_manager_start_with_streams(const char* id, const struct AppStreamBin
*/
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);
/**
* Starts an app directly from @a location, without it having to be pre-registered via
* app_manager_add() first. Independent of the id-based app_manager_start*() family: performs no
* checks of its own beyond what AppLoaderApi::load() itself rejects. Not manifest lookup, and
* not whether @a location should be allowed to run (see app_exec_is_executable_path(),
* app/exec.h, for that): callers that need a check must run it themselves before calling this.
* @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
* @param[in] argv see app_manager_start_with_parameters()
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @retval ERROR_NONE on success
*/
error_t app_manager_start_location(struct AppLocation location, struct AppStackConfig stack, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Same as app_manager_start_location(), but as a modal child of @a parent_instance_id - see
* app_manager_start_for_result()'s own doc for the result-delivery contract. Still no manifest
* and no check of its own beyond what AppLoaderApi::load() itself rejects.
* @param[in] argv see app_manager_start_with_parameters()
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @retval ERROR_NONE on success
*/
error_t app_manager_start_location_for_result(struct AppLocation location, struct AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Same as app_manager_start_location(), but installs @a bindings into the new instance's fd
* table before its task begins executing - see app_manager_start_with_streams()'s own doc for
* stream ownership.
* @param[in] bindings see app_manager_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_manager_start_location_with_streams(struct AppLocation location, struct AppStackConfig stack, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
/**
* Combines app_manager_start_location_for_result() and app_manager_start_location_with_streams().
* @param[in] argv see app_manager_start_with_parameters()
* @param[in] bindings see app_manager_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_manager_start_location_for_result_with_streams(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);
/**
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
* if it was running.

View File

@ -38,6 +38,7 @@ 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_manager_start_location() (no manifest involved). */
const AppManifest* manifest;
AppInstanceState state;
/** The FreeRTOS task currently executing AppLoaderApi::run() for this instance; NULL when not running. */

View File

@ -5,8 +5,6 @@
#include <app/loader.h>
#include <app/location.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <service/instance.h>
#include <service/manager.h>
@ -17,12 +15,10 @@
#include <tactility/paths.h>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <unistd.h>
#include <unordered_map>
constexpr auto* TAG = "app_exec";
@ -81,6 +77,19 @@ bool has_dotdot_segment(const std::string& path) {
return false;
}
// Resolves @a absolute_path through every symlink on its way (POSIX realpath()), so a symlink
// sitting inside an allowed directory can't point at a target outside it. is_under_directory()
// only ever sees where a path physically ends up, never the lexical path a caller wrote. Falls
// back to @a absolute_path unresolved if realpath() fails (e.g. nothing exists there yet): a
// nonexistent path can't be dlopen()ed either way, so comparing it unresolved is still safe.
std::string resolve_physical(const std::string& absolute_path) {
char resolved[PATH_MAX];
if (realpath(absolute_path.c_str(), resolved) == nullptr) {
return absolute_path;
}
return std::string(resolved);
}
// True when @a file_path lives strictly under @a dir (a direct or nested child). Never equal to
// @a dir itself: a bare directory is never itself an executable candidate.
bool is_under_directory(const std::string& file_path, const std::string& dir) {
@ -98,49 +107,6 @@ const AppLoaderApi* find_path_loader_api() {
return static_cast<const AppLoaderApi*>(service_instance_get_data(instance));
}
// region Ad hoc "run this path directly" registry: owns the AppManifest (and its id/name/path
// strings) that app_manager's own ledger only keeps a non-owning pointer to (see
// app_manager_add()'s contract; install.cpp's InstallRegistry exists for the same reason).
// Keyed by path so re-running the same file reuses its manifest instead of re-adding it.
struct RunRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
};
struct RunRegistry {
std::unordered_map<std::string, std::unique_ptr<RunRecord>> apps;
Mutex mutex {};
RunRegistry() { mutex_construct(&mutex); }
};
RunRegistry& run_registry() {
static RunRegistry registry;
return registry;
}
std::string last_path_segment(const std::string& path) {
auto index = path.find_last_of('/');
return index == std::string::npos ? path : path.substr(index + 1);
}
// A short, stable-per-path id that fits AppManifest::id's APP_ID_LENGTH limit regardless of how
// long @a path is. FNV-1a's collision odds are irrelevant at the scale of "distinct executables
// one user runs from the Files app".
std::string make_run_id(const std::string& path) {
uint32_t hash = 2166136261u;
for (unsigned char c: path) {
hash ^= c;
hash *= 16777619u;
}
char id[16];
std::snprintf(id, sizeof(id), "run.%08x", static_cast<unsigned int>(hash));
return id;
}
} // namespace
extern "C" {
@ -193,12 +159,16 @@ bool app_exec_path_allowed(const char* file_path) {
if (has_dotdot_segment(path)) {
return false;
}
std::string resolved_path = resolve_physical(path);
auto& reg = registry();
mutex_lock(&reg.mutex);
bool allowed = false;
for (size_t i = 0; i < reg.count; i++) {
if (is_under_directory(path, reg.paths[i])) {
// Resolved at check time, not at app_exec_path_add() time: the registered directory
// itself may not exist yet when it's registered (e.g. before the first app_install()).
std::string resolved_dir = resolve_physical(reg.paths[i]);
if (is_under_directory(resolved_path, resolved_dir)) {
allowed = true;
break;
}
@ -217,49 +187,6 @@ bool app_exec_is_executable_path(const char* path) {
return loader->is_executable(location);
}
error_t app_exec_run_path(const char* path, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
if (!app_exec_is_executable_path(path)) {
return ERROR_NOT_ALLOWED;
}
std::string path_str(path);
auto& reg = run_registry();
mutex_lock(&reg.mutex);
auto iterator = reg.apps.find(path_str);
std::string id;
if (iterator != reg.apps.end()) {
id = iterator->second->id;
mutex_unlock(&reg.mutex);
} else {
auto record = std::make_unique<RunRecord>();
record->id = make_run_id(path_str);
record->name = last_path_segment(path_str);
record->path = path_str;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 0, .desired_memory_capability = 0 },
};
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
mutex_unlock(&reg.mutex);
LOG_E(TAG, "Failed to register %s: %s", path, error_to_string(add_result));
return add_result;
}
id = record->id;
reg.apps[path_str] = std::move(record);
mutex_unlock(&reg.mutex);
}
return app_manager_start_with_parameters(id.c_str(), argc, argv, out_app_instance_id);
}
void app_exec_register_default_paths() {
char data_root[192];
if (paths_get_data_path(data_root, sizeof(data_root)) == ERROR_NONE) {

View File

@ -92,8 +92,10 @@ char** copy_arguments(int argc, const char* const argv[]) {
// 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) {
// task begins executing (see app_manager_start_with_streams()). @a manifest may be NULL, for a
// location-based start with no manifest at all (see app_manager_start_location()); it's stored
// on the ledger record as-is and never dereferenced here beyond the log line below.
error_t start_internal(const AppManifest* manifest, AppLocation location, AppStackConfig stack, 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);
return ERROR_INVALID_ARGUMENT;
@ -102,14 +104,6 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
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;
@ -120,7 +114,7 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
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);
@ -141,7 +135,7 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
}
}
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.
@ -160,26 +154,60 @@ error_t start_internal(const char* id, AppInstanceId parent_instance_id, int arg
return ERROR_NONE;
}
// Looks @a id up in the manifest registry, then delegates to start_internal(). The only path
// that requires a registered manifest; app_manager_start_location() bypasses this entirely.
error_t start_internal_by_id(const char* id, AppInstanceId parent_instance_id, int argc, char* 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);
app_ledger_free_arguments(argc, argv);
return ERROR_NOT_FOUND;
}
const AppManifest* manifest = manifest_iterator->second;
mutex_unlock(&ledger.mutex);
return start_internal(manifest, manifest->location, manifest->stack, parent_instance_id, argc, argv, bindings, binding_count, out_app_instance_id);
}
} // 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);
return start_internal_by_id(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);
return start_internal_by_id(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);
return start_internal_by_id(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);
return start_internal_by_id(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);
return start_internal_by_id(id, parent_instance_id, argc, copy_arguments(argc, argv), bindings, binding_count, out_app_instance_id);
}
error_t app_manager_start_location(AppLocation location, AppStackConfig stack, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
return start_internal(nullptr, location, stack, 0, argc, copy_arguments(argc, argv), nullptr, 0, out_app_instance_id);
}
error_t app_manager_start_location_for_result(AppLocation location, AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
return start_internal(nullptr, location, stack, parent_instance_id, argc, copy_arguments(argc, argv), nullptr, 0, out_app_instance_id);
}
error_t app_manager_start_location_with_streams(AppLocation location, AppStackConfig stack, const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
return start_internal(nullptr, location, stack, 0, 0, nullptr, bindings, binding_count, out_app_instance_id);
}
error_t app_manager_start_location_for_result_with_streams(AppLocation location, AppStackConfig stack, 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(nullptr, location, stack, parent_instance_id, argc, copy_arguments(argc, argv), bindings, binding_count, out_app_instance_id);
}
error_t app_manager_stop(AppInstanceId app_instance_id) {
@ -230,7 +258,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) {

View File

@ -10,7 +10,9 @@
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
extern ServiceManifest app_internal_loader_service_manifest;
@ -30,6 +32,47 @@ int32_t fake_entry(int, char*[]) {
return 0;
}
// Creates <tmp>/{bin,outside} plus a real file at outside/target.so, for the symlink-escape
// tests below. Caller removes everything via remove_symlink_fixture() once done.
struct SymlinkFixture {
std::string root;
std::string bin_dir;
std::string outside_dir;
std::string outside_target;
bool create() {
char temp_template[] = "/tmp/exec-test-symlink-XXXXXX";
char* temp_dir = mkdtemp(temp_template);
if (temp_dir == nullptr) {
return false;
}
root = temp_dir;
bin_dir = root + "/bin";
outside_dir = root + "/outside";
outside_target = outside_dir + "/target.so";
if (mkdir(bin_dir.c_str(), 0755) != 0 || mkdir(outside_dir.c_str(), 0755) != 0) {
return false;
}
FILE* file = fopen(outside_target.c_str(), "w");
if (file == nullptr) {
return false;
}
fclose(file);
return true;
}
};
void remove_symlink_fixture(const SymlinkFixture& fixture) {
unlink((fixture.bin_dir + "/escape.so").c_str());
unlink((fixture.bin_dir + "/inside.so").c_str());
unlink((fixture.bin_dir + "/real.so").c_str());
unlink(fixture.outside_target.c_str());
rmdir(fixture.bin_dir.c_str());
rmdir(fixture.outside_dir.c_str());
rmdir(fixture.root.c_str());
}
} // namespace
TEST_CASE("app_exec_path_add/app_exec_path_allowed accept nested children of a registered directory") {
@ -104,6 +147,44 @@ TEST_CASE("app_exec_path_allowed resolves a relative incoming path to absolute,
app_exec_path_remove(absolute_dir.c_str());
}
TEST_CASE("app_exec_path_allowed rejects a symlink inside a registered directory that resolves outside it") {
SymlinkFixture fixture;
REQUIRE(fixture.create());
std::string escape_link = fixture.bin_dir + "/escape.so";
REQUIRE_EQ(symlink(fixture.outside_target.c_str(), escape_link.c_str()), 0);
REQUIRE_EQ(app_exec_path_add(fixture.bin_dir.c_str()), ERROR_NONE);
// Lexically "escape.so" is inside bin_dir, but it physically resolves to outside_dir. Must
// be rejected, or the posix loader would dlopen() a binary the caller never allowed.
CHECK_FALSE(app_exec_path_allowed(escape_link.c_str()));
app_exec_path_remove(fixture.bin_dir.c_str());
remove_symlink_fixture(fixture);
}
TEST_CASE("app_exec_path_allowed accepts a real file, and a symlink that stays inside, a registered directory") {
SymlinkFixture fixture;
REQUIRE(fixture.create());
std::string real_file = fixture.bin_dir + "/real.so";
FILE* file = fopen(real_file.c_str(), "w");
REQUIRE(file != nullptr);
fclose(file);
std::string inside_link = fixture.bin_dir + "/inside.so";
REQUIRE_EQ(symlink(real_file.c_str(), inside_link.c_str()), 0);
REQUIRE_EQ(app_exec_path_add(fixture.bin_dir.c_str()), ERROR_NONE);
CHECK(app_exec_path_allowed(real_file.c_str()));
CHECK(app_exec_path_allowed(inside_link.c_str()));
app_exec_path_remove(fixture.bin_dir.c_str());
remove_symlink_fixture(fixture);
}
TEST_CASE("app_exec_path_add returns ERROR_OUT_OF_MEMORY once the registry is full") {
char path[32];
int added = 0;

View File

@ -1,9 +1,11 @@
#include "doctest.h"
#include <app/event.h>
#include <app/io.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/stream.h>
#include <service/manager.h>
@ -173,6 +175,22 @@ bool wait_for_arguments_stashed(uint32_t timeout_ms) {
return arguments_stashed.load(std::memory_order_acquire);
}
// Writes a fixed string to its own stdout then returns 7 as its result, for the
// app_manager_start_location_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;
}
} // namespace
TEST_CASE("app_manager_start activates an app instance, app_manager_stop terminates it") {
@ -507,3 +525,162 @@ TEST_CASE("app_manager_get_topmost_app_id returns BUFFER_OVERFLOW for a too-smal
app_manager_stop(id);
app_manager_remove("test.app.top_overflow");
}
TEST_CASE("app_manager_start_location runs a location with no manifest at all, and reports no topmost app id for it") {
ensure_fake_loader_registered();
AppLocation location { APP_LOCATION_PATH, nullptr };
uint32_t instance_id = 0;
REQUIRE_EQ(app_manager_start_location(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_manager_start_location doesn't disturb app_manager_get_topmost_app_id for a normal manifest-backed app started afterward") {
ensure_fake_loader_registered();
AppLocation location { APP_LOCATION_PATH, nullptr };
uint32_t unregistered_id = 0;
REQUIRE_EQ(app_manager_start_location(location, AppStackConfig {}, 0, nullptr, &unregistered_id), ERROR_NONE);
CHECK(wait_for_state(unregistered_id, APP_INSTANCE_STATE_ACTIVE, 1000));
AppManifest manifest { "test.app.location.after", "After", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
uint32_t registered_id = 0;
REQUIRE_EQ(app_manager_start("test.app.location.after", &registered_id), ERROR_NONE);
CHECK(wait_for_state(registered_id, APP_INSTANCE_STATE_ACTIVE, 1000));
char buffer[64];
CHECK_EQ(app_manager_get_topmost_app_id(buffer, sizeof(buffer)), ERROR_NONE);
CHECK_EQ(std::string(buffer), "test.app.location.after");
app_manager_stop(unregistered_id);
app_manager_stop(registered_id);
app_manager_remove("test.app.location.after");
}
TEST_CASE("app_manager_start_location_for_result delivers APP_EVENT_RESULT to the parent, with no manifest for the child either") {
ensure_fake_loader_registered();
AppManifest parent_manifest { "test.app.location.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_manager_start("test.app.location.parent", &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_PATH, nullptr };
const char* argv[] = { "42" }; // fake_run's single-arg shortcut - returns 42 immediately
uint32_t child_id = 0;
REQUIRE_EQ(app_manager_start_location_for_result(location, AppStackConfig {}, parent_id, 1, argv, &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.location.parent");
}
TEST_CASE("app_manager_start_location_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_manager_start_location_with_streams(location, AppStackConfig {}, &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_manager_start_location_for_result_with_streams delivers both the stream data and the APP_EVENT_RESULT") {
ensure_fake_loader_registered();
ensure_memory_loader_registered();
AppManifest parent_manifest { "test.app.location.parent_streams", "Parent", APP_CATEGORY_USER, { APP_LOCATION_PATH, nullptr } };
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
uint32_t parent_id = 0;
REQUIRE_EQ(app_manager_start("test.app.location.parent_streams", &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_manager_start_location_for_result_with_streams(location, AppStackConfig {}, parent_id, 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_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.location.parent_streams");
}

View File

@ -1,6 +1,7 @@
#include <app/event.h>
#include <app/exec.h>
#include <app/install.h>
#include <app/manager.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
@ -209,8 +210,16 @@ void View::viewFile(const std::string& path, const std::string& filename) {
void View::runFile(const std::string& file_path) {
LOG_I(TAG, "Running %s", file_path.c_str());
if (!app_exec_is_executable_path(file_path.c_str())) {
LOG_W(TAG, "Not executable: %s", file_path.c_str());
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
return;
}
AppLocation location { APP_LOCATION_PATH, const_cast<char*>(file_path.c_str()) };
AppInstanceId instance_id = 0;
if (app_exec_run_path(file_path.c_str(), 0, nullptr, &instance_id) != ERROR_NONE) {
if (app_manager_start_location(location, AppStackConfig {}, 0, nullptr, &instance_id) != ERROR_NONE) {
LOG_W(TAG, "Failed to run %s", file_path.c_str());
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
}