Ken Van Hoeylandt f8172446fd
App loading improvements and more (#675)
- External apps can opt into automatic cleanup when they exit, including tracked tasks, threads, files, directories, and memory.
- App-relative file paths resolve against the app’s working directory, with `.` and `..` normalized.
- The shell identifies ELF binaries that cannot be executed instead of treating them as scripts.
- Shared icon fonts now offer distinct default and large sizes, with updated icons across the interface.
- ESP32 apps can load additional ELF formats when address mirroring is enabled.
- Terminal colors render clearly on monochrome displays, and toolbar buttons have cleaner styling.
- GPS settings safely refresh their device list when their window is hidden or its widgets are destroyed.
2026-10-06 07:45:58 +02:00

131 lines
4.5 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include "doctest.h"
#include <app/loader.h>
#include <app/manager.h>
#include <app/start.h>
#include <service/manager.h>
#include <tactility/delay.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
extern ServiceManifest loader_service_manifest;
namespace {
struct LeakedFds {
int fd = -1;
int file_fd = -1;
int dir_fd = -1;
};
bool is_open(int fd) {
return fcntl(fd, F_GETFD) != -1;
}
void ensure_path_loader_registered() {
if (service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID) == nullptr) {
service_manager_add(&loader_service_manifest, /*auto_start=*/true);
}
}
/** Runs the leak fixture as a top-level app and reads back the fds it leaked. */
bool run_leak_fixture(uint8_t flags, bool with_thread, LeakedFds& out_fds) {
ensure_path_loader_registered();
char output_path[] = "/tmp/cleanup_test_XXXXXX";
const int output_fd = mkstemp(output_path);
if (output_fd == -1) {
return false;
}
close(output_fd);
AppManifest manifest { "test.posix.leak", "Leak", APP_CATEGORY_USER, { APP_LOCATION_PATH, const_cast<char*>(LEAK_FIXTURE_APP_PATH) }, flags };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
const char* argv[] = { LEAK_FIXTURE_APP_PATH, output_path, with_thread ? "thread" : "" };
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.posix.leak", &context), ERROR_NONE);
app_start_context_set_arguments_ext(&context, 3, argv);
AppInstanceId app_instance_id = 0;
REQUIRE_EQ(app_start_with_context(&context, &app_instance_id), ERROR_NONE);
// Beyond the task grace period and thread join timeout
for (int waited = 0; waited < 5000 && app_manager_get_state(app_instance_id) != APP_INSTANCE_STATE_STOPPED; waited += 10) {
delay_millis(10);
}
const bool stopped = app_manager_get_state(app_instance_id) == APP_INSTANCE_STATE_STOPPED;
app_manager_remove("test.posix.leak");
FILE* output = fopen(output_path, "r");
const bool read = output != nullptr && fscanf(output, "%d %d %d", &out_fds.fd, &out_fds.file_fd, &out_fds.dir_fd) == 3;
if (output != nullptr) {
fclose(output);
}
unlink(output_path);
return stopped && read;
}
/** Runs the static fixture with cleanup, which crashes on a double free or double close if releasing happens too early. */
bool run_static_fixture(bool exit) {
ensure_path_loader_registered();
AppManifest manifest { "test.posix.static", "Static", APP_CATEGORY_USER, { APP_LOCATION_PATH, const_cast<char*>(STATIC_FIXTURE_APP_PATH) }, APP_MANIFEST_FLAG_CLEANUP };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
const char* argv[] = { STATIC_FIXTURE_APP_PATH, exit ? "exit" : "" };
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.posix.static", &context), ERROR_NONE);
app_start_context_set_arguments_ext(&context, 2, argv);
AppInstanceId app_instance_id = 0;
REQUIRE_EQ(app_start_with_context(&context, &app_instance_id), ERROR_NONE);
for (int waited = 0; waited < 5000 && app_manager_get_state(app_instance_id) != APP_INSTANCE_STATE_STOPPED; waited += 10) {
delay_millis(10);
}
const bool stopped = app_manager_get_state(app_instance_id) == APP_INSTANCE_STATE_STOPPED;
// Waits out a deferred unload, which releases the resources
delay_millis(100);
app_manager_remove("test.posix.static");
return stopped;
}
} // namespace
TEST_CASE("an app with APP_MANIFEST_FLAG_CLEANUP has what its static destructors release left alone") {
CHECK(run_static_fixture(false));
}
TEST_CASE("an app with APP_MANIFEST_FLAG_CLEANUP that calls exit() has what its static destructors release left alone") {
CHECK(run_static_fixture(true));
}
TEST_CASE("an app with APP_MANIFEST_FLAG_CLEANUP has its leaked files closed when it ends") {
LeakedFds fds;
REQUIRE(run_leak_fixture(APP_MANIFEST_FLAG_CLEANUP, false, fds));
CHECK_FALSE(is_open(fds.fd));
CHECK_FALSE(is_open(fds.file_fd));
CHECK_FALSE(is_open(fds.dir_fd));
}
TEST_CASE("an app with APP_MANIFEST_FLAG_CLEANUP has its leftover thread ended before it's unloaded") {
LeakedFds fds;
REQUIRE(run_leak_fixture(APP_MANIFEST_FLAG_CLEANUP, true, fds));
CHECK_FALSE(is_open(fds.fd));
}
TEST_CASE("an app without APP_MANIFEST_FLAG_CLEANUP keeps leaking") {
LeakedFds fds;
REQUIRE(run_leak_fixture(0, false, fds));
CHECK(is_open(fds.fd));
CHECK(is_open(fds.file_fd));
CHECK(is_open(fds.dir_fd));
close(fds.fd);
close(fds.file_fd);
close(fds.dir_fd);
}