From 378aee7ab1886961e2e0e7debab54b6158446e83 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 30 Jul 2023 13:14:58 +0200 Subject: [PATCH] Refactor Native Messages (#4738) * refactor: move ipc queue into its own class * refactor: move windows.h related functions to AW * refactor: make NM-Client methods static * refactor: json access * refactor: use struct initializer * refactor: move `handleMessage` to anon-namespace * refactor: clean-up includes * refactor: move action handler to functions * refactor: cleanup `handleSelect` * fix: cleanup clang-tidy warnings * chore: simplify json * revert: keep handlers as methods This is more readable and extensible. * fix: typo * fix: namespace * fix: rename define * refactor: `IpcQueue` to be simpler * fix: rename cmake option * fix: use variant when constructing * fix: make it a ref * fix: its a pair now --- src/Application.cpp | 2 +- src/BrowserExtension.cpp | 8 +- src/CMakeLists.txt | 8 + src/singletons/NativeMessaging.cpp | 327 +++++++++++++---------------- src/singletons/NativeMessaging.hpp | 10 +- src/util/IpcQueue.cpp | 87 ++++++++ src/util/IpcQueue.hpp | 35 +++ src/widgets/AttachedWindow.cpp | 7 + src/widgets/AttachedWindow.hpp | 3 + 9 files changed, 301 insertions(+), 186 deletions(-) create mode 100644 src/util/IpcQueue.cpp create mode 100644 src/util/IpcQueue.hpp diff --git a/src/Application.cpp b/src/Application.cpp index 683a873af..12333c71a 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -270,7 +270,7 @@ void Application::initNm(Paths &paths) (void)paths; #ifdef Q_OS_WIN -# if defined QT_NO_DEBUG || defined C_DEBUG_NM +# if defined QT_NO_DEBUG || defined CHATTERINO_DEBUG_NM registerNmHost(paths); this->nmServer.start(); # endif diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp index 8ae25ac99..3ac8dc2dd 100644 --- a/src/BrowserExtension.cpp +++ b/src/BrowserExtension.cpp @@ -30,7 +30,7 @@ namespace { #endif } - void runLoop(NativeMessagingClient &client) + void runLoop() { auto received_message = std::make_shared(true); @@ -73,7 +73,7 @@ namespace { received_message->store(true); - client.sendMessage(data); + nm::client::sendMessage(data); } _Exit(0); } @@ -83,9 +83,7 @@ void runBrowserExtensionHost() { initFileMode(); - NativeMessagingClient client; - - runLoop(client); + runLoop(); } } // namespace chatterino diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b1df0023a..936495b65 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,6 +3,9 @@ set(VERSION_PROJECT "${LIBRARY_PROJECT}-version") set(EXECUTABLE_PROJECT "${PROJECT_NAME}") add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00) +# registers the native messageing host +option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF) + set(SOURCE_FILES Application.cpp Application.hpp @@ -408,6 +411,8 @@ set(SOURCE_FILES util/IncognitoBrowser.hpp util/InitUpdateButton.cpp util/InitUpdateButton.hpp + util/IpcQueue.cpp + util/IpcQueue.hpp util/LayoutHelper.cpp util/LayoutHelper.hpp util/NuulsUploader.cpp @@ -847,6 +852,9 @@ if (WIN32) set_target_properties(${EXECUTABLE_PROJECT} PROPERTIES WIN32_EXECUTABLE TRUE) endif () endif () +if (CHATTERINO_DEBUG_NATIVE_MESSAGES) + target_compile_definitions(${LIBRARY_PROJECT} PRIVATE CHATTERINO_DEBUG_NM) +endif () if (MSVC) target_compile_options(${LIBRARY_PROJECT} PUBLIC /EHsc /bigobj) diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 5e72e131f..ef42e706d 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -1,38 +1,38 @@ #include "singletons/NativeMessaging.hpp" #include "Application.hpp" +#include "common/Literals.hpp" #include "common/QLogging.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Paths.hpp" +#include "util/IpcQueue.hpp" #include "util/PostToThread.hpp" -#include -#include #include #include #include #include #include #include - -namespace ipc = boost::interprocess; +#include #ifdef Q_OS_WIN -// clang-format off -# include -# include -// clang-format on -# include "singletons/WindowManager.hpp" # include "widgets/AttachedWindow.hpp" #endif -#include +namespace { -#define EXTENSION_ID "glknmaideaikkmemifbfkhnomoknepka" -#define MESSAGE_SIZE 1024 +using namespace chatterino::literals; + +const QString EXTENSION_ID = u"glknmaideaikkmemifbfkhnomoknepka"_s; +constexpr const size_t MESSAGE_SIZE = 1024; + +} // namespace namespace chatterino { +using namespace literals; + void registerNmManifest(Paths &paths, const QString &manifestFilename, const QString ®istryKeyName, const QJsonDocument &document); @@ -40,47 +40,42 @@ void registerNmManifest(Paths &paths, const QString &manifestFilename, void registerNmHost(Paths &paths) { if (paths.isPortable()) + { return; + } - auto getBaseDocument = [&] { - QJsonObject obj; - obj.insert("name", "com.chatterino.chatterino"); - obj.insert("description", "Browser interaction with chatterino."); - obj.insert("path", QCoreApplication::applicationFilePath()); - obj.insert("type", "stdio"); - - return obj; + auto getBaseDocument = [] { + return QJsonObject{ + {u"name"_s, "com.chatterino.chatterino"_L1}, + {u"description"_s, "Browser interaction with chatterino."_L1}, + {u"path"_s, QCoreApplication::applicationFilePath()}, + {u"type"_s, "stdio"_L1}, + }; }; // chrome { - QJsonDocument document; - auto obj = getBaseDocument(); - QJsonArray allowed_origins_arr = {"chrome-extension://" EXTENSION_ID - "/"}; - obj.insert("allowed_origins", allowed_origins_arr); - document.setObject(obj); + QJsonArray allowedOriginsArr = { + u"chrome-extension://%1/"_s.arg(EXTENSION_ID)}; + obj.insert("allowed_origins", allowedOriginsArr); registerNmManifest(paths, "/native-messaging-manifest-chrome.json", "HKCU\\Software\\Google\\Chrome\\NativeMessagingHost" "s\\com.chatterino.chatterino", - document); + QJsonDocument(obj)); } // firefox { - QJsonDocument document; - auto obj = getBaseDocument(); - QJsonArray allowed_extensions = {"chatterino_native@chatterino.com"}; - obj.insert("allowed_extensions", allowed_extensions); - document.setObject(obj); + QJsonArray allowedExtensions = {"chatterino_native@chatterino.com"}; + obj.insert("allowed_extensions", allowedExtensions); registerNmManifest(paths, "/native-messaging-manifest-firefox.json", "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com." "chatterino.chatterino", - document); + QJsonDocument(obj)); } } @@ -112,32 +107,26 @@ std::string &getNmQueueName(Paths &paths) // CLIENT -void NativeMessagingClient::sendMessage(const QByteArray &array) -{ - try +namespace nm::client { + + void sendMessage(const QByteArray &array) { - ipc::message_queue messageQueue(ipc::open_only, "chatterino_gui"); - - messageQueue.try_send(array.data(), size_t(array.size()), 1); - // messageQueue.timed_send(array.data(), size_t(array.size()), 1, - // boost::posix_time::second_clock::local_time() + - // boost::posix_time::seconds(10)); + ipc::sendMessage("chatterino_gui", array); } - catch (ipc::interprocess_exception &ex) + + void writeToCout(const QByteArray &array) { - qCDebug(chatterinoNativeMessage) << "send to gui process:" << ex.what(); + const auto *data = array.data(); + auto size = uint32_t(array.size()); + + // We're writing the raw bytes to cout. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + std::cout.write(reinterpret_cast(&size), 4); + std::cout.write(data, size); + std::cout.flush(); } -} -void NativeMessagingClient::writeToCout(const QByteArray &array) -{ - auto *data = array.data(); - auto size = uint32_t(array.size()); - - std::cout.write(reinterpret_cast(&size), 4); - std::cout.write(data, size); - std::cout.flush(); -} +} // namespace nm::client // SERVER @@ -148,146 +137,132 @@ void NativeMessagingServer::start() void NativeMessagingServer::ReceiverThread::run() { - try - { - ipc::message_queue::remove("chatterino_gui"); - ipc::message_queue messageQueue(ipc::open_or_create, "chatterino_gui", - 100, MESSAGE_SIZE); + auto [messageQueue, error] = + ipc::IpcQueue::tryReplaceOrCreate("chatterino_gui", 100, MESSAGE_SIZE); - while (true) - { - try - { - auto buf = std::make_unique(MESSAGE_SIZE); - auto retSize = ipc::message_queue::size_type(); - auto priority = static_cast(0); - - messageQueue.receive(buf.get(), MESSAGE_SIZE, retSize, - priority); - - auto document = QJsonDocument::fromJson( - QByteArray::fromRawData(buf.get(), retSize)); - - this->handleMessage(document.object()); - } - catch (ipc::interprocess_exception &ex) - { - qCDebug(chatterinoNativeMessage) - << "received from gui process:" << ex.what(); - } - } - } - catch (ipc::interprocess_exception &ex) + if (!error.isEmpty()) { qCDebug(chatterinoNativeMessage) - << "run ipc message queue:" << ex.what(); + << "Failed to create message queue:" << error; - nmIpcError().set(QString::fromLatin1(ex.what())); + nmIpcError().set(error); + return; + } + + while (true) + { + auto buf = messageQueue->receive(); + if (buf.isEmpty()) + { + continue; + } + auto document = QJsonDocument::fromJson(buf); + + this->handleMessage(document.object()); } } void NativeMessagingServer::ReceiverThread::handleMessage( const QJsonObject &root) { - QString action = root.value("action").toString(); - - if (action.isNull()) - { - qCDebug(chatterinoNativeMessage) << "NM action was null"; - return; - } + QString action = root["action"_L1].toString(); if (action == "select") { - QString _type = root.value("type").toString(); - bool attach = root.value("attach").toBool(); - bool attachFullscreen = root.value("attach_fullscreen").toBool(); - QString name = root.value("name").toString(); - -#ifdef USEWINSDK - AttachedWindow::GetArgs args; - args.winId = root.value("winId").toString(); - args.yOffset = root.value("yOffset").toInt(-1); - - { - const auto sizeObject = root.value("size").toObject(); - args.x = sizeObject.value("x").toDouble(-1.0); - args.pixelRatio = sizeObject.value("pixelRatio").toDouble(-1.0); - args.width = sizeObject.value("width").toInt(-1); - args.height = sizeObject.value("height").toInt(-1); - } - - args.fullscreen = attachFullscreen; - - qCDebug(chatterinoNativeMessage) - << args.x << args.pixelRatio << args.width << args.height - << args.winId; - - if (_type.isNull() || args.winId.isNull()) - { - qCDebug(chatterinoNativeMessage) - << "NM type, name or winId missing"; - attach = false; - attachFullscreen = false; - return; - } -#endif - - if (_type == "twitch") - { - postToThread([=] { - auto *app = getApp(); - - if (!name.isEmpty()) - { - auto channel = app->twitch->getOrAddChannel(name); - if (app->twitch->watchingChannel.get() != channel) - { - app->twitch->watchingChannel.reset(channel); - } - } - - if (attach || attachFullscreen) - { -#ifdef USEWINSDK - auto *window = - AttachedWindow::get(::GetForegroundWindow(), args); - if (!name.isEmpty()) - { - window->setChannel(app->twitch->getOrAddChannel(name)); - } -#endif - } - }); - } - else - { - qCDebug(chatterinoNativeMessage) << "NM unknown channel type"; - } + this->handleSelect(root); + return; } - else if (action == "detach") + if (action == "detach") { - QString winId = root.value("winId").toString(); - - if (winId.isNull()) - { - qCDebug(chatterinoNativeMessage) << "NM winId missing"; - return; - } - -#ifdef USEWINSDK - postToThread([winId] { - qCDebug(chatterinoNativeMessage) << "NW detach"; - AttachedWindow::detach(winId); - }); -#endif - } - else - { - qCDebug(chatterinoNativeMessage) << "NM unknown action " + action; + this->handleDetach(root); + return; } + + qCDebug(chatterinoNativeMessage) << "NM unknown action" << action; } +// NOLINTBEGIN(readability-convert-member-functions-to-static) +void NativeMessagingServer::ReceiverThread::handleSelect( + const QJsonObject &root) +{ + QString type = root["type"_L1].toString(); + bool attach = root["attach"_L1].toBool(); + bool attachFullscreen = root["attach_fullscreen"_L1].toBool(); + QString name = root["name"_L1].toString(); + +#ifdef USEWINSDK + const auto sizeObject = root["size"_L1].toObject(); + AttachedWindow::GetArgs args = { + .winId = root["winId"_L1].toString(), + .yOffset = root["yOffset"_L1].toInt(-1), + .x = sizeObject["x"_L1].toDouble(-1.0), + .pixelRatio = sizeObject["pixelRatio"_L1].toDouble(-1.0), + .width = sizeObject["width"_L1].toInt(-1), + .height = sizeObject["height"_L1].toInt(-1), + .fullscreen = attachFullscreen, + }; + + qCDebug(chatterinoNativeMessage) + << args.x << args.pixelRatio << args.width << args.height << args.winId; + + if (args.winId.isNull()) + { + qCDebug(chatterinoNativeMessage) << "winId in select is missing"; + return; + } +#endif + + if (type != u"twitch"_s) + { + qCDebug(chatterinoNativeMessage) << "NM unknown channel type"; + return; + } + + postToThread([=] { + auto *app = getApp(); + + if (!name.isEmpty()) + { + auto channel = app->twitch->getOrAddChannel(name); + if (app->twitch->watchingChannel.get() != channel) + { + app->twitch->watchingChannel.reset(channel); + } + } + + if (attach || attachFullscreen) + { +#ifdef USEWINSDK + auto *window = AttachedWindow::getForeground(args); + if (!name.isEmpty()) + { + window->setChannel(app->twitch->getOrAddChannel(name)); + } +#endif + } + }); +} + +void NativeMessagingServer::ReceiverThread::handleDetach( + const QJsonObject &root) +{ + QString winId = root["winId"_L1].toString(); + + if (winId.isNull()) + { + qCDebug(chatterinoNativeMessage) << "NM winId missing"; + return; + } + +#ifdef USEWINSDK + postToThread([winId] { + qCDebug(chatterinoNativeMessage) << "NW detach"; + AttachedWindow::detach(winId); + }); +#endif +} +// NOLINTEND(readability-convert-member-functions-to-static) + Atomic> &nmIpcError() { static Atomic> x; diff --git a/src/singletons/NativeMessaging.hpp b/src/singletons/NativeMessaging.hpp index 9c46a08a1..4f39057c0 100644 --- a/src/singletons/NativeMessaging.hpp +++ b/src/singletons/NativeMessaging.hpp @@ -16,12 +16,12 @@ std::string &getNmQueueName(Paths &paths); Atomic> &nmIpcError(); -class NativeMessagingClient final -{ -public: +namespace nm::client { + void sendMessage(const QByteArray &array); void writeToCout(const QByteArray &array); -}; + +} // namespace nm::client class NativeMessagingServer final { @@ -36,6 +36,8 @@ private: private: void handleMessage(const QJsonObject &root); + void handleSelect(const QJsonObject &root); + void handleDetach(const QJsonObject &root); }; ReceiverThread thread; diff --git a/src/util/IpcQueue.cpp b/src/util/IpcQueue.cpp new file mode 100644 index 000000000..0efc878fc --- /dev/null +++ b/src/util/IpcQueue.cpp @@ -0,0 +1,87 @@ +#include "util/IpcQueue.hpp" + +#include "common/QLogging.hpp" + +#include +#include +#include +#include + +namespace boost_ipc = boost::interprocess; + +namespace chatterino::ipc { + +void sendMessage(const char *name, const QByteArray &data) +{ + try + { + boost_ipc::message_queue messageQueue(boost_ipc::open_only, name); + + messageQueue.try_send(data.data(), size_t(data.size()), 1); + } + catch (boost_ipc::interprocess_exception &ex) + { + qCDebug(chatterinoNativeMessage) + << "Failed to send message:" << ex.what(); + } +} + +class IpcQueuePrivate +{ +public: + IpcQueuePrivate(const char *name, size_t maxMessages, size_t maxMessageSize) + : queue(boost_ipc::open_or_create, name, maxMessages, maxMessageSize) + { + } + + boost_ipc::message_queue queue; +}; + +IpcQueue::IpcQueue(IpcQueuePrivate *priv) + : private_(priv){}; +IpcQueue::~IpcQueue() = default; + +std::pair, QString> IpcQueue::tryReplaceOrCreate( + const char *name, size_t maxMessages, size_t maxMessageSize) +{ + try + { + boost_ipc::message_queue::remove(name); + return std::make_pair( + std::unique_ptr(new IpcQueue( + new IpcQueuePrivate(name, maxMessages, maxMessageSize))), + QString()); + } + catch (boost_ipc::interprocess_exception &ex) + { + return {nullptr, QString::fromLatin1(ex.what())}; + } +} + +QByteArray IpcQueue::receive() +{ + try + { + auto *d = this->private_.get(); + + QByteArray buf; + // The new storage is uninitialized + buf.resize(static_cast(d->queue.get_max_msg_size())); + + size_t messageSize = 0; + unsigned int priority = 0; + d->queue.receive(buf.data(), buf.size(), messageSize, priority); + + // truncate to the initialized storage + buf.truncate(static_cast(messageSize)); + return buf; + } + catch (boost_ipc::interprocess_exception &ex) + { + qCDebug(chatterinoNativeMessage) + << "Failed to receive message:" << ex.what(); + } + return {}; +} + +} // namespace chatterino::ipc diff --git a/src/util/IpcQueue.hpp b/src/util/IpcQueue.hpp new file mode 100644 index 000000000..467aa2873 --- /dev/null +++ b/src/util/IpcQueue.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +class QByteArray; +class QString; + +namespace chatterino::ipc { + +void sendMessage(const char *name, const QByteArray &data); + +class IpcQueuePrivate; +class IpcQueue +{ +public: + ~IpcQueue(); + + static std::pair, QString> tryReplaceOrCreate( + const char *name, size_t maxMessages, size_t maxMessageSize); + + // TODO: use std::expected + /// Try to receive a message. + /// In the case of an error, the buffer is empty. + QByteArray receive(); + +private: + IpcQueue(IpcQueuePrivate *priv); + + std::unique_ptr private_; + + friend class IpcQueuePrivate; +}; + +} // namespace chatterino::ipc diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 57a879641..acb00263b 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -136,6 +136,13 @@ AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args) return window; } +#ifdef USEWINSDK +AttachedWindow *AttachedWindow::getForeground(const GetArgs &args) +{ + return AttachedWindow::get(::GetForegroundWindow(), args); +} +#endif + void AttachedWindow::detach(const QString &winId) { for (Item &item : items) diff --git a/src/widgets/AttachedWindow.hpp b/src/widgets/AttachedWindow.hpp index 2f4774502..630f9d4ae 100644 --- a/src/widgets/AttachedWindow.hpp +++ b/src/widgets/AttachedWindow.hpp @@ -31,6 +31,9 @@ public: virtual ~AttachedWindow() override; static AttachedWindow *get(void *target_, const GetArgs &args); +#ifdef USEWINSDK + static AttachedWindow *getForeground(const GetArgs &args); +#endif static void detach(const QString &winId); void setChannel(ChannelPtr channel);