From 06f646aec4dbce64d28bae1be6111bd833f8e79e Mon Sep 17 00:00:00 2001 From: Paul Nettle Date: Fri, 25 Aug 2017 09:30:39 -0500 Subject: Initial version 1.0 --- src/Init.cpp | 1207 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1207 insertions(+) create mode 100644 src/Init.cpp (limited to 'src/Init.cpp') diff --git a/src/Init.cpp b/src/Init.cpp new file mode 100644 index 0000000..5ed25e2 --- /dev/null +++ b/src/Init.cpp @@ -0,0 +1,1207 @@ +// Copyright 2017 Paul Nettle. +// +// This file is part of Gobbledegook. +// +// Gobbledegook is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Gobbledegook is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Gobbledegook. If not, see . + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// >> +// >>> INSIDE THIS FILE +// >> +// +// Herein lies the code that manages the full initialization (including the running) of the server +// +// >> +// >>> DISCUSSION +// >> +// +// This file contains the highest-level framework for our server: +// +// Initialization +// Adapter configuration (mode, settings, name, etc.) +// GATT server registration with BlueZ +// Signal handling (such as CTRL-C) +// Event management +// Graceful shutdown +// +// Want to poke around and see how things work? Here's a tip: Start at the bottom of the file and work upwards. It'll make a lot +// more sense, I promise. +// +// Want to become your own boss while working from home? (Just kidding.) +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#include +#include +#include +#include +#include +#include + +#include "Server.h" +#include "Globals.h" +#include "Mgmt.h" +#include "HciAdapter.h" +#include "DBusObject.h" +#include "DBusInterface.h" +#include "GattCharacteristic.h" +#include "GattProperty.h" +#include "Logger.h" +#include "Init.h" + +// +// Constants +// + +static const int kPeriodicTimerFrequencySeconds = 1; +static const int kRetryDelaySeconds = 2; +static const int kIdleFrequencyMS = 10; + +// +// Retries +// + +static time_t retryTimeStart = 0; + +// +// Adapter configuration +// + +GDBusConnection *pBusConnection = nullptr; +static guint ownedNameId = 0; +static guint periodicTimeoutId = 0; +static std::vector registeredObjectIds; +static std::atomic pMainLoop(nullptr); +static GDBusObjectManager *pBluezObjectManager = nullptr; +static GDBusObject *pBluezAdapterObject = nullptr; +static GDBusObject *pBluezDeviceObject = nullptr; +static GDBusProxy *pBluezGattManagerProxy = nullptr; +static GDBusProxy *pBluezAdapterInterfaceProxy = nullptr; +static GDBusProxy *pBluezDeviceInterfaceProxy = nullptr; +static GDBusProxy *pBluezAdapterPropertiesInterfaceProxy = nullptr; +static bool bOwnedNameAcquired = false; +static bool bAdapterConfigured = false; +static bool bApplicationRegistered = false; +static std::string bluezGattManagerInterfaceName = ""; + +// +// Externs +// + +extern void setServerRunState(enum GGKServerRunState newState); +extern void setServerHealth(enum GGKServerHealth newState); + +// +// Forward declarations +// + +static void initializationStateProcessor(); + +// --------------------------------------------------------------------------------------------------------------------------------- +// ___ _ _ __ _ _ _ +// |_ _|__| | | ___ / / __| | __ _| |_ __ _ _ __ _ __ ___ ___ ___ ___ ___(_)_ __ __ _ +// | |/ _` | |/ _ \ / / / _` |/ _` | __/ _` | | '_ \| '__/ _ \ / __/ _ \/ __/ __| | '_ \ / _` | +// | | (_| | | __/ / / | (_| | (_| | || (_| | | |_) | | | (_) | (_| __/\__ \__ \ | | | | (_| | +// |___\__,_|_|\___| /_/ \__,_|\__,_|\__\__,_| | .__/|_| \___/ \___\___||___/___/_|_| |_|\__, | +// |_| |___/ +// +// Our idle funciton is what processes data updates. We handle this in a simple way. We update the data directly in our global +// `TheServer` object, then call `ggkPushUpdateQueue` to trigger that data to be updated (in whatever way the service responsible +// for that data() sees fit. +// +// This is done using the `ggkPushUpdateQueue` / `ggkPopUpdateQueue` methods to manage the queue of pending update messages. Each +// entry represents an interface that needs to be updated. The idleFunc calls the interface's `onUpdatedValue` method for each +// update. +// +// The idle processor will perform one update per idle tick, however, it will notify that there is more data so the idle ticks +// do not lag behind. +// --------------------------------------------------------------------------------------------------------------------------------- + +// Our idle function +// +// This method is used to process data on the same thread as our main loop. This allows us to communicate with our service from +// the outside. +// +// IMPORTANT: This method must return 'true' if any work was performed, otherwise it must return 'false'. Returning 'true' will +// cause the idle loop to continue to call this method to process data at the maximum rate (which can peg the CPU at 100%.) By +// returning false when there is no work to do, we are nicer to the system. +bool idleFunc(void *pUserData) +{ + // Don't do anything unless we're running + if (ggkGetServerRunState() != ERunning) + { + return false; + } + + // Try to get an update + const int kQueueEntryLen = 1024; + char queueEntry[kQueueEntryLen]; + if (ggkPopUpdateQueue(queueEntry, kQueueEntryLen, 0) != 1) + { + return false; + } + + std::string entryString = queueEntry; + auto token = entryString.find('|'); + if (token == std::string::npos) + { + Logger::error("Queue entry was not formatted properly - could not find separating token"); + return false; + } + + DBusObjectPath objectPath = DBusObjectPath(entryString.substr(0, token)); + std::string interfaceName = entryString.substr(token+1); + + // We have an update - call the onUpdatedValue method on the interface + std::shared_ptr pInterface = TheServer->findInterface(objectPath, interfaceName); + if (nullptr == pInterface) + { + Logger::warn(SSTR << "Unable to find interface for update: path[" << objectPath << "], name[" << interfaceName << "]"); + } + else + { + // Is it a characteristic? + if (std::shared_ptr pCharacteristic = TRY_GET_CONST_INTERFACE_OF_TYPE(pInterface, GattCharacteristic)) + { + Logger::info(SSTR << "Processing updated value for interface '" << interfaceName << "' at path '" << objectPath << "'"); + pCharacteristic->callOnUpdatedValue(pBusConnection, pUserData); + } + } + + return false; +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ _ _ _ _ _ _ _ +// | _ \ ___(_)_ __ (_) |_(_) __ _| (_)______ _| |_(_) ___ _ ___ +// | | | |/ _ \ | '_ \| | __| |/ _` | | |_ / _` | __| |/ _ \| '_ | +// | |_| | __/ | | | | | |_| | (_| | | |/ / (_| | |_| | (_) | | | | +// |____/ \___|_|_| |_|_|\__|_|\__,_|_|_/___\__,_|\__|_|\___/|_| |_| +// +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Perform final cleanup of various resources that were allocated while the server was initialized and/or running +void uninit() +{ + // We've left our main loop - nullify its pointer so we know we're no longer running + pMainLoop = nullptr; + + if (nullptr != pBluezAdapterObject) + { + g_object_unref(pBluezAdapterObject); + pBluezAdapterObject = nullptr; + } + + if (nullptr != pBluezDeviceObject) + { + g_object_unref(pBluezDeviceObject); + pBluezDeviceObject = nullptr; + } + + if (nullptr != pBluezAdapterInterfaceProxy) + { + g_object_unref(pBluezAdapterInterfaceProxy); + pBluezAdapterInterfaceProxy = nullptr; + } + + if (nullptr != pBluezDeviceInterfaceProxy) + { + g_object_unref(pBluezDeviceInterfaceProxy); + pBluezDeviceInterfaceProxy = nullptr; + } + + if (nullptr != pBluezAdapterPropertiesInterfaceProxy) + { + g_object_unref(pBluezAdapterPropertiesInterfaceProxy); + pBluezAdapterPropertiesInterfaceProxy = nullptr; + } + + if (nullptr != pBluezGattManagerProxy) + { + g_object_unref(pBluezGattManagerProxy); + pBluezGattManagerProxy = nullptr; + } + + if (nullptr != pBluezObjectManager) + { + g_object_unref(pBluezObjectManager); + pBluezObjectManager = nullptr; + } + + if (!registeredObjectIds.empty()) + { + for (guint id : registeredObjectIds) + { + g_dbus_connection_unregister_object(pBusConnection, id); + } + registeredObjectIds.clear(); + } + + if (0 != periodicTimeoutId) + { + g_source_remove(periodicTimeoutId); + periodicTimeoutId = 0; + } + + if (ownedNameId > 0) + { + g_bus_unown_name(ownedNameId); + } + + if (nullptr != pBusConnection) + { + g_object_unref(pBusConnection); + pBusConnection = nullptr; + } + + if (nullptr != pMainLoop) + { + g_main_loop_unref(pMainLoop); + pMainLoop = nullptr; + } +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ _ _ +// / ___|| |__ _ _| |_ __| | _____ ___ ___ +// \___ \| '_ \| | | | __/ _` |/ _ \ \ /\ / / '_ | +// ___) | | | | |_| | || (_| | (_) \ V V /| | | | +// |____/|_| |_|\__,_|\__\__,_|\___/ \_/\_/ |_| |_| +// +// This is how we shutdown our server gracefully. +// --------------------------------------------------------------------------------------------------------------------------------- + +// Trigger a graceful, asynchronous shutdown of the server +// +// This method is non-blocking and as such, will only trigger the shutdown process but not wait for it +void shutdown() +{ + if (ggkGetServerRunState() >= EStopping) + { + Logger::warn(SSTR << "shutdown() called while already shutting down"); + } + + // Our new state: shutting down + setServerRunState(EStopping); + + // If we still have a main loop, ask it to quit + if (nullptr != pMainLoop) + { + g_main_loop_quit(pMainLoop); + } +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ _ _ _ _ +// | _ \ ___ _ __(_) ___ __| (_) ___ | |_(_)_ __ ___ ___ _ __ +// | |_) / _ \ '__| |/ _ \ / _` | |/ __| | __| | '_ ` _ \ / _ \ '__| +// | __/ __/ | | | (_) | (_| | | (__ | |_| | | | | | | __/ | +// |_| \___|_| |_|\___/ \__,_|_|\___| \__|_|_| |_| |_|\___|_| +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Periodic timer handler +// +// A periodic timer is a timer fires every so often (see kPeriodicTimerFrequencySeconds.) This is used for our initialization +// failure retries, but custom code can also be added to a server description (see `onEvent()`) +gboolean onPeriodicTimer(gpointer pUserData) +{ + // Deal with retry timers + if (0 != retryTimeStart) + { + Logger::debug(SSTR << "Ticking retry timer"); + + // Has the retry time expired? + int secondsRemaining = time(nullptr) - retryTimeStart - kRetryDelaySeconds; + if (secondsRemaining >= 0) + { + retryTimeStart = 0; + initializationStateProcessor(); + } + } + + // If we're registered, then go ahead and emit signals + if (bApplicationRegistered) + { + // Tick the object hierarchy + // + // The real goal here is to have the objects tick their interfaces (see `onEvent()` method when adding interfaces inside + // 'Server::Server()' + for (const DBusObject &object : TheServer->getObjects()) + { + if (object.isPublished()) + { + object.tickEvents(pBusConnection, pUserData); + } + } + } + + return TRUE; +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// _____ _ +// | ____|_ _____ _ __ | |_ ___ +// | _| \ \ / / _ \ '_ \| __/ __| +// | |___ \ V / __/ | | | |_\__ ) +// |_____| \_/ \___|_| |_|\__|___/ +// +// Our event handlers. These are generic, as they parcel out the work to the appropriate server objects (see 'Server::Server()' for +// the code that manages event handlers.) +// --------------------------------------------------------------------------------------------------------------------------------- + +// Handle D-Bus method calls +void onMethodCall +( + GDBusConnection *pConnection, + const gchar *pSender, + const gchar *pObjectPath, + const gchar *pInterfaceName, + const gchar *pMethodName, + GVariant *pParameters, + GDBusMethodInvocation *pInvocation, + gpointer pUserData +) +{ + // Convert our input path into our custom type for path management + DBusObjectPath objectPath(pObjectPath); + + if (!TheServer->callMethod(objectPath, pInterfaceName, pMethodName, pConnection, pParameters, pInvocation, pUserData)) + { + Logger::error(SSTR << " + Method not found: [" << pSender << "]:[" << objectPath << "]:[" << pInterfaceName << "]:[" << pMethodName << "]"); + g_dbus_method_invocation_return_dbus_error(pInvocation, kErrorNotImplemented.c_str(), "This method is not implemented"); + return; + } + + return; +} + +// Handle D-Bus requests to get a property +GVariant *onGetProperty +( + GDBusConnection *pConnection, + const gchar *pSender, + const gchar *pObjectPath, + const gchar *pInterfaceName, + const gchar *pPropertyName, + GError **ppError, + gpointer pUserData +) +{ + // Convert our input path into our custom type for path management + DBusObjectPath objectPath(pObjectPath); + + const GattProperty *pProperty = TheServer->findProperty(objectPath, pInterfaceName, pPropertyName); + + std::string propertyPath = std::string("[") + pSender + "]:[" + objectPath.toString() + "]:[" + pInterfaceName + "]:[" + pPropertyName + "]"; + if (!pProperty) + { + Logger::error(SSTR << "Property(get) not found: " << propertyPath); + g_set_error(ppError, G_IO_ERROR, G_IO_ERROR_FAILED, ("Property(get) not found: " + propertyPath).c_str(), pSender); + return nullptr; + } + + if (!pProperty->getGetterFunc()) + { + Logger::error(SSTR << "Property(get) func not found: " << propertyPath); + g_set_error(ppError, G_IO_ERROR, G_IO_ERROR_FAILED, ("Property(get) func not found: " + propertyPath).c_str(), pSender); + return nullptr; + } + + Logger::info(SSTR << "Calling property getter: " << propertyPath); + GVariant *pResult = pProperty->getGetterFunc()(pConnection, pSender, objectPath.c_str(), pInterfaceName, pPropertyName, ppError, pUserData); + + if (nullptr == pResult) + { + g_set_error(ppError, G_IO_ERROR, G_IO_ERROR_FAILED, ("Property(get) failed: " + propertyPath).c_str(), pSender); + return nullptr; + } + + return pResult; +} + +// Handle D-Bus requests to set a property +gboolean onSetProperty +( + GDBusConnection *pConnection, + const gchar *pSender, + const gchar *pObjectPath, + const gchar *pInterfaceName, + const gchar *pPropertyName, + GVariant *pValue, + GError **ppError, + gpointer pUserData +) +{ + // Convert our input path into our custom type for path management + DBusObjectPath objectPath(pObjectPath); + + const GattProperty *pProperty = TheServer->findProperty(objectPath, pInterfaceName, pPropertyName); + + std::string propertyPath = std::string("[") + pSender + "]:[" + objectPath.toString() + "]:[" + pInterfaceName + "]:[" + pPropertyName + "]"; + if (!pProperty) + { + Logger::error(SSTR << "Property(set) not found: " << propertyPath); + g_set_error(ppError, G_IO_ERROR, G_IO_ERROR_FAILED, ("Property(set) not found: " + propertyPath).c_str(), pSender); + return false; + } + + if (!pProperty->getSetterFunc()) + { + Logger::error(SSTR << "Property(set) func not found: " << propertyPath); + g_set_error(ppError, G_IO_ERROR, G_IO_ERROR_FAILED, ("Property(set) func not found: " + propertyPath).c_str(), pSender); + return false; + } + + Logger::info(SSTR << "Calling property getter: " << propertyPath); + if (!pProperty->getSetterFunc()(pConnection, pSender, objectPath.c_str(), pInterfaceName, pPropertyName, pValue, ppError, pUserData)) + { + g_set_error(ppError, G_IO_ERROR, G_IO_ERROR_FAILED, ("Property(set) failed: " + propertyPath).c_str(), pSender); + return false; + } + + return true; +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// _____ _ _ _ +// | ___|_ _(_) |_ _ _ __ ___ _ __ ___ __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_ +// | |_ / _` | | | | | | '__/ _ \ | '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __| +// | _| (_| | | | |_| | | | __/ | | | | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_ +// |_| \__,_|_|_|\__,_|_| \___| |_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__| +// |___/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Convenience method for setting a retry timer so that failures (related to initialization) can be continuously retried until we +// eventually succeed. +void setRetryFailure() +{ + retryTimeStart = time(nullptr); + Logger::info(SSTR << " + Will retry this failed operation in about " << kRetryDelaySeconds << " seconds"); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ _____ _____ _ _ _ _ +// / ___| / \|_ _|_ _| _ __ ___ __ _(_)___| |_ _ __ __ _| |_(_) ___ _ ___ +// | | _ / _ \ | | | | | '__/ _ \/ _` | / __| __| '__/ _` | __| |/ _ \| '_ | +// | |_| |/ ___ \| | | | | | | __/ (_| | \__ \ |_| | | (_| | |_| | (_) | | | | +// \____/_/ \_\_| |_| |_| \___|\__, |_|___/\__|_| \__,_|\__|_|\___/|_| |_| +// |___/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Use the BlueZ GATT Manager proxy to register our GATT application with BlueZ +void doRegisterApplication() +{ + g_auto(GVariantBuilder) builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + GVariant *pParams = g_variant_new("(oa{sv})", "/", &builder); + + g_dbus_proxy_call + ( + pBluezGattManagerProxy, // GDBusProxy *proxy + "RegisterApplication", // const gchar *method_name (ex: "GetManagedObjects") + pParams, // GVariant *parameters + G_DBUS_CALL_FLAGS_NONE, // GDBusCallFlags flags + -1, // gint timeout_msec + nullptr, // GCancellable *cancellable + + // GAsyncReadyCallback callback + [] (GObject * /*pSourceObject*/, GAsyncResult *pAsyncResult, gpointer /*pUserData*/) + { + GError *pError = nullptr; + GVariant *pVariant = g_dbus_proxy_call_finish(pBluezGattManagerProxy, pAsyncResult, &pError); + if (nullptr == pVariant) + { + Logger::error(SSTR << "Failed to register application: " << (nullptr == pError ? "Unknown" : pError->message)); + setRetryFailure(); + } + else + { + g_variant_unref(pVariant); + Logger::info(SSTR << "Application registered"); + bApplicationRegistered = true; + } + + // Keep going... + initializationStateProcessor(); + }, + + nullptr // gpointer user_data + ); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ___ _ _ _ _ _ _ _ +// / _ \| |__ (_) ___ ___| |_ _ __ ___ __ _(_)___| |_ _ __ __ _| |_(_) ___ _ ___ +// | | | | '_ \| |/ _ \/ __| __| | '__/ _ \/ _` | / __| __| '__/ _` | __| |/ _ \| '_ | +// | |_| | |_) | | __/ (__| |_ | | | __/ (_| | \__ \ |_| | | (_| | |_| | (_) | | | | +// \___/|_.__// |\___|\___|\__| |_| \___|\__, |_|___/\__|_| \__,_|\__|_|\___/|_| |_| +// |__/ |___/ +// +// Before we can register our service(s) with BlueZ, we must first register ourselves with D-Bus. The easiest way to do this is to +// use an XML description of our D-Bus objects. +// --------------------------------------------------------------------------------------------------------------------------------- + +void registerNodeHierarchy(GDBusNodeInfo *pNode, const DBusObjectPath &basePath = DBusObjectPath(), int depth = 1) +{ + std::string prefix; + prefix.insert(0, depth * 2, ' '); + + static GDBusInterfaceVTable interfaceVtable; + interfaceVtable.method_call = onMethodCall; + interfaceVtable.get_property = onGetProperty; + interfaceVtable.set_property = onSetProperty; + + GDBusInterfaceInfo **ppInterface = pNode->interfaces; + + Logger::debug(SSTR << prefix << "+ " << pNode->path); + + while(nullptr != *ppInterface) + { + GError *pError = nullptr; + Logger::debug(SSTR << prefix << " (iface: " << (*ppInterface)->name << ")"); + guint registeredObjectId = g_dbus_connection_register_object + ( + pBusConnection, // GDBusConnection *connection + basePath.c_str(), // const gchar *object_path + *ppInterface, // GDBusInterfaceInfo *interface_info + &interfaceVtable, // const GDBusInterfaceVTable *vtable + nullptr, // gpointer user_data + nullptr, // GDestroyNotify user_data_free_func + &pError // GError **error + ); + + if (0 == registeredObjectId) + { + Logger::error(SSTR << "Failed to register object: " << (nullptr == pError ? "Unknown" : pError->message)); + + // Cleanup and pretend like we were never here + g_dbus_node_info_unref(pNode); + registeredObjectIds.clear(); + + // Try again later + setRetryFailure(); + return; + } + + // Save the registered object Id so we can clean it up later + registeredObjectIds.push_back(registeredObjectId); + + ++ppInterface; + } + + GDBusNodeInfo **ppChild = pNode->nodes; + while(nullptr != *ppChild) + { + registerNodeHierarchy(*ppChild, basePath + (*ppChild)->path, depth + 1); + + ++ppChild; + } +} + +void registerObjects() +{ + // Parse each object into an XML interface tree + for (const DBusObject &object : TheServer->getObjects()) + { + GError *pError = nullptr; + std::string xmlString = object.generateIntrospectionXML(); + GDBusNodeInfo *pNode = g_dbus_node_info_new_for_xml(xmlString.c_str(), &pError); + if (nullptr == pNode) + { + Logger::error(SSTR << "Failed to introspect XML: " << (nullptr == pError ? "Unknown" : pError->message)); + setRetryFailure(); + return; + } + + Logger::debug(SSTR << "Registering object hierarchy with D-Bus hierarchy"); + + // Register the node hierarchy + registerNodeHierarchy(pNode, DBusObjectPath(pNode->path)); + + // Cleanup the node + g_dbus_node_info_unref(pNode); + } + + // Keep going + initializationStateProcessor(); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// _ _ _ __ _ _ _ +// / \ __| | __ _ _ __ | |_ ___ _ __ ___ ___ _ __ / _(_) __ _ _ _ _ __ __ _| |_(_) ___ _ ___ +// / _ \ / _` |/ _` | '_ \| __/ _ \ '__| / __/ _ \| '_ \| |_| |/ _` | | | | '__/ _` | __| |/ _ \| '_ | +// / ___ \ (_| | (_| | |_) | || __/ | | (_| (_) | | | | _| | (_| | |_| | | | (_| | |_| | (_) | | | | +// /_/ \_\__,_|\__,_| .__/ \__\___|_| \___\___/|_| |_|_| |_|\__, |\__,_|_| \__,_|\__|_|\___/|_| |_| +// |_| |___/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Configure an adapter to ensure it is setup the way we need. We turn things on that we need and turn everything else off +// (to maximize security.) +// +// Note that this only works for the first adapter (index 0). Search for kControllerIndex for information. +// +// See also: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/mgmt-api.txt +void configureAdapter() +{ + Mgmt mgmt; + + // Find out what our current settings are + Logger::info(SSTR << "Getting device information"); + Mgmt::ControllerInformation *pInfo = mgmt.getControllerInformation(); + if (nullptr == pInfo) + { + setRetryFailure(); + return; + } + + // We need it off to start with + if ((pInfo->currentSettings & Mgmt::EHciPowered) != 0) + { + Logger::info(SSTR << "Powering off"); + if (!mgmt.setPowered(false)) + { + setRetryFailure(); + return; + } + } + + // Change the Br/Edr state? + bool bredrCurrentState = (pInfo->currentSettings & Mgmt::EHciBasicRate_EnhancedDataRate) != 0 ? true:false; + if (TheServer->getEnableBREDR() != bredrCurrentState) + { + Logger::info(SSTR << (TheServer->getEnableBREDR() ? "Enabling":"Disabling") << " BR/EDR"); + if (!mgmt.setBredr(TheServer->getEnableBREDR())) + { + setRetryFailure(); + return; + } + } + + // Change the Secure Connectinos state? + bool scCurrentState = (pInfo->currentSettings & Mgmt::EHciSecureConnections) != 0 ? true:false; + if (TheServer->getEnableSecureConnection() != scCurrentState) + { + Logger::info(SSTR << (TheServer->getEnableSecureConnection() ? "Enabling":"Disabling") << " Secure Connections"); + if (!mgmt.setSecureConnections(TheServer->getEnableSecureConnection() ? 1 : 0)) + { + setRetryFailure(); + return; + } + } + + // Change the Bondable state? + bool bondableCurrentState = (pInfo->currentSettings & Mgmt::EHciBondable) != 0 ? true:false; + if (TheServer->getEnableBondable() != bondableCurrentState) + { + Logger::info(SSTR << (TheServer->getEnableBondable() ? "Enabling":"Disabling") << " Bondable"); + if (!mgmt.setBondable(TheServer->getEnableBondable())) + { + setRetryFailure(); + return; + } + } + + // Change the Connectable state? + bool connectableCurrentState = (pInfo->currentSettings & Mgmt::EHciConnectable) != 0 ? true:false; + if (TheServer->getEnableConnectable() != connectableCurrentState) + { + Logger::info(SSTR << (TheServer->getEnableConnectable() ? "Enabling":"Disabling") << " Connectable"); + if (!mgmt.setConnectable(TheServer->getEnableConnectable())) + { + setRetryFailure(); + return; + } + } + + // Enable the LE state (we always set this state if it's not set) + if ((pInfo->currentSettings & Mgmt::EHciLowEnergy) == 0) + { + Logger::info(SSTR << "Enabling LE"); + if (!mgmt.setLE(true)) + { + setRetryFailure(); + return; + } + } + + // Change the Advertising state? + bool advertisingCurrentState = (pInfo->currentSettings & Mgmt::EHciAdvertising) != 0 ? true:false; + if (TheServer->getEnableAdvertising() != advertisingCurrentState) + { + Logger::info(SSTR << (TheServer->getEnableAdvertising() ? "Enabling":"Disabling") << " Advertising"); + if (!mgmt.setAdvertising(TheServer->getEnableAdvertising() ? 1 : 0)) + { + setRetryFailure(); + return; + } + } + + // Set the name? + if (kCustomGlobalAdvertisingName.length() != 0 || kCustomGlobalAdvertisingShortName.length() != 0) + { + if (Mgmt::truncateName(kCustomGlobalAdvertisingName) != pInfo->name || + Mgmt::truncateShortName(kCustomGlobalAdvertisingShortName) != pInfo->shortName) + { + Logger::info(SSTR << "Setting name to '" << kCustomGlobalAdvertisingName << "'"); + if (!mgmt.setName(kCustomGlobalAdvertisingName.c_str(), kCustomGlobalAdvertisingName.c_str())) + { + setRetryFailure(); + return; + } + } + } + + // Turn it back on + Logger::info(SSTR << "Powering on"); + if (!mgmt.setPowered(true)) + { + setRetryFailure(); + return; + } + + // We can ignore errors on this - we're just letting it dump the output + mgmt.getControllerInformation(); + + bAdapterConfigured = true; + initializationStateProcessor(); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// _ _ _ +// / \ __| | __ _ _ __ | |_ ___ _ __ +// / _ \ / _` |/ _` | '_ \| __/ _ \ '__| +// / ___ \ (_| | (_| | |_) | || __/ | +// /_/ \_\__,_|\__,_| .__/ \__\___|_| +// |_| +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Find the BlueZ's GATT Manager interface for the *first* Bluetooth adapter provided by BlueZ. We'll need this to register our +// GATT server with BlueZ. +void findAdapterInterface() +{ + // Get a list of the BlueZ's D-Bus objects + GList *pObjects = g_dbus_object_manager_get_objects(pBluezObjectManager); + if (nullptr == pObjects) + { + Logger::error(SSTR << "Unable to get ObjectManager objects"); + setRetryFailure(); + return; + } + + // Scan the list of objects we find one with a GATT manager interface + // + // Note that if there are multiple interfaces, we will only find the first + for (guint i = 0; i < g_list_length(pObjects) && bluezGattManagerInterfaceName.empty(); ++i) + { + // Current object in question + pBluezAdapterObject = static_cast(g_list_nth_data(pObjects, i)); + if (nullptr == pBluezAdapterObject) { continue; } + + // See if it has a GATT manager interface + pBluezGattManagerProxy = reinterpret_cast(g_dbus_object_get_interface(pBluezAdapterObject, "org.bluez.GattManager1")); + if (nullptr == pBluezGattManagerProxy) { continue; } + + // Get the interface proxy for this adapter - this will come in handy later + pBluezAdapterInterfaceProxy = reinterpret_cast(g_dbus_object_get_interface(pBluezAdapterObject, "org.bluez.Adapter1")); + if (nullptr == pBluezAdapterInterfaceProxy) + { + Logger::warn(SSTR << "Failed to get adapter proxy for interface 'org.bluez.Adapter1'"); + continue; + } + + // Get the interface proxy for this adapter's properties - this will come in handy later + pBluezAdapterPropertiesInterfaceProxy = reinterpret_cast(g_dbus_object_get_interface(pBluezAdapterObject, "org.freedesktop.DBus.Properties")); + if (nullptr == pBluezAdapterPropertiesInterfaceProxy) + { + Logger::warn(SSTR << "Failed to get adapter properties proxy for interface 'org.freedesktop.DBus.Properties'"); + continue; + } + + // Finally, save off the interface name, we're done! + bluezGattManagerInterfaceName = g_dbus_proxy_get_object_path(pBluezGattManagerProxy); + break; + } + + // Get a fresh copy of our objects so we can release the entire list + pBluezAdapterObject = g_dbus_object_manager_get_object(pBluezObjectManager, g_dbus_object_get_object_path(pBluezAdapterObject)); + + // We'll need access to the device object so we can set properties on it + pBluezDeviceObject = g_dbus_object_manager_get_object(pBluezObjectManager, g_dbus_object_get_object_path(pBluezAdapterObject)); + + // Cleanup the list + for (guint i = 0; i < g_list_length(pObjects) && bluezGattManagerInterfaceName.empty(); ++i) + { + g_object_unref(g_list_nth_data(pObjects, i)); + } + + g_list_free(pObjects); + + // If we didn't find the adapter object, reset things and we'll try again later + if (nullptr == pBluezAdapterObject || nullptr == pBluezDeviceObject) + { + Logger::info(SSTR << "Unable to find BlueZ objects outside of object list"); + bluezGattManagerInterfaceName.clear(); + } + + // If we never ended up with an interface name, bail now + if (bluezGattManagerInterfaceName.empty()) + { + Logger::error(SSTR << "Unable to find the adapter"); + setRetryFailure(); + return; + } + + // Keep going + initializationStateProcessor(); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ _____ ___ _ _ _ __ __ +// | __ )| |_ _ ___|__ / / _ \| |__ (_) ___ ___| |_| \/ | __ _ _ __ __ _ __ _ ___ _ __ +// | _ \| | | | |/ _ \ / / | | | | '_ \| |/ _ \/ __| __| |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__| +// | |_) | | |_| | __// /_ | |_| | |_) | | __/ (__| |_| | | | (_| | | | | (_| | (_| | __/ | +// |____/|_|\__,_|\___/____| \___/|_.__// |\___|\___|\__|_| |_|\__,_|_| |_|\__,_|\__, |\___|_| +// |__/ |___/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Get the D-Bus Object Manager client to the BlueZ ObjectManager object +// +// An ObjectManager allows us to find out what objects (and from those, interfaces, etc.) are available from an owned name. We'll +// use this to interrogate BlueZ's objects to find an adapter we can use, among other things. +void getBluezObjectManager() +{ + g_dbus_object_manager_client_new + ( + pBusConnection, // GDBusConnection + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, // GDBusObjectManagerClientFlags + "org.bluez", // Owner name (or well-known name) + "/", // Object path + nullptr, // GDBusProxyTypeFunc get_proxy_type_func + nullptr, // gpointer get_proxy_type_user_data + nullptr, // GDestroyNotify get_proxy_type_destroy_notify + nullptr, // GCancellable *cancellable + + // GAsyncReadyCallback callback + [] (GObject * /*pSourceObject*/, GAsyncResult *pAsyncResult, gpointer /*pUserData*/) + { + // Store BlueZ's ObjectManager + GError *pError = nullptr; + pBluezObjectManager = g_dbus_object_manager_client_new_finish(pAsyncResult, &pError); + + if (nullptr == pBluezObjectManager) + { + Logger::error(SSTR << "Failed to get an ObjectManager client: " << (nullptr == pError ? "Unknown" : pError->message)); + setRetryFailure(); + return; + } + + // Keep going + initializationStateProcessor(); + }, + + nullptr // gpointer user_data + ); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ___ _ +// / _ \__ ___ __ ___ __| | _ __ __ _ _ __ ___ ___ +// | | | \ \ /\ / / '_ \ / _ \/ _` | | '_ \ / _` | '_ ` _ \ / _ ) +// | |_| |\ V V /| | | | __/ (_| | | | | | (_| | | | | | | __/ +// \___/ \_/\_/ |_| |_|\___|\__,_| |_| |_|\__,_|_| |_| |_|\___| +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Acquire an "owned name" with D-Bus. This name represents our server as a whole, identifying us on D-Bus and allowing others +// (BlueZ) to communicate back to us. All of the D-Bus objects (which represent our BlueZ services, characteristics, etc.) will all +// reside under this owned name. +// +// Note about error management: We don't yet hwave a timeout callback running for retries; errors are considered fatal +void doOwnedNameAcquire() +{ + // Our name is not presently lost + bOwnedNameAcquired = false; + + ownedNameId = g_bus_own_name_on_connection + ( + pBusConnection, // GDBusConnection *connection + kServerOwnedName.c_str(), // const gchar *name + G_BUS_NAME_OWNER_FLAGS_NONE, // GBusNameOwnerFlags flags + + // GBusNameAcquiredCallback name_acquired_handler + [](GDBusConnection *, const gchar *, gpointer) + { + // Handy way to get periodic activity + periodicTimeoutId = g_timeout_add_seconds(kPeriodicTimerFrequencySeconds, onPeriodicTimer, pBusConnection); + if (periodicTimeoutId <= 0) + { + Logger::fatal(SSTR << "Failed to add a periodic timer"); + setServerHealth(EFailedInit); + shutdown(); + } + + // Bus name acquired + bOwnedNameAcquired = true; + + // Keep going... + initializationStateProcessor(); + }, + + // GBusNameLostCallback name_lost_handler + [](GDBusConnection *, const gchar *, gpointer) + { + // Bus name lost + bOwnedNameAcquired = false; + + // If we don't have a periodicTimeout (which we use for error recovery) then we're done + if (0 == periodicTimeoutId) + { + Logger::fatal(SSTR << "Unable to acquire an owned name ('" << kServerOwnedName << "') on the bus"); + setServerHealth(EFailedInit); + shutdown(); + } + else + { + Logger::warn(SSTR << "Owned name ('" << kServerOwnedName << "') lost"); + setRetryFailure(); + return; + } + + // Keep going... + initializationStateProcessor(); + }, + + nullptr, // gpointer user_data + nullptr // GDestroyNotify user_data_free_func + ); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ +// | __ ) _ _ ___ +// | _ \| | | / __| +// | |_) | |_| \__ ) +// |____/ \__,_|___/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Acquire a connection to the SYSTEM bus so we can communicate with BlueZ. +// +// Note about error management: We don't yet hwave a timeout callback running for retries; errors are considered fatal +void doBusAcquire() +{ + // Acquire a connection to the SYSTEM bus + g_bus_get + ( + G_BUS_TYPE_SYSTEM, // GBusType bus_type + nullptr, // GCancellable *cancellable + + // GAsyncReadyCallback callback + [] (GObject */*pSourceObject*/, GAsyncResult *pAsyncResult, gpointer /*pUserData*/) + { + GError *pError = nullptr; + pBusConnection = g_bus_get_finish(pAsyncResult, &pError); + + if (nullptr == pBusConnection) + { + Logger::fatal(SSTR << "Failed to get bus connection: " << (nullptr == pError ? "Unknown" : pError->message)); + setServerHealth(EFailedInit); + shutdown(); + } + + // Continue + initializationStateProcessor(); + }, + + nullptr // gpointer user_data + ); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ _ _ +// / ___|| |_ __ _| |_ ___ _ __ ___ __ _ _ __ __ _ __ _ ___ _ __ ___ ___ _ __ | |_ +// \___ \| __/ _` | __/ _ \ | '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \ '_ ` _ \ / _ \ '_ \| __| +// ___) | || (_| | || __/ | | | | | | (_| | | | | (_| | (_| | __/ | | | | | __/ | | | |_ +// |____/ \__\__,_|\__\___| |_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| |_| |_|\___|_| |_|\__| +// |___/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Poor-man's state machine, which effectively ensures everything is initialized in order by verifying actual initialization state +// rather than stepping through a set of numeric states. This way, if something fails in an out-of-order sort of way, we can still +// handle it and recover nicely. +void initializationStateProcessor() +{ + // If we're in our end-of-life or waiting for a retry, don't process states + if (ggkGetServerRunState() > ERunning || 0 != retryTimeStart) { return; } + + // + // Get a bus connection + // + if (nullptr == pBusConnection) + { + Logger::info(SSTR << "Acquiring bus connection"); + doBusAcquire(); + return; + } + + // + // Acquire an owned name on the bus + // + if (!bOwnedNameAcquired) + { + Logger::info(SSTR << "Acquiring owned name: '" << kServerOwnedName << "'"); + doOwnedNameAcquire(); + return; + } + + // + // Get BlueZ's ObjectManager + // + if (nullptr == pBluezObjectManager) + { + Logger::debug(SSTR << "Getting BlueZ ObjectManager"); + getBluezObjectManager(); + return; + } + + // + // Find the adapter interface + // + if (bluezGattManagerInterfaceName.empty()) + { + Logger::debug(SSTR << "Finding BlueZ GattManager1 interface"); + findAdapterInterface(); + return; + } + + // + // Find the adapter interface + // + if (!bAdapterConfigured) + { + Logger::info(SSTR << "Configuring BlueZ adapter '" << bluezGattManagerInterfaceName << "'"); + configureAdapter(); + return; + } + + // + // Register our object with D-bus + // + if (registeredObjectIds.empty()) + { + Logger::info(SSTR << "Registering with D-Bus"); + registerObjects(); + return; + } + + // Register our appliation with the BlueZ GATT manager + if (!bApplicationRegistered) + { + Logger::info(SSTR << "Registering application with BlueZ GATT manager"); + + doRegisterApplication(); + return; + } + + // At this point, we should be fully initialized + // + // It shouldn't ever happen, but just in case, let's double-check that we're healthy and if not, shutdown immediately + if (ggkGetServerHealth() != EOk) + { + shutdown(); + return; + } + + // Successful initialization - switch to running state + setServerRunState(ERunning); +} + +// --------------------------------------------------------------------------------------------------------------------------------- +// ____ _ +// | _ \ _ _ _ __ ___ ___ _ ____ _____ _ __ _ __ _ _ _ __ | | +// | |_) | | | | '_ \ / __|/ _ \ '__\ \ / / _ \ '__| | '__| | | | '_ \| | +// | _ <| |_| | | | | \__ \ __/ | \ V / __/ | _ | | | |_| | | | |_| +// |_| \_\\__,_|_| |_| |___/\___|_| \_/ \___|_|( ) |_| \__,_|_| |_(_) +// |/ +// +// --------------------------------------------------------------------------------------------------------------------------------- + +// Entry point for the asynchronous server thread +// +// This method should not be called directly, instead, direct your attention over to `ggkStart()` +void runServerThread() +{ + // Set the initialization state + setServerRunState(EInitializing); + + // Start our state processor, which is really just a simplified state machine that steps us through an asynchronous + // initialization process. + // + // In case you're wondering if these really need to be async, the answer is yes. For one, it's the right way to do it. But + // the more practical response is that the main loop must be running (see below) in order for us to receive and respond to + // events from BlueZ. Well, one of the calls we need to make during initialization is 'RegisterApplication'. This method will + // will require that we respond the 'GetNamedObjects' method before it returns. If we were to call the synchronous version of + // 'RegisterApplication', then we've effectively created a deadlock. + // + // There are alternatives, but using async methods is the recommended way. + initializationStateProcessor(); + + Logger::info(SSTR << "Starting main loop"); + pMainLoop = g_main_loop_new(NULL, FALSE); + + // Add the idle function + // + // Note that we actually run the idle function from a lambda. This allows us to manage the inter-idle sleep so we don't + // soak up 100% of our CPU. + guint res = g_idle_add + ( + [](gpointer pUserData) -> gboolean + { + // Try to process some data and if no data is processed, sleep for the requested frequency + if (!idleFunc(pUserData)) + { + std::this_thread::sleep_for(std::chrono::milliseconds(kIdleFrequencyMS)); + } + + // Always return TRUE so our idle remains in tact + return TRUE; + }, + nullptr + ); + + if (res == 0) + { + Logger::error(SSTR << "Unable to add idle to main loop"); + } + else + { + Logger::info("Idle function added successfully"); + } + + g_main_loop_run(pMainLoop); + + // We have stopped + setServerRunState(EStopped); + + // Cleanup + uninit(); +} -- cgit v1.2.3