Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Buildscripts/sdkconfig/default.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Increase stack size for Wi-Fi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
# Ensure large enough stack for network operations
CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096
# Ensure large enough stack for network operations (e.g. AppHub)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=6144
# Fixes static assertion: FLASH and PSRAM Mode configuration are not supported
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
# Free up IRAM
Expand Down
4 changes: 1 addition & 3 deletions Documentation/ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

## Higher Priority

- AppEventSubscription, SystemEventSubscription: should use TaskHandle_t notification. That way, there can be a single wait event for a task instead of X separate ones with each their timeout.
- AppHubApp: Prevent download callbacks from accessing a destroyed view.
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
- wifi: wifi_add_event_callback() and wifi_remove_event_callback() should be replaced by a subscribe/await pattern like system events.
Expand Down Expand Up @@ -46,14 +47,11 @@

## Medium Priority

- `platform-esp32`'s module drivers are declared in start/stop of the module but they should be set via `Module::drivers`
- `struct Driver` has an `.owner`, but it's not always set. Either validate on Module construct that it matches, or otherwise set it during module start. The problem: NULL parent currently means that driver is not removable. This clashes with setting it dynamically. Consider some kind of flag to determine removability.
- Consider moving certain drivers into separate modules: audio, bt, wifi, etc
- Consider using https://github.com/Graphify-Labs/graphify
- Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html
- Make USB host driver disabled by default, so it doesn't consume memory
- Filtering for apps in App Hub:
- apps that only work on a specific device
- Diceware app has large "+" and "-' buttons on Cardputer. It should be smaller.
- TactilityTool: Make API compatibility table (and check for compatibility in the tool itself)
- Improve EspLcdDisplay to contain all the standard configuration options, and implement a default init function. Add a configuration class.
Expand Down
6 changes: 4 additions & 2 deletions Modules/app-module/include/app/event.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include "instance.h"

#include <stddef.h>
#include <stdint.h>

Expand Down Expand Up @@ -54,7 +56,7 @@ struct AppEvent {
*/
struct AppEventSubscription {
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
uint32_t app_instance_id;
AppInstanceId app_instance_id;

TaskHandle_t task;

Expand Down Expand Up @@ -89,7 +91,7 @@ error_t app_event_unsubscribe(struct AppEventSubscription* sub);
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
* dropped for that subscription (still delivered to any other matching subscription)
*/
error_t app_event_emit(uint32_t app_instance_id, const struct AppEvent* event);
error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event);

/**
* Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty.
Expand Down
8 changes: 8 additions & 0 deletions Modules/app-module/include/app/manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ error_t app_manager_install_path_add(const char* path);
*/
void app_manager_install_path_scan(void);

/**
* Uninstalls an app that was registered via app_manager_install_path_scan() (i.e. discovered on
* disk, not installed via app_install()). Stops running instances, removes the manifest
* registration, and deletes the app directory. Returns ERROR_NOT_FOUND if the app id is not in
* the scan registry.
*/
error_t app_manager_install_path_uninstall(const char* app_id);

#ifdef __cplusplus
}
#endif
86 changes: 86 additions & 0 deletions Modules/app-module/private/app/private/app_fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
#include <dirent.h>
#include <string>
#include <sys/stat.h>
#ifdef ESP_PLATFORM
#include <sys/unistd.h>
#else
#include <unistd.h>
#endif
#include <vector>

inline bool app_fs_is_directory(const std::string& path) {
Expand All @@ -36,6 +41,87 @@ inline bool app_fs_is_file(const std::string& path) {

// Appends the full path of every direct subdirectory of @a path to @a out.
// No-op (not an error) if @a path can't be opened.
inline bool app_fs_delete_recursively(const std::string& path) {
if (path.empty() || path == "/" || path == "." || path == "..") {
return true;
}

// Use lstat() so symbolic links are not followed: a symlink that points at
// an external directory must be removed as a leaf entry (unlink), not
// recursed into. app_fs_is_directory() uses stat() and would follow the
// link, potentially deleting files outside the target tree.
// ESP-IDF newlib has no lstat(); ESP32 filesystems (FAT/SPIFFS) don't
// support symlinks, so stat() is equivalent there.
struct stat st {};
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
#ifdef ESP_PLATFORM
int rc = stat(path.c_str(), &st);
#else
int rc = lstat(path.c_str(), &st);
#endif
file_mutex_unlock(&file_mutex);

if (rc != 0) {
return false;
}

#ifndef ESP_PLATFORM
if (S_ISLNK(st.st_mode)) {
// Symlink — remove as a leaf regardless of its target.
file_mutex_lock(&file_mutex);
bool result = unlink(path.c_str()) == 0;
file_mutex_unlock(&file_mutex);
return result;
}
#endif

if (S_ISDIR(st.st_mode)) {
// Collect child names while locked, then release before recursing —
// child paths can resolve to the same mount mutex (see
// app_fs_list_direct_subdirectories comment), so holding the parent
// lock across the recursive call would self-deadlock.
std::vector<std::string> children;

file_mutex_lock(&file_mutex);
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
file_mutex_unlock(&file_mutex);
return false;
}

struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
continue;
}
children.push_back(path + "/" + entry->d_name);
}
closedir(dir);
file_mutex_unlock(&file_mutex);

bool success = true;
for (const auto& child : children) {
success = app_fs_delete_recursively(child);
if (!success) {
return false;
}
}

file_mutex_lock(&file_mutex);
bool result = rmdir(path.c_str()) == 0;
file_mutex_unlock(&file_mutex);
return result;
Comment thread
KenVanHoeylandt marked this conversation as resolved.
}

