From da043e092a871419fdc94698b1d71177b26b8a74 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 30 Aug 2024 21:45:20 -0700 Subject: [PATCH] core/ipc: add ipc server/client Currently can only kill a remote instance. --- src/core/CMakeLists.txt | 3 +- src/core/crashinfo.cpp | 19 -- src/core/instanceinfo.cpp | 31 +++ src/core/{crashinfo.hpp => instanceinfo.hpp} | 10 + src/core/ipc.cpp | 119 ++++++++++ src/core/ipc.hpp | 74 ++++++ src/core/logging.cpp | 64 ++--- src/core/logging.hpp | 2 + src/core/main.cpp | 236 +++++++++++++++++-- src/core/paths.cpp | 218 ++++++++++++++--- src/core/paths.hpp | 28 ++- src/crash/handler.cpp | 4 +- src/crash/handler.hpp | 4 +- src/crash/main.cpp | 18 +- 14 files changed, 710 insertions(+), 120 deletions(-) delete mode 100644 src/core/crashinfo.cpp create mode 100644 src/core/instanceinfo.cpp rename src/core/{crashinfo.hpp => instanceinfo.hpp} (66%) create mode 100644 src/core/ipc.cpp create mode 100644 src/core/ipc.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 800da496..75c16537 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -41,8 +41,9 @@ qt_add_library(quickshell-core STATIC clock.cpp logging.cpp paths.cpp - crashinfo.cpp + instanceinfo.cpp common.cpp + ipc.cpp ) if (CRASH_REPORTER) diff --git a/src/core/crashinfo.cpp b/src/core/crashinfo.cpp deleted file mode 100644 index f441530f..00000000 --- a/src/core/crashinfo.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "crashinfo.hpp" - -#include - -QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.configPath << info.shellId << info.launchTime << info.noColor; - return stream; -} - -QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.configPath >> info.shellId >> info.launchTime >> info.noColor; - return stream; -} - -namespace qs::crash { - -CrashInfo CrashInfo::INSTANCE = {}; // NOLINT - -} diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp new file mode 100644 index 00000000..794212b8 --- /dev/null +++ b/src/core/instanceinfo.cpp @@ -0,0 +1,31 @@ +#include "instanceinfo.hpp" + +#include + +QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { + stream << info.instanceId << info.configPath << info.shellId << info.launchTime; + return stream; +} + +QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { + stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime; + return stream; +} + +QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) { + stream << info.instance << info.noColor << info.sparseLogsOnly; + return stream; +} + +QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) { + stream >> info.instance >> info.noColor >> info.sparseLogsOnly; + return stream; +} + +InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT + +namespace qs::crash { + +CrashInfo CrashInfo::INSTANCE = {}; // NOLINT + +} diff --git a/src/core/crashinfo.hpp b/src/core/instanceinfo.hpp similarity index 66% rename from src/core/crashinfo.hpp rename to src/core/instanceinfo.hpp index a867563f..21bb62d3 100644 --- a/src/core/crashinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -4,10 +4,17 @@ #include struct InstanceInfo { + QString instanceId; QString configPath; QString shellId; QString initialWorkdir; QDateTime launchTime; + + static InstanceInfo CURRENT; // NOLINT +}; + +struct RelaunchInfo { + InstanceInfo instance; bool noColor = false; bool sparseLogsOnly = false; }; @@ -15,6 +22,9 @@ struct InstanceInfo { QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info); QDataStream& operator>>(QDataStream& stream, InstanceInfo& info); +QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info); +QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info); + namespace qs::crash { struct CrashInfo { diff --git a/src/core/ipc.cpp b/src/core/ipc.cpp new file mode 100644 index 00000000..406d0909 --- /dev/null +++ b/src/core/ipc.cpp @@ -0,0 +1,119 @@ +#include "ipc.hpp" +#include + +#include +#include +#include +#include +#include + +#include "generation.hpp" +#include "paths.hpp" + +namespace qs::ipc { + +Q_LOGGING_CATEGORY(logIpc, "quickshell.ipc", QtWarningMsg); + +IpcServer::IpcServer(const QString& path) { + QObject::connect(&this->server, &QLocalServer::newConnection, this, &IpcServer::onNewConnection); + + QLocalServer::removeServer(path); + + if (!this->server.listen(path)) { + qCCritical(logIpc) << "Failed to start IPC server on path" << path; + return; + } + + qCInfo(logIpc) << "Started IPC server on path" << path; +} + +void IpcServer::start() { + if (auto* run = QsPaths::instance()->instanceRunDir()) { + auto path = run->filePath("ipc.sock"); + new IpcServer(path); + } else { + qCCritical(logIpc + ) << "Could not start IPC server as the instance runtime path could not be created."; + } +} + +void IpcServer::onNewConnection() { + while (auto* connection = this->server.nextPendingConnection()) { + new IpcServerConnection(connection, this); + } +} + +IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server) + : QObject(server) + , socket(socket) { + socket->setParent(this); + this->stream.setDevice(socket); + QObject::connect(socket, &QLocalSocket::disconnected, this, &IpcServerConnection::onDisconnected); + QObject::connect(socket, &QLocalSocket::readyRead, this, &IpcServerConnection::onReadyRead); + + qCInfo(logIpc) << "New IPC connection" << this; +} + +void IpcServerConnection::onDisconnected() { + qCInfo(logIpc) << "IPC connection disconnected" << this; +} + +void IpcServerConnection::onReadyRead() { + this->stream.startTransaction(); + + this->stream.startTransaction(); + auto command = IpcCommand::Unknown; + this->stream >> command; + if (!this->stream.commitTransaction()) return; + + switch (command) { + case IpcCommand::Kill: + qInfo() << "Exiting due to IPC request."; + EngineGeneration::currentGeneration()->quit(); + break; + default: + qCCritical(logIpc) << "Received invalid IPC command from" << this; + this->socket->disconnectFromServer(); + break; + } + + if (!this->stream.commitTransaction()) return; +} + +IpcClient::IpcClient(const QString& path) { + QObject::connect(&this->socket, &QLocalSocket::connected, this, &IpcClient::connected); + QObject::connect(&this->socket, &QLocalSocket::disconnected, this, &IpcClient::disconnected); + QObject::connect(&this->socket, &QLocalSocket::errorOccurred, this, &IpcClient::onError); + + this->socket.connectToServer(path); + this->stream.setDevice(&this->socket); +} + +bool IpcClient::isConnected() const { return this->socket.isValid(); } + +void IpcClient::waitForConnected() { this->socket.waitForConnected(); } +void IpcClient::waitForDisconnected() { this->socket.waitForDisconnected(); } + +void IpcClient::kill() { + qCDebug(logIpc) << "Sending kill command..."; + this->stream << IpcCommand::Kill; + this->socket.flush(); +} + +void IpcClient::onError(QLocalSocket::LocalSocketError error) { + qCCritical(logIpc) << "Socket Error" << error; +} + +bool IpcClient::connect(const QString& id, const std::function& callback) { + auto path = QsPaths::ipcPath(id); + auto client = IpcClient(path); + qCDebug(logIpc) << "Connecting to instance" << id << "at" << path; + + client.waitForConnected(); + if (!client.isConnected()) return false; + qCDebug(logIpc) << "Connected."; + + callback(client); + return true; +} +} // namespace qs::ipc diff --git a/src/core/ipc.hpp b/src/core/ipc.hpp new file mode 100644 index 00000000..a62f7b77 --- /dev/null +++ b/src/core/ipc.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace qs::ipc { + +enum class IpcCommand : quint8 { + Unknown = 0, + Kill, +}; + +class IpcServer: public QObject { + Q_OBJECT; + +public: + explicit IpcServer(const QString& path); + + static void start(); + +private slots: + void onNewConnection(); + +private: + QLocalServer server; +}; + +class IpcServerConnection: public QObject { + Q_OBJECT; + +public: + explicit IpcServerConnection(QLocalSocket* socket, IpcServer* server); + +private slots: + void onDisconnected(); + void onReadyRead(); + +private: + QLocalSocket* socket; + QDataStream stream; +}; + +class IpcClient: public QObject { + Q_OBJECT; + +public: + explicit IpcClient(const QString& path); + + [[nodiscard]] bool isConnected() const; + void waitForConnected(); + void waitForDisconnected(); + + void kill(); + + [[nodiscard]] static bool + connect(const QString& id, const std::function& callback); + +signals: + void connected(); + void disconnected(); + +private slots: + static void onError(QLocalSocket::LocalSocketError error); + +private: + QLocalSocket socket; + QDataStream stream; +}; + +} // namespace qs::ipc diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 887e145f..fe319f54 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -24,11 +24,13 @@ #include #include -#include "crashinfo.hpp" +#include "instanceinfo.hpp" #include "logging_p.hpp" #include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" +Q_LOGGING_CATEGORY(logBare, "quickshell.bare"); + namespace qs::log { Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); @@ -53,37 +55,41 @@ void LogMessage::formatMessage( stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); } - if (color) { - switch (msg.type) { - case QtDebugMsg: stream << "\033[34m DEBUG"; break; - case QtInfoMsg: stream << "\033[32m INFO"; break; - case QtWarningMsg: stream << "\033[33m WARN"; break; - case QtCriticalMsg: stream << "\033[31m ERROR"; break; - case QtFatalMsg: stream << "\033[31m FATAL"; break; - } + if (msg.category == "quickshell.bare") { + stream << msg.body; } else { - switch (msg.type) { - case QtDebugMsg: stream << " DEBUG"; break; - case QtInfoMsg: stream << " INFO"; break; - case QtWarningMsg: stream << " WARN"; break; - case QtCriticalMsg: stream << " ERROR"; break; - case QtFatalMsg: stream << " FATAL"; break; + if (color) { + switch (msg.type) { + case QtDebugMsg: stream << "\033[34m DEBUG"; break; + case QtInfoMsg: stream << "\033[32m INFO"; break; + case QtWarningMsg: stream << "\033[33m WARN"; break; + case QtCriticalMsg: stream << "\033[31m ERROR"; break; + case QtFatalMsg: stream << "\033[31m FATAL"; break; + } + } else { + switch (msg.type) { + case QtDebugMsg: stream << " DEBUG"; break; + case QtInfoMsg: stream << " INFO"; break; + case QtWarningMsg: stream << " WARN"; break; + case QtCriticalMsg: stream << " ERROR"; break; + case QtFatalMsg: stream << " FATAL"; break; + } } + + const auto isDefault = msg.category == "default"; + + if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; + + if (!isDefault) { + stream << ' ' << msg.category; + } + + if (color && msg.type != QtFatalMsg) stream << "\033[0m"; + + stream << ": " << msg.body; + + if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } - - const auto isDefault = msg.category == "default"; - - if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; - - if (!isDefault) { - stream << ' ' << msg.category; - } - - if (color && msg.type != QtFatalMsg) stream << "\033[0m"; - - stream << ": " << msg.body; - - if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } bool CategoryFilter::shouldDisplay(QtMsgType type) const { diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 88fd6716..618a1744 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -11,6 +11,8 @@ #include #include +Q_DECLARE_LOGGING_CATEGORY(logBare); + namespace qs::log { struct LogMessage { diff --git a/src/core/main.cpp b/src/core/main.cpp index 84d778c4..68d0a40d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,4 +1,6 @@ #include "main.hpp" +#include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -17,8 +20,12 @@ #include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -27,10 +34,13 @@ #include #include #include +#include +#include #include "build.hpp" #include "common.hpp" -#include "crashinfo.hpp" +#include "instanceinfo.hpp" +#include "ipc.hpp" #include "logging.hpp" #include "paths.hpp" #include "plugin.hpp" @@ -40,6 +50,8 @@ #include "../crash/main.hpp" #endif +using qs::ipc::IpcClient; + struct CommandInfo { QString configPath; QString manifestPath; @@ -52,6 +64,8 @@ struct CommandInfo { bool& sparseLogsOnly; }; +QString commandConfigPath(QString path, QString manifest, QString config, bool printInfo); + void processCommand(int argc, char** argv, CommandInfo& info) { QCoreApplication::setApplicationName("quickshell"); @@ -162,12 +176,97 @@ void processCommand(int argc, char** argv, CommandInfo& info) { readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages."); readLog->add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)"); + /// --- + QStringOption instanceId; + pid_t instancePid = -1; + + auto sortInstances = [](QVector& list) { + std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + return a.instance.launchTime < b.instance.launchTime; + }); + }; + + auto selectInstance = [&]() { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) exit(-1); // NOLINT + + QString path; + InstanceLockInfo instance; + + if (instancePid != -1) { + path = QDir(basePath->filePath("by-pid")).filePath(QString::number(instancePid)); + if (!QsPaths::checkLock(path, &instance)) { + qCInfo(logBare) << "No instance found for pid" << instancePid; + exit(-1); // NOLINT + } + } else if (!(*instanceId).isEmpty()) { + path = basePath->filePath("by-pid"); + auto instances = QsPaths::collectInstances(path); + + auto itr = + std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*instanceId); + }); + + instances.erase(itr, instances.end()); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances start with" << *instanceId; + } else if (instances.length() != 1) { + qCInfo(logBare) << "More than one instance starts with" << *instanceId; + + for (auto& instance: instances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + + exit(-1); // NOLINT + } else { + instance = instances.value(0); + } + } else { + auto configFilePath = + commandConfigPath(info.configPath, info.manifestPath, info.configName, info.printInfo); + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + + auto instances = QsPaths::collectInstances(path); + sortInstances(instances); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances for" << configFilePath; + exit(-1); // NOLINT + } + + instance = instances.value(0); + } + + return instance; + }; + + auto* instances = + app.add_subcommand("instances", "List running quickshell instances.")->fallthrough(); + + auto* allInstances = + instances->add_flag("-a,--all", "List all instances instead of just the current config."); + + auto* instancesJson = instances->add_flag("-j,--json", "Output the list as a json."); + + auto* kill = app.add_subcommand("kill", "Kill an instance.")->fallthrough(); + auto* kInstance = app.add_option("-i,--instance", instanceId, "The instance id to kill."); + app.add_option("-p,--pid", instancePid, "The process id to kill.")->excludes(kInstance); + try { app.parse(argc, argv); } catch (const CLI::ParseError& e) { exit(app.exit(e)); // NOLINT }; + // Start log manager - has to happen with an active event loop or offthread can't be started. + LogManager::init(!info.noColor, info.sparseLogsOnly); + if (*printVersion) { std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl; exit(0); // NOLINT @@ -183,6 +282,86 @@ void processCommand(int argc, char** argv, CommandInfo& info) { exit( // NOLINT qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1 ); + } else if (*instances) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) exit(-1); // NOLINT + + QString path; + QString configFilePath; + if (*allInstances) { + path = basePath->filePath("by-pid"); + } else { + configFilePath = + commandConfigPath(info.configPath, info.manifestPath, info.configName, info.printInfo); + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + } + + auto instances = QsPaths::collectInstances(path); + + if (instances.isEmpty()) { + if (*allInstances) { + qCInfo(logBare) << "No running instances."; + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Use --all to list all instances."; + } + } else { + sortInstances(instances); + + if (*instancesJson) { + auto array = QJsonArray(); + + for (auto& instance: instances) { + auto json = QJsonObject(); + + json["id"] = instance.instance.instanceId; + json["pid"] = instance.pid; + json["shell_id"] = instance.instance.shellId; + json["config_path"] = instance.instance.configPath; + json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); + + array.push_back(json); + } + + auto document = QJsonDocument(array); + QTextStream(stdout) << document.toJson(QJsonDocument::Indented); + } else { + for (auto& instance: instances) { + auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); + + auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); + auto remSeconds = runSeconds % 60; + auto runMinutes = (runSeconds - remSeconds) / 60; + auto remMinutes = runMinutes % 60; + auto runHours = (runMinutes - remMinutes) / 60; + auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") + .arg(runHours) + .arg(remMinutes) + .arg(remSeconds); + + qCInfo(logBare).noquote().nospace() + << "Instance " << instance.instance.instanceId << ":\n" + << " Process ID: " << instance.pid << '\n' + << " Shell ID: " << instance.instance.shellId << '\n' + << " Config path: " << instance.instance.configPath << '\n' + << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + } + } + } + exit(0); // NOLINT + } else if (*kill) { + auto instance = selectInstance(); + + auto r = IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + client.kill(); + qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; + }); + + exit(r ? 0 : -1); // NOLINT } } @@ -373,6 +552,26 @@ foundp:; return configFilePath; } +template +QString base36Encode(T number) { + const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + QString result; + + do { + result.prepend(digits[number % 36]); + number /= 36; + } while (number > 0); + + for (auto i = 0; i < result.length() / 2; i++) { + auto opposite = result.length() - i - 1; + auto c = result.at(i); + result[i] = result.at(opposite); + result[opposite] = c; + } + + return result; +} + int qs_main(int argc, char** argv) { #if CRASH_REPORTER qsCheckCrash(argc, argv); @@ -385,6 +584,7 @@ int qs_main(int argc, char** argv) { QString configFilePath; QString initialWorkdir; QString shellId; + QString pathId; int debugPort = -1; bool waitForDebug = false; @@ -411,11 +611,11 @@ int qs_main(int argc, char** argv) { file.seek(0); auto ds = QDataStream(&file); - InstanceInfo info; + RelaunchInfo info; ds >> info; - configFilePath = info.configPath; - initialWorkdir = info.initialWorkdir; + configFilePath = info.instance.configPath; + initialWorkdir = info.instance.initialWorkdir; noColor = info.noColor; sparseLogsOnly = info.sparseLogsOnly; @@ -426,9 +626,9 @@ int qs_main(int argc, char** argv) { << " (Coredumps will be available under that pid.)"; qCritical() << "Further crash information is stored under" - << QsPaths::crashDir(info.shellId, info.launchTime).path(); + << QsPaths::crashDir(info.instance.instanceId).path(); - if (info.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " "a crash loop."; return 0; @@ -452,9 +652,6 @@ int qs_main(int argc, char** argv) { processCommand(argc, argv, command); - // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(!noColor, sparseLogsOnly); - #if CRASH_REPORTER // Started after log manager for pretty debug logs. Unlikely anything will crash before this point, but // this can be moved if it happens. @@ -469,7 +666,8 @@ int qs_main(int argc, char** argv) { ); } - shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + pathId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + shellId = pathId; qInfo() << "Config file path:" << configFilePath; @@ -521,12 +719,18 @@ int qs_main(int argc, char** argv) { if (printInfo) return 0; -#if CRASH_REPORTER - crashHandler.setInstanceInfo(InstanceInfo { + auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + InstanceInfo::CURRENT = InstanceInfo { + .instanceId = base36Encode(getpid()) + base36Encode(launchTime), .configPath = configFilePath, .shellId = shellId, .initialWorkdir = initialWorkdir, .launchTime = qs::Common::LAUNCH_TIME, + }; + +#if CRASH_REPORTER + crashHandler.setInstanceInfo(RelaunchInfo { + .instance = InstanceInfo::CURRENT, .noColor = noColor, .sparseLogsOnly = sparseLogsOnly, }); @@ -536,8 +740,9 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } - QsPaths::init(shellId); - QsPaths::instance()->linkPidRunDir(); + QsPaths::init(shellId, pathId); + QsPaths::instance()->linkRunDir(); + QsPaths::instance()->linkPathDir(); if (auto* cacheDir = QsPaths::instance()->cacheDir()) { auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); @@ -617,6 +822,9 @@ int qs_main(int argc, char** argv) { QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); } + qs::ipc::IpcServer::start(); + QsPaths::instance()->createLock(); + auto root = RootWrapper(configFilePath, shellId); QGuiApplication::setQuitOnLastWindowClosed(false); diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 7162da5c..7b7f91f1 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -1,8 +1,11 @@ #include "paths.hpp" #include +#include #include -#include +#include +#include +#include #include #include #include @@ -10,7 +13,7 @@ #include #include -#include "common.hpp" +#include "instanceinfo.hpp" Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); @@ -19,17 +22,27 @@ QsPaths* QsPaths::instance() { return instance; } -void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(shellId); } +void QsPaths::init(QString shellId, QString pathId) { + auto* instance = QsPaths::instance(); + instance->shellId = std::move(shellId); + instance->pathId = std::move(pathId); +} -QDir QsPaths::crashDir(const QString& shellId, const QDateTime& launchTime) { +QDir QsPaths::crashDir(const QString& id) { auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); dir = QDir(dir.filePath("crashes")); - dir = QDir(dir.filePath(shellId)); - dir = QDir(dir.filePath(QString("run-%1").arg(launchTime.toMSecsSinceEpoch()))); + dir = QDir(dir.filePath(id)); return dir; } +QString QsPaths::ipcPath(const QString& id) { + auto ipcPath = QsPaths::instance()->baseRunDir()->filePath("by-id"); + ipcPath = QDir(ipcPath).filePath(id); + ipcPath = QDir(ipcPath).filePath("ipc.sock"); + return ipcPath; +} + QDir* QsPaths::cacheDir() { if (this->cacheState == DirState::Unknown) { auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); @@ -77,42 +90,45 @@ QDir* QsPaths::baseRunDir() { else return &this->mBaseRunDir; } -QDir* QsPaths::runDir() { - if (this->runState == DirState::Unknown) { +QDir* QsPaths::shellRunDir() { + if (this->shellRunState == DirState::Unknown) { if (auto* baseRunDir = this->baseRunDir()) { - this->mRunDir = QDir(baseRunDir->filePath(this->shellId)); + this->mShellRunDir = QDir(baseRunDir->filePath("by-shell")); + this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId)); - qCDebug(logPaths) << "Initialized runtime path:" << this->mRunDir.path(); + qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path(); - if (!this->mRunDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create runtime directory at" << this->mRunDir.path(); - this->runState = DirState::Failed; + if (!this->mShellRunDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create runtime directory at" + << this->mShellRunDir.path(); + this->shellRunState = DirState::Failed; } else { - this->runState = DirState::Ready; + this->shellRunState = DirState::Ready; } } else { qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to " "create the base runtime path."; - this->runState = DirState::Failed; + this->shellRunState = DirState::Failed; } } - if (this->runState == DirState::Failed) return nullptr; - else return &this->mRunDir; + if (this->shellRunState == DirState::Failed) return nullptr; + else return &this->mShellRunDir; } QDir* QsPaths::instanceRunDir() { if (this->instanceRunState == DirState::Unknown) { - auto* runtimeDir = this->runDir(); + auto* runDir = this->baseRunDir(); - if (!runtimeDir) { + if (!runDir) { qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory " "could not be created."; this->instanceRunState = DirState::Failed; } else { - this->mInstanceRunDir = - runtimeDir->filePath(QString("run-%1").arg(qs::Common::LAUNCH_TIME.toMSecsSinceEpoch())); + auto byIdDir = QDir(runDir->filePath("by-id")); + + this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId); qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); @@ -126,34 +142,162 @@ QDir* QsPaths::instanceRunDir() { } } - if (this->runState == DirState::Failed) return nullptr; + if (this->shellRunState == DirState::Failed) return nullptr; else return &this->mInstanceRunDir; } -void QsPaths::linkPidRunDir() { +void QsPaths::linkRunDir() { if (auto* runDir = this->instanceRunDir()) { auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); + auto* shellDir = this->shellRunDir(); + + if (!shellDir) { + qCCritical(logPaths + ) << "Could not create by-id symlink as the shell runtime path could not be created."; + } else { + auto shellPath = shellDir->filePath(runDir->dirName()); + + QFile::remove(shellPath); + auto r = + symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create id symlink to " << runDir->path() << " at " << shellPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path" + << runDir->path(); + } + } if (!pidDir.mkpath(".")) { qCCritical(logPaths) << "Could not create PID symlink directory."; - return; - } - - auto pidPath = pidDir.filePath(QString::number(getpid())); - - QFile::remove(pidPath); - auto r = symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str()); - - if (r != 0) { - qCCritical(logPaths).nospace() - << "Could not create PID symlink to " << runDir->path() << " at " << pidPath - << " with error code " << errno << ": " << qt_error_string(); } else { - qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path" - << runDir->path(); + auto pidPath = pidDir.filePath(QString::number(getpid())); + + QFile::remove(pidPath); + auto r = + symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create PID symlink to " << runDir->path() << " at " << pidPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path" + << runDir->path(); + } } } else { qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime " "directory could not be created."; } } + +void QsPaths::linkPathDir() { + if (auto* runDir = this->shellRunDir()) { + auto pathDir = QDir(this->baseRunDir()->filePath("by-path")); + + if (!pathDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create path symlink directory."; + return; + } + + auto linkPath = pathDir.filePath(this->pathId); + + QFile::remove(linkPath); + auto r = + symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create path symlink to " << runDir->path() << " at " << linkPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path" + << runDir->path(); + } + } else { + qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the " + "shell runtime directory could not be created."; + } +} + +void QsPaths::createLock() { + if (auto* runDir = this->instanceRunDir()) { + auto path = runDir->filePath("instance.lock"); + auto* file = new QFile(path); // leaked + + if (!file->open(QFile::ReadWrite | QFile::Truncate)) { + qCCritical(logPaths) << "Could not create instance lock at" << path; + return; + } + + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT + qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path + << " with error code " << errno << ": " << qt_error_string(); + } else { + auto stream = QDataStream(file); + stream << InstanceInfo::CURRENT; + file->flush(); + qCDebug(logPaths) << "Created instance lock at" << path; + } + } else { + qCCritical(logPaths + ) << "Could not create instance lock, as the instance runtime directory could not be created."; + } +} + +bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) { + auto file = QFile(QDir(path).filePath("instance.lock")); + if (!file.open(QFile::ReadOnly)) return false; + + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + fcntl(file.handle(), F_GETLK, &lock); // NOLINT + if (lock.l_type == F_UNLCK) return false; + + if (info) { + info->pid = lock.l_pid; + + auto stream = QDataStream(&file); + stream >> info->instance; + } + + return true; +} + +QVector QsPaths::collectInstances(const QString& path) { + qCDebug(logPaths) << "Collecting instances from" << path; + auto instances = QVector(); + auto dir = QDir(path); + + InstanceLockInfo info; + for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + auto path = dir.filePath(entry); + + if (QsPaths::checkLock(path, &info)) { + qCDebug(logPaths).nospace() << "Found live instance " << info.instance.instanceId << " (pid " + << info.pid << ") at " << path; + + instances.push_back(info); + } else { + qCDebug(logPaths) << "Skipped dead instance at" << path; + } + } + + return instances; +} diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 866a33cf..62858bdb 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -2,17 +2,32 @@ #include #include +#include "instanceinfo.hpp" + +struct InstanceLockInfo { + pid_t pid = -1; + InstanceInfo instance; +}; + +QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info); +QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info); + class QsPaths { public: static QsPaths* instance(); - static void init(QString shellId); - static QDir crashDir(const QString& shellId, const QDateTime& launchTime); + static void init(QString shellId, QString pathId); + static QDir crashDir(const QString& id); + static QString ipcPath(const QString& id); + static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr); + static QVector collectInstances(const QString& path); QDir* cacheDir(); QDir* baseRunDir(); - QDir* runDir(); + QDir* shellRunDir(); QDir* instanceRunDir(); - void linkPidRunDir(); + void linkRunDir(); + void linkPathDir(); + void createLock(); private: enum class DirState { @@ -22,12 +37,13 @@ private: }; QString shellId; + QString pathId; QDir mCacheDir; QDir mBaseRunDir; - QDir mRunDir; + QDir mShellRunDir; QDir mInstanceRunDir; DirState cacheState = DirState::Unknown; DirState baseRunState = DirState::Unknown; - DirState runState = DirState::Unknown; + DirState shellRunState = DirState::Unknown; DirState instanceRunState = DirState::Unknown; }; diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index dea6192c..496aaba6 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -14,7 +14,7 @@ #include #include -#include "../core/crashinfo.hpp" +#include "../core/instanceinfo.hpp" extern char** environ; // NOLINT @@ -64,7 +64,7 @@ void CrashHandler::init() { qCInfo(logCrashHandler) << "Crash handler initialized."; } -void CrashHandler::setInstanceInfo(const InstanceInfo& info) { +void CrashHandler::setInstanceInfo(const RelaunchInfo& info) { this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); if (this->d->infoFd == -1) { diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp index de7b46bc..dd618f7f 100644 --- a/src/crash/handler.hpp +++ b/src/crash/handler.hpp @@ -2,7 +2,7 @@ #include -#include "../core/crashinfo.hpp" +#include "../core/instanceinfo.hpp" namespace qs::crash { struct CrashHandlerPrivate; @@ -14,7 +14,7 @@ public: Q_DISABLE_COPY_MOVE(CrashHandler); void init(); - void setInstanceInfo(const InstanceInfo& info); + void setInstanceInfo(const RelaunchInfo& info); private: CrashHandlerPrivate* d; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 52776190..8583ff91 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -15,7 +14,7 @@ #include #include -#include "../core/crashinfo.hpp" +#include "../core/instanceinfo.hpp" #include "../core/logging.hpp" #include "../core/paths.hpp" #include "build.hpp" @@ -23,14 +22,14 @@ Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); -void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instanceInfo); +void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance); void qsCheckCrash(int argc, char** argv) { auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); if (fd.isEmpty()) return; auto app = QApplication(argc, argv); - InstanceInfo instance; + RelaunchInfo info; auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); @@ -42,15 +41,15 @@ void qsCheckCrash(int argc, char** argv) { file.seek(0); auto ds = QDataStream(&file); - ds >> instance; + ds >> info; } - LogManager::init(!instance.noColor, false); - auto crashDir = QsPaths::crashDir(instance.shellId, instance.launchTime); + LogManager::init(!info.noColor, false); + auto crashDir = QsPaths::crashDir(info.instance.instanceId); qCInfo(logCrashReporter) << "Starting crash reporter..."; - recordCrashInfo(crashDir, instance); + recordCrashInfo(crashDir, info.instance); auto gui = CrashReporterGui(crashDir.path(), crashProc); gui.show(); @@ -125,8 +124,7 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "===== Quickshell Crash =====\n"; stream << "Git Revision: " << GIT_REVISION << '\n'; stream << "Crashed process ID: " << crashProc << '\n'; - stream << "Run ID: " << QString("run-%1").arg(instance.launchTime.toMSecsSinceEpoch()) - << '\n'; + stream << "Run ID: " << instance.instanceId << '\n'; stream << "\n===== Shell Information =====\n"; stream << "Shell ID: " << instance.shellId << '\n';