blob: 82aedf7aad6fe8f3f897ad4d212f75a0511ae02b [file]
From 108513fce9904acf6d7d291ab2b87fc8909a621a Mon Sep 17 00:00:00 2001
From: Stephen Foulds <stephen.foulds@sky.uk>
Date: Thu, 8 Sep 2022 12:06:21 +0100
Subject: [PATCH] RDKTV-15347: Add PluginActivator utility for systemd
integration (#1059)
* Add PluginActivator utility for systemd integration
* Update copyright headers
* Code style changes to match project style
* Minor formatting fix
* Address code review comments
---
CMakeLists.txt | 7 +
Utils/PluginActivator/CMakeLists.txt | 42 +++++
Utils/PluginActivator/README.md | 18 ++
.../PluginActivator/source/COMRPCStarter.cpp | 154 ++++++++++++++++++
Utils/PluginActivator/source/COMRPCStarter.h | 47 ++++++
Utils/PluginActivator/source/IPluginStarter.h | 43 +++++
Utils/PluginActivator/source/Log.h | 68 ++++++++
Utils/PluginActivator/source/Module.cpp | 23 +++
Utils/PluginActivator/source/Module.h | 31 ++++
Utils/PluginActivator/source/main.cpp | 132 +++++++++++++++
10 files changed, 565 insertions(+)
create mode 100644 Utils/PluginActivator/CMakeLists.txt
create mode 100644 Utils/PluginActivator/README.md
create mode 100644 Utils/PluginActivator/source/COMRPCStarter.cpp
create mode 100644 Utils/PluginActivator/source/COMRPCStarter.h
create mode 100755 Utils/PluginActivator/source/IPluginStarter.h
create mode 100644 Utils/PluginActivator/source/Log.h
create mode 100644 Utils/PluginActivator/source/Module.cpp
create mode 100644 Utils/PluginActivator/source/Module.h
create mode 100644 Utils/PluginActivator/source/main.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4ba768b1..3e9004d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -27,6 +27,9 @@ set(VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
message(STATUS "Setting up ${PROJECT_NAME} v${VERSION}")
+option(BUILD_PLUGIN_ACTIVATOR
+ "Build the standalone plugin activator utility to activate plugins using systemd" OFF)
+
if (BUILD_REFERENCE)
add_definitions (-DBUILD_REFERENCE=${BUILD_REFERENCE})
endif()
@@ -96,3 +99,7 @@ endif()
add_subdirectory(Source)
add_subdirectory(Tests)
+
+if (BUILD_PLUGIN_ACTIVATOR)
+ add_subdirectory(Utils/PluginActivator)
+endif()
diff --git a/Utils/PluginActivator/CMakeLists.txt b/Utils/PluginActivator/CMakeLists.txt
new file mode 100644
index 00000000..bd465e3a
--- /dev/null
+++ b/Utils/PluginActivator/CMakeLists.txt
@@ -0,0 +1,42 @@
+
+# If not stated otherwise in this file or this component's license file the
+# following copyright and licenses apply:
+#
+# Copyright 2020 Metrological
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+cmake_minimum_required(VERSION 3.10.3)
+
+project( PluginActivator )
+
+set( CMAKE_CXX_STANDARD 11 )
+
+add_executable(${PROJECT_NAME}
+ source/Module.cpp
+ source/main.cpp
+ source/COMRPCStarter.cpp
+)
+
+target_link_libraries(${PROJECT_NAME}
+ PRIVATE
+ CompileSettingsDebug::CompileSettingsDebug
+ ${NAMESPACE}Core::${NAMESPACE}Core
+ ${NAMESPACE}COM::${NAMESPACE}COM
+ ${NAMESPACE}Plugins::${NAMESPACE}Plugins
+)
+
+install(
+ TARGETS ${PROJECT_NAME}
+ RUNTIME DESTINATION bin
+)
diff --git a/Utils/PluginActivator/README.md b/Utils/PluginActivator/README.md
new file mode 100644
index 00000000..0ab0cff6
--- /dev/null
+++ b/Utils/PluginActivator/README.md
@@ -0,0 +1,18 @@
+# Plugin Activator
+A command-line tool to activate a specified Thunder plugin.
+
+Will automatically retry activation mutliple times if the plugin does not successfully activate.
+
+Designed to be used with systemd where each plugin becomes a standalone systemd service, activated with this tool.
+
+## Usage
+```
+Usage: PluginActivator <option(s)> [callsign]
+ Utility that starts a given thunder plugin
+
+ -h, --help Print this help and exit
+ -r, --retries Maximum amount of retries to attempt to start the plugin before giving up
+ -d, --delay Delay (in ms) between each attempt to start the plugin if it fails
+
+ [callsign] Callsign of the plugin to activate (Required)
+```
\ No newline at end of file
diff --git a/Utils/PluginActivator/source/COMRPCStarter.cpp b/Utils/PluginActivator/source/COMRPCStarter.cpp
new file mode 100644
index 00000000..88f50558
--- /dev/null
+++ b/Utils/PluginActivator/source/COMRPCStarter.cpp
@@ -0,0 +1,154 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "COMRPCStarter.h"
+
+#include "Log.h"
+
+#include <chrono>
+#include <thread>
+
+COMRPCStarter::COMRPCStarter(const string& pluginName)
+ : _pluginName(pluginName)
+ , _engine(Core::ProxyType<RPC::InvokeServerType<1, 0, 4>>::Create())
+ , _client(Core::ProxyType<RPC::CommunicatorClient>::Create(getConnectionEndpoint(),
+ Core::ProxyType<Core::IIPCServer>(_engine)))
+{
+ // Announce our arrival
+ _engine->Announcements(_client->Announcement());
+
+ if (!_client.IsValid()) {
+ LOG_ERROR(pluginName.c_str(), "Failed to create valid COM-RPC client");
+ }
+}
+
+COMRPCStarter::~COMRPCStarter()
+{
+ // Close the connection just in case something left it open
+ if (_client->IsOpen()) {
+ _client->Close(RPC::CommunicationTimeOut);
+ }
+
+ if (_client.IsValid()) {
+ _client.Release();
+ }
+}
+
+/**
+ * @brief Attempt to activate the plugin and automatically retry on failure
+ *
+ * @param[in] maxRetries Maximum amount of times to retry activation if it fails
+ * @param[in] retryDelayMs Delay in ms between retry attempts
+ *
+ * @return True if plugin successfully activated, false if failed to activate
+ */
+bool COMRPCStarter::activatePlugin(const uint8_t maxRetries, const uint16_t retryDelayMs)
+{
+ // Attempt to open the plugin shell
+ bool success = false;
+ int currentRetry = 1;
+
+ PluginHost::IShell* shell = nullptr;
+
+ while (!success && currentRetry <= maxRetries) {
+ LOG_INF(_pluginName.c_str(), "Attempting to activate plugin - attempt %d/%d", currentRetry, maxRetries);
+
+ auto start = Core::Time::Now();
+
+ if (!shell) {
+ shell = _client->Open<PluginHost::IShell>(_pluginName, ~0, RPC::CommunicationTimeOut);
+ }
+
+ // We could not open IShell for some reason - Thunder doesn't give an error about why this might have failed
+ // Normally this is because either:
+ // a) Thunder isn't running or we can't connect to /tmp/communicator
+ // b) A plugin with the specified callsign does not exist
+ if (!shell) {
+ LOG_ERROR(_pluginName.c_str(), "Failed to open IShell interface for %s", _pluginName.c_str());
+ currentRetry++;
+
+ // Must close the connection before we retry, since even if shell is null the underlying
+ // connection will still have been opened
+ _client->Close(RPC::CommunicationTimeOut);
+
+ // Sleep, then try again
+ std::this_thread::sleep_for(std::chrono::milliseconds(retryDelayMs));
+ } else {
+ // Connected to Thunder successfully and got the plugin shell
+ if (shell->State() == PluginHost::IShell::ACTIVATED) {
+ LOG_INF(_pluginName.c_str(), "Plugin is already activated - nothing to do");
+ success = true;
+ } else {
+ // Will block until plugin is activated
+ uint32_t result = shell->Activate(PluginHost::IShell::REQUESTED);
+
+ auto duration = Core::Time::Now().Sub(start.MilliSeconds());
+
+ if (result != Core::ERROR_NONE) {
+ if (result == Core::ERROR_PENDING_CONDITIONS) {
+ // Ideally we'd print out which preconditions are un-met for debugging, but that data is not exposed through the IShell interface
+ LOG_ERROR(_pluginName.c_str(), "Failed to activate plugin due to unmet preconditions after %dms", duration.MilliSeconds());
+ } else {
+ LOG_ERROR(_pluginName.c_str(), "Failed to activate plugin with error %u (%s) after %dms", result, Core::ErrorToString(result), duration.MilliSeconds());
+ }
+
+ // Try activation again up until the max number of retries
+ currentRetry++;
+ std::this_thread::sleep_for(std::chrono::milliseconds(retryDelayMs));
+ } else {
+ // Our work here is done!
+ LOG_INF(_pluginName.c_str(), "Successfully activated plugin after %dms", duration.MilliSeconds());
+ success = true;
+ }
+ }
+ }
+ }
+
+ if (!success) {
+ LOG_ERROR(_pluginName.c_str(), "Max retries hit - giving up activating the plugin");
+ }
+
+ if (shell) {
+ shell->Release();
+ }
+
+ // Be a good citizen and don't leave any open connections
+ if (_client->IsOpen()) {
+ _client->Close(RPC::CommunicationTimeOut);
+ }
+
+ return success;
+}
+
+/**
+ * Retrieves the socket we will communicate over for COM-RPC
+ */
+Core::NodeId COMRPCStarter::getConnectionEndpoint() const
+{
+ string communicatorPath;
+ Core::SystemInfo::GetEnvironment(_T("COMMUNICATOR_PATH"), communicatorPath);
+
+ // On linux, Thunder defaults to /tmp/communicator for the generic COM-RPC
+ // interface
+ if (communicatorPath.empty()) {
+ communicatorPath = _T("/tmp/communicator");
+ }
+
+ return Core::NodeId(communicatorPath.c_str());
+}
\ No newline at end of file
diff --git a/Utils/PluginActivator/source/COMRPCStarter.h b/Utils/PluginActivator/source/COMRPCStarter.h
new file mode 100644
index 00000000..dec8fa46
--- /dev/null
+++ b/Utils/PluginActivator/source/COMRPCStarter.h
@@ -0,0 +1,47 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include "Module.h"
+
+#include "IPluginStarter.h"
+
+using namespace WPEFramework;
+
+/**
+ * @brief COM-RPC implementation of a plugin starter
+ *
+ * Connects to Thunder over COM-RPC and attempts to start a given plugin
+ */
+class COMRPCStarter : public IPluginStarter {
+public:
+ explicit COMRPCStarter(const string& pluginName);
+ ~COMRPCStarter() override;
+
+ bool activatePlugin(const uint8_t maxRetries, const uint16_t retryDelayMs) override;
+
+private:
+ Core::NodeId getConnectionEndpoint() const;
+
+private:
+ const string _pluginName;
+
+ Core::ProxyType<RPC::InvokeServerType<1, 0, 4>> _engine;
+ Core::ProxyType<RPC::CommunicatorClient> _client;
+};
\ No newline at end of file
diff --git a/Utils/PluginActivator/source/IPluginStarter.h b/Utils/PluginActivator/source/IPluginStarter.h
new file mode 100755
index 00000000..32128417
--- /dev/null
+++ b/Utils/PluginActivator/source/IPluginStarter.h
@@ -0,0 +1,43 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+/**
+ * Interface to start a specified plugin
+ *
+ * Could be implemented with JSON-RPC or COM-RPC
+ */
+class IPluginStarter {
+public:
+ virtual ~IPluginStarter() = default;
+
+ /**
+ * @brief Activate a Thunder plugin
+ *
+ * Will block until either the Thunder plugin has activated or until the maximum amount of retires has occurred
+ *
+ * @param[in] maxRetries Maximum amount of attempts to activate the plugin - if plugin is not activated within the amount of retries
+ * this method will return false
+ * @param[in] retryDelayMs Amount of time to wait after a failed activation before retrying again
+ */
+ virtual bool activatePlugin(const uint8_t maxRetries, const uint16_t retryDelayMs) = 0;
+};
\ No newline at end of file
diff --git a/Utils/PluginActivator/source/Log.h b/Utils/PluginActivator/source/Log.h
new file mode 100644
index 00000000..87dff0b2
--- /dev/null
+++ b/Utils/PluginActivator/source/Log.h
@@ -0,0 +1,68 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdio.h>
+#include <string.h>
+
+#define LOG_LEVEL 3
+
+#ifndef __FILENAME__
+#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
+#endif
+
+#define LEVEL_DEBUG 3
+#define LEVEL_INFO 2
+#define LEVEL_WARN 1
+#define LEVEL_ERROR 0
+
+#define LOG_DBG(plugin, fmt, ...) \
+ __LOG(LEVEL_DEBUG, plugin, fmt, ##__VA_ARGS__)
+
+#define LOG_INF(plugin, fmt, ...) \
+ __LOG(LEVEL_INFO, plugin, fmt, ##__VA_ARGS__)
+
+#define LOG_WARN(plugin, fmt, ...) \
+ __LOG(LEVEL_WARN, plugin, fmt, ##__VA_ARGS__)
+
+#define LOG_ERROR(plugin, fmt, ...) \
+ __LOG(LEVEL_ERROR, plugin, fmt, ##__VA_ARGS__)
+
+#define __LOG(level, plugin, fmt, ...) \
+ do { \
+ if (__builtin_expect(((level) <= LOG_LEVEL), 0)) \
+ fprintf(stderr, "%s[%s:%d][%s] (%s) " fmt "\n", getLogLevel(level), __FILENAME__, __LINE__, __FUNCTION__, plugin, ##__VA_ARGS__); \
+ } while (0)
+
+inline const char* getLogLevel(int level)
+{
+ switch (level) {
+ case LEVEL_DEBUG:
+ return "[DBG]";
+ case LEVEL_INFO:
+ return "[NFO]";
+ case LEVEL_WARN:
+ return "[WRN]";
+ case LEVEL_ERROR:
+ return "[ERR]";
+ default:
+ return "";
+ }
+}
\ No newline at end of file
diff --git a/Utils/PluginActivator/source/Module.cpp b/Utils/PluginActivator/source/Module.cpp
new file mode 100644
index 00000000..aa310602
--- /dev/null
+++ b/Utils/PluginActivator/source/Module.cpp
@@ -0,0 +1,23 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Module.h"
+
+// If not set in CMake flags, BUILD_REFERENCE defaults to "engineering_build_for_debug_purpose_only"
+MODULE_NAME_DECLARATION(BUILD_REFERENCE)
\ No newline at end of file
diff --git a/Utils/PluginActivator/source/Module.h b/Utils/PluginActivator/source/Module.h
new file mode 100644
index 00000000..b2500eec
--- /dev/null
+++ b/Utils/PluginActivator/source/Module.h
@@ -0,0 +1,31 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#ifndef MODULE_NAME
+#define MODULE_NAME PluginStarter
+#endif
+
+#include <com/com.h>
+#include <core/core.h>
+#include <plugins/plugins.h>
+
+#undef EXTERNAL
+#define EXTERNAL
diff --git a/Utils/PluginActivator/source/main.cpp b/Utils/PluginActivator/source/main.cpp
new file mode 100644
index 00000000..fa1ac9dd
--- /dev/null
+++ b/Utils/PluginActivator/source/main.cpp
@@ -0,0 +1,132 @@
+/*
+ * If not stated otherwise in this file or this component's LICENSE file the
+ * following copyright and licenses apply:
+ *
+ * Copyright 2020 Metrological
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "COMRPCStarter.h"
+#include "Module.h"
+
+#include <memory>
+
+static int gRetryCount = 100;
+static int gRetryDelayMs = 500;
+static string gPluginName;
+
+/**
+ * @brief Display a help message for the tool
+ */
+static void displayUsage()
+{
+ printf("Usage: PluginActivator <option(s)> [callsign]\n");
+ printf(" Utility that starts a given thunder plugin\n\n");
+ printf(" -h, --help Print this help and exit\n");
+ printf(" -r, --retries Maximum amount of retries to attempt to start the plugin before giving up\n");
+ printf(" -d, --delay Delay (in ms) between each attempt to start the plugin if it fails\n");
+ printf("\n");
+ printf(" [callsign] Callsign of the plugin to activate (Required)\n");
+}
+
+/**
+ * @brief Parse the provided command line arguments
+ *
+ * Must be given the name of the plugin to activate, everything else
+ * is optional and will fallback to sane defaults
+ */
+static void parseArgs(const int argc, char** argv)
+{
+ if (argc == 1) {
+ displayUsage();
+ exit(EXIT_SUCCESS);
+ }
+
+ struct option longopts[] = {
+ { "help", no_argument, nullptr, (int)'h' },
+ { "retries", required_argument, nullptr, (int)'r' },
+ { "delay", required_argument, nullptr, (int)'d' },
+ { nullptr, 0, nullptr, 0 }
+ };
+
+ opterr = 0;
+
+ int option;
+ int longindex;
+
+ while ((option = getopt_long(argc, argv, "hr:d:", longopts, &longindex)) != -1) {
+ switch (option) {
+ case 'h':
+ displayUsage();
+ exit(EXIT_SUCCESS);
+ break;
+ case 'r':
+ gRetryCount = std::atoi(optarg);
+ if (gRetryCount < 0) {
+ fprintf(stderr, "Error: Retry count must be > 0\n");
+ exit(EXIT_FAILURE);
+ }
+ break;
+ case 'd':
+ gRetryDelayMs = std::atoi(optarg);
+ if (gRetryDelayMs < 0) {
+ fprintf(stderr, "Error: Delay ms must be > 0\n");
+ exit(EXIT_FAILURE);
+ }
+ break;
+ case '?':
+ if (optopt == 'c')
+ fprintf(stderr, "Warning: Option -%c requires an argument.\n", optopt);
+ else if (isprint(optopt))
+ fprintf(stderr, "Warning: Unknown option `-%c'.\n", optopt);
+ else
+ fprintf(stderr, "Warning: Unknown option character `\\x%x'.\n", optopt);
+
+ exit(EXIT_FAILURE);
+ break;
+ default:
+ exit(EXIT_FAILURE);
+ break;
+ }
+ }
+
+ if (optind == argc) {
+ fprintf(stderr, "Error: Must provide plugin name to activate\n");
+ exit(EXIT_FAILURE);
+ }
+
+ gPluginName = argv[optind];
+
+ optind++;
+ for (int i = optind; i < argc; i++) {
+ printf("Warning: Non-option argument %s ignored\n", argv[i]);
+ }
+}
+
+int main(int argc, char* argv[])
+{
+ parseArgs(argc, argv);
+
+ // For now, we only implement the starter in COM-RPC but could do a JSON-RPC version
+ // in the future
+ bool success;
+ std::unique_ptr<IPluginStarter> starter(new COMRPCStarter(gPluginName));
+ success = starter->activatePlugin(gRetryCount, gRetryDelayMs);
+
+ // Destruct the COM-RPC starter so it cleans up after itself before we dispose WPEFramework singletons
+ starter.reset();
+ Core::Singleton::Dispose();
+
+ return success ? EXIT_SUCCESS : EXIT_FAILURE;
+}
\ No newline at end of file
--
2.34.1