// Regular file or other — unlink.
file_mutex_lock(&file_mutex);
bool result = unlink(path.c_str()) == 0;
file_mutex_unlock(&file_mutex);
return result;
}

inline void app_fs_list_direct_subdirectories(const std::string& path, std::vector<std::string>& out) {
// Collect child names while the directory lock is held, then release it before classifying
// each one with app_fs_is_directory() - that function looks up and locks a FileMutex too,
Expand Down
74 changes: 20 additions & 54 deletions Modules/app-module/source/app_install.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,62 +66,13 @@ bool ensure_directory_recursive(const std::string& 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;
LOG_I(TAG, "Deleting %s...", path.c_str());
return app_fs_delete_recursively(path);
}

bool get_app_install_directory(std::string& out_path) {
char root[192];
if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) {
if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) {
return false;
}
out_path = std::string(root) + "/app";
Expand Down Expand Up @@ -277,6 +228,11 @@ error_t uninstall_locked(const std::string& app_id) {
return ERROR_NOT_FOUND;
}

// Can't uninstall in-memory apps
if (iterator->second->manifest.location.type != APP_LOCATION_PATH) {
return ERROR_NOT_SUPPORTED;
}

stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
delete_recursively(iterator->second->path);
Expand Down Expand Up @@ -404,10 +360,20 @@ error_t app_uninstall(const char* app_id) {

auto& registry = install_registry();
mutex_lock(&registry.mutex);
error_t result = uninstall_locked(app_id);
error_t error = uninstall_locked(app_id);
mutex_unlock(&registry.mutex);

return result;
if (error == ERROR_NOT_FOUND) {
error = app_manager_install_path_uninstall(app_id);
}

if (error == ERROR_NONE) {
LOG_I(TAG, "Uninstalled %s", app_id);
} else {
LOG_I(TAG, "Uninstalling %s failed: %s", app_id, error_to_string(error));
}

return error;
}

} // extern "C"
15 changes: 11 additions & 4 deletions Modules/app-module/source/app_paths.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: Apache-2.0

#include <app/location.h>
#include <app/manager.h>
#include <app/paths.h>
#include <tactility/paths.h>

Expand All @@ -9,7 +11,7 @@ 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));
error_t error = paths_get_data_path(root, sizeof(root));
if (error != ERROR_NONE) {
return error;
}
Expand All @@ -34,12 +36,17 @@ error_t app_paths_get_user_data_path(const char* app_id, const char* child_path,
}

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));
AppManifest manifest;
error_t error = app_manager_find_manifest(app_id, &manifest);
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", directory);

if (manifest.location.type != APP_LOCATION_PATH) {
return ERROR_NOT_FOUND;
}

int written = std::snprintf(out_path, out_path_size, "%s/assets", static_cast<const char*>(manifest.location.location));
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
Expand Down
2 changes: 1 addition & 1 deletion Modules/app-module/source/event.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ error_t app_event_unsubscribe(AppEventSubscription* sub) {
return result;
}

error_t app_event_emit(uint32_t app_instance_id, const AppEvent* event) {
error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) {
AppEvent stamped_event = *event;
stamped_event.timestamp = get_micros_since_boot();

Expand Down
54 changes: 52 additions & 2 deletions Modules/app-module/source/manager.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
// 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/error.h>
#include <tactility/log.h>

#include <algorithm>
Expand Down Expand Up @@ -340,4 +339,55 @@ void app_manager_install_path_scan(void) {
mutex_unlock(&registry.mutex);
}

error_t app_manager_install_path_uninstall(const char* app_id) {
auto& registry = install_path_registry();

mutex_lock(&registry.mutex);
auto iterator = registry.scanned.find(app_id);
if (iterator == registry.scanned.end()) {
mutex_unlock(&registry.mutex);
return ERROR_NOT_FOUND;
}

const AppManifest* manifest = &iterator->second->manifest;
auto path = iterator->second->path;
mutex_unlock(&registry.mutex);

// Stop every running instance that retains this manifest pointer, mirroring
// stop_all_instances_of() in app_install.cpp. Collect under ledger.mutex,
// then call app_manager_stop() outside it (that call bound-joins the
// instance's thread, which itself takes ledger.mutex in its thread_main).
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);
}

// app_manager_remove takes ledger.mutex internally - call outside both
// registry.mutex and ledger.mutex to match the lock ordering in
// app_manager_install_path_scan().
app_manager_remove(app_id);

// Every instance has stopped and the manifest is unregistered — safe to
// delete the on-disk directory. Delete before erasing the scan record so
// that a failed deletion leaves the entry discoverable for a retry.
if (!app_fs_delete_recursively(path)) {
return ERROR_RESOURCE;
}
Comment thread
KenVanHoeylandt marked this conversation as resolved.

mutex_lock(&registry.mutex);
registry.scanned.erase(app_id);
mutex_unlock(&registry.mutex);

return ERROR_NONE;
Comment thread
KenVanHoeylandt marked this conversation as resolved.
}

} // extern "C"
Loading
Loading