// 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 . #include #include "LegacyPlugin.h" #include "PluginManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "API.h" #include "Log.h" #include "ManualPlugin.h" #include "PluginInstaller.h" #include "PluginUpdater.h" #include "ProcessResolver.h" #include "ServerHandler.h" #include "Global.h" #include #ifdef Q_OS_WIN # include # include #endif #ifdef Q_OS_LINUX # include #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(""), "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(); } }