mumble-voip_mumble/src/mumble/PluginManager.cpp

1030 lines
33 KiB
C++

// Copyright 2021-2023 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 "LegacyPlugin.h"
#include "PluginManager.h"
#include <QByteArray>
#include <QChar>
#include <QDir>
#include <QFileInfo>
#include <QFileInfoList>
#include <QHashIterator>
#include <QKeyEvent>
#include <QMutexLocker>
#include <QReadLocker>
#include <QTimer>
#include <QVector>
#include <QWriteLocker>
#include "API.h"
#include "Log.h"
#include "PluginInstaller.h"
#include "PluginUpdater.h"
#include "ProcessResolver.h"
#include "ServerHandler.h"
#include "Global.h"
#ifdef USE_MANUAL_PLUGIN
# include "ManualPlugin.h"
#endif
#include <cassert>
#include <cstdint>
#include <memory>
#include <vector>
#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() {
qRegisterMetaType< mumble_plugin_id_t >("mumble_plugin_id_t");
std::vector< QString > pluginPaths;
// Setup search-paths
if (additionalSearchPaths) {
pluginPaths.insert(pluginPaths.end(), additionalSearchPaths->begin(), additionalSearchPaths->end());
}
#ifdef Q_OS_MAC
// Path to plugins inside AppBundle
pluginPaths.push_back(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath()));
#endif
#ifdef MUMBLE_PLUGIN_PATH
// Path to where plugins are/will be installed on the system
pluginPaths.push_back(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
pluginPaths.push_back(
QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()));
// Path to where the plugin installer will write plugins
pluginPaths.push_back(PluginInstaller::getInstallDir());
for (const QString &currentPath : pluginPaths) {
// Transform currentPath to an absolute, canonical path and only then add it to m_pluginSearchPaths in order
// to ensure that each path is contained only once.
QDir dir(currentPath);
if (dir.exists()) {
m_pluginSearchPaths.insert(dir.canonicalPath());
}
}
#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);
QObject::connect(this, &PluginManager::pluginLostLink, this, &PluginManager::reportLostLink);
QObject::connect(this, &PluginManager::pluginLinked, this, &PluginManager::reportPluginLinked);
QObject::connect(this, &PluginManager::pluginEncounteredPermanentError, this, &PluginManager::reportPermanentError);
}
PluginManager::~PluginManager() {
clearPlugins();
#ifdef Q_OS_WIN
AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL);
CloseHandle(m_hToken);
#endif
}
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(static_cast< unsigned int >(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 acquire a read-lock for the pluginHashMap which is impossible after we acquire 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;
}
const ProcessResolver procRes(true);
const ProcessResolver::ProcessMap &map = procRes.getProcessMap();
// We require 2 separate arrays holding the names and the PIDs -> create them from the given map
std::vector< uint64_t > pids;
std::vector< const char * > names;
pids.reserve(procRes.amountOfProcesses());
names.reserve(procRes.amountOfProcesses());
for (const std::pair< const uint64_t, std::unique_ptr< char[] > > &currentEntry : map) {
pids.push_back(currentEntry.first);
names.push_back(currentEntry.second.get());
}
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(names.data(), pids.data(), procRes.amountOfProcesses())) {
case MUMBLE_PDEC_OK:
// the plugin is ready to provide positional data
m_activePositionalDataPlugin = currentPlugin;
emit pluginLinked(currentPlugin->getID());
return true;
case MUMBLE_PDEC_ERROR_PERM:
// the plugin encountered a permanent error -> disable it
emit pluginEncounteredPermanentError(currentPlugin->getID());
currentPlugin->enablePositionalData(false);
break;
case MUMBLE_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) {
// 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 &el) {
Q_UNUSED(el);
// 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\" (%2)")
.arg(currentInfo.absoluteFilePath())
.arg(QString::fromUtf8(e.what())));
}
}
}
}
// 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 & MUMBLE_FEATURE_POSITIONAL)) {
// try to deactivate the feature if the setting says so
plugin->deactivateFeatures(MUMBLE_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);
if (!m_positionalData.m_context.isEmpty()) {
m_positionalData.m_context =
m_activePositionalDataPlugin->getPositionalDataContextPrefix() + QChar::Null + m_positionalData.m_context;
}
if (!retStatus) {
// We won't be making changes to the positional data anymore, so we can drop the lock
posDataLock.unlock();
// Shut the currently active plugin down and set a new one (if available)
m_activePositionalDataPlugin->shutdownPositionalData();
emit pluginLostLink(m_activePositionalDataPlugin->getID());
// 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();
emit pluginLostLink(m_activePositionalDataPlugin->getID());
// 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() == MUMBLE_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 MUMBLE_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) {
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 ? static_cast< int >(prevChannel->iId) : -1,
static_cast< int >(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, static_cast< int >(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 = MUMBLE_TS_INVALID;
switch (user->tsState) {
case Settings::TalkState::Passive:
ts = MUMBLE_TS_PASSIVE;
break;
case Settings::TalkState::Talking:
ts = MUMBLE_TS_TALKING;
break;
case Settings::TalkState::Whispering:
ts = MUMBLE_TS_WHISPERING;
break;
case Settings::TalkState::Shouting:
ts = MUMBLE_TS_SHOUTING;
break;
case Settings::TalkState::MutedTalking:
ts = MUMBLE_TS_TALKING_MUTED;
break;
}
if (ts == MUMBLE_TS_INVALID) {
qWarning("PluginManager.cpp: Invalid talking state encountered");
// An error occurred
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, static_cast< std::uint16_t >(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, static_cast< std::uint16_t >(channelCount), sampleRate,
isSpeech, user ? user->uiSession : static_cast< unsigned int >(-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, static_cast< std::uint16_t >(channelCount),
sampleRate)) {
*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
// guarantee 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(),
static_cast< std::size_t >(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);
}
}
}
} else {
QMutexLocker mLock(&m_sentDataMutex);
if (!m_sentData.identity.isEmpty() || !m_sentData.context.isEmpty()) {
// The server has been sent non-empty identity and/or context but we are now no longer able to fetch
// positional data. That means that the respective plugin has been unlinked and thus we want to clear the
// identity and context set on the server.
MumbleProto::UserState mpus;
mpus.set_plugin_context("");
mpus.set_plugin_identity("");
if (Global::get().sh) {
Global::get().sh->sendMessage(mpus);
}
m_sentData.identity.clear();
m_sentData.context.clear();
}
}
}
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();
}
}
void PluginManager::reportLostLink(mumble_plugin_id_t pluginID) {
// We are calling GUI code, so we must only execute this function from the GUI (main) thread - which we assume is
// where the plugin manager object is living in.
assert(this->thread() == QThread::currentThread());
const_plugin_ptr_t plugin = getPlugin(pluginID);
if (plugin) {
Global::get().l->log(Log::Information,
PluginManager::tr("%1 lost link").arg(plugin->getName().toHtmlEscaped()));
}
}
void PluginManager::reportPluginLinked(mumble_plugin_id_t pluginID) {
// We are calling GUI code, so we must only execute this function from the GUI (main) thread - which we assume is
// where the plugin manager object is living in.
assert(this->thread() == QThread::currentThread());
const_plugin_ptr_t plugin = getPlugin(pluginID);
if (plugin) {
Global::get().l->log(Log::Information, tr("%1 linked").arg(plugin->getName().toHtmlEscaped()));
}
}
void PluginManager::reportPermanentError(mumble_plugin_id_t pluginID) {
// We are calling GUI code, so we must only execute this function from the GUI (main) thread - which we assume is
// where the plugin manager object is living in.
assert(this->thread() == QThread::currentThread());
const_plugin_ptr_t plugin = getPlugin(pluginID);
if (plugin) {
Global::get().l->log(
Log::Warning,
tr("Plugin \"%1\" encountered a permanent error in positional data gathering").arg(plugin->getName()));
}
}