Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/mumble/PluginManager.cpp')
-rw-r--r--src/mumble/PluginManager.cpp933
1 files changed, 933 insertions, 0 deletions
diff --git a/src/mumble/PluginManager.cpp b/src/mumble/PluginManager.cpp
new file mode 100644
index 000000000..817eb69be
--- /dev/null
+++ b/src/mumble/PluginManager.cpp
@@ -0,0 +1,933 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include <limits>
+
+#include "PluginManager.h"
+#include "LegacyPlugin.h"
+#include <QReadLocker>
+#include <QWriteLocker>
+#include <QReadLocker>
+#include <QDir>
+#include <QFileInfoList>
+#include <QFileInfo>
+#include <QVector>
+#include <QByteArray>
+#include <QChar>
+#include <QMutexLocker>
+#include <QHashIterator>
+#include <QKeyEvent>
+#include <QTimer>
+
+#include "ManualPlugin.h"
+#include "Log.h"
+#include "PluginInstaller.h"
+#include "ProcessResolver.h"
+#include "ServerHandler.h"
+#include "PluginUpdater.h"
+#include "API.h"
+#include "Global.h"
+
+#include <memory>
+
+#ifdef Q_OS_WIN
+ #include <tlhelp32.h>
+ #include <string>
+#endif
+
+#ifdef Q_OS_LINUX
+ #include <QtCore/QStringList>
+#endif
+
+PluginManager::PluginManager(QSet<QString> *additionalSearchPaths, QObject *p)
+ : QObject(p),
+ m_pluginCollectionLock(QReadWriteLock::NonRecursive),
+ m_pluginHashMap(),
+ m_positionalData(),
+ m_positionalDataCheckTimer(),
+ m_sentDataMutex(),
+ m_sentData(),
+ m_activePosDataPluginLock(QReadWriteLock::NonRecursive),
+ m_activePositionalDataPlugin(),
+ m_updater() {
+
+ // Setup search-paths
+ if (additionalSearchPaths) {
+ for (const auto &currentPath : *additionalSearchPaths) {
+ m_pluginSearchPaths.insert(currentPath);
+ }
+ }
+
+#ifdef Q_OS_MAC
+ // Path to plugins inside AppBundle
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath()));
+#endif
+
+#ifdef MUMBLE_PLUGIN_PATH
+ // Path to where plugins are/will be installed on the system
+ m_pluginSearchPaths.insert(QString::fromLatin1(MUMTEXT(MUMBLE_PLUGIN_PATH)));
+#endif
+
+ // Path to "plugins" dir right next to the executable's location. This is the case for when Mumble
+ // is run after compilation without having installed it anywhere special
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()));
+
+ // Path to where the plugin installer will write plugins
+ m_pluginSearchPaths.insert(PluginInstaller::getInstallDir());
+
+#ifdef Q_OS_WIN
+ // According to MS KB Q131065, we need this to OpenProcess()
+
+ m_hToken = nullptr;
+
+ if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken)) {
+ if (GetLastError() == ERROR_NO_TOKEN) {
+ ImpersonateSelf(SecurityImpersonation);
+ OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken);
+ }
+ }
+
+ TOKEN_PRIVILEGES tp;
+ LUID luid;
+ m_cbPrevious=sizeof(TOKEN_PRIVILEGES);
+
+ LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
+
+ tp.PrivilegeCount = 1;
+ tp.Privileges[0].Luid = luid;
+ tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
+
+ AdjustTokenPrivileges(m_hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &m_tpPrevious, &m_cbPrevious);
+#endif
+
+ // Synchronize the positional data in a regular interval
+ // By making this the parent of the created timer, we don't have to delete it explicitly
+ QTimer *serverSyncTimer = new QTimer(this);
+ QObject::connect(serverSyncTimer, &QTimer::timeout, this, &PluginManager::on_syncPositionalData);
+ serverSyncTimer->start(POSITIONAL_SERVER_SYNC_INTERVAL);
+
+ // Install this manager as a global eventFilter in order to get notified about all keypresses
+ if (QCoreApplication::instance()) {
+ QCoreApplication::instance()->installEventFilter(this);
+ }
+
+ // Set up the timer for regularly checking for available positional data plugins
+ m_positionalDataCheckTimer.setInterval(POSITIONAL_DATA_CHECK_INTERVAL);
+ m_positionalDataCheckTimer.start();
+ QObject::connect(&m_positionalDataCheckTimer, &QTimer::timeout, this, &PluginManager::checkForAvailablePositionalDataPlugin);
+
+ QObject::connect(&m_updater, &PluginUpdater::updatesAvailable, this, &PluginManager::on_updatesAvailable);
+ QObject::connect(this, &PluginManager::keyEvent, this, &PluginManager::on_keyEvent);
+}
+
+PluginManager::~PluginManager() {
+ clearPlugins();
+
+#ifdef Q_OS_WIN
+ AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL);
+ CloseHandle(m_hToken);
+#endif
+}
+
+/// Emits a log about a plugin with the given name having lost link (positional audio)
+///
+/// @param pluginName The name of the plugin that lost link
+void reportLostLink(const QString& pluginName) {
+ Global::get().l->log(Log::Information, PluginManager::tr("%1 lost link").arg(pluginName.toHtmlEscaped()));
+}
+
+bool PluginManager::eventFilter(QObject *target, QEvent *event) {
+ if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
+ static QVector<QKeyEvent*> processedEvents;
+
+ QKeyEvent *kEvent = static_cast<QKeyEvent *>(event);
+
+ // We have to keep track of which events we have processed already as
+ // the same event might be sent to multiple targets and since this is
+ // installed as a global event filter, we get notified about each of
+ // them. However we want to process each event only once.
+ if (!kEvent->isAutoRepeat() && !processedEvents.contains(kEvent)) {
+ // Fire event
+ emit keyEvent(kEvent->key(), kEvent->modifiers(), kEvent->type() == QEvent::KeyPress);
+
+ processedEvents << kEvent;
+
+ if (processedEvents.size() == 1) {
+ // Make sure to clear the list of processed events after each iteration
+ // of the event loop (we don't want to let the vector grow to infinity
+ // over time. Firing the timer only when the size of processedEvents is
+ // exactly 1, we avoid adding multiple timers in a single iteration.
+ QTimer::singleShot(0, []() { processedEvents.clear(); });
+ }
+ }
+
+ }
+
+ // standard event processing
+ return QObject::eventFilter(target, event);
+}
+
+void PluginManager::unloadPlugins() const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ auto it = m_pluginHashMap.begin();
+
+ while (it != m_pluginHashMap.end()) {
+ unloadPlugin(*it.value());
+ it++;
+ }
+}
+
+void PluginManager::clearPlugins() {
+ // Unload plugins so that they aren't implicitly unloaded once they go out of scope after having been
+ // removed from the pluginHashMap.
+ // This could lead to one of the plugins making an API call in its shutdown function which then would try
+ // to verify the plugin's ID. For that it'll ask this PluginManager for a plugin with that ID. To check
+ // that it will have to aquire a read-lock for the pluginHashMap which is impossible after we aquire the
+ // write-lock in this function leading to a deadlock.
+ unloadPlugins();
+
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // Clear the list itself
+ m_pluginHashMap.clear();
+}
+
+bool PluginManager::selectActivePositionalDataPlugin() {
+ QReadLocker pluginLock(&m_pluginCollectionLock);
+ QWriteLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!Global::get().s.bTransmitPosition) {
+ // According to the settings the position shall not be transmitted meaning that we don't have to select any plugin
+ // for positional data
+ m_activePositionalDataPlugin = nullptr;
+
+ return false;
+ }
+
+ ProcessResolver procRes(true);
+
+ auto it = m_pluginHashMap.begin();
+
+ // We assume that there is only one (enabled) plugin for the currently played game so we don't have to remember
+ // which plugin was active last
+ while (it != m_pluginHashMap.end()) {
+ plugin_ptr_t currentPlugin = it.value();
+
+ if (currentPlugin->isPositionalDataEnabled() && currentPlugin->isLoaded()) {
+ switch(currentPlugin->initPositionalData(procRes.getProcessNames().data(),
+ procRes.getProcessPIDs().data(), procRes.amountOfProcesses())) {
+ case PDEC_OK:
+ // the plugin is ready to provide positional data
+ m_activePositionalDataPlugin = currentPlugin;
+
+ Global::get().l->log(Log::Information, tr("%1 linked").arg(currentPlugin->getName().toHtmlEscaped()));
+
+ return true;
+
+ case PDEC_ERROR_PERM:
+ // the plugin encountered a permanent error -> disable it
+ Global::get().l->log(Log::Warning, tr(
+ "Plugin \"%1\" encountered a permanent error in positional data gathering").arg(currentPlugin->getName()));
+
+ currentPlugin->enablePositionalData(false);
+ break;
+
+ case PDEC_ERROR_TEMP:
+ //The plugin encountered a temporary error -> skip it for now (that is: do nothing)
+ break;
+ }
+ }
+
+ it++;
+ }
+
+ m_activePositionalDataPlugin = nullptr;
+
+ return false;
+}
+
+#define LOG_FOUND(plugin, path, legacyStr) qDebug("Found %splugin '%s' at \"%s\"", legacyStr, qUtf8Printable(plugin->getName()), qUtf8Printable(path));\
+ qDebug() << "Its description:" << qUtf8Printable(plugin->getDescription())
+#define LOG_FOUND_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "")
+#define LOG_FOUND_LEGACY_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "legacy ")
+#define LOG_FOUND_BUILTIN(plugin) LOG_FOUND(plugin, QString::fromLatin1("<builtin>"), "built-in ")
+void PluginManager::rescanPlugins() {
+ clearPlugins();
+
+ {
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // iterate over all files in the respective directories and try to construct a plugin from them
+ for (const auto &currentPath : m_pluginSearchPaths) {
+ QFileInfoList currentList = QDir(currentPath).entryInfoList();
+
+ for (int k=0; k<currentList.size(); k++) {
+ QFileInfo currentInfo = currentList[k];
+
+ if (!QLibrary::isLibrary(currentInfo.absoluteFilePath())) {
+ // consider only files that actually could be libraries
+ continue;
+ }
+
+ try {
+ plugin_ptr_t p(Plugin::createNew<Plugin>(currentInfo.absoluteFilePath()));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_PLUGIN(p, currentInfo.absoluteFilePath());
+#endif
+
+ // if this code block is reached, the plugin was instantiated successfully so we can add it to the map
+ m_pluginHashMap.insert(p->getID(), p);
+ } catch(const PluginError& e) {
+ Q_UNUSED(e);
+ // If an exception is thrown, this library does not represent a proper plugin
+ // Check if it might be a legacy plugin instead
+ try {
+ legacy_plugin_ptr_t lp(Plugin::createNew<LegacyPlugin>(currentInfo.absoluteFilePath()));
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_LEGACY_PLUGIN(lp, currentInfo.absoluteFilePath());
+#endif
+ m_pluginHashMap.insert(lp->getID(), lp);
+ } catch(const PluginError& e) {
+ Q_UNUSED(e);
+
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning,
+ tr("Non-plugin found in plugin directory: \"%1\"").arg(currentInfo.absoluteFilePath()));
+ }
+ }
+ }
+ }
+
+ // handle built-in plugins
+#ifdef USE_MANUAL_PLUGIN
+ try {
+ std::shared_ptr<ManualPlugin> mp(Plugin::createNew<ManualPlugin>());
+
+ m_pluginHashMap.insert(mp->getID(), mp);
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_BUILTIN(mp);
+#endif
+ } catch(const PluginError& e) {
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning, tr("Failed at loading manual plugin: %1").arg(QString::fromUtf8(e.what())));
+ }
+#endif
+ }
+
+ QReadLocker readLock(&m_pluginCollectionLock);
+
+ // load plugins based on settings
+ // iterate over all plugins that have saved settings
+ auto it = Global::get().s.qhPluginSettings.constBegin();
+ while (it != Global::get().s.qhPluginSettings.constEnd()) {
+ // for this we need a way to get a plugin based on the filepath
+ const QString pluginKey = it.key();
+ const PluginSetting setting = it.value();
+
+ // iterate over all loaded plugins to see if the current setting is applicable
+ auto pluginIt = m_pluginHashMap.begin();
+ while (pluginIt != m_pluginHashMap.end()) {
+ plugin_ptr_t plugin = pluginIt.value();
+ QString pluginHash = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex());
+ if (pluginKey == pluginHash) {
+ if (setting.enabled) {
+ loadPlugin(plugin->getID());
+
+ const uint32_t features = plugin->getFeatures();
+
+ if (!setting.positionalDataEnabled && (features & FEATURE_POSITIONAL)) {
+ // try to deactivate the feature if the setting says so
+ plugin->deactivateFeatures(FEATURE_POSITIONAL);
+ }
+ }
+
+ // positional data is a special feature that has to be enabled/disabled in the Plugin wrapper class
+ // additionally to telling the plugin library that the feature shall be deactivated
+ plugin->enablePositionalData(setting.positionalDataEnabled);
+
+ plugin->allowKeyboardMonitoring(setting.allowKeyboardMonitoring);
+
+ break;
+ }
+
+ pluginIt++;
+ }
+
+ it++;
+ }
+}
+
+const_plugin_ptr_t PluginManager::getPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.value(pluginID);
+}
+
+void PluginManager::checkForPluginUpdates() {
+ m_updater.checkForUpdates();
+}
+
+bool PluginManager::fetchPositionalData() {
+ if (Global::get().bPosTest) {
+ // This is for testing-purposes only so the "fetched" position doesn't have any real meaning
+ m_positionalData.reset();
+
+ m_positionalData.m_playerDir.z = 1.0f;
+ m_positionalData.m_playerAxis.y = 1.0f;
+ m_positionalData.m_cameraDir.z = 1.0f;
+ m_positionalData.m_cameraAxis.y = 1.0f;
+
+ return true;
+ }
+
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!m_activePositionalDataPlugin) {
+ // It appears as if there is currently no plugin capable of delivering positional audio
+ // Set positional data to zero-values
+ m_positionalData.reset();
+
+ return false;
+ }
+
+ QWriteLocker posDataLock(&m_positionalData.m_lock);
+
+ bool retStatus = m_activePositionalDataPlugin->fetchPositionalData(m_positionalData.m_playerPos, m_positionalData.m_playerDir,
+ m_positionalData.m_playerAxis, m_positionalData.m_cameraPos, m_positionalData.m_cameraDir, m_positionalData.m_cameraAxis,
+ m_positionalData.m_context, m_positionalData.m_identity);
+
+ // Add the plugin's name to the context as well to prevent name-clashes between plugins
+ if (!m_positionalData.m_context.isEmpty()) {
+ m_positionalData.m_context = m_activePositionalDataPlugin->getName() + QChar::Null + m_positionalData.m_context;
+ }
+
+ if (!retStatus) {
+ // Shut the currently active plugin down and set a new one (if available)
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // unlock the read-lock in order to allow selectActivePositionaldataPlugin to gain a write-lock
+ activePluginLock.unlock();
+
+ selectActivePositionalDataPlugin();
+ } else {
+ // If the return-status doesn't indicate an error, we can assume that positional data is available
+ // The remaining problematic case is, if the player is exactly at position (0,0,0) as this is used as an indicator for the
+ // absence of positional data in the mix() function in AudioOutput.cpp
+ // Thus we have to make sure that this position is never set if positional data is actually available.
+ // We solve this problem by shifting the player a minimal amount on the z-axis
+ if (m_positionalData.m_playerPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_playerPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ if (m_positionalData.m_cameraPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_cameraPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ }
+
+ return retStatus;
+}
+
+void PluginManager::unlinkPositionalData() {
+ QWriteLocker lock(&m_activePosDataPluginLock);
+
+ if (m_activePositionalDataPlugin) {
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // Set the pointer to nullptr
+ m_activePositionalDataPlugin = nullptr;
+ }
+}
+
+bool PluginManager::isPositionalDataAvailable() const {
+ QReadLocker lock(&m_activePosDataPluginLock);
+
+ return m_activePositionalDataPlugin != nullptr;
+}
+
+const PositionalData& PluginManager::getPositionalData() const {
+ return m_positionalData;
+}
+
+void PluginManager::enablePositionalDataFor(plugin_id_t pluginID, bool enable) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ plugin->enablePositionalData(enable);
+ }
+}
+
+const QVector<const_plugin_ptr_t > PluginManager::getPlugins(bool sorted) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ QVector<const_plugin_ptr_t> pluginList;
+
+ auto it = m_pluginHashMap.constBegin();
+ if (sorted) {
+ QList<plugin_id_t> ids = m_pluginHashMap.keys();
+
+ // sort keys so that the corresponding Plugins are in alphabetical order based on their name
+ std::sort(ids.begin(), ids.end(), [this](plugin_id_t first, plugin_id_t second) {
+ return QString::compare(m_pluginHashMap.value(first)->getName(), m_pluginHashMap.value(second)->getName(),
+ Qt::CaseInsensitive) <= 0;
+ });
+
+ foreach(plugin_id_t currentID, ids) {
+ pluginList.append(m_pluginHashMap.value(currentID));
+ }
+ } else {
+ while (it != m_pluginHashMap.constEnd()) {
+ pluginList.append(it.value());
+
+ it++;
+ }
+ }
+
+ return pluginList;
+}
+
+bool PluginManager::loadPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ if (plugin->isLoaded()) {
+ // Don't attempt to load a plugin if it already is loaded.
+ // This can happen if the user clicks the apply button in the settings
+ // before hitting ok.
+ return true;
+ }
+
+ return plugin->init() == STATUS_OK;
+ }
+
+ return false;
+}
+
+void PluginManager::unloadPlugin(plugin_id_t pluginID) const {
+ plugin_ptr_t plugin;
+ {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin = m_pluginHashMap.value(pluginID);
+ }
+
+ if (plugin) {
+ unloadPlugin(*plugin);
+ }
+}
+
+void PluginManager::unloadPlugin(Plugin &plugin) const {
+ if (plugin.isLoaded()) {
+ // Only shut down loaded plugins
+ plugin.shutdown();
+ }
+}
+
+bool PluginManager::clearPlugin(plugin_id_t pluginID) {
+ // We have to unload the plugin before we take the write lock. The reasoning being that if
+ // the plugin makes an API call in its shutdown callback, that'll lead to this manager being
+ // asked for whether a plugin with such an ID exists. The function performing this check will
+ // take a read lock which is not possible if we hold a write lock here already (deadlock).
+ unloadPlugin(pluginID);
+
+ QWriteLocker lock(&m_pluginCollectionLock);
+
+ // Remove the plugin from the list of known plugins
+ plugin_ptr_t plugin = m_pluginHashMap.take(pluginID);
+
+ return plugin != nullptr;
+}
+
+uint32_t PluginManager::deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ return plugin->deactivateFeatures(features);
+ }
+
+ return FEATURE_NONE;
+}
+
+void PluginManager::allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ plugin_ptr_t plugin = m_pluginHashMap.value(pluginID);
+
+ if (plugin) {
+ return plugin->allowKeyboardMonitoring(allow);
+ }
+}
+
+bool PluginManager::pluginExists(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.contains(pluginID);
+}
+
+void PluginManager::foreachPlugin(std::function<void(Plugin&)> pluginProcessor) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ auto it = m_pluginHashMap.constBegin();
+
+ while (it != m_pluginHashMap.constEnd()) {
+ pluginProcessor(*it.value());
+
+ it++;
+ }
+}
+
+void PluginManager::on_serverConnected() const {
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug("PluginManager: Connected to a server with connection ID %d", connectionID);
+#endif
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerConnected(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_serverDisconnected() const {
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug("PluginManager: Disconnected from a server with connection ID %d", connectionID);
+#endif
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerDisconnected(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: User" << user->qsName << "entered channel" << newChannel->qsName << "- ID:" << newChannel->iId;
+#endif
+
+ if (!Global::get().sh) {
+ // if there is no server-handler, there is no (real) channel to enter
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, newChannel, prevChannel, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelEntered(connectionID, user->uiSession, prevChannel ? prevChannel->iId : -1, newChannel->iId);
+ }
+ });
+}
+
+void PluginManager::on_channelExited(const Channel *channel, const User *user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: User" << user->qsName << "left channel" << channel->qsName << "- ID:" << channel->iId;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, channel, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelExited(connectionID, user->uiSession, channel->iId);
+ }
+ });
+}
+
+QString getTalkingStateStr(Settings::TalkState ts) {
+ switch(ts) {
+ case Settings::TalkState::Passive:
+ return QString::fromLatin1("Passive");
+ case Settings::TalkState::Talking:
+ return QString::fromLatin1("Talking");
+ case Settings::TalkState::Whispering:
+ return QString::fromLatin1("Whispering");
+ case Settings::TalkState::Shouting:
+ return QString::fromLatin1("Shouting");
+ case Settings::TalkState::MutedTalking:
+ return QString::fromLatin1("MutedTalking");
+ }
+
+ return QString::fromLatin1("Unknown");
+}
+
+void PluginManager::on_userTalkingStateChanged() const {
+ const ClientUser *user = qobject_cast<ClientUser*>(QObject::sender());
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ if (user) {
+ qDebug() << "PluginManager: User" << user->qsName << "changed talking state to" << getTalkingStateStr(user->tsState);
+ } else {
+ qCritical() << "PluginManager: Unable to identify ClientUser";
+ }
+#endif
+
+ if (user) {
+ // Convert Mumble's talking state to the TalkingState used in the API
+ mumble_talking_state_t ts = INVALID;
+
+ switch(user->tsState) {
+ case Settings::TalkState::Passive:
+ ts = PASSIVE;
+ break;
+ case Settings::TalkState::Talking:
+ ts = TALKING;
+ break;
+ case Settings::TalkState::Whispering:
+ ts = WHISPERING;
+ break;
+ case Settings::TalkState::Shouting:
+ ts = SHOUTING;
+ break;
+ case Settings::TalkState::MutedTalking:
+ ts = TALKING_MUTED;
+ break;
+ }
+
+ if (ts == INVALID) {
+ qWarning("PluginManager.cpp: Invalid talking state encountered");
+ // An error occured
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, ts, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserTalkingStateChanged(connectionID, user->uiSession, ts);
+ }
+ });
+ }
+}
+
+void PluginManager::on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioInput with" << channelCount << "channels and" << sampleCount << "samples per channel. IsSpeech:" << isSpeech;
+#endif
+
+ foreachPlugin([inputPCM, sampleCount, channelCount, sampleRate, isSpeech](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech);
+ }
+ });
+}
+
+void PluginManager::on_audioSourceFetched(float* outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser* user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioSource with" << channelCount << "channels and" << sampleCount << "samples per channel fetched. IsSpeech:" << isSpeech;
+ if (user != nullptr) {
+ qDebug() << "Sender-ID:" << user->uiSession;
+ }
+#endif
+
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user ? user->uiSession : -1);
+ }
+ });
+}
+
+void PluginManager::on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioOutput with" << channelCount << "channels and" << sampleCount << "samples per channel";
+#endif
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, modifiedAudio](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ if(plugin.onAudioOutputAboutToPlay(outputPCM, sampleCount, sampleRate, channelCount)) {
+ *modifiedAudio = true;
+ }
+ }
+ });
+}
+
+void PluginManager::on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Data with ID" << dataID << "and length" << dataLength << "received. Sender-ID:" << sender->uiSession;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([sender, data, dataLength, dataID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onReceiveData(connectionID, sender->uiSession, data, dataLength, dataID);
+ }
+ });
+}
+
+void PluginManager::on_serverSynchronized() const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Server synchronized";
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerSynchronized(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_userAdded(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserAdded(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_userRemoved(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserRemoved(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_channelAdded(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelAdded(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRemoved(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRemoved(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRenamed(int channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Renamed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRenamed(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Key event detected: keyCode =" << key << "modifiers:"
+ << modifiers << "isPress =" << isPress;
+#else
+ Q_UNUSED(modifiers);
+#endif
+
+ // Convert from Qt encoding to our own encoding
+ mumble_keycode_t keyCode = API::qtKeyCodeToAPIKeyCode(key);
+
+ foreachPlugin([keyCode, isPress](Plugin &plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onKeyEvent(keyCode, isPress);
+ }
+ });
+}
+
+void PluginManager::on_syncPositionalData() {
+ // fetch positional data
+ if (fetchPositionalData()) {
+ // Sync the gathered data (context + identity) with the server
+ if (!Global::get().uiSession) {
+ // For some reason the local session ID is not set -> clear all data sent to the server in order to gurantee
+ // a re-send once the session is restored and there is data available
+ QMutexLocker mLock(&m_sentDataMutex);
+
+ m_sentData.context.clear();
+ m_sentData.identity.clear();
+ } else {
+ // Check if the identity and/or the context has changed and if it did, send that new info to the server
+ QMutexLocker mLock(&m_sentDataMutex);
+ QReadLocker rLock(&m_positionalData.m_lock);
+
+ if (m_sentData.context != m_positionalData.m_context || m_sentData.identity != m_positionalData.m_identity ) {
+ MumbleProto::UserState mpus;
+ mpus.set_session(Global::get().uiSession);
+
+ if (m_sentData.context != m_positionalData.m_context) {
+ m_sentData.context = m_positionalData.m_context;
+ mpus.set_plugin_context(m_sentData.context.toUtf8().constData(), m_sentData.context.size());
+ }
+ if (m_sentData.identity != m_positionalData.m_identity) {
+ m_sentData.identity = m_positionalData.m_identity;
+ mpus.set_plugin_identity(m_sentData.identity.toUtf8().constData());
+ }
+
+ if (Global::get().sh) {
+ // send the message if the serverHandler is available
+ Global::get().sh->sendMessage(mpus);
+ }
+ }
+ }
+ }
+}
+
+void PluginManager::on_updatesAvailable() {
+ if (Global::get().s.bPluginAutoUpdate) {
+ m_updater.update();
+ } else {
+ m_updater.promptAndUpdate();
+ }
+}
+
+void PluginManager::checkForAvailablePositionalDataPlugin() {
+ bool performSearch = false;
+ {
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ performSearch = !m_activePositionalDataPlugin;
+ }
+
+ if (performSearch) {
+ selectActivePositionalDataPlugin();
+ }
+}