mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-17 23:55:04 +00:00
Add app loading and window manager modules
This commit is contained in:
parent
8e7f997597
commit
f61a30726f
11
Modules/app-esp32-module/CMakeLists.txt
Normal file
11
Modules/app-esp32-module/CMakeLists.txt
Normal file
@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(app-esp32-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel app-module service-module elf_loader
|
||||
)
|
||||
4
Modules/app-esp32-module/devicetree.yaml
Normal file
4
Modules/app-esp32-module/devicetree.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
- Modules/app-module
|
||||
- Modules/service-module
|
||||
12
Modules/app-esp32-module/include/app_esp32/module.h
Normal file
12
Modules/app-esp32-module/include/app_esp32/module.h
Normal file
@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module app_esp32_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
141
Modules/app-esp32-module/source/app_esp32_loader_service.cpp
Normal file
141
Modules/app-esp32-module/source/app_esp32_loader_service.cpp
Normal file
@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "../../../TactilityKernel/include/tactility/error.h"
|
||||
#include "../../../TactilityKernel/include/tactility/filesystem/file_mutex.h"
|
||||
#include "../../app-module/include/app/loader.h"
|
||||
#include "../../app-module/include/app/location.h"
|
||||
|
||||
|
||||
#include <app/loader.h>
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <esp_elf.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
/** load()-allocated state, passed back through run()/unload(). */
|
||||
struct Esp32AppRuntime {
|
||||
esp_elf_t elf {};
|
||||
uint8_t* file_data = nullptr;
|
||||
};
|
||||
|
||||
error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
|
||||
FileMutex mutex;
|
||||
file_mutex_get(&mutex, path);
|
||||
file_mutex_lock(&mutex);
|
||||
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (file == nullptr) {
|
||||
file_mutex_unlock(&mutex);
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
fseek(file, 0, SEEK_END);
|
||||
long size = ftell(file);
|
||||
fseek(file, 0, SEEK_SET);
|
||||
if (size <= 0) {
|
||||
fclose(file);
|
||||
file_mutex_unlock(&mutex);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = static_cast<uint8_t*>(malloc(static_cast<size_t>(size)));
|
||||
if (data == nullptr) {
|
||||
fclose(file);
|
||||
file_mutex_unlock(&mutex);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
size_t read = fread(data, 1, static_cast<size_t>(size), file);
|
||||
fclose(file);
|
||||
file_mutex_unlock(&mutex);
|
||||
|
||||
if (read != static_cast<size_t>(size)) {
|
||||
free(data);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*out_data = data;
|
||||
*out_size = static_cast<size_t>(size);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
auto* runtime = new (std::nothrow) Esp32AppRuntime();
|
||||
if (runtime == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
size_t size = 0;
|
||||
error_t read_result = read_file(static_cast<const char*>(location.location), &runtime->file_data, &size);
|
||||
if (read_result != ERROR_NONE) {
|
||||
delete runtime;
|
||||
return read_result;
|
||||
}
|
||||
|
||||
if (esp_elf_init(&runtime->elf) != ESP_OK) {
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_elf_relocate(&runtime->elf, runtime->file_data) != 0) {
|
||||
esp_elf_deinit(&runtime->elf);
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*out_runtime = runtime;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
|
||||
auto* runtime = static_cast<Esp32AppRuntime*>(runtime_ptr);
|
||||
// A side-loaded ELF's own main() only ever gets a real argc/argv from esp_elf_request()'s
|
||||
// fixed signature - there's no slot for app_instance_id there, and side-loaded apps don't
|
||||
// need one yet.
|
||||
return esp_elf_request(&runtime->elf, 0, argc, argv);
|
||||
}
|
||||
|
||||
void api_unload(AppRuntime runtime_ptr) {
|
||||
auto* runtime = static_cast<Esp32AppRuntime*>(runtime_ptr);
|
||||
esp_elf_deinit(&runtime->elf);
|
||||
free(runtime->file_data);
|
||||
delete runtime;
|
||||
}
|
||||
|
||||
AppLoaderApi loader_api = {
|
||||
.load = api_load,
|
||||
.run = api_run,
|
||||
.unload = api_unload,
|
||||
};
|
||||
|
||||
void* create_service(const ServiceManifest*) {
|
||||
return &loader_api;
|
||||
}
|
||||
|
||||
void destroy_service(const ServiceManifest*, void*) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern ServiceManifest loader_service_manifest = {
|
||||
.id = APP_LOADER_PATH_SERVICE_ID,
|
||||
.create_service = create_service,
|
||||
.destroy_service = destroy_service,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
};
|
||||
30
Modules/app-esp32-module/source/module.cpp
Normal file
30
Modules/app-esp32-module/source/module.cpp
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app_esp32/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest loader_service_manifest;
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
return service_manager_remove(loader_service_manifest.id);
|
||||
}
|
||||
|
||||
Module app_esp32_module = {
|
||||
.name = "app-esp32",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
12
Modules/app-module/CMakeLists.txt
Normal file
12
Modules/app-module/CMakeLists.txt
Normal file
@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(app-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel service-module minitar
|
||||
)
|
||||
3
Modules/app-module/devicetree.yaml
Normal file
3
Modules/app-module/devicetree.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
- Modules/service-module
|
||||
103
Modules/app-module/include/app/event.h
Normal file
103
Modules/app-module/include/app/event.h
Normal file
@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies the kind of app-lifecycle event delivered through app_event_await(). */
|
||||
enum AppEventType {
|
||||
APP_EVENT_RESULT, // struct AppResultEventData
|
||||
APP_EVENT_CLOSE, // no data - terminate now, permanently
|
||||
};
|
||||
|
||||
/** Data for APP_EVENT_RESULT. */
|
||||
struct AppResultEventData {
|
||||
uint32_t launch_id;
|
||||
/** The child app instance's own AppMainFn/AppLoaderApi::run() return value. By convention:
|
||||
* 0 = Ok, 1 = Cancelled, 2 = Error. Apps that need to hand back more than this (e.g. picked
|
||||
* text, a path) expose their own "get last result" getter instead - see e.g.
|
||||
* tt::app::inputdialog::getLastText(). */
|
||||
int32_t result;
|
||||
};
|
||||
|
||||
struct AppEvent {
|
||||
enum AppEventType type;
|
||||
/** Stamped by app_event_emit(); any value passed in by the caller is ignored. */
|
||||
uint64_t timestamp;
|
||||
/** Valid only when type == APP_EVENT_RESULT. */
|
||||
struct AppResultEventData result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Number of events that can be queued per subscription before app_event_emit() starts
|
||||
* returning ERROR_RESOURCE (dropping the newest event, preserving FIFO order of what's
|
||||
* already queued). Deliberately generous: app-module's scheduler is the only emitter and it
|
||||
* serializes app-lifecycle transitions, so a given app can't realistically receive events
|
||||
* faster than the scheduler produces them one at a time.
|
||||
*/
|
||||
#define APP_EVENT_QUEUE_CAPACITY 4
|
||||
|
||||
/**
|
||||
* Caller-owned subscription node. Unlike TactilityKernel's system_event poll subscription
|
||||
* (which coalesces to the latest value), this queues events by value (FIFO) since dropping an
|
||||
* APP_EVENT_RESULT would be unacceptable.
|
||||
* @warning Fields other than `app_instance_id` are for internal use only; do not read or write
|
||||
* them directly.
|
||||
*/
|
||||
struct AppEventSubscription {
|
||||
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
|
||||
uint32_t app_instance_id;
|
||||
|
||||
TaskHandle_t task;
|
||||
|
||||
struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
|
||||
uint8_t head;
|
||||
uint8_t count;
|
||||
|
||||
struct AppEventSubscription* next;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a subscription for events addressed to @a sub->app_instance_id.
|
||||
* @warning Does not work in ISR context.
|
||||
* @param[in,out] sub subscription to register; caller sets @a sub->app_instance_id beforehand,
|
||||
* owns the storage, and must keep it alive (and stationary) until unsubscribed
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t app_event_subscribe(struct AppEventSubscription* sub);
|
||||
|
||||
/**
|
||||
* Remove a previously registered subscription.
|
||||
* @warning Does not work in ISR context.
|
||||
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
|
||||
*/
|
||||
error_t app_event_unsubscribe(struct AppEventSubscription* sub);
|
||||
|
||||
/**
|
||||
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
|
||||
* @warning Does not work in ISR context.
|
||||
* @retval ERROR_NONE delivered to at least one subscription
|
||||
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty.
|
||||
* @retval ERROR_NONE @a out_event was filled
|
||||
* @retval ERROR_TIMEOUT no event arrived before the timeout elapsed
|
||||
*/
|
||||
error_t app_event_await(struct AppEventSubscription* sub, struct AppEvent* out_event, TickType_t timeout);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
50
Modules/app-module/include/app/install.h
Normal file
50
Modules/app-module/include/app/install.h
Normal file
@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Computes the install directory for @a app_id (does not check whether anything is actually
|
||||
* installed there).
|
||||
* @param[out] path always NULL-terminated on return, even on failure (empty string if
|
||||
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
|
||||
* NULL terminator)
|
||||
* @retval ERROR_NOT_FOUND the app install location isn't available (e.g. no SD card)
|
||||
*/
|
||||
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
|
||||
|
||||
/**
|
||||
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
|
||||
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
|
||||
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
|
||||
* If an app with the same id is already installed (via a previous app_install() call), it is
|
||||
* uninstalled first - stopped if running, its old install directory removed - before the new
|
||||
* one takes its place.
|
||||
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
|
||||
* at its root)
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
|
||||
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
|
||||
*/
|
||||
error_t app_install(const char* source_path);
|
||||
|
||||
/**
|
||||
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
|
||||
* install directory, and unregisters it (app_manager_remove()).
|
||||
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
|
||||
*/
|
||||
error_t app_uninstall(const char* app_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
19
Modules/app-module/include/app/instance.h
Normal file
19
Modules/app-module/include/app/instance.h
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Lifecycle state of a running (or previously running) app instance. Every app instance owns
|
||||
* its own task for its entire lifetime - there is no "saved, task given up" state. */
|
||||
typedef enum {
|
||||
APP_INSTANCE_STATE_STARTING,
|
||||
APP_INSTANCE_STATE_ACTIVE,
|
||||
APP_INSTANCE_STATE_STOPPING,
|
||||
APP_INSTANCE_STATE_STOPPED,
|
||||
} AppInstanceState;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
59
Modules/app-module/include/app/loader.h
Normal file
59
Modules/app-module/include/app/loader.h
Normal file
@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/manifest.h>
|
||||
#include <tactility/error.h>
|
||||
#include <stdint.h>
|
||||
#include "location.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** service-module id the AppLoaderApi implementation for AppManifest::location.type ==
|
||||
* APP_LOCATION_MEMORY must register under. Implemented by app-module itself (source/app_internal_loader.cpp). */
|
||||
#define APP_LOADER_MEMORY_SERVICE_ID "app-loader-memory"
|
||||
|
||||
/** service-module id the AppLoaderApi implementation for AppManifest::location.type ==
|
||||
* APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */
|
||||
#define APP_LOADER_PATH_SERVICE_ID "app-loader-path"
|
||||
|
||||
/**
|
||||
* Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this
|
||||
* firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance,
|
||||
* blocking for the app's whole lifetime - same contract as an external app's main(), plus
|
||||
* @a app_instance_id identifying this running instance (use it with
|
||||
* app_event_subscribe()/window_manager_create()/app_manager_finish()/etc.).
|
||||
* AppManifest::location.location holds this cast to void*.
|
||||
*/
|
||||
typedef int32_t (*AppMainFn)(uint32_t app_instance_id, int argc, char* argv[]);
|
||||
|
||||
typedef void* AppRuntime;
|
||||
|
||||
/**
|
||||
* Pluggable mechanism for loading and executing an app.
|
||||
*/
|
||||
struct AppLoaderApi {
|
||||
/**
|
||||
* Prepares an app instance for execution (e.g. read + relocate its binary).
|
||||
* @param[in] location the location to load the elf from
|
||||
* @param[out] out_runtime opaque handle to whatever load() allocated; passed back to run()/unload()
|
||||
*/
|
||||
error_t (*load)(struct AppLocation location, AppRuntime* out_runtime);
|
||||
|
||||
/**
|
||||
* Blocking: runs the app to completion.
|
||||
* @param[in] runtime handle produced by load()
|
||||
* @param[in] app_instance_id the running instance's id
|
||||
* @param[in] argc the amount of arguments in @a argv
|
||||
* @param[in] argv the array of string pointers (can be NULL)
|
||||
*/
|
||||
int32_t (*run)(AppRuntime runtime, uint32_t app_instance_id, int argc, char* argv[]);
|
||||
|
||||
/** Releases whatever load() allocated. Called after run() returns. */
|
||||
void (*unload)(AppRuntime runtime);
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
20
Modules/app-module/include/app/location.h
Normal file
20
Modules/app-module/include/app/location.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum AppLocationType {
|
||||
APP_LOCATION_MEMORY,
|
||||
APP_LOCATION_PATH,
|
||||
};
|
||||
|
||||
struct AppLocation {
|
||||
enum AppLocationType type;
|
||||
/** Meaning depends on `type`; see AppLocationType. */
|
||||
void* location;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
135
Modules/app-module/include/app/manager.h
Normal file
135
Modules/app-module/include/app/manager.h
Normal file
@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
|
||||
typedef uint32_t AppInstanceId;
|
||||
|
||||
/**
|
||||
* Register an app manifest.
|
||||
* @retval ERROR_INVALID_ARGUMENT a manifest with the same id is already registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_add(const struct AppManifest* manifest);
|
||||
|
||||
/**
|
||||
* Unregister a previously-added manifest.
|
||||
* @retval ERROR_NOT_FOUND no manifest with this id is registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_remove(const char* id);
|
||||
|
||||
/** @return the manifest, or NULL if not found. */
|
||||
const struct AppManifest* app_manager_find_manifest(const char* id);
|
||||
|
||||
/**
|
||||
* Calls @a visitor once for every registered manifest (e.g. for AppList/Settings to enumerate
|
||||
* apps to show). Iteration order is unspecified. Safe to call app_manager_add()/_remove() from
|
||||
* within @a visitor is NOT guaranteed - do not mutate the registry from inside the callback.
|
||||
*/
|
||||
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
|
||||
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
|
||||
|
||||
/**
|
||||
* Starts a new instance of the app registered under @a id. Every app instance gets its own
|
||||
* dedicated task for its entire lifetime - starting an app never asks any other app to give up
|
||||
* its task, and multiple instances (of the same or different apps) can be Active at once.
|
||||
* @param[in] id the manifest id to start
|
||||
* @param[out] out_app_instance_id the id of the new app instance
|
||||
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id);
|
||||
|
||||
/**
|
||||
* Same as app_manager_start(), but also passes @a argc/@a argv to the new instance's own main
|
||||
* function (see app/loader.h's AppMainFn) - modelled on a C program's main(argc, argv). For
|
||||
* regular (non-modal) navigations that need to pass data to the target app (e.g. "show details
|
||||
* for this app id") without expecting a result back.
|
||||
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so
|
||||
* @a argv and the strings it points to may be freed/go out of scope immediately after this call
|
||||
* returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s).
|
||||
*/
|
||||
error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
|
||||
|
||||
/**
|
||||
* Starts @a id as a modal child of @a parent_instance_id, for the purpose of receiving a
|
||||
* result. The parent keeps running (window_manager's own multi-window stack handles burying its
|
||||
* window while the child is shown).
|
||||
*
|
||||
* When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id -
|
||||
* result is whatever the child's AppMainFn/AppLoaderApi::run() returned - unless
|
||||
* @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for
|
||||
* callers with no app_instance_id of their own). The parent is then responsible for calling
|
||||
* app_manager_stop() on the child's instance id to fully reap it. Children that need to hand
|
||||
* back more than an int32_t (e.g. picked text, a path) expose their own "get last result"
|
||||
* getter for the parent to call after receiving the event - see e.g.
|
||||
* tt::app::inputdialog::getLastText().
|
||||
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as
|
||||
* app_manager_start_with_parameters()), so @a argv and the strings it points to may be
|
||||
* freed/go out of scope immediately after this call returns.
|
||||
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
|
||||
* if it was running.
|
||||
* @warning Must not be called from the instance's own task (it bound-waits via thread_join(),
|
||||
* which asserts against joining yourself) - an app closing itself must call app_manager_finish()
|
||||
* instead, right before returning from its own AppMainFn/AppLoaderApi::run().
|
||||
*/
|
||||
error_t app_manager_stop(AppInstanceId app_instance_id);
|
||||
|
||||
/**
|
||||
* Called by an app instance, from its own task, right before it returns in response to
|
||||
* APP_EVENT_CLOSE - whether that close was self-initiated (e.g. its own back button) or came
|
||||
* from someone else. Marks this instance Stopped immediately (rather than waiting for its task
|
||||
* to actually exit) so app_manager_get_state()/app_manager_get_topmost_instance_id() reflect the
|
||||
* closure as soon as the app has decided to close, not just once its task has fully unwound.
|
||||
* @warning Does not join or free this instance's own task/ledger entry (can't - this runs on
|
||||
* that very task); those are cleaned up on a later app_manager_stop() call, same as any
|
||||
* self-terminating instance.
|
||||
*/
|
||||
error_t app_manager_finish(AppInstanceId app_instance_id);
|
||||
|
||||
/** @return the instance's current state, or APP_INSTANCE_STATE_STOPPED if the id is unknown. */
|
||||
AppInstanceState app_manager_get_state(AppInstanceId app_instance_id);
|
||||
|
||||
/**
|
||||
* @param[out] out_app_instance_id set to the instance id of the topmost currently-Active app -
|
||||
* the most recently started of whichever instances are Active (a modal child launched via
|
||||
* app_manager_start_for_result() stays Active alongside its parent while shown, so this
|
||||
* correctly picks the child, not the parent, while a dialog is up).
|
||||
* @retval ERROR_NOT_FOUND no app is Active
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id);
|
||||
|
||||
/**
|
||||
* Same as app_manager_get_topmost_instance_id(), but resolves straight to the topmost app's
|
||||
* manifest id string.
|
||||
* @param[out] buffer always NULL-terminated on return, even on failure (empty string if
|
||||
* @a buffer_size == 0 - nothing is written in that case; otherwise at least "" is written)
|
||||
* @retval ERROR_NOT_FOUND no app is Active
|
||||
* @retval ERROR_BUFFER_OVERFLOW @a buffer_size is too small to hold the id (including the NULL
|
||||
* terminator)
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
41
Modules/app-module/include/app/manifest.h
Normal file
41
Modules/app-module/include/app/manifest.h
Normal file
@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include "location.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Broad classification of an app, used for grouping/launcher presentation. */
|
||||
enum AppCategory {
|
||||
APP_CATEGORY_SYSTEM,
|
||||
APP_CATEGORY_SETTINGS,
|
||||
APP_CATEGORY_USER,
|
||||
};
|
||||
|
||||
/** Bit flags for AppManifest::flags. */
|
||||
enum AppManifestFlags {
|
||||
/** Excluded from generic app-browsing UIs (AppList, Settings) - for apps only ever reached
|
||||
* by direct navigation (modal dialogs, detail views that require parameters, wizard/
|
||||
* bootstrap steps). */
|
||||
APP_MANIFEST_FLAG_HIDDEN = 0b00000001,
|
||||
};
|
||||
|
||||
/** Describes a registrable app. One manifest exists per app id. */
|
||||
struct AppManifest {
|
||||
/** Unique app identifier. Should never be NULL. */
|
||||
const char* id;
|
||||
/** Human-readable name. Should never be NULL. */
|
||||
const char* name;
|
||||
enum AppCategory category;
|
||||
struct AppLocation location;
|
||||
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
|
||||
uint8_t flags;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
59
Modules/app-module/include/app/metadata.h
Normal file
59
Modules/app-module/include/app/metadata.h
Normal file
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define APP_METADATA_TARGET_SDK_LENGTH 16
|
||||
#define APP_METADATA_APP_ID_LENGTH 32
|
||||
#define APP_METADATA_APP_NAME_LENGTH 32
|
||||
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
|
||||
|
||||
struct AppMetadata {
|
||||
|
||||
/**
|
||||
* The SDK version that was used to compile this app. (e.g. "0.6.0")
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The identifier by which the app is launched by the system and other apps.
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The user-readable name of the app. Used in UI.
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The version as it is displayed to the user (e.g. "1.2.0")
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
|
||||
|
||||
/** The technical version (must be incremented with new releases of the app */
|
||||
uint64_t app_version_code = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
|
||||
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
|
||||
* first line.
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
|
||||
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
|
||||
* @a out_metadata's fixed-size buffers
|
||||
*/
|
||||
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
12
Modules/app-module/include/app/module.h
Normal file
12
Modules/app-module/include/app/module.h
Normal file
@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module app_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
54
Modules/app-module/private/app/private/app_ledger.h
Normal file
54
Modules/app-module/private/app/private/app_ledger.h
Normal file
@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/** A registered/running app instance, as tracked internally by app-module. */
|
||||
struct AppInstanceRecord {
|
||||
uint32_t id;
|
||||
const AppManifest* manifest;
|
||||
AppInstanceState state;
|
||||
/** The kernel thread currently executing AppLoaderApi::run() for this instance; NULL when not running. */
|
||||
Thread* thread;
|
||||
|
||||
/** 0 for a top-level launch (app_manager_start()). Non-zero for a modal child launched via
|
||||
* app_manager_start_for_result() - the instance that receives this child's APP_EVENT_RESULT. */
|
||||
uint32_t parent_id = 0;
|
||||
};
|
||||
|
||||
struct AppLedger {
|
||||
std::unordered_map<std::string, const AppManifest*> manifests;
|
||||
std::unordered_map<uint32_t, AppInstanceRecord> instances;
|
||||
uint32_t next_instance_id = 1;
|
||||
Mutex mutex {};
|
||||
|
||||
AppLedger() { mutex_construct(&mutex); }
|
||||
~AppLedger() { mutex_destruct(&mutex); }
|
||||
};
|
||||
|
||||
inline AppLedger& app_ledger() {
|
||||
static AppLedger ledger;
|
||||
return ledger;
|
||||
}
|
||||
|
||||
/** Frees a deep-copied argv previously built by app_manager_start_with_parameters()/
|
||||
* app_manager_start_for_result() (see app_scheduler.cpp's ThreadContext::argv) - each
|
||||
* individually heap-allocated string, then the array itself. Safe to call with count == 0 /
|
||||
* values == nullptr (no-op). */
|
||||
inline void app_ledger_free_arguments(int count, char** values) {
|
||||
if (values == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
delete[] values[i];
|
||||
}
|
||||
delete[] values;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
/** Shared helpers + per-format parsers for app_metadata_parse() (source/app_metadata_parsing.cpp)
|
||||
* - split out like the old tt::app manifest parser (AppManifestParsing/V1/V2.cpp) that this is
|
||||
* modelled on, one file per format plus a shared dispatcher. */
|
||||
|
||||
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
|
||||
|
||||
bool app_metadata_is_valid_format_version(const std::string& version);
|
||||
bool app_metadata_is_valid_id(const std::string& id);
|
||||
bool app_metadata_is_valid_name(const std::string& name);
|
||||
bool app_metadata_is_valid_version_name(const std::string& version);
|
||||
bool app_metadata_is_valid_version_code(const std::string& version);
|
||||
|
||||
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
|
||||
* terminator) if it fits.
|
||||
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
|
||||
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value);
|
||||
|
||||
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map into @a out_metadata. */
|
||||
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
|
||||
|
||||
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
|
||||
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
|
||||
43
Modules/app-module/private/app/private/app_scheduler.h
Normal file
43
Modules/app-module/private/app/private/app_scheduler.h
Normal file
@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Owns per-app task lifecycle on behalf of app_manager_*(). AppLoaderApi implementations
|
||||
* stay task-agnostic; all of thread_alloc_full()/thread_start()/thread_join() happen here.
|
||||
* Every app instance gets its own dedicated task for its entire lifetime - no task is ever
|
||||
* reused for a different instance.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Loads and starts an app instance: spawns a dedicated task that calls
|
||||
* AppLoaderApi::load()/run(), marking the instance ACTIVE for the duration of run().
|
||||
* @param[in] app_instance_id id already allocated in the ledger for this instance
|
||||
* @param[in] location the location of the app
|
||||
* @param[in] argc the amount of arguments to pass to the app's main function
|
||||
* @param[in] argv the array of arguments to pass to the app's main function - ownership is
|
||||
* taken by the scheduler regardless of outcome (freed once the spawned task's run() returns, or
|
||||
* immediately on a failure to start it)
|
||||
*/
|
||||
error_t app_scheduler_start(uint32_t app_instance_id, struct AppLocation location, int argc, char* argv[]);
|
||||
|
||||
/**
|
||||
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its
|
||||
* task to exit, and removes it from the ledger.
|
||||
*/
|
||||
error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
386
Modules/app-module/source/app_install.cpp
Normal file
386
Modules/app-module/source/app_install.cpp
Normal file
@ -0,0 +1,386 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/install.h>
|
||||
|
||||
#include <app/manager.h>
|
||||
#include <app/metadata.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);
|
||||
}
|
||||
|
||||
bool is_directory(const std::string& path) {
|
||||
struct stat result {};
|
||||
return stat(path.c_str(), &result) == 0 && S_ISDIR(result.st_mode);
|
||||
}
|
||||
|
||||
bool is_file(const std::string& path) {
|
||||
struct stat result {};
|
||||
return stat(path.c_str(), &result) == 0 && S_ISREG(result.st_mode);
|
||||
}
|
||||
|
||||
// mkdir -p.
|
||||
bool ensure_directory(const std::string& path) {
|
||||
if (path.empty() || 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 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) {
|
||||
if (path.empty() || path == "/" || path == "." || path == "..") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_directory(path)) {
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
LOG_E(TAG, "Failed to scan directory %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
struct 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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FileMutex mutex {};
|
||||
file_mutex_get(&mutex, path.c_str());
|
||||
file_mutex_lock(&mutex);
|
||||
bool result = rmdir(path.c_str()) == 0;
|
||||
file_mutex_unlock(&mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (is_file(path)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Doesn't exist - nothing to do.
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Takes install_registry().mutex - caller must not already hold it.
|
||||
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;
|
||||
}
|
||||
|
||||
// Extract to a staging directory named after the tarball first - the real app id (and so
|
||||
// the final directory name) is only known once the manifest inside it is parsed.
|
||||
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 (!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_W(TAG, "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(metadata.app_id);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
auto record = std::make_unique<InstalledAppRecord>();
|
||||
record->id = metadata.app_id;
|
||||
record->name = metadata.app_name;
|
||||
record->path = final_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,
|
||||
};
|
||||
|
||||
error_t add_result = app_manager_add(&record->manifest);
|
||||
if (add_result != ERROR_NONE) {
|
||||
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
|
||||
// already removed any previous registration for this exact id.
|
||||
mutex_unlock(®istry.mutex);
|
||||
return add_result;
|
||||
}
|
||||
|
||||
registry.apps[record->id] = std::move(record);
|
||||
mutex_unlock(®istry.mutex);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
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"
|
||||
48
Modules/app-module/source/app_internal_loader.cpp
Normal file
48
Modules/app-module/source/app_internal_loader.cpp
Normal file
@ -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
|
||||
|
||||
extern 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,
|
||||
};
|
||||
157
Modules/app-module/source/app_metadata_parsing.cpp
Normal file
157
Modules/app-module/source/app_metadata_parsing.cpp
Normal file
@ -0,0 +1,157 @@
|
||||
// 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 (!got_first_line) {
|
||||
out_first_line = trimmed_line;
|
||||
got_first_line = true;
|
||||
}
|
||||
|
||||
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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) {
|
||||
return !version.empty() && 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;
|
||||
}
|
||||
95
Modules/app-module/source/app_metadata_parsing_v1.cpp
Normal file
95
Modules/app-module/source/app_metadata_parsing_v1.cpp
Normal file
@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
out_metadata.app_version_code = std::stoull(version_code_string);
|
||||
|
||||
// [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;
|
||||
}
|
||||
95
Modules/app-module/source/app_metadata_parsing_v2.cpp
Normal file
95
Modules/app-module/source/app_metadata_parsing_v2.cpp
Normal file
@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <app/private/app_metadata_parsing_internal.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
out_metadata.app_version_code = std::stoull(version_code_string);
|
||||
|
||||
// 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;
|
||||
}
|
||||
190
Modules/app-module/source/app_scheduler.cpp
Normal file
190
Modules/app-module/source/app_scheduler.cpp
Normal file
@ -0,0 +1,190 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/app_scheduler.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/loader.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#define TAG "app_scheduler"
|
||||
|
||||
namespace {
|
||||
|
||||
struct ThreadContext {
|
||||
const AppLoaderApi* loader;
|
||||
void* runtime;
|
||||
uint32_t app_instance_id;
|
||||
int argc;
|
||||
char** argv;
|
||||
};
|
||||
|
||||
void set_state(uint32_t 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);
|
||||
}
|
||||
|
||||
Thread* get_thread(uint32_t app_instance_id) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
Thread* thread = (iterator != ledger.instances.end()) ? iterator->second.thread : nullptr;
|
||||
mutex_unlock(&ledger.mutex);
|
||||
return thread;
|
||||
}
|
||||
|
||||
void set_thread(uint32_t app_instance_id, Thread* thread) {
|
||||
auto& ledger = app_ledger();
|
||||
mutex_lock(&ledger.mutex);
|
||||
auto iterator = ledger.instances.find(app_instance_id);
|
||||
if (iterator != ledger.instances.end()) {
|
||||
iterator->second.thread = thread;
|
||||
}
|
||||
mutex_unlock(&ledger.mutex);
|
||||
}
|
||||
|
||||
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(uint32_t app_instance_id, int32_t result) {
|
||||
auto& ledger = app_ledger();
|
||||
|
||||
uint32_t 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);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t thread_main(void* context) {
|
||||
auto* ctx = static_cast<ThreadContext*>(context);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
delete ctx;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_scheduler_start(uint32_t 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) {
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return load_result;
|
||||
}
|
||||
|
||||
auto* context = new (std::nothrow) ThreadContext { loader, runtime, app_instance_id, argc, argv };
|
||||
if (context == nullptr) {
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// -1 (no affinity) matches the FreeRTOS POSIX/simulator port; ESP-IDF's tskNO_AFFINITY is
|
||||
// a numerically equivalent SMP-only constant not available in the plain FreeRTOS-Kernel port.
|
||||
Thread* thread = thread_alloc_full("app", 8192, thread_main, context, -1);
|
||||
if (thread == nullptr) {
|
||||
delete context;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
set_thread(app_instance_id, thread);
|
||||
|
||||
error_t start_result = thread_start(thread);
|
||||
if (start_result != ERROR_NONE) {
|
||||
set_thread(app_instance_id, nullptr);
|
||||
thread_free(thread);
|
||||
delete context;
|
||||
loader->unload(runtime);
|
||||
app_ledger_free_arguments(argc, argv);
|
||||
return start_result;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_scheduler_stop(uint32_t app_instance_id, TickType_t join_timeout) {
|
||||
Thread* thread = get_thread(app_instance_id);
|
||||
if (thread != nullptr) {
|
||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
||||
app_event_emit(app_instance_id, &event);
|
||||
|
||||
if (thread_join(thread, join_timeout, pdMS_TO_TICKS(10)) != ERROR_NONE) {
|
||||
LOG_W(TAG, "App instance %u did not stop in time", app_instance_id);
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
thread_free(thread);
|
||||
set_thread(app_instance_id, nullptr);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
116
Modules/app-module/source/event.cpp
Normal file
116
Modules/app-module/source/event.cpp
Normal file
@ -0,0 +1,116 @@
|
||||
// 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 (not broadcast by
|
||||
// type, unlike TactilityKernel's system_event) - an app should only ever see events addressed
|
||||
// to it. Guarded by a single coarse-grained mutex, same tradeoff system_event.cpp makes for its
|
||||
// poll-subscription list: 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"
|
||||
199
Modules/app-module/source/manager.cpp
Normal file
199
Modules/app-module/source/manager.cpp
Normal file
@ -0,0 +1,199 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/manager.h>
|
||||
|
||||
#include <app/private/app_ledger.h>
|
||||
#include <app/private/app_scheduler.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#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"
|
||||
30
Modules/app-module/source/module.cpp
Normal file
30
Modules/app-module/source/module.cpp
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
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 = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
11
Modules/lvgl-window-manager/CMakeLists.txt
Normal file
11
Modules/lvgl-window-manager/CMakeLists.txt
Normal file
@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(lvgl-window-manager
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel lvgl-module
|
||||
)
|
||||
2
Modules/lvgl-window-manager/devicetree.yaml
Normal file
2
Modules/lvgl-window-manager/devicetree.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module lvgl_window_manager_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef uint32_t WindowId;
|
||||
|
||||
enum WindowState {
|
||||
/** id is the current topmost window and has live widgets. */
|
||||
WINDOW_STATE_GRANTED,
|
||||
/** id is not currently topmost - either buried under a newer window (its widgets don't
|
||||
* exist right now, but it may resurface and get rebuilt if everything above it is removed)
|
||||
* or it no longer exists at all (removed). */
|
||||
WINDOW_STATE_REVOKED,
|
||||
};
|
||||
|
||||
/**
|
||||
* Called once by window_manager_start(), given the real root widget (a raw, full-size
|
||||
* container created directly under the default display's active screen). May add extra chrome
|
||||
* (e.g. a statusbar) as children of @a root_widget.
|
||||
* @param[in] root_widget the real root widget; owned by this module, deleted automatically
|
||||
* (along with everything added under it) by window_manager_stop()
|
||||
* @return the widget windows should actually be placed into - @a root_widget itself, or a
|
||||
* child of it. Returning NULL falls back to @a root_widget.
|
||||
* @warning Called on the LVGL task with the LVGL lock already held.
|
||||
*/
|
||||
typedef lv_obj_t* (*WindowManagerScreenInitFn)(lv_obj_t* root_widget);
|
||||
|
||||
/**
|
||||
* Configures the screen-init callback window_manager_start() invokes to build the root/content
|
||||
* widgets. Pass NULL to restore the default (no chrome - the raw root widget is used directly).
|
||||
* @warning Must be called before window_manager_start(); has no effect once already started.
|
||||
*/
|
||||
void window_manager_configure(WindowManagerScreenInitFn screen_init);
|
||||
|
||||
/**
|
||||
* Creates the root widget (under the default display's active screen) and, via the configured
|
||||
* screen-init callback, whatever chrome/content widget it wants around it. Idempotent - a
|
||||
* second call while already started is a no-op.
|
||||
* @retval ERROR_RESOURCE no default display is active (lv_screen_active() returned NULL)
|
||||
* @retval ERROR_NONE on success (including if already started)
|
||||
*/
|
||||
error_t window_manager_start(void);
|
||||
|
||||
/**
|
||||
* Deletes the root widget created by window_manager_start() (and everything under it - any
|
||||
* chrome plus whatever the topmost window had drawn), removing it from the display, and drops
|
||||
* every tracked window. Idempotent - a second call while already stopped is a no-op.
|
||||
*/
|
||||
error_t window_manager_stop(void);
|
||||
|
||||
/**
|
||||
* Called to populate a window's widgets: once by window_manager_create() when the window is
|
||||
* first created, and again later by window_manager_remove() if this window resurfaces as the
|
||||
* new topmost after whatever was above it is removed. Only the current topmost window ever has
|
||||
* live widgets - everything below it in the stack exists as tracked state only.
|
||||
* @param[in] root a fresh, full-size container created directly under the content widget for
|
||||
* this window; deleted automatically once this window stops being topmost
|
||||
* @param[in] user_data whatever was passed to window_manager_create() for this window
|
||||
* @warning Called on the LVGL task with the LVGL lock already held.
|
||||
* @warning May run on a different kernel thread than the one that called window_manager_create()
|
||||
* for this window - the rebuild-on-remove path runs on whichever thread called
|
||||
* window_manager_remove() for the window that used to be on top (e.g. a dialog's own thread as
|
||||
* it closes). Do NOT rely on thread_local state set by this window's own app thread; use
|
||||
* @a user_data instead.
|
||||
*/
|
||||
typedef void (*WindowCreateWidgetsFn)(lv_obj_t* root, void* user_data);
|
||||
|
||||
/**
|
||||
* Creates a new window on top of the stack (last created = topmost). Deletes the previously
|
||||
* topmost window's widgets (if any) and builds this window's widgets immediately via
|
||||
* @a create_widgets - only the topmost window ever has live widgets.
|
||||
* @param[in] user_data opaque; passed back to @a create_widgets on every call, including a
|
||||
* later rebuild triggered by window_manager_remove() - see its @warning about which thread that
|
||||
* can run on. Typically the calling app's own Context*.
|
||||
* @return the new window's id, or 0 if window_manager_start() hasn't been called
|
||||
*/
|
||||
WindowId window_manager_create(uint32_t app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data);
|
||||
|
||||
/**
|
||||
* Removes a window, wherever it is in the stack - not necessarily the topmost one. If it was
|
||||
* topmost, its widgets are deleted and whichever window is now on top (if any) has its
|
||||
* create_widgets called again to rebuild its widgets.
|
||||
*/
|
||||
void window_manager_remove(WindowId id);
|
||||
|
||||
/** @return the current state of @a id; WINDOW_STATE_REVOKED if @a id is buried or doesn't exist. */
|
||||
enum WindowState window_manager_get_state(WindowId id);
|
||||
|
||||
/**
|
||||
* Blocks the calling task until @a id's state changes away from WINDOW_STATE_GRANTED, or
|
||||
* @a timeout elapses. Returns immediately with WINDOW_STATE_REVOKED if @a id isn't currently
|
||||
* topmost (nothing to wait for).
|
||||
* @return the state after waking (or immediately, if there was nothing to wait for)
|
||||
*/
|
||||
enum WindowState window_manager_await_state_change(WindowId id, TickType_t timeout);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
19
Modules/lvgl-window-manager/source/module.cpp
Normal file
19
Modules/lvgl-window-manager/source/module.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl_window_manager/module.h>
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
Module lvgl_window_manager_module = {
|
||||
.name = "lvgl-window-manager",
|
||||
.start = window_manager_start,
|
||||
.stop = window_manager_stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
279
Modules/lvgl-window-manager/source/window_manager.cpp
Normal file
279
Modules/lvgl-window-manager/source/window_manager.cpp
Normal file
@ -0,0 +1,279 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
struct WindowRecord {
|
||||
WindowId id;
|
||||
uint32_t app_instance_id;
|
||||
WindowCreateWidgetsFn create_widgets;
|
||||
void* user_data;
|
||||
};
|
||||
|
||||
struct WindowManagerState {
|
||||
Mutex mutex {};
|
||||
|
||||
bool started = false;
|
||||
WindowManagerScreenInitFn screen_init = nullptr;
|
||||
|
||||
/** The raw, full-size container window_manager_start() creates; owns (and deletion
|
||||
* cascades to) whatever the screen-init callback added under it. */
|
||||
lv_obj_t* real_root_widget = nullptr;
|
||||
/** The stable parent each window's own widget is created under - real_root_widget itself,
|
||||
* unless the screen-init callback returned a nested content widget instead. */
|
||||
lv_obj_t* content_root_widget = nullptr;
|
||||
|
||||
WindowId next_id = 1;
|
||||
/** windows.back() is topmost; only it ever has a live widget (top_widget). */
|
||||
std::vector<WindowRecord> windows;
|
||||
lv_obj_t* top_widget = nullptr;
|
||||
|
||||
/** Task blocked in window_manager_await_state_change(), if any. */
|
||||
TaskHandle_t waiting_task = nullptr;
|
||||
|
||||
WindowManagerState() { mutex_construct(&mutex); }
|
||||
};
|
||||
|
||||
WindowManagerState& state() {
|
||||
static WindowManagerState instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
if (content == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
lvgl_lock();
|
||||
lv_obj_t* widget = lv_obj_create(content);
|
||||
lv_obj_set_size(widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(widget, 0, LV_STATE_DEFAULT);
|
||||
if (create_widgets != nullptr) {
|
||||
create_widgets(widget, user_data);
|
||||
}
|
||||
lvgl_unlock();
|
||||
return widget;
|
||||
}
|
||||
|
||||
void delete_widget(lv_obj_t* widget) {
|
||||
if (widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
lvgl_lock();
|
||||
lv_obj_delete(widget);
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
void window_manager_configure(WindowManagerScreenInitFn screen_init) {
|
||||
auto& s = state();
|
||||
mutex_lock(&s.mutex);
|
||||
s.screen_init = screen_init;
|
||||
mutex_unlock(&s.mutex);
|
||||
}
|
||||
|
||||
error_t window_manager_start(void) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
WindowManagerScreenInitFn screen_init = s.screen_init;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
lv_obj_t* real_widget = nullptr;
|
||||
lv_obj_t* content_widget = nullptr;
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_t* screen = lv_screen_active();
|
||||
if (screen != nullptr) {
|
||||
real_widget = lv_obj_create(screen);
|
||||
lv_obj_set_size(real_widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(real_widget, 0, LV_STATE_DEFAULT);
|
||||
|
||||
content_widget = (screen_init != nullptr) ? screen_init(real_widget) : nullptr;
|
||||
if (content_widget == nullptr) {
|
||||
content_widget = real_widget;
|
||||
}
|
||||
}
|
||||
lvgl_unlock();
|
||||
|
||||
if (real_widget == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
s.real_root_widget = real_widget;
|
||||
s.content_root_widget = content_widget;
|
||||
s.started = true;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t window_manager_stop(void) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
lv_obj_t* widget = s.real_root_widget;
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.real_root_widget = nullptr;
|
||||
s.content_root_widget = nullptr;
|
||||
s.top_widget = nullptr;
|
||||
s.windows.clear();
|
||||
s.started = false;
|
||||
s.waiting_task = nullptr;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
xTaskNotifyGive(waiter);
|
||||
}
|
||||
|
||||
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
|
||||
delete_widget(widget);
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
WindowId window_manager_create(uint32_t app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
if (!s.started) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return 0;
|
||||
}
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_top_widget = s.top_widget;
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.waiting_task = nullptr;
|
||||
s.top_widget = nullptr;
|
||||
WindowId new_id = s.next_id++;
|
||||
s.windows.push_back(WindowRecord { new_id, app_instance_id, create_widgets, user_data });
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
xTaskNotifyGive(waiter);
|
||||
}
|
||||
|
||||
delete_widget(old_top_widget);
|
||||
lv_obj_t* new_widget = build_window_widget(content, create_widgets, user_data);
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool still_topmost = !s.windows.empty() && s.windows.back().id == new_id;
|
||||
if (still_topmost) {
|
||||
s.top_widget = new_widget;
|
||||
new_widget = nullptr; // consumed
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
// Something else became topmost while we were building (e.g. a concurrent create() from
|
||||
// another app thread) - discard what we just made.
|
||||
delete_widget(new_widget);
|
||||
|
||||
return new_id;
|
||||
}
|
||||
|
||||
void window_manager_remove(WindowId id) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
|
||||
[id](const WindowRecord& window) { return window.id == id; });
|
||||
if (iterator == s.windows.end()) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return;
|
||||
}
|
||||
bool was_topmost = (iterator + 1 == s.windows.end());
|
||||
s.windows.erase(iterator);
|
||||
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_widget = nullptr;
|
||||
WindowCreateWidgetsFn next_create_widgets = nullptr;
|
||||
void* next_user_data = nullptr;
|
||||
WindowId next_id = 0;
|
||||
bool has_next = false;
|
||||
|
||||
if (was_topmost) {
|
||||
old_widget = s.top_widget;
|
||||
s.top_widget = nullptr;
|
||||
if (!s.windows.empty()) {
|
||||
next_create_widgets = s.windows.back().create_widgets;
|
||||
next_user_data = s.windows.back().user_data;
|
||||
next_id = s.windows.back().id;
|
||||
has_next = true;
|
||||
}
|
||||
}
|
||||
|
||||
TaskHandle_t waiter = s.waiting_task;
|
||||
s.waiting_task = nullptr;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (waiter != nullptr) {
|
||||
xTaskNotifyGive(waiter);
|
||||
}
|
||||
|
||||
if (!was_topmost) {
|
||||
// A buried window was removed - the topmost window's widgets are unaffected.
|
||||
return;
|
||||
}
|
||||
|
||||
delete_widget(old_widget);
|
||||
lv_obj_t* new_widget = has_next ? build_window_widget(content, next_create_widgets, next_user_data) : nullptr;
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool still_topmost = has_next && !s.windows.empty() && s.windows.back().id == next_id;
|
||||
if (still_topmost) {
|
||||
s.top_widget = new_widget;
|
||||
new_widget = nullptr; // consumed
|
||||
}
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
delete_widget(new_widget);
|
||||
}
|
||||
|
||||
WindowState window_manager_get_state(WindowId id) {
|
||||
auto& s = state();
|
||||
mutex_lock(&s.mutex);
|
||||
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
||||
mutex_unlock(&s.mutex);
|
||||
return is_top ? WINDOW_STATE_GRANTED : WINDOW_STATE_REVOKED;
|
||||
}
|
||||
|
||||
WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
|
||||
auto& s = state();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
||||
if (!is_top) {
|
||||
mutex_unlock(&s.mutex);
|
||||
return WINDOW_STATE_REVOKED;
|
||||
}
|
||||
s.waiting_task = xTaskGetCurrentTaskHandle();
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, timeout);
|
||||
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
Loading…
x
Reference in New Issue
Block a user