Ken Van Hoeylandt 79d3dd4fdb
Improvements for apps, filesystems, terminal and shell (#659)
app-module

- Apps now have support for:
  - environment variables (inherit from parent app)
  - current workdir
  - window size (when running in terminal)
- Add type aliases like `AppId` and `PackageId`
- Remove data duplication between ledger and app instance records: create `AppManifestSet`
- `AppManifest` usage safety improved: pass by value where needed, or use `AppId` instead of a manifest.

Terminal and shell

- Moved shell commands into coreutils-module as proper apps.
- Refine stack size for terminal and shell because apps now run with their own task and stack (including in-memory apps)

Platform

- Fix for listing root dir on ESP32: wrap posix directory functions and wire in file systems.
- Moved all platform-specific file system init code to platform subprojects

Other

- Fix for screenshot capture (issue with paths)
- Fixes for coreutils app correctness
- Memory optimizations (use more PSRAM where possible) in various locations such as app-module, shell and terminal.
- `ls` and `help` commands now use terminal width and show columns of data by default
- Created fs helpers at `TactilityKernel/include/tactility/filesystem/fs.h` (partially ported from `ShellFs`)
2026-09-25 08:29:27 +02:00

216 lines
7.5 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include "doctest.h"
#include <app/dir.h>
#include <app/event.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <service/manager.h>
#include <tactility/delay.h>
#include <tactility/freertos/task.h>
#include <tactility/paths.h>
#include <atomic>
#include <cstring>
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;
}
std::atomic<bool> results_ready { false };
bool result_default_is_root;
bool result_set_root_ok;
bool result_set_real_dir_ok;
bool result_get_after_set_matches;
bool result_set_relative_rejected;
bool result_set_missing_rejected;
bool result_cwd_unchanged_after_rejection;
// Runs entirely on the app's own task, matching env_test.cpp's own convention: every app_dir_*()
// call resolves against app_scheduler_current_app_id(), so this has to run on the instance's own
// task to test the real call path.
int32_t dir_test_app_main(int, char**) {
char buf[FILE_MAX_PATH_STRING_LENGTH];
result_default_is_root = app_dir_get_cwd(buf, sizeof(buf)) == ERROR_NONE && strcmp(buf, "/") == 0;
result_set_root_ok = app_dir_set_cwd("/") == ERROR_NONE;
result_set_real_dir_ok = app_dir_set_cwd("/tmp") == ERROR_NONE;
result_get_after_set_matches = app_dir_get_cwd(buf, sizeof(buf)) == ERROR_NONE && strcmp(buf, "/tmp") == 0;
result_set_relative_rejected = app_dir_set_cwd("relative") == ERROR_INVALID_ARGUMENT;
result_set_missing_rejected = app_dir_set_cwd("/no/such/directory") == ERROR_NOT_FOUND;
result_cwd_unchanged_after_rejection = app_dir_get_cwd(buf, sizeof(buf)) == ERROR_NONE && strcmp(buf, "/tmp") == 0;
results_ready.store(true, std::memory_order_release);
// Same subscribe-until-close contract as every other fake app in this test suite.
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;
}
bool wait_for_results(uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
if (results_ready.load(std::memory_order_acquire)) {
return true;
}
delay_millis(10);
waited += 10;
}
return results_ready.load(std::memory_order_acquire);
}
std::atomic<bool> child_result_ready { false };
bool result_child_inherited_parent_cwd;
// The child in the inheritance test below: checks what it sees, stashes the result, and returns
// immediately (no subscribe loop needed: nothing sends it APP_EVENT_CLOSE).
int32_t dir_child_main(int, char**) {
char buf[FILE_MAX_PATH_STRING_LENGTH];
result_child_inherited_parent_cwd = app_dir_get_cwd(buf, sizeof(buf)) == ERROR_NONE && strcmp(buf, "/tmp") == 0;
child_result_ready.store(true, std::memory_order_release);
return 0;
}
// The parent in the inheritance test below: `cd`s itself (app_dir_set_cwd(), mirroring what
// ShellFs::changeDirectory() does for a real `cd`), then starts a child that checks it inherited
// that cwd - the exact chain a coreutils app relies on to agree with the shell's own `pwd`.
int32_t dir_parent_main(int, char**) {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
app_event_subscribe(&sub, &event_group);
app_dir_set_cwd("/tmp");
AppLocation child_location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(dir_child_main) };
AppStartContext child_context = app_start_context_for_location(child_location);
app_start_context_set_parent(&child_context, app_scheduler_current_app_id());
uint32_t child_id = 0;
app_start_with_context(&child_context, &child_id);
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;
}
bool wait_for_child_result(uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
if (child_result_ready.load(std::memory_order_acquire)) {
return true;
}
delay_millis(10);
waited += 10;
}
return child_result_ready.load(std::memory_order_acquire);
}
} // namespace
TEST_CASE("app_dir_* operate on the calling app instance's own cwd") {
ensure_memory_loader_registered();
AppLocation location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(dir_test_app_main) };
AppStartContext context = app_start_context_for_location(location);
results_ready.store(false, std::memory_order_release);
uint32_t instance_id = 0;
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000));
REQUIRE(wait_for_results(1000));
CHECK(result_default_is_root);
CHECK(result_set_root_ok);
CHECK(result_set_real_dir_ok);
CHECK(result_get_after_set_matches);
CHECK(result_set_relative_rejected);
CHECK(result_set_missing_rejected);
CHECK(result_cwd_unchanged_after_rejection);
app_manager_stop(instance_id);
}
TEST_CASE("app_dir_* report ERROR_NOT_FOUND when the calling task isn't a running app instance") {
char buf[FILE_MAX_PATH_STRING_LENGTH];
CHECK_EQ(app_dir_get_cwd(buf, sizeof(buf)), ERROR_NOT_FOUND);
CHECK_EQ(app_dir_set_cwd("/tmp"), ERROR_NOT_FOUND);
}
TEST_CASE("a child started with a parent inherits the parent's cwd, set via app_dir_set_cwd") {
ensure_memory_loader_registered();
AppLocation parent_location { APP_LOCATION_MEMORY, reinterpret_cast<void*>(dir_parent_main) };
AppStartContext parent_context = app_start_context_for_location(parent_location);
child_result_ready.store(false, std::memory_order_release);
uint32_t parent_id = 0;
REQUIRE_EQ(app_start_with_context(&parent_context, &parent_id), ERROR_NONE);
CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000));
REQUIRE(wait_for_child_result(1000));
CHECK(result_child_inherited_parent_cwd);
app_manager_stop(parent_id);
}