Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
+413
View File
@@ -0,0 +1,413 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <minitar.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
constexpr auto* TAG = "app_install";
namespace {
// region Filesystem helpers (app-module may not depend upward on Tactility::file - see
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
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);
}
// mkdir -p.
bool ensure_directory(const std::string& path) {
if (path.empty() || app_fs_is_directory(path)) {
return true;
}
FileMutex mutex {};
file_mutex_get(&mutex, path.c_str());
file_mutex_lock(&mutex);
bool created = mkdir(path.c_str(), 0777) == 0 || errno == EEXIST;
file_mutex_unlock(&mutex);
if (!created) {
return false;
}
return app_fs_is_directory(path);
}
bool ensure_directory_recursive(const std::string& path) {
for (size_t index = path.find('/', 1); index != std::string::npos; index = path.find('/', index + 1)) {
if (!ensure_directory(path.substr(0, index))) {
return false;
}
}
return ensure_directory(path);
}
bool delete_recursively(const std::string& path) {
LOG_D(TAG, "Deleting %s...", path.c_str());
if (path.empty() || path == "/" || path == "." || path == "..") {
return true;
}
if (app_fs_is_directory(path)) {
LOG_D(TAG, "Deleting dir %s", path.c_str());
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
LOG_E(TAG, "Failed to scan directory %s", path.c_str());
file_mutex_unlock(&file_mutex);
return false;
}
bool success = true;
dirent* entry;
while (success && (entry = readdir(dir)) != nullptr) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
continue;
}
success = delete_recursively(path + "/" + entry->d_name);
}
closedir(dir);
if (!success) {
file_mutex_unlock(&file_mutex);
return false;
}
bool result = rmdir(path.c_str()) == 0;
file_mutex_unlock(&file_mutex);
return result;
}
if (app_fs_is_file(path)) {
LOG_D(TAG, "Deleting file %s", path.c_str());
FileMutex mutex {};
file_mutex_get(&mutex, path.c_str());
file_mutex_lock(&mutex);
bool result = remove(path.c_str()) == 0;
file_mutex_unlock(&mutex);
return result;
}
LOG_D(TAG, "Deleting done");
return true;
}
bool get_app_install_directory(std::string& out_path) {
char root[192];
if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) {
return false;
}
out_path = std::string(root) + "/app";
return true;
}
// endregion
// region Tar extraction (ported from the old Tactility::app AppInstall.cpp)
bool untar_file(minitar* archive, const minitar_entry* entry, const std::string& destination_path) {
auto absolute_path = destination_path + "/" + entry->metadata.path;
if (!ensure_directory_recursive(destination_path)) {
LOG_E(TAG, "Can't find or create directory %s", destination_path.c_str());
return false;
}
if (!minitar_read_contents_to_file(archive, entry, absolute_path.c_str())) {
LOG_E(TAG, "Failed to write data to %s", absolute_path.c_str());
return false;
}
// Note: fchmod() doesn't exist on ESP-IDF and chmod() does nothing on that platform.
chmod(absolute_path.c_str(), entry->metadata.mode);
return true;
}
bool untar_directory(const minitar_entry* entry, const std::string& destination_path) {
return ensure_directory_recursive(destination_path + "/" + entry->metadata.path);
}
bool untar(const std::string& tar_path, const std::string& destination_path) {
minitar archive {};
if (minitar_open(tar_path.c_str(), &archive) != 0) {
LOG_E(TAG, "Failed to open %s", tar_path.c_str());
return false;
}
bool success = true;
minitar_entry entry {};
while (minitar_read_entry(&archive, &entry) == 0) {
LOG_I(TAG, "Extracting %s", entry.metadata.path);
if (entry.metadata.type == MTAR_DIRECTORY) {
if (std::strcmp(entry.metadata.name, ".") == 0 || std::strcmp(entry.metadata.name, "..") == 0 || std::strcmp(entry.metadata.name, "/") == 0) {
continue;
}
success = untar_directory(&entry, destination_path);
} else if (entry.metadata.type == MTAR_REGULAR) {
success = untar_file(&archive, &entry, destination_path);
} else {
LOG_E(TAG, "Unsupported entry type: %d", static_cast<int>(entry.metadata.type));
success = false;
}
if (!success) {
LOG_E(TAG, "Failed to extract %s", entry.metadata.path);
break;
}
}
minitar_close(&archive);
return success;
}
// endregion
// region Installed-app registry: owns the AppManifest (and its id/name/path strings) that
// app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract).
struct InstalledAppRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
};
struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps;
Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); }
};
InstallRegistry& install_registry() {
static InstallRegistry registry;
return registry;
}
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
// @warning Caller must hold install_registry().mutex, and must have already ensured
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
auto& registry = install_registry();
auto record = std::make_unique<InstalledAppRecord>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir_path;
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 = 0,
};
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
// already cleared any stale registration for this id (e.g. left over from
// app_manager_install_path_scan()'s separate registry), but that call happens before the
// tarball is even extracted - remove once more, right before add, so a duplicate id can
// never turn a filesystem-level install success into a reported failure.
app_manager_remove(record->id.c_str());
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->id.c_str(), error_to_string(add_result));
return add_result;
}
registry.apps[record->id] = std::move(record);
return ERROR_NONE;
}
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
// the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) {
std::vector<uint32_t> instance_ids;
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (const auto& [id, record]: ledger.instances) {
if (record.manifest == manifest) {
instance_ids.push_back(id);
}
}
mutex_unlock(&ledger.mutex);
for (uint32_t id: instance_ids) {
app_manager_stop(id);
}
}
// Caller must already hold install_registry().mutex
error_t uninstall_locked(const std::string& app_id) {
auto& registry = install_registry();
auto iterator = registry.apps.find(app_id);
if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND;
}
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
delete_recursively(iterator->second->path);
registry.apps.erase(iterator);
return ERROR_NONE;
}
// endregion
} // namespace
extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size) {
if (path_size == 0) {
return ERROR_BUFFER_OVERFLOW;
}
path[0] = '\0';
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
return ERROR_NOT_FOUND;
}
int written = std::snprintf(path, path_size, "%s/%s", app_parent_path.c_str(), app_id);
if (written < 0 || static_cast<size_t>(written) >= path_size) {
path[0] = '\0';
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_install(const char* source_path) {
LOG_I(TAG, "Installing app from %s", source_path);
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
return ERROR_NOT_FOUND;
}
if (!ensure_directory_recursive(app_parent_path)) {
LOG_E(TAG, "Failed to create %s", app_parent_path.c_str());
return ERROR_NOT_FOUND;
}
auto staging_path = app_parent_path + "/" + last_path_segment(source_path);
delete_recursively(staging_path);
FileMutex target_mutex {};
file_mutex_get(&target_mutex, app_parent_path.c_str());
FileMutex source_mutex {};
file_mutex_get(&source_mutex, source_path);
file_mutex_lock(&target_mutex);
file_mutex_lock(&source_mutex);
bool untar_success = untar(source_path, staging_path);
file_mutex_unlock(&source_mutex);
file_mutex_unlock(&target_mutex);
if (!untar_success) {
LOG_E(TAG, "Failed to extract %s", source_path);
delete_recursively(staging_path);
return ERROR_NOT_FOUND;
}
auto manifest_path = staging_path + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
delete_recursively(staging_path);
return ERROR_INVALID_ARGUMENT;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path);
return ERROR_INVALID_ARGUMENT;
}
auto& registry = install_registry();
mutex_lock(&registry.mutex);
// Replace any previous install of this app id (mirrors the old install()'s "already
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
// registry - the same app id may instead be registered by app_manager_install_path_scan()
// (manager.cpp's separate registry, scanning this same directory tree), which
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
// too, or app_manager_add() below rejects the re-add as a duplicate.
uninstall_locked(metadata.app_id);
error_t remove_result = app_manager_remove(metadata.app_id);
if (remove_result != ERROR_NONE && remove_result != ERROR_NOT_FOUND) {
LOG_E(TAG, "Install failed: failed to remove existing installation");
mutex_unlock(&registry.mutex);
delete_recursively(staging_path);
return ERROR_RESOURCE;
}
auto final_path = app_parent_path + "/" + metadata.app_id;
delete_recursively(final_path);
file_mutex_lock(&target_mutex);
bool rename_success = rename(staging_path.c_str(), final_path.c_str()) == 0;
file_mutex_unlock(&target_mutex);
if (!rename_success) {
LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", staging_path.c_str(), final_path.c_str());
delete_recursively(staging_path);
mutex_unlock(&registry.mutex);
return ERROR_NOT_FOUND;
}
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
// already removed any previous registration for this exact id.
error_t add_result = register_installed_app_locked(final_path, metadata);
mutex_unlock(&registry.mutex);
return add_result;
}
error_t app_uninstall(const char* app_id) {
LOG_I(TAG, "Uninstalling app %s", app_id);
auto& registry = install_registry();
mutex_lock(&registry.mutex);
error_t result = uninstall_locked(app_id);
mutex_unlock(&registry.mutex);
return result;
}
} // extern "C"
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/loader.h>
#include <app/manifest.h>
#include <service/instance.h>
#include <service/manager.h>
namespace {
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
if (location.type != APP_LOCATION_MEMORY) {
return ERROR_NOT_SUPPORTED;
}
*out_runtime = location.location;
return ERROR_NONE;
}
int32_t api_run(AppRuntime runtime, uint32_t app_instance_id, int argc, char* argv[]) {
auto entry = reinterpret_cast<AppMainFn>(runtime);
return entry(app_instance_id, argc, argv);
}
void api_unload(AppRuntime /*unused*/) {
}
AppLoaderApi memory_loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
};
void* create_service(const ServiceManifest*) {
return &memory_loader_api;
}
void destroy_service(const ServiceManifest*, void*) {
}
} // namespace
ServiceManifest app_internal_loader_service_manifest = {
.id = APP_LOADER_MEMORY_SERVICE_ID,
.create_service = create_service,
.destroy_service = destroy_service,
.on_start = nullptr,
.on_stop = nullptr,
};
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: Apache-2.0
#include "tactility/filesystem/file_mutex.h"
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
namespace {
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
bool validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
/** manifest.properties format: "key=value" lines, "[section]" lines prefix every following key
* until the next section, "#" lines are comments, blank lines are skipped. Deliberately a local,
* minimal re-implementation rather than depending on Tactility's file::loadPropertiesFile() -
* app-module (like every other kernel module) may not depend upward on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties, std::string& out_first_line) {
FileMutex mutex;
file_mutex_get(&mutex, path.c_str());
file_mutex_lock(&mutex);
std::ifstream file(path);
if (!file.is_open()) {
file_mutex_unlock(&mutex);
return false;
}
std::string line;
std::string section_prefix;
bool got_first_line = false;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
if (!got_first_line) {
out_first_line = trimmed_line;
got_first_line = true;
}
if (trimmed_line.starts_with("[")) {
section_prefix = trimmed_line;
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = section_prefix + trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
file_mutex_unlock(&mutex);
return true;
}
} // namespace
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_metadata_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_id(const std::string& id) {
return id.size() >= 5 && id.size() <= APP_METADATA_APP_ID_LENGTH && validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_name(const std::string& name) {
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_metadata_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= APP_METADATA_APP_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_metadata_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
std::map<std::string, std::string> properties;
std::string first_line;
if (!load_properties(path, properties, first_line)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
// The V1 format's first line is always the literal "[manifest]" section header; V2 files are
// flat from the first line onward.
bool is_v1_format = first_line == "[manifest]";
bool success = is_v1_format
? app_metadata_parse_v1(properties, *out_metadata)
: app_metadata_parse_v2(properties, *out_metadata);
return success ? ERROR_NONE : ERROR_INVALID_ARGUMENT;
}
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v1";
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// [manifest]
std::string format_version;
if (!app_metadata_get_value(properties, "[manifest]version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// [app]
std::string id;
if (!app_metadata_get_value(properties, "[app]id", id)) {
return false;
}
if (!app_metadata_is_valid_id(id)) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "[app]name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "[app]versionName", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "[app]versionCode", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
std::string target_sdk;
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
return true;
}
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// manifest
std::string format_version;
if (!app_metadata_get_value(properties, "manifest.version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// app
std::string id;
if (!app_metadata_get_value(properties, "app.id", id)) {
return false;
}
if (!app_metadata_is_valid_id(id)) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "app.name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "app.version.name", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "app.version.code", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
// target
std::string target_sdk;
if (!app_metadata_get_value(properties, "target.sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
return true;
}
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/paths.h>
#include <tactility/paths.h>
#include <cstdio>
extern "C" {
error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size) {
char root[192];
error_t error = paths_get_user_data_path(root, sizeof(root));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/app/%s", root, app_id);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", directory);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = app_paths_get_assets_directory(app_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
} // extern "C"
+299
View File
@@ -0,0 +1,299 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <app/event.h>
#include <app/instance.h>
#include <app/loader.h>
#include <app/scheduler.h>
#include <service/instance.h>
#include <service/manager.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <cstdint>
#include <cstdio>
#include <new>
constexpr auto* TAG = "app_scheduler";
// Slot 0 is reserved by ESP-IDF's pthread API (see TactilityKernel's Thread wrapper for the
// same convention/comment) - app tasks use slot 1 to stash their own app_instance_id, so any
// code running on an app's own task can retrieve it via app_scheduler_current_app_id() without
// needing it threaded through as a parameter.
constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
constexpr UBaseType_t APP_TASK_PRIORITY = 4;
namespace {
struct TaskContext {
const AppLoaderApi* loader;
void* runtime;
AppInstanceId app_instance_id;
int argc;
char** argv;
AppCompletionSignal* completion;
};
void set_state(AppInstanceId app_instance_id, AppInstanceState state) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.state = state;
}
mutex_unlock(&ledger.mutex);
}
void set_task(AppInstanceId app_instance_id, TaskHandle_t task) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.task = task;
}
mutex_unlock(&ledger.mutex);
}
void set_completion(AppInstanceId app_instance_id, AppCompletionSignal* completion) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.completion = completion;
}
mutex_unlock(&ledger.mutex);
}
// Takes a reference on app_instance_id's completion signal (see AppCompletionSignal), for the
// caller to wait on. @return the signal to wait on, or NULL if the instance has already fully
// finished (its ledger entry - and so its reference to the signal - is already gone) and so
// there's nothing left to wait for, or if the instance is still starting up (start_internal()
// in manager.cpp inserts the ledger entry before app_scheduler_start() has gotten as far as
// set_completion() - `completion` is NULL for that whole window) and so there's nothing to
// take a reference on yet.
AppCompletionSignal* acquire_completion_signal(AppInstanceId app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
AppCompletionSignal* completion = nullptr;
if (iterator != ledger.instances.end() && iterator->second.completion != nullptr) {
completion = iterator->second.completion;
completion->refcount++;
}
mutex_unlock(&ledger.mutex);
return completion;
}
// Releases a reference taken by acquire_completion_signal(), deleting the signal (and its
// semaphore) if this was the last one.
void release_completion_signal(AppCompletionSignal* completion) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
bool should_delete = (--completion->refcount == 0);
mutex_unlock(&ledger.mutex);
if (should_delete) {
vSemaphoreDelete(completion->semaphore);
delete completion;
}
}
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));
}
// If this instance was launched via app_manager_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) {
auto& ledger = app_ledger();
AppInstanceId parent_id;
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = {} };
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator == ledger.instances.end()) {
mutex_unlock(&ledger.mutex);
return;
}
parent_id = iterator->second.parent_id;
event.result.launch_id = app_instance_id;
event.result.result = result;
mutex_unlock(&ledger.mutex);
if (parent_id != 0) {
app_event_emit(parent_id, &event);
}
}
void app_task_main(void* context) {
auto* ctx = static_cast<TaskContext*>(context);
check(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX) == nullptr);
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, reinterpret_cast<void*>(static_cast<uintptr_t>(ctx->app_instance_id)));
LOG_I(TAG, "Thread for %d started", ctx->app_instance_id);
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, ctx->argc, ctx->argv);
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, nullptr);
ctx->loader->unload(ctx->runtime);
deliver_result_to_parent_if_any(ctx->app_instance_id, result);
// A safe default terminal marker for CLOSE (and any other exit): an app that calls
// app_manager_finish() already marked itself Stopped before returning, so this is a no-op
// for it - but it's still needed as the terminal marker for any other exit path.
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_STOPPED);
app_ledger_free_arguments(ctx->argc, ctx->argv);
AppInstanceId app_instance_id = ctx->app_instance_id;
AppCompletionSignal* completion = ctx->completion;
delete ctx;
LOG_I(TAG, "Thread for %d finished", app_instance_id);
// Erase the ledger entry before self-deleting - see "Reap self-terminated app tasks":
// nothing else is guaranteed to ever call app_scheduler_stop() for this instance (the
// common case is the app just closing itself), so this can't wait for that to happen.
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
ledger.instances.erase(app_instance_id);
mutex_unlock(&ledger.mutex);
// Signal completion as the literal last action before this task ceases to exist, so
// app_scheduler_stop() can't observe "stopped" one step early - unlike watching the ledger
// entry disappear, this can only happen once the task is truly done running. A dedicated
// semaphore rather than this task's default FreeRTOS notification, since app_event.cpp's
// AppEventSubscription also uses that shared slot - an unrelated event (e.g. a child's
// APP_EVENT_RESULT) delivered to this same task could otherwise unblock a concurrent
// app_scheduler_stop() early.
xSemaphoreGive(completion->semaphore);
release_completion_signal(completion); // releases app_task_main()'s own reference
vTaskDelete(nullptr);
}
} // namespace
extern "C" {
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, int argc, char* argv[]) {
const AppLoaderApi* loader = find_loader_api(location.type);
if (loader == nullptr) {
LOG_E(TAG, "No app loader is registered (service '%s' not found)", loader_service_id_for(location.type));
app_ledger_free_arguments(argc, argv);
return ERROR_NOT_FOUND;
}
void* runtime = nullptr;
error_t load_result = loader->load(location, &runtime);
if (load_result != ERROR_NONE) {
LOG_E(TAG, "Failed to load app: %s", error_to_string(load_result));
app_ledger_free_arguments(argc, argv);
return load_result;
}
auto* completion = new (std::nothrow) AppCompletionSignal();
if (completion == nullptr) {
LOG_E(TAG, "Failed to allocate app");
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
completion->semaphore = xSemaphoreCreateBinary();
if (completion->semaphore == nullptr) {
LOG_E(TAG, "Failed to allocate app");
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
auto* context = new (std::nothrow) TaskContext { loader, runtime, app_instance_id, argc, argv, completion };
if (context == nullptr) {
LOG_E(TAG, "Failed to allocate app");
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
char task_name[16];
snprintf(task_name, sizeof(task_name), "app_%lu", static_cast<unsigned long>(app_instance_id));
TaskHandle_t task_handle = nullptr;
// 8192 bytes -> stack depth in words, matching what TactilityKernel's Thread wrapper does with the stack size it's given.
// Created at idle priority so it can't preempt us before vTaskSuspend() below runs, then suspended immediately -
// the ledger must record the handle (set_task()) before the task can possibly observe or erase its own entry.
// (see app_scheduler_stop()'s liveness check and app_task_main()'s exit path)
BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, tskIDLE_PRIORITY, &task_handle);
if (create_result != pdPASS) {
delete context;
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
vTaskSuspend(task_handle);
set_task(app_instance_id, task_handle);
set_completion(app_instance_id, completion);
vTaskPrioritySet(task_handle, APP_TASK_PRIORITY);
vTaskResume(task_handle);
return ERROR_NONE;
}
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout) {
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
if (completion != nullptr) {
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
app_event_emit(app_instance_id, &event);
// Blocks until app_task_main() gives this dedicated semaphore as the literal last thing it does before vTaskDelete().
// Uses aa dedicated semaphore rather than this task's default FreeRTOS notification because app_event.cpp's AppEventSubscription also uses that shared slot.
// An unrelated event (e.g. a different child's APP_EVENT_RESULT) delivered to this same task could otherwise unblock this early.
BaseType_t taken = xSemaphoreTake(completion->semaphore, join_timeout);
release_completion_signal(completion);
if (taken == pdFALSE) {
LOG_W(TAG, "App instance %u did not stop in time", app_instance_id);
return ERROR_TIMEOUT;
}
}
set_state(app_instance_id, APP_INSTANCE_STATE_STOPPED);
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
ledger.instances.erase(app_instance_id);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
AppInstanceId app_scheduler_current_app_id(void) {
void* value = pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX);
return reinterpret_cast<uintptr_t>(value);
}
} // extern "C"
+115
View File
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/time.h>
/**
* Intrusive singly-linked list of subscriptions, keyed by app_instance_id.
* Guarded by a single coarse-grained mutex, notifying a subscriber here never invokes caller code
* (just a struct copy and an xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
*/
static AppEventSubscription* subscriptions = nullptr;
struct AppEventMutex {
Mutex handle {};
AppEventMutex() { mutex_construct(&handle); }
~AppEventMutex() { mutex_destruct(&handle); }
};
static AppEventMutex subscriptions_mutex;
extern "C" {
error_t app_event_subscribe(AppEventSubscription* sub) {
sub->task = xTaskGetCurrentTaskHandle();
sub->head = 0;
sub->count = 0;
mutex_lock(&subscriptions_mutex.handle);
sub->next = subscriptions;
subscriptions = sub;
mutex_unlock(&subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t app_event_unsubscribe(AppEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&subscriptions_mutex.handle);
for (AppEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->next) {
if (*link == sub) {
*link = sub->next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&subscriptions_mutex.handle);
return result;
}
error_t app_event_emit(uint32_t app_instance_id, const AppEvent* event) {
AppEvent stamped_event = *event;
stamped_event.timestamp = get_micros_since_boot();
error_t result = ERROR_NOT_FOUND;
mutex_lock(&subscriptions_mutex.handle);
for (AppEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->next) {
if (sub->app_instance_id != app_instance_id) {
continue;
}
if (sub->count >= APP_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->head + sub->count) % APP_EVENT_QUEUE_CAPACITY;
sub->queue[tail] = stamped_event;
sub->count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
xTaskNotifyGive(sub->task);
}
mutex_unlock(&subscriptions_mutex.handle);
return result;
}
static bool try_pop(AppEventSubscription* sub, AppEvent* out_event) {
mutex_lock(&subscriptions_mutex.handle);
bool has_event = sub->count > 0;
if (has_event) {
*out_event = sub->queue[sub->head];
sub->head = (sub->head + 1) % APP_EVENT_QUEUE_CAPACITY;
sub->count--;
}
mutex_unlock(&subscriptions_mutex.handle);
return has_event;
}
error_t app_event_await(AppEventSubscription* sub, AppEvent* out_event, TickType_t timeout) {
if (try_pop(sub, out_event)) {
// Drain any notification credit this (or an earlier) push accumulated on this task's
// FreeRTOS notification value: each app_event_emit() calls xTaskNotifyGive() regardless
// of whether the consumer takes this fast path or the blocking path below, so without
// this the credit would carry over and cause a future ulTaskNotifyTake() below to
// return immediately for a notification that was already accounted for here.
ulTaskNotifyTake(pdTRUE, 0);
return ERROR_NONE;
}
if (ulTaskNotifyTake(pdTRUE, timeout) == 0) {
return ERROR_TIMEOUT;
}
// Single-consumer by design (one task per subscription), so a wakeup implies the event
// this call was notified for is still there for us to pop.
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
}
} // extern "C"
+337
View File
@@ -0,0 +1,337 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
#include <algorithm>
#include <cstring>
#include <memory>
#include <unordered_map>
#include <vector>
#define TAG "app_manager"
extern "C" {
error_t app_manager_add(const AppManifest* manifest) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
if (ledger.manifests.contains(manifest->id)) {
mutex_unlock(&ledger.mutex);
LOG_E(TAG, "Manifest with id '%s' is already registered", manifest->id);
return ERROR_INVALID_ARGUMENT;
}
ledger.manifests[manifest->id] = manifest;
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_remove(const char* id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.manifests.find(id);
if (iterator == ledger.manifests.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
ledger.manifests.erase(iterator);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
const AppManifest* app_manager_find_manifest(const char* id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.manifests.find(id);
const AppManifest* manifest = (iterator != ledger.manifests.end()) ? iterator->second : nullptr;
mutex_unlock(&ledger.mutex);
return manifest;
}
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (auto& [id, manifest] : ledger.manifests) {
visitor(manifest, 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;
}
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.
error_t start_internal(const char* id, AppInstanceId parent_instance_id, int argc, char* argv[], AppInstanceId* out_app_instance_id) {
const AppManifest* manifest = app_manager_find_manifest(id);
if (manifest == nullptr) {
app_ledger_free_arguments(argc, argv);
return ERROR_NOT_FOUND;
}
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
AppInstanceId target_id = ledger.next_instance_id++;
AppInstanceRecord record { target_id, manifest, APP_INSTANCE_STATE_STARTING, nullptr };
record.parent_id = parent_instance_id;
ledger.instances[target_id] = record;
mutex_unlock(&ledger.mutex);
error_t result = app_scheduler_start(target_id, manifest->location, argc, argv);
if (result != ERROR_NONE) {
mutex_lock(&ledger.mutex);
ledger.instances.erase(target_id);
mutex_unlock(&ledger.mutex);
return result;
}
*out_app_instance_id = target_id;
return ERROR_NONE;
}
} // namespace
error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id) {
return start_internal(id, 0, 0, nullptr, 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), 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), out_app_instance_id);
}
error_t app_manager_stop(AppInstanceId app_instance_id) {
return app_scheduler_stop(app_instance_id, pdMS_TO_TICKS(2000));
}
error_t app_manager_finish(AppInstanceId app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.state = APP_INSTANCE_STATE_STOPPED;
}
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
AppInstanceState app_manager_get_state(AppInstanceId app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
AppInstanceState state = (iterator != ledger.instances.end()) ? iterator->second.state : APP_INSTANCE_STATE_STOPPED;
mutex_unlock(&ledger.mutex);
return state;
}
error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id) {
auto& ledger = app_ledger();
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.
if (record.state == APP_INSTANCE_STATE_ACTIVE && instance_id > topmost_id) {
topmost_id = instance_id;
}
}
mutex_unlock(&ledger.mutex);
if (topmost_id == 0) {
return ERROR_NOT_FOUND;
}
*out_app_instance_id = topmost_id;
return ERROR_NONE;
}
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
if (buffer_size == 0) {
return ERROR_BUFFER_OVERFLOW;
}
buffer[0] = '\0';
AppInstanceId topmost_id = 0;
error_t result = app_manager_get_topmost_instance_id(&topmost_id);
if (result != ERROR_NONE) {
return result;
}
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;
mutex_unlock(&ledger.mutex);
if (app_id == nullptr) {
return ERROR_NOT_FOUND;
}
size_t length = strlen(app_id);
if (length >= buffer_size) {
buffer[0] = '\0';
return ERROR_BUFFER_OVERFLOW;
}
memcpy(buffer, app_id, length + 1);
return ERROR_NONE;
}
} // extern "C"
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()).
struct ScannedAppManifest {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
};
struct InstallPathRegistry {
std::vector<std::string> paths;
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
Mutex mutex {};
InstallPathRegistry() { mutex_construct(&mutex); }
};
InstallPathRegistry& install_path_registry() {
static InstallPathRegistry registry;
return registry;
}
} // namespace
extern "C" {
error_t app_manager_install_path_add(const char* path) {
auto& registry = install_path_registry();
mutex_lock(&registry.mutex);
if (std::ranges::find(registry.paths, path) == registry.paths.end()) {
registry.paths.emplace_back(path);
}
mutex_unlock(&registry.mutex);
return ERROR_NONE;
}
void app_manager_install_path_scan(void) {
auto& registry = install_path_registry();
mutex_lock(&registry.mutex);
auto paths_copy = registry.paths;
mutex_unlock(&registry.mutex);
std::vector<std::string> found_app_dirs;
for (const auto& root : paths_copy) {
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
mutex_lock(&registry.mutex);
std::unordered_map<std::string, std::string> known_paths; // id -> path
for (const auto& [id, record] : registry.scanned) {
known_paths.emplace(id, record->path);
}
mutex_unlock(&registry.mutex);
// Stat each manifest and parse it entirely without registry.mutex held (due to filesystem IO being slow)
std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
continue;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue;
}
if (known_paths.contains(metadata.app_id)) {
continue; // already registered by an earlier scan
}
auto record = std::make_unique<ScannedAppManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir;
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 = 0,
};
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)) {
missing_ids.push_back(id);
}
}
// 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).
for (const auto& id : missing_ids) {
app_manager_remove(id.c_str());
}
std::vector<std::unique_ptr<ScannedAppManifest>> added_records;
for (auto& record : new_records) {
if (app_manager_add(&record->manifest) == ERROR_NONE) {
added_records.push_back(std::move(record));
} else {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
}
}
mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) {
registry.scanned.erase(id);
}
for (auto& record : added_records) {
registry.scanned[record->id] = std::move(record);
}
mutex_unlock(&registry.mutex);
}
} // extern "C"
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <service/manager.h>
#include <tactility/error.h>
#include <tactility/module.h>
extern "C" {
extern ServiceManifest app_internal_loader_service_manifest;
const ModuleSymbol app_module_symbols[] = {
// app/event
DEFINE_MODULE_SYMBOL(app_event_subscribe),
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
DEFINE_MODULE_SYMBOL(app_event_emit),
DEFINE_MODULE_SYMBOL(app_event_await),
// app/install
DEFINE_MODULE_SYMBOL(app_get_install_path),
DEFINE_MODULE_SYMBOL(app_install),
DEFINE_MODULE_SYMBOL(app_uninstall),
// 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_stop),
DEFINE_MODULE_SYMBOL(app_manager_finish),
DEFINE_MODULE_SYMBOL(app_manager_get_state),
DEFINE_MODULE_SYMBOL(app_manager_find_manifest),
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
DEFINE_MODULE_SYMBOL(app_manager_add),
DEFINE_MODULE_SYMBOL(app_manager_remove),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id),
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
// app/metadata
DEFINE_MODULE_SYMBOL(app_metadata_parse),
// app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path),
DEFINE_MODULE_SYMBOL(app_paths_get_assets_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// terminator
MODULE_SYMBOL_TERMINATOR
};
static error_t start() {
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
}
static error_t stop() {
return service_manager_remove(app_internal_loader_service_manifest.id);
}
Module app_module = {
.name = "app",
.start = start,
.stop = stop,
.drivers = nullptr,
.symbols = app_module_symbols,
.internal = nullptr
};
}