diff options
Diffstat (limited to 'src/mumble/PluginManager.cpp')
-rw-r--r-- | src/mumble/PluginManager.cpp | 933 |
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 ¤tPath : *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 ¤tPath : 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(); + } +} |