Refactor app loading and window management (#609)
This commit is contained in:
committed by
GitHub
parent
dc3f6104b8
commit
37c507544b
@@ -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(®istry.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(®istry.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(®istry.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(®istry.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(®istry.mutex);
|
||||
error_t result = uninstall_locked(app_id);
|
||||
mutex_unlock(®istry.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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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(®istry.mutex);
|
||||
if (std::ranges::find(registry.paths, path) == registry.paths.end()) {
|
||||
registry.paths.emplace_back(path);
|
||||
}
|
||||
mutex_unlock(®istry.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void app_manager_install_path_scan(void) {
|
||||
auto& registry = install_path_registry();
|
||||
|
||||
mutex_lock(®istry.mutex);
|
||||
auto paths_copy = registry.paths;
|
||||
mutex_unlock(®istry.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(®istry.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(®istry.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(®istry.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(®istry.mutex);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user