diff --git a/Tactility/Include/Tactility/service/Service.h b/Tactility/Include/Tactility/service/Service.h index dfb81b877..670090a96 100644 --- a/Tactility/Include/Tactility/service/Service.h +++ b/Tactility/Include/Tactility/service/Service.h @@ -1,15 +1,12 @@ #pragma once +#include + #include namespace tt::service { -enum class State { - Starting, - Started, - Stopping, - Stopped -}; +using State = ::ServiceState; // Forward declaration class ServiceContext; @@ -26,6 +23,6 @@ class Service { }; template -std::shared_ptr create() { return std::shared_ptr(new T); } +void* create(const ::ServiceManifest* /*manifest*/) { return new std::shared_ptr(std::shared_ptr(new T)); } } \ No newline at end of file diff --git a/Tactility/Include/Tactility/service/ServiceContext.h b/Tactility/Include/Tactility/service/ServiceContext.h index 7362ebe13..3f9555ec9 100644 --- a/Tactility/Include/Tactility/service/ServiceContext.h +++ b/Tactility/Include/Tactility/service/ServiceContext.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +struct ServiceInstance; namespace tt::service { @@ -8,23 +11,23 @@ struct ServiceManifest; class ServicePaths; /** - * The public representation of a service instance. + * Thin, non-owning wrapper around the running TactilityKernel service instance + * (ServiceInstance, which the kernel API also calls ServiceContext). * @warning Do not store references or pointers to these! You can retrieve them via the Loader service. */ -class ServiceContext { - -protected: +class ServiceContext final { - virtual ~ServiceContext() = default; + ::ServiceInstance* service_instance; public: + explicit ServiceContext(::ServiceInstance* instance) : service_instance(instance) {} + /** @return a reference to the service's manifest */ - virtual const ServiceManifest& getManifest() const = 0; + const ::ServiceManifest* getManifest() const; /** Retrieve the paths that are relevant to this service */ - virtual std::unique_ptr getPaths() const = 0; + std::unique_ptr getPaths() const; }; - } // namespace diff --git a/Tactility/Include/Tactility/service/ServiceManifest.h b/Tactility/Include/Tactility/service/ServiceManifest.h index 484855364..61b61a9b8 100644 --- a/Tactility/Include/Tactility/service/ServiceManifest.h +++ b/Tactility/Include/Tactility/service/ServiceManifest.h @@ -9,7 +9,7 @@ namespace tt::service { // Forward declarations class ServiceContext; -typedef std::shared_ptr(*CreateService)(); +using CreateService = ::ServiceCreate; /** A ledger that describes the main parts of a service. */ struct ServiceManifest { diff --git a/Tactility/Include/Tactility/service/ServicePaths.h b/Tactility/Include/Tactility/service/ServicePaths.h index b9cbd524b..04be7db87 100644 --- a/Tactility/Include/Tactility/service/ServicePaths.h +++ b/Tactility/Include/Tactility/service/ServicePaths.h @@ -1,45 +1,67 @@ #pragma once +#include +#include +#include +#include + +#include #include -#include namespace tt::service { -// Forward declarations -class ServiceManifest; +constexpr size_t PATH_BUFFER_SIZE = 255; class ServicePaths { - std::shared_ptr manifest; + const ::ServiceManifest* manifest; public: - explicit ServicePaths(std::shared_ptr manifest) : manifest(std::move(manifest)) {} + explicit ServicePaths(const ::ServiceManifest* manifest) : manifest(manifest) {} /** * The user data directory is intended to survive OS upgrades. * The path will not end with a "/". */ - std::string getUserDataDirectory() const; + std::string getUserDataDirectory() const { + char buffer[PATH_BUFFER_SIZE]; + check(service_paths_get_user_data_directory(manifest->id, buffer, sizeof(buffer)) == ERROR_NONE); + return buffer; + } /** * The user data directory is intended to survive OS upgrades. * Configuration data should be stored here. * @param[in] childPath the path without a "/" prefix */ - std::string getUserDataPath(const std::string& childPath) const; + std::string getUserDataPath(const std::string& childPath) const { + assert(!childPath.starts_with('/')); + char buffer[PATH_BUFFER_SIZE]; + check(service_paths_get_user_data_path(manifest->id, childPath.c_str(), buffer, sizeof(buffer)) == ERROR_NONE); + return buffer; + } /** * You should not store configuration data here. * The path will not end with a "/". */ - std::string getAssetsDirectory() const; + std::string getAssetsDirectory() const { + char buffer[PATH_BUFFER_SIZE]; + check(service_paths_get_assets_directory(manifest->id, buffer, sizeof(buffer)) == ERROR_NONE); + return buffer; + } /** * You should not store configuration data here. * @param[in] childPath the path without a "/" prefix */ - std::string getAssetsPath(const std::string& childPath) const; + std::string getAssetsPath(const std::string& childPath) const { + assert(!childPath.starts_with('/')); + char buffer[PATH_BUFFER_SIZE]; + check(service_paths_get_assets_path(manifest->id, childPath.c_str(), buffer, sizeof(buffer)) == ERROR_NONE); + return buffer; + } }; -} \ No newline at end of file +} diff --git a/Tactility/Include/Tactility/service/ServiceRegistration.h b/Tactility/Include/Tactility/service/ServiceRegistration.h index a23a882ac..fe85c87b9 100644 --- a/Tactility/Include/Tactility/service/ServiceRegistration.h +++ b/Tactility/Include/Tactility/service/ServiceRegistration.h @@ -40,7 +40,7 @@ State getState(const std::string& id); * @param[in] id the id as defined in the manifest * @return the matching manifest or nullptr when it wasn't found */ -std::shared_ptr findManifestById(const std::string& id); +const ::ServiceManifest* findManifestById(const std::string& id); /** Find a ServiceContext by its manifest id. * @param[in] id the id as defined in the manifest diff --git a/Tactility/Private/Tactility/service/ServiceInstance.h b/Tactility/Private/Tactility/service/ServiceInstance.h deleted file mode 100644 index b37e14a0d..000000000 --- a/Tactility/Private/Tactility/service/ServiceInstance.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace tt::service { - -class ServiceInstance final : public ServiceContext { - - Mutex mutex; - std::shared_ptr manifest; - std::shared_ptr service; - State state = State::Stopped; - -public: - - explicit ServiceInstance(std::shared_ptr manifest); - ~ServiceInstance() override = default; - - /** @return a reference to the service's manifest */ - const ServiceManifest& getManifest() const override; - - /** Retrieve the paths that are relevant to this service */ - std::unique_ptr getPaths() const override; - - std::shared_ptr getService() const { return service; } - - State getState() const { return state; } - - void setState(State newState) { state = newState; } -}; - -} diff --git a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp index a6b2c99dc..c425b48aa 100644 --- a/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp +++ b/Tactility/Source/bluetooth/BluetoothPairedDevice.cpp @@ -1,5 +1,7 @@ #include +#include "Tactility/Paths.h" + #include #include #include @@ -16,13 +18,16 @@ namespace tt::bluetooth::settings { constexpr auto* TAG = "BluetoothPairedDevice"; // Use the same directory as the old service for backward compatibility. -constexpr auto* DATA_DIR = "/data/service/bluetooth"; constexpr auto* DEVICE_SETTINGS_FORMAT = "{}/{}.device.properties"; constexpr auto* KEY_NAME = "name"; constexpr auto* KEY_ADDR = "addr"; constexpr auto* KEY_AUTO_CONNECT = "autoConnect"; constexpr auto* KEY_PROFILE_ID = "profileId"; +static std::string getSettingsFilePath() { + return getUserDataPath() + "/service/bluetooth"; +} + std::string addrToHex(const std::array& addr) { std::stringstream stream; stream << std::hex; @@ -52,7 +57,7 @@ static bool hexToAddr(const std::string& hex, std::array& addr) { } static std::string getFilePath(const std::string& addr_hex) { - return std::format(DEVICE_SETTINGS_FORMAT, DATA_DIR, addr_hex); + return std::format(DEVICE_SETTINGS_FORMAT, getSettingsFilePath(), addr_hex); } bool hasFileForDevice(const std::string& addr_hex) { @@ -102,7 +107,10 @@ bool remove(const std::string& addr_hex) { std::vector loadAll() { std::vector entries; - file::scandir(DATA_DIR, entries, [](const dirent* entry) -> int { + if (!file::isDirectory(getSettingsFilePath())) { + return {}; + } + file::scandir(getSettingsFilePath(), entries, [](const dirent* entry) -> int { if (entry->d_type != file::TT_DT_REG && entry->d_type != file::TT_DT_UNKNOWN) return -1; std::string name = entry->d_name; return name.ends_with(".device.properties") ? 0 : -1; diff --git a/Tactility/Source/lvgl/Lvgl.cpp b/Tactility/Source/lvgl/Lvgl.cpp index 672ab46fb..48c596345 100644 --- a/Tactility/Source/lvgl/Lvgl.cpp +++ b/Tactility/Source/lvgl/Lvgl.cpp @@ -102,7 +102,7 @@ void attachDevices() { // We search for the manifest first, because during the initial start() during boot // the service won't be registered yet. if (service::findManifestById("Gui") != nullptr) { - if (service::getState("Gui") == service::State::Stopped) { + if (service::getState("Gui") == SERVICE_STATE_STOPPED) { service::startService("Gui"); } else { LOG_E(TAG, "Gui service is not in Stopped state"); @@ -112,7 +112,7 @@ void attachDevices() { // We search for the manifest first, because during the initial start() during boot // the service won't be registered yet. if (service::findManifestById("Statusbar") != nullptr) { - if (service::getState("Statusbar") == service::State::Stopped) { + if (service::getState("Statusbar") == SERVICE_STATE_STOPPED) { service::startService("Statusbar"); } else { LOG_E(TAG, "Statusbar service is not in Stopped state"); diff --git a/Tactility/Source/service/ServiceContext.cpp b/Tactility/Source/service/ServiceContext.cpp new file mode 100644 index 000000000..b500f3c37 --- /dev/null +++ b/Tactility/Source/service/ServiceContext.cpp @@ -0,0 +1,17 @@ +#include + +#include + +#include + +namespace tt::service { + +const ::ServiceManifest* ServiceContext::getManifest() const { + return service_instance_get_manifest(service_instance); +} + +std::unique_ptr ServiceContext::getPaths() const { + return std::make_unique(getManifest()); +} + +} // namespace diff --git a/Tactility/Source/service/ServiceInstance.cpp b/Tactility/Source/service/ServiceInstance.cpp deleted file mode 100644 index cf76beb54..000000000 --- a/Tactility/Source/service/ServiceInstance.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include - -#include -#include - -namespace tt::service { - -ServiceInstance::ServiceInstance(std::shared_ptr manifest) : - manifest(manifest), - service(manifest->createService()) -{} - -const ServiceManifest& ServiceInstance::getManifest() const { return *manifest; } - -std::unique_ptr ServiceInstance::getPaths() const { - return std::make_unique(manifest); -} - -} \ No newline at end of file diff --git a/Tactility/Source/service/ServicePaths.cpp b/Tactility/Source/service/ServicePaths.cpp deleted file mode 100644 index 0cf6d4306..000000000 --- a/Tactility/Source/service/ServicePaths.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include - -#include -#include - -#include -#include - -namespace tt::service { - -std::string ServicePaths::getUserDataDirectory() const { - return std::format("{}/service/{}", tt::getUserDataPath(), manifest->id); -} - -std::string ServicePaths::getUserDataPath(const std::string& childPath) const { - assert(!childPath.starts_with('/')); - return std::format("{}/{}", getUserDataDirectory(), childPath); -} - -std::string ServicePaths::getAssetsDirectory() const { - return std::format("{}/service/{}/assets", tt::getUserDataPath(), manifest->id); -} - -std::string ServicePaths::getAssetsPath(const std::string& childPath) const { - assert(!childPath.starts_with('/')); - return std::format("{}/{}", getAssetsDirectory(), childPath); -} - -} diff --git a/Tactility/Source/service/ServiceRegistration.cpp b/Tactility/Source/service/ServiceRegistration.cpp index ed2dfc78c..02cc3e915 100644 --- a/Tactility/Source/service/ServiceRegistration.cpp +++ b/Tactility/Source/service/ServiceRegistration.cpp @@ -1,43 +1,68 @@ #include -#include -#include +#include #include -#include - +#include #include +#include + +#include +#include namespace tt::service { constexpr auto* TAG = "ServiceRegistration"; -typedef std::unordered_map> ManifestMap; -typedef std::unordered_map> ServiceInstanceMap; +// Bridges the kernel's C ServiceManifest/Service callbacks to the C++ Service +// instances they wrap. Declared extern "C" to match the linkage of the C +// function-pointer types they're assigned to (see e.g. gpio_controller.cpp). +extern "C" { + +static void cppDestroyServiceTrampoline(const ::ServiceManifest* /*manifest*/, void* data) { + delete static_cast*>(data); +} -static ManifestMap service_manifest_map; -static ServiceInstanceMap service_instance_map; +static error_t cppOnStartTrampoline(::ServiceInstance* instance, void* data) { + auto& servicePtr = *static_cast*>(data); + ServiceContext context(instance); + return servicePtr->onStart(context) ? ERROR_NONE : ERROR_RESOURCE; +} -static Mutex manifest_mutex; -static Mutex instance_mutex; +static void cppOnStopTrampoline(::ServiceInstance* instance, void* data) { + auto& servicePtr = *static_cast*>(data); + ServiceContext context(instance); + servicePtr->onStop(context); +} + +} // extern "C" void addService(std::shared_ptr manifest, bool autoStart) { assert(manifest != nullptr); - // We'll move the manifest pointer, but we'll need to id later const auto& id = manifest->id; LOG_I(TAG, "Adding %s", id.c_str()); - manifest_mutex.lock(); - if (service_manifest_map[id] == nullptr) { - service_manifest_map[id] = std::move(manifest); - } else { + if (service_manager_find_manifest(id.c_str()) != nullptr) { LOG_E(TAG, "Service id in use: %s", id.c_str()); + return; } - manifest_mutex.unlock(); - if (autoStart) { - startService(id); + // Intentionally never freed: services are registered once and live for the + // process lifetime (there is no removeService()). Keeps id's backing string + // alive for cManifest.id below. + auto* persistentManifest = new std::shared_ptr(manifest); + auto* cManifest = new ::ServiceManifest { + .id = (*persistentManifest)->id.c_str(), + .create_service = (*persistentManifest)->createService, + .destroy_service = cppDestroyServiceTrampoline, + .on_start = cppOnStartTrampoline, + .on_stop = cppOnStopTrampoline, + }; + + error_t error = service_manager_add(cManifest, autoStart); + if (error != ERROR_NONE) { + LOG_E(TAG, "Failed to add service %s: %s", id.c_str(), error_to_string(error)); } } @@ -45,94 +70,50 @@ void addService(const ServiceManifest& manifest, bool autoStart) { addService(std::make_shared(manifest), autoStart); } -std::shared_ptr findManifestById(const std::string& id) { - manifest_mutex.lock(); - auto iterator = service_manifest_map.find(id); - auto manifest = iterator != service_manifest_map.end() ? iterator->second : nullptr; - manifest_mutex.unlock(); - return manifest; -} - -static std::shared_ptr findServiceInstanceById(const std::string& id) { - manifest_mutex.lock(); - auto iterator = service_instance_map.find(id); - auto service = iterator != service_instance_map.end() ? iterator->second : nullptr; - manifest_mutex.unlock(); - return service; +const ::ServiceManifest* findManifestById(const std::string& id) { + return service_manager_find_manifest(id.c_str()); } -// TODO: Return proper error/status instead of BOOL? bool startService(const std::string& id) { LOG_I(TAG, "Starting %s", id.c_str()); - auto manifest = findManifestById(id); - if (manifest == nullptr) { - LOG_E(TAG, "manifest not found for service %s", id.c_str()); + const error_t error = service_manager_start(id.c_str()); + if (error != ERROR_NONE) { + LOG_E(TAG, "Starting %s failed: %s", id.c_str(), error_to_string(error)); return false; } - - auto service_instance = std::make_shared(manifest); - - // Register first, so that a service can retrieve itself during onStart() - instance_mutex.lock(); - service_instance_map[manifest->id] = service_instance; - instance_mutex.unlock(); - - service_instance->setState(State::Starting); - if (service_instance->getService()->onStart(*service_instance)) { - service_instance->setState(State::Started); - } else { - LOG_E(TAG, "Starting %s failed", id.c_str()); - service_instance->setState(State::Stopped); - instance_mutex.lock(); - service_instance_map.erase(manifest->id); - instance_mutex.unlock(); - } - LOG_I(TAG, "Started %s", id.c_str()); - return true; } std::shared_ptr findServiceContextById(const std::string& id) { - return findServiceInstanceById(id); + auto* instance = service_manager_find_instance(id.c_str()); + if (instance == nullptr) { + return nullptr; + } + return std::make_shared(instance); } std::shared_ptr findServiceById(const std::string& id) { - auto instance = findServiceInstanceById(id); - return instance != nullptr ? instance->getService() : nullptr; + auto* instance = service_manager_find_instance(id.c_str()); + if (instance == nullptr) { + return nullptr; + } + return *static_cast*>(instance->data); } bool stopService(const std::string& id) { LOG_I(TAG, "Stopping %s", id.c_str()); - auto service_instance = findServiceInstanceById(id); - if (service_instance == nullptr) { + error_t error = service_manager_stop(id.c_str()); + if (error != ERROR_NONE) { LOG_W(TAG, "Service not running: %s", id.c_str()); return false; } - - service_instance->setState(State::Stopping); - service_instance->getService()->onStop(*service_instance); - service_instance->setState(State::Stopped); - - instance_mutex.lock(); - service_instance_map.erase(id); - instance_mutex.unlock(); - - if (service_instance.use_count() > 1) { - LOG_W(TAG, "Possible memory leak: service %s still has %d references", service_instance->getManifest().id.c_str(), (int)(service_instance.use_count() - 1)); - } - LOG_I(TAG, "Stopped %s", id.c_str()); - return true; } State getState(const std::string& id) { - auto service_instance = findServiceInstanceById(id); - if (service_instance == nullptr) { - return State::Stopped; - } - return service_instance->getState(); + return service_manager_get_state(id.c_str()); } } // namespace diff --git a/TactilityKernel/include/tactility/paths.h b/TactilityKernel/include/tactility/paths.h new file mode 100644 index 000000000..e8277c283 --- /dev/null +++ b/TactilityKernel/include/tactility/paths.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Get the root path for user data. Survives OS upgrades. + * @param[out] out_path buffer to store the path (no trailing "/") + * @param[in] out_path_size size of the output buffer + * @retval ERROR_NOT_FOUND if the configured storage location isn't available (e.g. no SD card) + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t paths_get_user_data_path(char* out_path, size_t out_path_size); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/service/service_instance.h b/TactilityKernel/include/tactility/service/service_instance.h new file mode 100644 index 000000000..9a1adb678 --- /dev/null +++ b/TactilityKernel/include/tactility/service/service_instance.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct ServiceInstanceInternal; + +/** The lifecycle state of a ServiceInstance. */ +typedef enum { + SERVICE_STATE_STARTING, + SERVICE_STATE_STARTED, + SERVICE_STATE_STOPPING, + SERVICE_STATE_STOPPED, +} ServiceState; + +/** Represents a running (or about-to-run) instance of a service. */ +struct ServiceInstance { + /** The manifest that spawned this instance. */ + const struct ServiceManifest* manifest; + /** Service-specific data, owned by the service implementation. Can be NULL. */ + void* data; + /** + * Internal state managed by the kernel. + * ServiceInstance implementers should initialize this to NULL. + * Do not access or modify directly; use service_instance_* functions. + */ + struct ServiceInstanceInternal* internal; +}; + +/** + * @brief Construct a service instance. + * @details This calls manifest->create_service() to build the instance's data. + * @param[in,out] instance a service instance with manifest set and internal set to NULL + * @param[in] manifest non-null manifest to construct the instance from + * @retval ERROR_OUT_OF_MEMORY if internal data allocation failed + * @retval ERROR_NONE on success + */ +error_t service_instance_construct(struct ServiceInstance* instance, const struct ServiceManifest* manifest); + +/** + * @brief Destruct a service instance. + * @details This calls manifest->destroy_service() on the instance's data. + * @param[in,out] instance non-null service instance pointer + * @retval ERROR_INVALID_STATE if the instance is not stopped + * @retval ERROR_NONE on success + */ +error_t service_instance_destruct(struct ServiceInstance* instance); + +/** + * @brief Get the manifest of a service instance. + * @param[in] instance non-null service instance pointer + * @return the manifest + */ +const struct ServiceManifest* service_instance_get_manifest(struct ServiceInstance* instance); + +/** + * @brief Get the data of a service instance. + * @param[in] instance non-null service instance pointer + * @return the data (can be NULL) + */ +void* service_instance_get_data(struct ServiceInstance* instance); + +/** + * @brief Get the state of a service instance. + * @param[in] instance non-null service instance pointer + * @return the current state + */ +ServiceState service_instance_get_state(struct ServiceInstance* instance); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/service/service_manager.h b/TactilityKernel/include/tactility/service/service_manager.h new file mode 100644 index 000000000..299dcfbf4 --- /dev/null +++ b/TactilityKernel/include/tactility/service/service_manager.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Register a service manifest. + * @param[in] manifest non-null manifest to register + * @param[in] auto_start if true, the service is started immediately after registration + * @retval ERROR_INVALID_ARGUMENT if a manifest with the same id is already registered + * @retval ERROR_RESOURCE if auto_start is true and starting the service failed + * @retval ERROR_NONE on success + */ + error_t service_manager_add(const struct ServiceManifest* manifest, bool auto_start); + +/** + * @brief Unregister a previously-added manifest. + * @param[in] id non-null service id + * @retval ERROR_INVALID_STATE if the service is still running + * @retval ERROR_NOT_FOUND if no manifest with this id is registered + * @retval ERROR_NONE on success + */ +error_t service_manager_remove(const char* id); + +/** + * @brief Find a registered manifest by id. + * @param[in] id non-null service id + * @return the manifest, or NULL if not found + */ +const struct ServiceManifest* service_manager_find_manifest(const char* id); + +/** + * @brief Start a registered service by id. + * @param[in] id non-null service id + * @retval ERROR_NOT_FOUND if no manifest with this id is registered + * @retval ERROR_INVALID_STATE if the service is already running + * @retval ERROR_RESOURCE if the service's on_start callback failed + * @retval ERROR_NONE on success + */ +error_t service_manager_start(const char* id); + +/** + * @brief Stop a running service by id. + * @param[in] id non-null service id + * @retval ERROR_NOT_FOUND if no service with this id is running + * @retval ERROR_NONE on success + */ +error_t service_manager_stop(const char* id); + +/** + * @brief Get the state of a service by id. + * @param[in] id non-null service id + * @return the current state, or SERVICE_STATE_STOPPED if the id is unknown + */ +ServiceState service_manager_get_state(const char* id); + +/** + * @brief Find the service instance + * @param[in] id non-null service id + * @return the instance when found, otherwise return NULL + */ +struct ServiceInstance* service_manager_find_instance(const char* id); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/service/service_manifest.h b/TactilityKernel/include/tactility/service/service_manifest.h new file mode 100644 index 000000000..7a0cd67c2 --- /dev/null +++ b/TactilityKernel/include/tactility/service/service_manifest.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ServiceContext (declared in service_context.h) is a typedef alias of ServiceInstance, +// so the callback types below are declared directly in terms of ServiceInstance to avoid +// a conflicting forward-declaration of an unrelated "ServiceContext" struct tag. +struct ServiceInstance; + +/** + * Allocates and initializes the service's custom data. + * @param[in] manifest the manifest that owns this callback + * @return the service's custom data, or NULL if it has none + */ +typedef void* (*ServiceCreate)(const struct ServiceManifest* manifest); + +/** + * Frees custom data that was set up by the matching ServiceCreate function. + * @param[in] data the custom data returned by create_service + * @param[in] manifest the manifest that owns this callback + */ +typedef void (*ServiceDestroy)(const struct ServiceManifest* manifest, void* data); + +/** + * Called when a service instance is starting. + * Can be NULL, in which case starting always succeeds. + * @param[in,out] instance the starting service instance (also its own ServiceContext) + * @param[in] data the custom data returned by create_service + * @return ERROR_NONE if the service started successfully + */ +typedef error_t (*ServiceOnStart)(struct ServiceInstance* instance, void* data); + +/** + * Called when a service instance is stopping. + * Can be NULL, in which case stopping is a no-op. + * @param[in,out] instance the stopping service instance (also its own ServiceContext) + * @param[in] data the custom data returned by create_service + */ +typedef void (*ServiceOnStop)(struct ServiceInstance* instance, void* data); + +/** + * Describes a registrable service type. + * One manifest exists per service id, shared across start/stop cycles. + */ +struct ServiceManifest { + /** Unique service identifier. Should never be NULL. */ + const char* id; + /** Allocates the service's custom data. Should never be NULL. */ + ServiceCreate create_service; + /** Frees the service's custom data. Should never be NULL. */ + ServiceDestroy destroy_service; + /** Called when a service instance starts. Can be NULL. */ + ServiceOnStart on_start; + /** Called when a service instance stops. Can be NULL. */ + ServiceOnStop on_stop; +}; + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/service/service_paths.h b/TactilityKernel/include/tactility/service/service_paths.h new file mode 100644 index 000000000..e1d95e08c --- /dev/null +++ b/TactilityKernel/include/tactility/service/service_paths.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Get the user data directory for a service. Survives OS upgrades. No trailing "/". + * @param[in] service_id non-null service id + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t service_paths_get_user_data_directory(const char* service_id, char* out_path, size_t out_path_size); + +/** + * @brief Get a path within the user data directory for a service. + * @param[in] service_id non-null service id + * @param[in] child_path path without a "/" prefix + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t service_paths_get_user_data_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size); + +/** + * @brief Get the assets directory for a service. Do not store configuration data here. No trailing "/". + * @param[in] service_id non-null service id + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t service_paths_get_assets_directory(const char* service_id, char* out_path, size_t out_path_size); + +/** + * @brief Get a path within the assets directory for a service. + * @param[in] service_id non-null service id + * @param[in] child_path path without a "/" prefix + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t service_paths_get_assets_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index e06bef4aa..49e688443 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -21,6 +21,9 @@ #include #include #include +#include +#include +#include #ifndef ESP_PLATFORM #include @@ -247,6 +250,25 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(module_is_started), DEFINE_MODULE_SYMBOL(module_resolve_symbol), DEFINE_MODULE_SYMBOL(module_resolve_symbol_global), + // service/service_instance + DEFINE_MODULE_SYMBOL(service_instance_construct), + DEFINE_MODULE_SYMBOL(service_instance_destruct), + DEFINE_MODULE_SYMBOL(service_instance_get_manifest), + DEFINE_MODULE_SYMBOL(service_instance_get_data), + DEFINE_MODULE_SYMBOL(service_instance_get_state), + // service/service_manager + DEFINE_MODULE_SYMBOL(service_manager_add), + DEFINE_MODULE_SYMBOL(service_manager_remove), + DEFINE_MODULE_SYMBOL(service_manager_find_manifest), + DEFINE_MODULE_SYMBOL(service_manager_start), + DEFINE_MODULE_SYMBOL(service_manager_stop), + DEFINE_MODULE_SYMBOL(service_manager_get_state), + DEFINE_MODULE_SYMBOL(service_manager_find_instance), + // service/service_paths + DEFINE_MODULE_SYMBOL(service_paths_get_user_data_directory), + DEFINE_MODULE_SYMBOL(service_paths_get_user_data_path), + DEFINE_MODULE_SYMBOL(service_paths_get_assets_directory), + DEFINE_MODULE_SYMBOL(service_paths_get_assets_path), // terminator MODULE_SYMBOL_TERMINATOR }; diff --git a/TactilityKernel/source/paths.cpp b/TactilityKernel/source/paths.cpp new file mode 100644 index 000000000..08e86c6c4 --- /dev/null +++ b/TactilityKernel/source/paths.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include +#include + +static error_t get_user_data_root_path(char* out_path, size_t out_path_size) { +#if defined(CONFIG_TT_USER_DATA_LOCATION_INTERNAL) +#ifdef ESP_PLATFORM + const char* mount_point = "/data"; +#else + const char* mount_point = "data"; +#endif + if (std::strlen(mount_point) + 1 > out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + std::strcpy(out_path, mount_point); + return ERROR_NONE; +#elif defined(CONFIG_TT_USER_DATA_LOCATION_SD) + struct FileSystem* found = nullptr; + file_system_for_each(&found, [](FileSystem* fs, void* context) { + auto* owner = file_system_get_owner(fs); + if (owner == nullptr || device_get_type(owner) != &SDCARD_TYPE) { + return true; + } + *static_cast(context) = fs; + return false; + }); + if (found == nullptr) { + return ERROR_NOT_FOUND; + } + return file_system_get_path(found, out_path, out_path_size); +#else +#error CONFIG_TT_USER_DATA_* not set or unsupported +#endif +} + +extern "C" { + +error_t paths_get_user_data_path(char* out_path, size_t out_path_size) { +#ifdef ESP_PLATFORM + char root[64]; + error_t error = get_user_data_root_path(root, sizeof(root)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/tactility", root); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +#else + const char* fixed_path = "data"; + if (std::strlen(fixed_path) + 1 > out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + std::strcpy(out_path, fixed_path); + return ERROR_NONE; +#endif +} + +} // extern "C" diff --git a/TactilityKernel/source/service/service_instance.cpp b/TactilityKernel/source/service/service_instance.cpp new file mode 100644 index 000000000..525d30e19 --- /dev/null +++ b/TactilityKernel/source/service/service_instance.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include + +#define TAG "service_instance" + +struct ServiceInstanceInternal { + Mutex mutex {}; + ServiceState state = SERVICE_STATE_STOPPED; +}; + +extern "C" { + +error_t service_instance_construct(ServiceInstance* instance, const ServiceManifest* manifest) { + instance->internal = new(std::nothrow) ServiceInstanceInternal; + if (instance->internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + mutex_construct(&instance->internal->mutex); + + instance->manifest = manifest; + instance->data = manifest->create_service(manifest); + + LOG_D(TAG, "construct %s", manifest->id); + return ERROR_NONE; +} + +error_t service_instance_destruct(ServiceInstance* instance) { + auto* internal = instance->internal; + + mutex_lock(&internal->mutex); + if (internal->state != SERVICE_STATE_STOPPED) { + mutex_unlock(&internal->mutex); + return ERROR_INVALID_STATE; + } + mutex_unlock(&internal->mutex); + + LOG_D(TAG, "destruct %s", instance->manifest->id); + + instance->manifest->destroy_service(instance->manifest, instance->data); + instance->data = nullptr; + + instance->internal = nullptr; + mutex_destruct(&internal->mutex); + delete internal; + + return ERROR_NONE; +} + +const ServiceManifest* service_instance_get_manifest(ServiceInstance* instance) { + return instance->manifest; +} + +void* service_instance_get_data(ServiceInstance* instance) { + return instance->data; +} + +ServiceState service_instance_get_state(ServiceInstance* instance) { + mutex_lock(&instance->internal->mutex); + ServiceState state = instance->internal->state; + mutex_unlock(&instance->internal->mutex); + return state; +} + +/** Internal-only: mutate the state of a service instance. Used by service_registration.cpp. */ +void service_instance_set_state(ServiceInstance* instance, ServiceState state) { + mutex_lock(&instance->internal->mutex); + instance->internal->state = state; + mutex_unlock(&instance->internal->mutex); +} + +} // extern "C" diff --git a/TactilityKernel/source/service/service_manager.cpp b/TactilityKernel/source/service/service_manager.cpp new file mode 100644 index 000000000..e0bb58186 --- /dev/null +++ b/TactilityKernel/source/service/service_manager.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include +#include +#include + +#define TAG "service_registration" + +// Defined in service_instance.cpp. Internal-only: lets the registry drive state +// transitions without exposing mutation on the public ServiceInstance API. +extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state); + +struct ManifestLedger { + std::unordered_map manifests; + Mutex mutex {}; + + ManifestLedger() { mutex_construct(&mutex); } + ~ManifestLedger() { mutex_destruct(&mutex); } +}; + +struct InstanceLedger { + std::unordered_map instances; + Mutex mutex {}; + + InstanceLedger() { mutex_construct(&mutex); } + ~InstanceLedger() { mutex_destruct(&mutex); } +}; + +static ManifestLedger& get_manifest_ledger() { + static ManifestLedger ledger; + return ledger; +} + +static InstanceLedger& get_instance_ledger() { + static InstanceLedger ledger; + return ledger; +} + +#define manifest_ledger get_manifest_ledger() +#define instance_ledger get_instance_ledger() + +extern "C" { + +error_t service_manager_add(const ServiceManifest* manifest, bool auto_start) { + mutex_lock(&manifest_ledger.mutex); + if (manifest_ledger.manifests.contains(manifest->id)) { + mutex_unlock(&manifest_ledger.mutex); + LOG_E(TAG, "Manifest with id '%s' is already registered", manifest->id); + return ERROR_INVALID_ARGUMENT; + } + manifest_ledger.manifests[manifest->id] = manifest; + mutex_unlock(&manifest_ledger.mutex); + + LOG_I(TAG, "add %s", manifest->id); + + if (auto_start) { + return service_manager_start(manifest->id); + } + + return ERROR_NONE; +} + +error_t service_manager_remove(const char* id) { + if (service_manager_get_state(id) != SERVICE_STATE_STOPPED) { + return ERROR_INVALID_STATE; + } + + mutex_lock(&manifest_ledger.mutex); + const auto iterator = manifest_ledger.manifests.find(id); + if (iterator == manifest_ledger.manifests.end()) { + mutex_unlock(&manifest_ledger.mutex); + return ERROR_NOT_FOUND; + } + manifest_ledger.manifests.erase(iterator); + mutex_unlock(&manifest_ledger.mutex); + + LOG_I(TAG, "remove %s", id); + return ERROR_NONE; +} + +error_t service_manager_start(const char* id) { + mutex_lock(&manifest_ledger.mutex); + const auto manifest_iterator = manifest_ledger.manifests.find(id); + if (manifest_iterator == manifest_ledger.manifests.end()) { + mutex_unlock(&manifest_ledger.mutex); + return ERROR_NOT_FOUND; + } + const ServiceManifest* manifest = manifest_iterator->second; + mutex_unlock(&manifest_ledger.mutex); + + mutex_lock(&instance_ledger.mutex); + if (instance_ledger.instances.contains(id)) { + mutex_unlock(&instance_ledger.mutex); + return ERROR_INVALID_STATE; + } + + auto* instance = new(std::nothrow) ServiceInstance { .manifest = nullptr, .data = nullptr, .internal = nullptr }; + if (instance == nullptr) { + mutex_unlock(&instance_ledger.mutex); + return ERROR_OUT_OF_MEMORY; + } + + error_t error = service_instance_construct(instance, manifest); + if (error != ERROR_NONE) { + mutex_unlock(&instance_ledger.mutex); + delete instance; + return error; + } + + // Register before on_start() so a service can find itself while starting. + instance_ledger.instances[id] = instance; + mutex_unlock(&instance_ledger.mutex); + + service_instance_set_state(instance, SERVICE_STATE_STARTING); + + LOG_I(TAG, "start %s", id); + error = (manifest->on_start != nullptr) ? manifest->on_start(instance, instance->data) : ERROR_NONE; + + if (error == ERROR_NONE) { + service_instance_set_state(instance, SERVICE_STATE_STARTED); + return ERROR_NONE; + } + + LOG_E(TAG, "Failed to start %s: %s", id, error_to_string(error)); + service_instance_set_state(instance, SERVICE_STATE_STOPPED); + + mutex_lock(&instance_ledger.mutex); + instance_ledger.instances.erase(id); + mutex_unlock(&instance_ledger.mutex); + + service_instance_destruct(instance); + delete instance; + + return ERROR_RESOURCE; +} + +error_t service_manager_stop(const char* id) { + mutex_lock(&instance_ledger.mutex); + const auto iterator = instance_ledger.instances.find(id); + if (iterator == instance_ledger.instances.end()) { + mutex_unlock(&instance_ledger.mutex); + return ERROR_NOT_FOUND; + } + ServiceInstance* instance = iterator->second; + mutex_unlock(&instance_ledger.mutex); + + LOG_I(TAG, "stop %s", id); + + service_instance_set_state(instance, SERVICE_STATE_STOPPING); + + if (instance->manifest->on_stop != nullptr) { + instance->manifest->on_stop(instance, instance->data); + } + + service_instance_set_state(instance, SERVICE_STATE_STOPPED); + + mutex_lock(&instance_ledger.mutex); + instance_ledger.instances.erase(id); + mutex_unlock(&instance_ledger.mutex); + + service_instance_destruct(instance); + delete instance; + + return ERROR_NONE; +} + +ServiceState service_manager_get_state(const char* id) { + mutex_lock(&instance_ledger.mutex); + const auto iterator = instance_ledger.instances.find(id); + if (iterator == instance_ledger.instances.end()) { + mutex_unlock(&instance_ledger.mutex); + return SERVICE_STATE_STOPPED; + } + ServiceInstance* instance = iterator->second; + mutex_unlock(&instance_ledger.mutex); + + return service_instance_get_state(instance); +} + +const ServiceManifest* service_manager_find_manifest(const char* id) { + mutex_lock(&manifest_ledger.mutex); + const auto iterator = manifest_ledger.manifests.find(id); + const ServiceManifest* manifest = (iterator != manifest_ledger.manifests.end()) ? iterator->second : nullptr; + mutex_unlock(&manifest_ledger.mutex); + return manifest; +} + +ServiceInstance* service_manager_find_instance(const char* id) { + mutex_lock(&instance_ledger.mutex); + const auto iterator = instance_ledger.instances.find(id); + auto* instance = (iterator != instance_ledger.instances.end()) ? iterator->second : nullptr; + mutex_unlock(&instance_ledger.mutex); + return instance; +} + +} // extern "C" diff --git a/TactilityKernel/source/service/service_paths.cpp b/TactilityKernel/source/service/service_paths.cpp new file mode 100644 index 000000000..b3156d286 --- /dev/null +++ b/TactilityKernel/source/service/service_paths.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include + +extern "C" { + +error_t service_paths_get_user_data_directory(const char* service_id, char* out_path, size_t out_path_size) { + char root[192]; + error_t error = paths_get_user_data_path(root, sizeof(root)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/service/%s", root, service_id); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +error_t service_paths_get_user_data_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size) { + char directory[224]; + error_t error = service_paths_get_user_data_directory(service_id, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +error_t service_paths_get_assets_directory(const char* service_id, char* out_path, size_t out_path_size) { + char directory[224]; + error_t error = service_paths_get_user_data_directory(service_id, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/assets", directory); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +error_t service_paths_get_assets_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size) { + char directory[224]; + error_t error = service_paths_get_assets_directory(service_id, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +} // extern "C" diff --git a/Tests/TactilityKernel/Source/ServicePathsTest.cpp b/Tests/TactilityKernel/Source/ServicePathsTest.cpp new file mode 100644 index 000000000..289994e8d --- /dev/null +++ b/Tests/TactilityKernel/Source/ServicePathsTest.cpp @@ -0,0 +1,69 @@ +#include "doctest.h" +#include +#include + +#include +#include + +TEST_CASE("paths_get_user_data_path returns a non-empty path") { + char buffer[192]; + CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE); + CHECK_GT(std::strlen(buffer), 0); +} + +TEST_CASE("paths_get_user_data_path reports overflow for a too-small buffer") { + char buffer[1]; + CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); +} + +TEST_CASE("service_paths_get_user_data_directory includes the service id") { + char root[192]; + REQUIRE_EQ(paths_get_user_data_path(root, sizeof(root)), ERROR_NONE); + + char buffer[224]; + CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE); + + std::string expected = std::string(root) + "/service/my-service"; + CHECK_EQ(std::string(buffer), expected); +} + +TEST_CASE("service_paths_get_user_data_path appends the child path") { + char directory[224]; + REQUIRE_EQ(service_paths_get_user_data_directory("my-service", directory, sizeof(directory)), ERROR_NONE); + + char buffer[256]; + CHECK_EQ(service_paths_get_user_data_path("my-service", "settings.properties", buffer, sizeof(buffer)), ERROR_NONE); + + std::string expected = std::string(directory) + "/settings.properties"; + CHECK_EQ(std::string(buffer), expected); +} + +TEST_CASE("service_paths_get_assets_directory is nested under the user data directory") { + char directory[224]; + REQUIRE_EQ(service_paths_get_user_data_directory("my-service", directory, sizeof(directory)), ERROR_NONE); + + char buffer[256]; + CHECK_EQ(service_paths_get_assets_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE); + + std::string expected = std::string(directory) + "/assets"; + CHECK_EQ(std::string(buffer), expected); +} + +TEST_CASE("service_paths_get_assets_path appends the child path") { + char directory[224]; + REQUIRE_EQ(service_paths_get_assets_directory("my-service", directory, sizeof(directory)), ERROR_NONE); + + char buffer[256]; + CHECK_EQ(service_paths_get_assets_path("my-service", "icon.png", buffer, sizeof(buffer)), ERROR_NONE); + + std::string expected = std::string(directory) + "/icon.png"; + CHECK_EQ(std::string(buffer), expected); +} + +TEST_CASE("service_paths functions report overflow for a too-small buffer") { + char buffer[1]; + CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(service_paths_get_user_data_path("my-service", "child", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(service_paths_get_assets_directory("my-service", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(service_paths_get_assets_path("my-service", "child", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW); +} diff --git a/Tests/TactilityKernel/Source/ServiceTest.cpp b/Tests/TactilityKernel/Source/ServiceTest.cpp new file mode 100644 index 000000000..ec8797dc2 --- /dev/null +++ b/Tests/TactilityKernel/Source/ServiceTest.cpp @@ -0,0 +1,168 @@ +#include "doctest.h" +#include + +// Defined in service_instance.cpp. Internal-only, exposed here to test try_get/put gating. +extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state); + +static int create_called = 0; +static int destroy_called = 0; +static int on_start_called = 0; +static int on_stop_called = 0; +static error_t on_start_result = ERROR_NONE; +static const ServiceManifest* last_create_manifest = nullptr; +static const ServiceManifest* last_destroy_manifest = nullptr; + +static void* test_create_service(const ServiceManifest* manifest) { + create_called++; + last_create_manifest = manifest; + return nullptr; +} + +static void test_destroy_service(const ServiceManifest* manifest, void*) { + destroy_called++; + last_destroy_manifest = manifest; +} + +static error_t test_on_start(ServiceInstance*, void*) { + on_start_called++; + return on_start_result; +} + +static void test_on_stop(ServiceInstance*, void*) { + on_stop_called++; +} + +static void reset_counters() { + create_called = 0; + destroy_called = 0; + on_start_called = 0; + on_stop_called = 0; + on_start_result = ERROR_NONE; + last_create_manifest = nullptr; + last_destroy_manifest = nullptr; +} + +TEST_CASE("ServiceInstance construction and destruction") { + reset_counters(); + + static const ServiceManifest manifest = { + .id = "instance-test", + .create_service = test_create_service, + .destroy_service = test_destroy_service, + .on_start = test_on_start, + .on_stop = test_on_stop + }; + + ServiceInstance instance = { .manifest = nullptr, .data = nullptr, .internal = nullptr }; + + CHECK_EQ(service_instance_construct(&instance, &manifest), ERROR_NONE); + CHECK_NE(instance.internal, nullptr); + CHECK_EQ(instance.manifest, &manifest); + CHECK_EQ(create_called, 1); + CHECK_EQ(last_create_manifest, &manifest); + CHECK_EQ(service_instance_get_state(&instance), SERVICE_STATE_STOPPED); + + CHECK_EQ(service_instance_destruct(&instance), ERROR_NONE); + CHECK_EQ(instance.internal, nullptr); + CHECK_EQ(destroy_called, 1); + CHECK_EQ(last_destroy_manifest, &manifest); +} + +TEST_CASE("service_manager_add rejects duplicate ids") { + reset_counters(); + + static const ServiceManifest manifest = { + .id = "duplicate-test", + .create_service = test_create_service, + .destroy_service = test_destroy_service + }; + + CHECK_EQ(service_manager_add(&manifest, false), ERROR_NONE); + CHECK_EQ(service_manager_add(&manifest, false), ERROR_INVALID_ARGUMENT); + + CHECK_EQ(service_manager_remove("duplicate-test"), ERROR_NONE); +} + +TEST_CASE("service_registration start/stop lifecycle") { + reset_counters(); + + static const ServiceManifest manifest = { + .id = "lifecycle-test", + .create_service = test_create_service, + .destroy_service = test_destroy_service, + .on_start = test_on_start, + .on_stop = test_on_stop + }; + + CHECK_EQ(service_manager_add(&manifest, false), ERROR_NONE); + CHECK_EQ(service_manager_get_state("lifecycle-test"), SERVICE_STATE_STOPPED); + + CHECK_EQ(service_manager_start("lifecycle-test"), ERROR_NONE); + CHECK_EQ(on_start_called, 1); + CHECK_EQ(service_manager_get_state("lifecycle-test"), SERVICE_STATE_STARTED); + CHECK_NE(service_manager_find_instance("lifecycle-test"), nullptr); + + // Starting again while already started should fail + CHECK_EQ(service_manager_start("lifecycle-test"), ERROR_INVALID_STATE); + + // Removing while running should fail + CHECK_EQ(service_manager_remove("lifecycle-test"), ERROR_INVALID_STATE); + + CHECK_EQ(service_manager_stop("lifecycle-test"), ERROR_NONE); + CHECK_EQ(on_stop_called, 1); + CHECK_EQ(service_manager_get_state("lifecycle-test"), SERVICE_STATE_STOPPED); + CHECK_EQ(service_manager_find_instance("lifecycle-test"), nullptr); + + // Stopping again while already stopped should fail + CHECK_EQ(service_manager_stop("lifecycle-test"), ERROR_NOT_FOUND); + + CHECK_EQ(service_manager_remove("lifecycle-test"), ERROR_NONE); +} + +TEST_CASE("service_manager_add with auto_start") { + reset_counters(); + + static const ServiceManifest manifest = { + .id = "auto-start-test", + .create_service = test_create_service, + .destroy_service = test_destroy_service, + .on_start = test_on_start, + .on_stop = test_on_stop + }; + + CHECK_EQ(service_manager_add(&manifest, true), ERROR_NONE); + CHECK_EQ(on_start_called, 1); + CHECK_EQ(service_manager_get_state("auto-start-test"), SERVICE_STATE_STARTED); + + CHECK_EQ(service_manager_stop("auto-start-test"), ERROR_NONE); + CHECK_EQ(service_manager_remove("auto-start-test"), ERROR_NONE); +} + +TEST_CASE("service_manager_start failure leaves service stopped") { + reset_counters(); + on_start_result = ERROR_RESOURCE; + + static const ServiceManifest manifest = { + .id = "failing-start-test", + .create_service = test_create_service, + .destroy_service = test_destroy_service, + .on_start = test_on_start, + .on_stop = test_on_stop + }; + + CHECK_EQ(service_manager_add(&manifest, false), ERROR_NONE); + CHECK_EQ(service_manager_start("failing-start-test"), ERROR_RESOURCE); + CHECK_EQ(service_manager_get_state("failing-start-test"), SERVICE_STATE_STOPPED); + CHECK_EQ(service_manager_find_instance("failing-start-test"), nullptr); + + CHECK_EQ(service_manager_remove("failing-start-test"), ERROR_NONE); +} + +TEST_CASE("service_registration lookup functions with unknown id") { + CHECK_EQ(service_manager_get_state("unknown-service-id"), SERVICE_STATE_STOPPED); + CHECK_EQ(service_manager_find_manifest("unknown-service-id"), nullptr); + CHECK_EQ(service_manager_find_instance("unknown-service-id"), nullptr); + CHECK_EQ(service_manager_start("unknown-service-id"), ERROR_NOT_FOUND); + CHECK_EQ(service_manager_stop("unknown-service-id"), ERROR_NOT_FOUND); + CHECK_EQ(service_manager_remove("unknown-service-id"), ERROR_NOT_FOUND); +}