From a7a7ec20c2059ae8681e3088afa370a3d4147892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=28xq=29=20Quei=C3=9Fner?= Date: Sat, 6 Mar 2021 13:32:46 +0100 Subject: [PATCH] Implements an IPC scheme for kristall. Closes #139. --- presets/DOS.kthm | 50 +++++-- src/about/help.gemini | 1 + src/about/updates.gemini | 6 +- src/main.cpp | 291 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 317 insertions(+), 31 deletions(-) diff --git a/presets/DOS.kthm b/presets/DOS.kthm index 0d147a2..9211f4b 100644 --- a/presets/DOS.kthm +++ b/presets/DOS.kthm @@ -1,34 +1,54 @@ [General] -name=DOS -version=1 -theme=0 +ansi_colors=black, darkred, darkgreen, darkgoldenrod, darkblue, darkmagenta, darkcyan, lightgray, gray, red, green, goldenrod, lightblue, magenta, cyan, white background_color=#00008b blockquote_color=#000000 -margins=55 +margins_h=55 +margins_v=55 +name=DOS +theme=0 +version=1 -[Standard] -font="PxPlus IBM VGA9,16,-1,5,50,0,0,0,0,0,Regular" +[Blockquote] color=#d3d3d3 +font="Source Code Pro,16,-1,5,50,0,0,0,0,0,Regular" -[Preformatted] -font="PxPlus IBM VGA9,16,-1,5,50,0,0,0,0,0,Regular" -color=#ffffff +[Formatting] +centre_h1=false +indent_bq=0 +indent_h=0 +indent_l=2 +indent_p=0 +indent_size=16 +justify_text=true +line_height_h=0 +line_height_p=0 +list_symbol=-3 +text_width=900 +text_width_enabled=true [H1] -font="PxPlus IBM VGA9,16,-1,5,50,0,0,0,0,0,Regular" color=#ffff00 +font="Source Code Pro,16,-1,5,50,0,0,0,0,0,Regular" [H2] -font="PxPlus IBM VGA9,16,-1,5,50,0,0,0,0,0,Regular" color=#00ff00 +font="Source Code Pro,16,-1,5,50,0,0,0,0,0,Regular" [H3] -font="PxPlus IBM VGA9,16,-1,5,50,0,0,0,0,0,Regular" color=#ffffff +font="Source Code Pro,16,-1,5,50,0,0,0,0,0,Regular" [Link] -color_internal=#00ffff -color_external=#00ffff color_cross_scheme=#00ffff -internal_prefix="\x2192 " +color_external=#00ffff +color_internal=#00ffff external_prefix="\x21d2 " +internal_prefix="\x2192 " + +[Preformatted] +color=#ffffff +font="Source Code Pro,16,-1,5,50,0,0,0,0,0,Regular" + +[Standard] +color=#d3d3d3 +font="Source Code Pro,16,-1,5,50,0,0,0,0,0,Regular" diff --git a/src/about/help.gemini b/src/about/help.gemini index 2a4b3a0..185266b 100644 --- a/src/about/help.gemini +++ b/src/about/help.gemini @@ -365,6 +365,7 @@ There is also the scheme about: which can be used to access internal sites for c => about:help => about:updates => about:style-preview +=> about:style-display => about:cache ## Security Concept diff --git a/src/about/updates.gemini b/src/about/updates.gemini index b3d4a1c..c0d3f1f 100644 --- a/src/about/updates.gemini +++ b/src/about/updates.gemini @@ -14,6 +14,10 @@ * New action: "View/Show document source" will display the raw data of the document in a small separate window * Change: Makes TLS editor columns sortable +-- There's a lot missing here still + +* Adds single-session mode so opening links will open them in the currently focuse kristall window instead of a new instance every time. + ## 0.3 - TLS and security * Adds support for transient client certificates * Adds support for permanent client certificates @@ -46,7 +50,7 @@ * Implement Ctrl+S/*Save as...* menu item * Add display for "non-recognized files" * Added support for gopher:// and gophermaps -* Added "go to home" menu +* Added "go to home" menu * Added support for video/* and audio/* via QMediaPlayer * Added support for file:// scheme * Added status bar with loading time, file size and mime type diff --git a/src/main.cpp b/src/main.cpp index 645630f..997fe01 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include ProtocolSetup kristall::protocols; @@ -96,6 +98,182 @@ static void addEmojiSubstitutions() } } +// Explanation to the IPC protocol: +// Each IPC request is required to open a new connection. +// The first some bytes are the ipc::Message struct that is inspected. +// After that, message-dependent bytes are sent and the IPC process is terminated. +// There is now way of indicating an error. +// Messages are not allowed to be larger than 64k +// We don't need to think about endianess or alignment as we only communicate with +// the same machine. +namespace ipc +{ + static char const * socket_name = "net.random-projects.kristall"; + + struct Message + { + enum Type + { + /// Requests that a series of urls URLs are opened + /// as new tabs in the currently focused window. + /// Payload description: + /// The message will contain a sequence of UTF-8 encoded bytes + /// that encode URLs. The URLs are separated by LF. + open_in_tabs = 0, + + /// Same format as open_in_tabs, but requests that these urls are + /// opened in a new window instead of new tabs. + open_in_window = 1, + }; + + enum Protocol : uint16_t + { + version_1 = 1, + }; + + Protocol version; + Type type; + }; + + static_assert(std::is_trivial_v, "Message needs to be flat-copyable!"); + static_assert(std::is_trivially_copyable_v, "Message needs to be flat-copyable!"); + + //! Implements the + struct ConnectedClient : QObject + { + QLocalSocket * socket; + QByteArray receive_buffer; + bool everything_ok; + + ConnectedClient(QLocalSocket * socket) : + QObject(socket), + socket(socket), + everything_ok(true) + { + QObject::connect(socket, &QLocalSocket::readyRead, this, &ConnectedClient::on_readyRead); + QObject::connect(socket, &QLocalSocket::disconnected, this, &ConnectedClient::on_disconnected); + } + + void on_readyRead() + { + auto const buffer = socket->readAll(); + if(buffer.size() + this->receive_buffer.size() > 65536) { + qCritical() << "ipc failure: IPC client sent more than 64k bytes of data!"; + this->everything_ok = false; + this->socket->close(); + } + + this->receive_buffer.append(buffer); + } + + void on_disconnected() + { + if(not this->everything_ok) + return; + if(size_t(this->receive_buffer.size()) < sizeof(Message)) { + qCritical() << "ipc failure: IPC client did not send enough data!"; + return; + } + Message message; + memcpy(&message, this->receive_buffer.data(), sizeof(Message)); + switch(message.version) + { + case Message::version_1: { + this->processRequest( + message.type, + this->receive_buffer.mid(sizeof(Message)) + ); + break; + } + default: { + qCritical() << "ipc failure: IPC client used a unsupported protocol version!"; + return; + } + } + } + + void processRequest(Message::Type type, QByteArray const & payload) + { + switch(type) + { + case Message::open_in_tabs: { + for(auto const & data : payload.split('\n')) + { + QUrl url { QString::fromUtf8(data) }; + if(url.isValid()) { + if(main_window != nullptr) { + main_window->addNewTab(true, url); + } + } + } + break; + } + case Message::open_in_window: { + for(auto const & data : payload.split('\n')) + { + QUrl url { QString::fromUtf8(data) }; + if(url.isValid()) { + // TODO: Implement opening these urls in a new + // window instead of a new tab! + if(main_window != nullptr) { + main_window->addNewTab(true, url); + } + } + } + break; + } + + default: { + qCritical() << "ipc failure: IPC client used a unsupported message type!"; + return; + } + } + } + }; + + void send(QLocalSocket & socket, void const * buffer, size_t length) + { + size_t offset = 0; + while(offset < length) + { + auto const sent = socket.write( + reinterpret_cast(buffer) + offset, + length - offset + ); + if(sent <= 0) + return; + offset += sent; + } + } + + /// Sends a open_in_tabs request and closes the socket. + void sendOpenInTabs(QLocalSocket & socket, QVector const & urls) + { + Message msg { Message::version_1, Message::open_in_tabs }; + send(socket, &msg, sizeof msg); + for(int i = 0; i < urls.size(); i++) + { + if(i > 0) + send(socket, "\n", 1); + auto const bits = urls[i].toString(QUrl::FullyEncoded).toUtf8(); + send(socket, bits.data(), bits.size()); + } + } + + /// Sends a open_in_window request and closes the socket. + void sendOpenInWindow(QLocalSocket & socket, QVector const & urls) + { + Message msg { Message::version_1, Message::open_in_window }; + send(socket, &msg, sizeof msg); + for(int i = 0; i < urls.size(); i++) + { + if(i > 0) + send(socket, "\n", 1); + auto const bits = urls[i].toString(QUrl::FullyEncoded).toUtf8(); + send(socket, bits.data(), bits.size()); + } + } +} int main(int argc, char *argv[]) { @@ -123,12 +301,94 @@ int main(int argc, char *argv[]) addEmojiSubstitutions(); QCommandLineParser cli_parser; + + QCommandLineOption newWindowOption { + { "w", "new-window" }, + app.tr("Opens the provided links in a new window instead of tabs."), + }; + + QCommandLineOption isolatedOption { + { "i", "isolated" }, + app.tr("Starts the instance of kristall as a isolated session that cannot communicate with other windows."), + }; + cli_parser.addVersionOption(); cli_parser.addHelpOption(); + cli_parser.addOption(newWindowOption); + cli_parser.addOption(isolatedOption); + cli_parser.addPositionalArgument("urls", app.tr("The urls that should be opened instead of the start page"), "[urls...]"); cli_parser.process(app); + QVector urls; + { + auto cli_args = cli_parser.positionalArguments(); + for(auto const & url_str : cli_args) + { + QUrl url { url_str }; + if (url.isRelative()) + { + if (QFile::exists(url_str)) { + url = QUrl::fromLocalFile(QFileInfo(url_str).absoluteFilePath()); + } else { + url = QUrl("gemini://" + url_str); + } + } + if(url.isValid()) { + urls.append(url); + } else { + qDebug() << "Invalid url: " << url_str; + } + } + } + + auto const open_new_window = cli_parser.isSet(newWindowOption); + + auto const isolated_session = cli_parser.isSet(isolatedOption); + + std::unique_ptr ipc_server { nullptr }; + + if(not isolated_session) + { + // try connecting to a already existing instance of kristall + { + QLocalSocket socket; + socket.connectToServer(ipc::socket_name); + // do not use less time here as we need to give the other task a bit + // of time. Most OS have a "loop time" of ~10 ms, so we use twice the + // time here to allow a response. + if(socket.waitForConnected(20)) + { + qDebug() << "we already have a kristall instance running!"; + if(urls.length() > 0) + { + if(open_new_window) + ipc::sendOpenInWindow(socket, urls); + else + ipc::sendOpenInTabs(socket, urls); + } + socket.waitForBytesWritten(); + return 0; + } + } + + // Otherwise, spawn a new local socket that will accept messages + // to provide proper IPC + { + std::unique_ptr server { new QLocalServer }; + if(server->listen(ipc::socket_name)) + { + qDebug() << "successfully started the IPC socket."; + ipc_server = std::move(server); + } + else + { + qCritical() << "failed to create IPC socket: " << server->errorString(); + } + } + } + QString cache_root = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QString config_root = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); @@ -299,22 +559,23 @@ int main(int argc, char *argv[]) MainWindow w(&app); main_window = &w; - auto urls = cli_parser.positionalArguments(); + QObject::connect(ipc_server.get(), &QLocalServer::newConnection, [&ipc_server]() { + auto * const socket = ipc_server->nextPendingConnection(); + if(socket != nullptr) { + // this will set up everything needed: + // - signals from socket + // - set itself as the socket child, so it will be deleted when the socket is closed + (void) new ipc::ConnectedClient(socket); + + // destroy the socket when the connection was closed. + QObject::connect(socket, &QLocalSocket::disconnected, socket, &QObject::deleteLater); + } + }); + + // Open all URLs in the new window if(urls.size() > 0) { - for(const auto &url_str : urls) { - QUrl url { url_str }; - if (url.isRelative()) { - if (QFile::exists(url_str)) { - url = QUrl::fromLocalFile(QFileInfo(url_str).absoluteFilePath()); - } else { - url = QUrl("gemini://" + url_str); - } - } - if(url.isValid()) { - w.addNewTab(false, url); - } else { - qDebug() << "Invalid url: " << url_str; - } + for(const auto & url : urls) { + w.addNewTab(false, url); } } else {