diff --git a/src/ipc.cpp b/src/ipc.cpp index b7aedf533..29c4178bc 100644 --- a/src/ipc.cpp +++ b/src/ipc.cpp @@ -16,37 +16,39 @@ #include "src/ipc.h" +#include "src/misc/settings.h" #include #include +#include -IPC::IPC() : - globalMemory{"qtox"} + +IPC::IPC() + : globalMemory{"qtox-" IPC_PROTOCOL_VERSION} { qRegisterMetaType("IPCEventHandler"); - ownerTimer.setInterval(EVENT_TIMER_MS); - ownerTimer.setSingleShot(true); - connect(&ownerTimer, &QTimer::timeout, this, &IPC::processEvents); + timer.setInterval(EVENT_TIMER_MS); + timer.setSingleShot(true); + connect(&timer, &QTimer::timeout, this, &IPC::processEvents); // The first started instance gets to manage the shared memory by taking ownership - // Every time it processes events it updates the global shared timestamp + // Every time it processes events it updates the global shared timestamp "lastProcessed" // If the timestamp isn't updated, that's a timeout and someone else can take ownership // This is a safety measure, in case one of the clients crashes // If the owner exits normally, it can set the timestamp to 0 first to immediately give ownership - // We keep one shared page, starting with the 64bit ID of the current owner and last timestamp - // then various events to be processed by the owner with a 16bit size then data each - // Each event is in its own chunk of data, the last chunk is followed by a chunk of size 0 qsrand(time(0)); globalId = ((uint64_t)qrand()) * ((uint64_t)qrand()) * ((uint64_t)qrand()); - qDebug() << "IPC: Our global ID is "<globalId = globalId; + mem->lastProcessed = time(0); globalMemory.unlock(); } else @@ -64,18 +66,59 @@ IPC::IPC() : return; // We won't be able to do any IPC without being attached, let's get outta here } - ownerTimer.start(); + timer.start(); } IPC::~IPC() { + if (isCurrentOwner()) + { + if (globalMemory.lock()) + { + global()->globalId = 0; + globalMemory.unlock(); + } + } +} + +time_t IPC::postEvent(const QString &name, const QByteArray& data/*=QByteArray()*/, uint32_t dest/*=0*/) +{ + QByteArray binName = name.toUtf8(); + if (binName.length() > (int32_t)sizeof(IPCEvent::name)) + return 0; + + if (data.length() > (int32_t)sizeof(IPCEvent::data)) + return 0; + if (globalMemory.lock()) { - char* data = (char*)globalMemory.data(); - if (data) - *(time_t*)(data+sizeof(globalId)) = 0; + IPCEvent* evt = 0; + IPCMemory* mem = global(); + time_t result = 0; + + for (uint32_t i = 0; !evt && i < EVENT_QUEUE_SIZE; i++) + { + if (mem->events[i].posted == 0) + evt = &mem->events[i]; + } + + if (evt) + { + memset(evt, 0, sizeof(IPCEvent)); + memcpy(evt->name, binName.constData(), binName.length()); + memcpy(evt->data, data.constData(), data.length()); + mem->lastEvent = evt->posted = result = qMax(mem->lastEvent + 1, time(0)); + evt->dest = dest; + evt->sender = getpid(); + qDebug() << "IPC: postEvent " << name << "to" << dest; + } globalMemory.unlock(); + return result; } + else + qDebug() << "IPC: Failed to lock in postEvent()"; + + return 0; } bool IPC::isCurrentOwner() @@ -88,190 +131,168 @@ bool IPC::isCurrentOwner() } else { - qWarning() << "IPC:isCurrentOwner failed to lock, returning false"; + qWarning() << "IPC: isCurrentOwner failed to lock, returning false"; return false; } } -void IPC::registerEventHandler(IPCEventHandler handler) +void IPC::registerEventHandler(const QString &name, IPCEventHandler handler) { - eventHandlers += handler; + eventHandlers[name] = handler; +} + +bool IPC::isEventProcessed(time_t time) +{ + bool result = false; + if (globalMemory.lock()) + { + if (difftime(global()->lastProcessed, time) > 0) + { + IPCMemory* mem = global(); + for (uint32_t i = 0; i < EVENT_QUEUE_SIZE; i++) + { + if (mem->events[i].posted == time && mem->events[i].processed) + { + result = true; + break; + } + } + } + globalMemory.unlock(); + } + else + { + qWarning() << "IPC: isEventProcessed failed to lock, returning false"; + } + return result; +} + +bool IPC::isEventAccepted(time_t time) +{ + bool result = false; + if (globalMemory.lock()) + { + // if (difftime(global()->lastProcessed, time) > 0) + { + IPCMemory* mem = global(); + for (uint32_t i = 0; i < EVENT_QUEUE_SIZE; i++) + { + if (mem->events[i].posted == time) + { + result = mem->events[i].accepted; + break; + } + } + } + globalMemory.unlock(); + } + else + { + qWarning() << "IPC: isEventAccepted failed to lock, returning false"; + } + return result; +} + +bool IPC::waitUntilProcessed(time_t postTime, int32_t timeout/*=-1*/) +{ + bool result = false; + time_t start = time(0); + while (!(result = isEventProcessed(postTime))) + { + qApp->processEvents(); + if (timeout > 0 && difftime(time(0), start) >= timeout) + break; + } + return result; +} + +IPC::IPCEvent *IPC::fetchEvent() +{ + IPCMemory* mem = global(); + for (uint32_t i = 0; i < EVENT_QUEUE_SIZE; i++) + { + IPCEvent* evt = &mem->events[i]; + + // Garbage-collect events that were not processed in EVENT_GC_TIMEOUT + // and events that were processed and EVENT_GC_TIMEOUT passed after + // so sending instance has time to react to those events. + if ((evt->processed && difftime(time(0), evt->processed) > EVENT_GC_TIMEOUT) || + (!evt->processed && difftime(time(0), evt->posted) > EVENT_GC_TIMEOUT)) + memset(evt, 0, sizeof(IPCEvent)); + + if (evt->posted && !evt->processed && evt->sender != getpid()) + { + if (evt->dest == Settings::getInstance().getCurrentProfileId() || (evt->dest == 0 && isCurrentOwner())) + { + evt->processed = time(0); + return evt; + } + } + } + return 0; +} + +bool IPC::runEventHandler(IPCEventHandler handler, const QByteArray& arg) +{ + bool result = false; + if (QThread::currentThread() != qApp->thread()) + { + QMetaObject::invokeMethod(this, "runEventHandler", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, result), + Q_ARG(IPCEventHandler, handler), + Q_ARG(const QByteArray&, arg)); + return result; + } + else + { + result = handler(arg); + return result; + } } void IPC::processEvents() { if (globalMemory.lock()) { - lastSeenTimestamp = getGlobalTimestamp(); + IPCMemory* mem = global(); - // Only the owner processes events. But if the previous owner's dead, we can take ownership now - if (*(uint64_t*)globalMemory.data() != globalId) + if (mem->globalId == globalId) { - if (difftime(time(0), getGlobalTimestamp()) >= OWNERSHIP_TIMEOUT_S) + // We're the owner, let's process those events + mem->lastProcessed = time(0); + } + else + { + // Only the owner processes events. But if the previous owner's dead, we can take ownership now + if (difftime(time(0), mem->lastProcessed) >= OWNERSHIP_TIMEOUT_S) { - qDebug() << "IPC: Previous owner timed out, taking ownership"; - *(uint64_t*)globalMemory.data() = globalId; + qDebug() << "IPC: Previous owner timed out, taking ownership" << mem->globalId << "->" << globalId; + // Ignore events that were not meant for this instance + memset(mem, 0, sizeof(IPCMemory)); + mem->globalId = globalId; + mem->lastProcessed = time(0); } - else + // Non-main instance is limited to events destined for specific profile it runs + } + + while (IPCEvent* evt = fetchEvent()) + { + QString name = QString::fromUtf8(evt->name); + auto it = eventHandlers.find(name); + if (it != eventHandlers.end()) { - goto unlockAndRestartTimer; + evt->accepted = runEventHandler(it.value(), evt->data); + qDebug() << "IPC: Processing event: " << name << ":" << evt->posted << "=" << evt->accepted; } } - // We're the owner, let's process those events - forever { - QByteArray eventData = fetchEvent(); - if (eventData.isEmpty()) - break; - - qDebug() << "IPC: Processing event: "<= 65535) - { - qWarning() << "IPC: sendEvent: Too much data for a single chunk, giving up"; - return 0; - } - - if (globalMemory.lock()) - { - // Check that we have enough room for that new chunk - char* nextChunk = getFirstFreeChunk(); - if (nextChunk == nullptr - || nextChunk + 2 + dataSize + 2 - (char*)globalMemory.data() >= MEMORY_SIZE) - { - qWarning() << "IPC: sendEvent: Not enough memory left, giving up"; - return 0; - } - - // Commit the new chunk to shared memory - *(uint16_t*)nextChunk = dataSize; - memcpy(nextChunk+2, data.data(), dataSize); - *(uint16_t*)(nextChunk+2+dataSize) = 0; - globalMemory.unlock(); - qDebug() << "IPC: Posted event: "<= MEMORY_SIZE) - return nullptr; - - ptr += chunkSize; - } - return nullptr; // Never reached -} - -char* IPC::getLastUsedChunk() -{ - char* ptr = (char*)globalMemory.data() + MEMORY_HEADER_SIZE; - char* lastPtr = nullptr; - - forever - { - uint16_t chunkSize = *(uint16_t*)ptr; - - if (!chunkSize) - return lastPtr; - - if ((ptr + chunkSize + 2) - (char*)globalMemory.data() > MEMORY_SIZE) - return lastPtr; - - lastPtr = ptr; - ptr += chunkSize; - } - return nullptr; // Never reached -} - -QByteArray IPC::fetchEvent() -{ - QByteArray eventData; - - // Get a pointer to the last chunk - char* nextChunk = getLastUsedChunk(); - if (nextChunk == nullptr) - return eventData; - - // Read that chunk and remove it from memory - uint16_t dataSize = *(uint16_t*)nextChunk; - *(uint16_t*)nextChunk = 0; - eventData.resize(dataSize); - memcpy(eventData.data(), nextChunk+2, dataSize); - - return eventData; -} - -void IPC::updateGlobalTimestamp() -{ - *(time_t*)((char*)globalMemory.data()+sizeof(globalId)) = time(0); -} - -time_t IPC::getGlobalTimestamp() -{ - return *(time_t*)((char*)globalMemory.data()+sizeof(globalId)); -} - -bool IPC::isEventProcessed(time_t postTime) -{ - return (difftime(lastSeenTimestamp, postTime) > 0); -} - -void IPC::waitUntilProcessed(time_t postTime) -{ - while (difftime(lastSeenTimestamp, postTime) <= 0) - qApp->processEvents(); -} - -void IPC::runEventHandler(IPCEventHandler handler, const QByteArray& arg) -{ - if (QThread::currentThread() != qApp->thread()) - { - QMetaObject::invokeMethod(this, "runEventHandler", Qt::BlockingQueuedConnection, - Q_ARG(IPCEventHandler, handler), Q_ARG(const QByteArray&, arg)); - return; - } - else - { - handler(arg); - return; - } + return (IPCMemory*)globalMemory.data(); } diff --git a/src/ipc.h b/src/ipc.h index 8fd6b6ae4..13de0918b 100644 --- a/src/ipc.h +++ b/src/ipc.h @@ -23,62 +23,71 @@ #include #include #include +#include #include #include -/// Handles an IPC event, must filter out and ignore events it doesn't recognize -using IPCEventHandler = std::function; +using IPCEventHandler = std::function; + +#define IPC_PROTOCOL_VERSION "2" -/// Class used for inter-process communication with other qTox instances -/// IPC event handlers will be called from the GUI thread after its event loop starts class IPC : public QThread { Q_OBJECT +protected: + static const int EVENT_TIMER_MS = 1000; + static const int EVENT_GC_TIMEOUT = 5; + static const int EVENT_QUEUE_SIZE = 32; + static const int OWNERSHIP_TIMEOUT_S = 5; public: IPC(); ~IPC(); - /// Posts an event to the global shared memory, returns the time at wich it was posted - time_t postEvent(const QByteArray& data); - bool isCurrentOwner(); ///< Returns whether we're responsible for event processing of the global shared memory - void registerEventHandler(IPCEventHandler handler); ///< Registers a function to be called whenever an event is received - bool isEventProcessed(time_t postTime); ///< Returns wether a previously posted event was already processed - void waitUntilProcessed(time_t postTime); ///< Blocks until a previously posted event is processed + + struct IPCEvent + { + uint32_t dest; + int32_t sender; + char name[16]; + char data[128]; + time_t posted; + time_t processed; + uint32_t flags; + bool accepted; + bool global; + }; + + struct IPCMemory + { + uint64_t globalId; + // When last event was posted + time_t lastEvent; + // When processEvents() ran last time + time_t lastProcessed; + IPCEvent events[IPC::EVENT_QUEUE_SIZE]; + }; + + // dest: Settings::getCurrentProfileId() or 0 (main instance). + time_t postEvent(const QString& name, const QByteArray &data=QByteArray(), uint32_t dest=0); + bool isCurrentOwner(); + void registerEventHandler(const QString& name, IPCEventHandler handler); + bool isEventProcessed(time_t time); + bool isEventAccepted(time_t time); + bool waitUntilProcessed(time_t time, int32_t timeout=-1); protected slots: void processEvents(); -private: - /// Runs an IPC event handler from the main (GUI) thread, will block until the handler returns - Q_INVOKABLE void runEventHandler(IPCEventHandler handler, const QByteArray& arg); - /// Assumes that the memory IS LOCKED - /// Returns a pointer to the first free chunk of shared memory or a nullptr on error - char* getFirstFreeChunk(); - /// Assumes that the memory IS LOCKED - /// Returns a pointer to the last used chunk of shared memory or a nullptr on error - char* getLastUsedChunk(); - /// Assumes that the memory IS LOCKED - /// Removes the last event from the shared memory and returns its data, or an empty object on error - QByteArray fetchEvent(); - /// Assumes that the memory IS LOCKED - /// Updates the global shared timestamp - void updateGlobalTimestamp(); - /// Assumes that the memory IS LOCKED - /// Returns the global shared timestamp - time_t getGlobalTimestamp(); +protected: + IPCMemory* global(); + bool runEventHandler(IPCEventHandler handler, const QByteArray& arg); + // Only called when global memory IS LOCKED, returns 0 if no evnts present + IPCEvent* fetchEvent(); -private: - QSharedMemory globalMemory; + QTimer timer; uint64_t globalId; - QTimer ownerTimer; - QVector eventHandlers; - time_t lastSeenTimestamp; - - // Constants - static const int MEMORY_SIZE = 4096; - static const int MEMORY_HEADER_SIZE = sizeof(globalId) + sizeof(time_t); - static const int EVENT_TIMER_MS = 1000; - static const int OWNERSHIP_TIMEOUT_S = 5; + QSharedMemory globalMemory; + QMap eventHandlers; }; #endif // IPC_H diff --git a/src/main.cpp b/src/main.cpp index 43395a9f4..76f6c9d9f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,14 +123,13 @@ int main(int argc, char *argv[]) AutoUpdater::installLocalUpdate(); ///< NORETURN #endif - Nexus::getInstance().start(); #ifndef Q_OS_ANDROID // Inter-process communication IPC ipc; - ipc.registerEventHandler(&toxURIEventHandler); - ipc.registerEventHandler(&toxSaveEventHandler); - ipc.registerEventHandler(&toxActivateEventHandler); + ipc.registerEventHandler("uri", &toxURIEventHandler); + ipc.registerEventHandler("save", &toxSaveEventHandler); + ipc.registerEventHandler("activate", &toxActivateEventHandler); if (parser.positionalArguments().size() > 0) { @@ -145,7 +144,7 @@ int main(int argc, char *argv[]) } else { - time_t event = ipc.postEvent(firstParam.toUtf8()); + time_t event = ipc.postEvent("uri", firstParam.toUtf8()); ipc.waitUntilProcessed(event); // If someone else processed it, we're done here, no need to actually start qTox if (!ipc.isCurrentOwner()) @@ -160,7 +159,7 @@ int main(int argc, char *argv[]) } else { - time_t event = ipc.postEvent(firstParam.toUtf8()); + time_t event = ipc.postEvent("save", firstParam.toUtf8()); ipc.waitUntilProcessed(event); // If someone else processed it, we're done here, no need to actually start qTox if (!ipc.isCurrentOwner()) @@ -175,13 +174,15 @@ int main(int argc, char *argv[]) } else if (!ipc.isCurrentOwner() && !parser.isSet("p")) { - time_t event = ipc.postEvent("$activate"); + time_t event = ipc.postEvent("activate"); ipc.waitUntilProcessed(event); if (!ipc.isCurrentOwner()) return EXIT_SUCCESS; } #endif + Nexus::getInstance().start(); + // Run a.setQuitOnLastWindowClosed(false); int errorcode = a.exec(); diff --git a/src/misc/settings.cpp b/src/misc/settings.cpp index bdc2722ac..86df23913 100644 --- a/src/misc/settings.cpp +++ b/src/misc/settings.cpp @@ -33,6 +33,8 @@ #include #include #include +#include + #define SHOW_SYSTEM_TRAY_DEFAULT (bool) true @@ -42,7 +44,7 @@ Settings* Settings::settings{nullptr}; bool Settings::makeToxPortable{false}; Settings::Settings() : - loaded(false), useCustomDhtList{false} + loaded(false), useCustomDhtList{false}, currentProfileId(0) { load(); } @@ -54,20 +56,16 @@ Settings& Settings::getInstance() return *settings; } -void Settings::resetInstance() -{ - if (settings) - { - delete settings; - settings = nullptr; - } -} - void Settings::switchProfile(const QString& profile) { + // Saves current profile as main profile if this instance is main instance setCurrentProfile(profile); save(false); - resetInstance(); + + // If this instance is not main instance previous save did not happen therefore + // we manually set profile again and load profile settings + setCurrentProfile(profile); + load(); } QString Settings::detectProfile() @@ -205,6 +203,7 @@ void Settings::load() proxyAddr = s.value("proxyAddr", "").toString(); proxyPort = s.value("proxyPort", 0).toInt(); currentProfile = s.value("currentProfile", "").toString(); + currentProfileId = makeProfileId(currentProfile); autoAwayTime = s.value("autoAwayTime", 10).toInt(); checkUpdates = s.value("checkUpdates", false).toBool(); showWindow = s.value("showWindow", true).toBool(); @@ -448,6 +447,13 @@ void Settings::save(QString path, bool writePersonal) } } +uint32_t Settings::makeProfileId(const QString& profile) +{ + QByteArray data = QCryptographicHash::hash(profile.toUtf8(), QCryptographicHash::Md5); + const uint32_t* dwords = (uint32_t*)data.constData(); + return dwords[0] ^ dwords[1] ^ dwords[2] ^ dwords[3]; +} + QString Settings::getSettingsDirPath() { if (makeToxPortable) @@ -733,9 +739,15 @@ QString Settings::getCurrentProfile() const return currentProfile; } +uint32_t Settings::getCurrentProfileId() const +{ + return currentProfileId; +} + void Settings::setCurrentProfile(QString profile) { currentProfile = profile; + currentProfileId = makeProfileId(currentProfile); } bool Settings::getEnableLogging() const diff --git a/src/misc/settings.h b/src/misc/settings.h index 2b2e4f63c..90ca981e0 100644 --- a/src/misc/settings.h +++ b/src/misc/settings.h @@ -31,7 +31,6 @@ class Settings : public QObject Q_OBJECT public: static Settings& getInstance(); - static void resetInstance(); void switchProfile(const QString& profile); QString detectProfile(); QList searchProfiles(); @@ -84,6 +83,7 @@ public: void setUseEmoticons(bool newValue); QString getCurrentProfile() const; + uint32_t getCurrentProfileId() const; void setCurrentProfile(QString profile); QString getTranslation() const; @@ -253,6 +253,7 @@ private: Settings(); Settings(Settings &settings) = delete; Settings& operator=(const Settings&) = delete; + static uint32_t makeProfileId(const QString& profile); static const QString FILENAME; static const QString OLDFILENAME; @@ -286,6 +287,7 @@ private: int proxyPort; QString currentProfile; + uint32_t currentProfileId; bool enableLogging; bool encryptLogs; diff --git a/src/widget/toxsave.cpp b/src/widget/toxsave.cpp index 8340b1e95..357424481 100644 --- a/src/widget/toxsave.cpp +++ b/src/widget/toxsave.cpp @@ -22,15 +22,16 @@ #include #include -void toxSaveEventHandler(const QByteArray& eventData) +bool toxSaveEventHandler(const QByteArray& eventData) { if (!eventData.endsWith(".tox")) - return; + return false; handleToxSave(eventData); + return true; } -void handleToxSave(const QString& path) +bool handleToxSave(const QString& path) { Core* core = Core::getInstance(); @@ -47,7 +48,7 @@ void handleToxSave(const QString& path) QFileInfo info(path); if (!info.exists()) - return; + return false; QString profile = info.completeBaseName(); @@ -55,17 +56,18 @@ void handleToxSave(const QString& path) { GUI::showWarning(QObject::tr("Ignoring non-Tox file", "popup title"), QObject::tr("Warning: you've chosen a file that is not a Tox save file; ignoring.", "popup text")); - return; + return false; } QString profilePath = QDir(Settings::getSettingsDirPath()).filePath(profile + Core::TOX_EXT); if (QFileInfo(profilePath).exists() && !GUI::askQuestion(QObject::tr("Profile already exists", "import confirm title"), QObject::tr("A profile named \"%1\" already exists. Do you want to erase it?", "import confirm text").arg(profile))) - return; + return false; QFile::copy(path, profilePath); // no good way to update the ui from here... maybe we need a Widget:refreshUi() function... // such a thing would simplify other code as well I believe GUI::showInfo(QObject::tr("Profile imported"), QObject::tr("%1.tox was successfully imported").arg(profile)); + return true; } diff --git a/src/widget/toxsave.h b/src/widget/toxsave.h index c4e40c6f5..53ccb3c45 100644 --- a/src/widget/toxsave.h +++ b/src/widget/toxsave.h @@ -21,9 +21,9 @@ class QString; class QByteArray; /// Will wait until the core is ready first -void handleToxSave(const QString& path); +bool handleToxSave(const QString& path); // Internals -void toxSaveEventHandler(const QByteArray& eventData); +bool toxSaveEventHandler(const QByteArray& eventData); #endif diff --git a/src/widget/toxuri.cpp b/src/widget/toxuri.cpp index a3f08d53e..6a844e528 100644 --- a/src/widget/toxuri.cpp +++ b/src/widget/toxuri.cpp @@ -30,15 +30,16 @@ #include #include -void toxURIEventHandler(const QByteArray& eventData) +bool toxURIEventHandler(const QByteArray& eventData) { if (!eventData.startsWith("tox:")) - return; + return false; handleToxURI(eventData); + return true; } -void handleToxURI(const QString &toxURI) +bool handleToxURI(const QString &toxURI) { Core* core = Core::getInstance(); @@ -71,6 +72,7 @@ void handleToxURI(const QString &toxURI) if (dialog.exec() == QDialog::Accepted) Core::getInstance()->requestFriendship(toxid, dialog.getRequestMessage()); } + return true; } ToxURIDialog::ToxURIDialog(QWidget *parent, const QString &userId, const QString &message) : diff --git a/src/widget/toxuri.h b/src/widget/toxuri.h index 039cffdb4..9dbf16273 100644 --- a/src/widget/toxuri.h +++ b/src/widget/toxuri.h @@ -22,12 +22,12 @@ /// Shows a dialog asking whether or not to add this tox address as a friend /// Will wait until the core is ready first -void handleToxURI(const QString& toxURI); +bool handleToxURI(const QString& toxURI); // Internals class QByteArray; class QPlainTextEdit; -void toxURIEventHandler(const QByteArray& eventData); +bool toxURIEventHandler(const QByteArray& eventData); class ToxURIDialog : public QDialog { Q_OBJECT diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index e0da17cdb..448c310a6 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -67,11 +67,11 @@ #define IS_ON_DESKTOP_GUI 1 #endif -void toxActivateEventHandler(const QByteArray& data) +bool toxActivateEventHandler(const QByteArray&) { - if (data != "$activate") - return; - Widget::getInstance()->forceShow(); + if (!Widget::getInstance()->isActiveWindow()) + Widget::getInstance()->forceShow(); + return true; } Widget *Widget::instance{nullptr}; diff --git a/src/widget/widget.h b/src/widget/widget.h index e8f7d0d2b..7627227fa 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -170,6 +170,6 @@ private: bool eventIcon; }; -void toxActivateEventHandler(const QByteArray& data); +bool toxActivateEventHandler(const QByteArray& data); #endif // WIDGET_H