From a82fbf40c2c00432952d865e8c3d29f0c8b983fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 03:31:49 -0700 Subject: [PATCH] core/command: add log --follow --- src/core/logging.cpp | 143 ++++++++++++++++++++++++++------- src/core/logging.hpp | 10 ++- src/core/logging_p.hpp | 66 +++++++++++++++ src/core/logging_qtprivate.cpp | 25 +----- src/core/logging_qtprivate.hpp | 44 ++++++++++ src/core/main.cpp | 25 ++++-- src/core/paths.cpp | 11 ++- src/core/paths.hpp | 1 + 8 files changed, 262 insertions(+), 63 deletions(-) create mode 100644 src/core/logging_qtprivate.hpp diff --git a/src/core/logging.cpp b/src/core/logging.cpp index a6360fe..265b83b 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -1,10 +1,14 @@ #include "logging.hpp" #include +#include #include +#include #include +#include #include #include +#include #include #include #include @@ -356,6 +360,18 @@ void ThreadLogging::initFs() { delete detailedFile; detailedFile = nullptr; } else { + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT + qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from " + "other instances will not work."; + } + qCInfo(logLogging) << "Saving detailed logs to" << path; } @@ -737,22 +753,13 @@ bool EncodedLogReader::registerCategory() { return true; } -bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString& rulespec) { - QList rules; - - { - QLoggingSettingsParser parser; - parser.setContent(rulespec); - rules = parser.rules(); - } - - auto reader = EncodedLogReader(); - reader.setDevice(device); +bool LogReader::initialize() { + this->reader.setDevice(this->file); bool readable = false; quint8 logVersion = 0; quint8 readerVersion = 0; - if (!reader.readHeader(&readable, &logVersion, &readerVersion)) { + if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) { qCritical() << "Failed to read log header."; return false; } @@ -765,29 +772,33 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString return false; } + return true; +} + +bool LogReader::continueReading() { auto color = LogManager::instance()->colorLogs; - - auto filters = QHash(); - - auto tailRing = RingBuffer(tail); + auto tailRing = RingBuffer(this->remainingTail); LogMessage message; auto stream = QTextStream(stdout); - while (reader.read(&message)) { + auto readCursor = this->file->pos(); + while (this->reader.read(&message)) { + readCursor = this->file->pos(); + CategoryFilter filter; - if (filters.contains(message.readCategoryId)) { - filter = filters.value(message.readCategoryId); + if (this->filters.contains(message.readCategoryId)) { + filter = this->filters.value(message.readCategoryId); } else { - for (const auto& rule: rules) { + for (const auto& rule: this->rules) { filter.applyRule(message.category, rule); } - filters.insert(message.readCategoryId, filter); + this->filters.insert(message.readCategoryId, filter); } if (filter.shouldDisplay(message.type)) { - if (tail == 0) { - LogMessage::formatMessage(stream, message, color, timestamps); + if (this->remainingTail == 0) { + LogMessage::formatMessage(stream, message, color, this->timestamps); stream << '\n'; } else { tailRing.emplace(message); @@ -795,19 +806,97 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString } } - if (tail != 0) { + if (this->remainingTail != 0) { for (auto i = tailRing.size() - 1; i != -1; i--) { auto& message = tailRing.at(i); - LogMessage::formatMessage(stream, message, color, timestamps); + LogMessage::formatMessage(stream, message, color, this->timestamps); stream << '\n'; } } stream << Qt::flush; - if (!device->atEnd()) { + if (this->file->pos() != readCursor) { qCritical() << "An error occurred parsing the end of this log file."; - qCritical() << "Remaining data:" << device->readAll(); + qCritical() << "Remaining data:" << this->file->readAll(); + return false; + } + + return true; +} + +void LogFollower::FcntlWaitThread::run() { + auto lock = flock { + .l_type = F_RDLCK, // won't block other read locks when we take it + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT + + if (r != 0) { + qCWarning(logLogging).nospace() + << "Failed to wait for write locks to be removed from log file with error code " << errno + << ": " << qt_error_string(); + } +} + +bool LogFollower::follow() { + QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked); + + QObject::connect( + &this->fileWatcher, + &QFileSystemWatcher::fileChanged, + this, + &LogFollower::onFileChanged + ); + + this->fileWatcher.addPath(this->path); + this->waitThread.start(); + + auto r = QCoreApplication::exec(); + return r == 0; +} + +void LogFollower::onFileChanged() { + if (!this->reader->continueReading()) { + QCoreApplication::exit(1); + } +} + +void LogFollower::onFileLocked() { + if (!this->reader->continueReading()) { + QCoreApplication::exit(1); + } else { + QCoreApplication::exit(0); + } +} + +bool readEncodedLogs( + QFile* file, + const QString& path, + bool timestamps, + int tail, + bool follow, + const QString& rulespec +) { + QList rules; + + { + QLoggingSettingsParser parser; + parser.setContent(rulespec); + rules = parser.rules(); + } + + auto reader = LogReader(file, timestamps, tail, rules); + + if (!reader.initialize()) return false; + if (!reader.continueReading()) return false; + + if (follow) { + auto follower = LogFollower(&reader, path); + return follower.follow(); } return true; diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 131e840..8b6ea61 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -130,7 +131,14 @@ private: LoggingThreadProxy threadProxy; }; -bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString& rulespec); +bool readEncodedLogs( + QFile* file, + const QString& path, + bool timestamps, + int tail, + bool follow, + const QString& rulespec +); } // namespace qs::log diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp index cb1c371..e248992 100644 --- a/src/core/logging_p.hpp +++ b/src/core/logging_p.hpp @@ -1,12 +1,18 @@ #pragma once +#include + #include #include #include +#include #include +#include +#include #include #include #include "logging.hpp" +#include "logging_qtprivate.hpp" #include "ringbuf.hpp" namespace qs::log { @@ -120,4 +126,64 @@ private: EncodedLogWriter detailedWriter; }; +class LogFollower; + +class LogReader { +public: + explicit LogReader( + QFile* file, + bool timestamps, + int tail, + QList rules + ) + : file(file) + , timestamps(timestamps) + , remainingTail(tail) + , rules(std::move(rules)) {} + + bool initialize(); + bool continueReading(); + +private: + QFile* file; + EncodedLogReader reader; + bool timestamps; + int remainingTail; + QHash filters; + QList rules; + + friend class LogFollower; +}; + +class LogFollower: public QObject { + Q_OBJECT; + +public: + explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {} + + bool follow(); + +private slots: + void onFileChanged(); + void onFileLocked(); + +private: + LogReader* reader; + QString path; + QFileSystemWatcher fileWatcher; + + class FcntlWaitThread: public QThread { + public: + explicit FcntlWaitThread(LogFollower* follower): follower(follower) {} + + protected: + void run() override; + + private: + LogFollower* follower; + }; + + FcntlWaitThread waitThread {this}; +}; + } // namespace qs::log diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp index 05393f0..5078eeb 100644 --- a/src/core/logging_qtprivate.cpp +++ b/src/core/logging_qtprivate.cpp @@ -16,34 +16,13 @@ #include #include +#include "logging_qtprivate.hpp" + namespace qs::log { Q_DECLARE_LOGGING_CATEGORY(logLogging); namespace qt_logging_registry { -class QLoggingRule { -public: - QLoggingRule(); - QLoggingRule(QStringView pattern, bool enabled); - [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; - - enum PatternFlag { - FullText = 0x1, - LeftFilter = 0x2, - RightFilter = 0x4, - MidFilter = LeftFilter | RightFilter - }; - Q_DECLARE_FLAGS(PatternFlags, PatternFlag) - - QString category; - int messageType; - PatternFlags flags; - bool enabled; - -private: - void parse(QStringView pattern); -}; - class QLoggingSettingsParser { public: void setContent(QStringView content); diff --git a/src/core/logging_qtprivate.hpp b/src/core/logging_qtprivate.hpp new file mode 100644 index 0000000..1756340 --- /dev/null +++ b/src/core/logging_qtprivate.hpp @@ -0,0 +1,44 @@ +#pragma once + +// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp. + +// Was unable to properly link the functions when directly using the headers (which we depend +// on anyway), so below is a slightly stripped down copy. Making the originals link would +// be preferable. + +#include +#include +#include +#include + +namespace qs::log { +Q_DECLARE_LOGGING_CATEGORY(logLogging); + +namespace qt_logging_registry { + +class QLoggingRule { +public: + QLoggingRule(); + QLoggingRule(QStringView pattern, bool enabled); + [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; + + enum PatternFlag { + FullText = 0x1, + LeftFilter = 0x2, + RightFilter = 0x4, + MidFilter = LeftFilter | RightFilter + }; + Q_DECLARE_FLAGS(PatternFlags, PatternFlag) + + QString category; + int messageType; + PatternFlags flags; + bool enabled; + +private: + void parse(QStringView pattern); +}; + +} // namespace qt_logging_registry + +} // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index 2e3a43f..e1a6885 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -96,6 +96,7 @@ struct CommandState { bool sparse = false; size_t verbosity = 0; int tail = 0; + bool follow = false; QStringOption rules; QStringOption readoutRules; QStringOption file; @@ -241,13 +242,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } { - auto* sub = cli.add_subcommand( - "log", - "Read quickshell logs.\n" - "If --file is specified, the given file will be read.\n" - "If not, the log of the first launched instance matching" - "the instance selection flags will be read." - ); + auto* sub = cli.add_subcommand("log", "Read quickshell logs.\n") + ->description("If --file is specified, the given file will be read.\n" + "If not, the log of the first launched instance matching" + "the instance selection flags will be read."); auto* file = sub->add_option("--file", state.log.file, "Log file to read."); @@ -255,6 +253,9 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { ->description("Maximum number of lines to print, starting from the bottom.") ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); + sub->add_flag("--follow", state.log.follow) + ->description("Keep reading the log until the logging process terminates."); + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); @@ -267,6 +268,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { { auto* sub = cli.add_subcommand("list", "List running quickshell instances."); + auto* all = sub->add_flag("-a,--all", state.instance.all) ->description("List all instances.\n" "If unspecified, only instances of" @@ -472,7 +474,14 @@ int readLogFile(CommandState& cmd) { return -1; } - return qs::log::readEncodedLogs(&file, cmd.log.timestamp, cmd.log.tail, *cmd.log.readoutRules) + return qs::log::readEncodedLogs( + &file, + path, + cmd.log.timestamp, + cmd.log.tail, + cmd.log.follow, + *cmd.log.readoutRules + ) ? 0 : -1; } diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 7b7f91f..e2b1530 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -36,11 +36,14 @@ QDir QsPaths::crashDir(const QString& id) { return dir; } +QString QsPaths::basePath(const QString& id) { + auto path = QsPaths::instance()->baseRunDir()->filePath("by-id"); + path = QDir(path).filePath(id); + return path; +} + 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; + return QDir(QsPaths::basePath(id)).filePath("ipc.sock"); } QDir* QsPaths::cacheDir() { diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 62858bd..b3042ba 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -17,6 +17,7 @@ public: static QsPaths* instance(); static void init(QString shellId, QString pathId); static QDir crashDir(const QString& id); + static QString basePath(const QString& id); static QString ipcPath(const QString& id); static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr); static QVector collectInstances(const QString& path);