diff --git a/connector/CMakeLists.txt b/connector/CMakeLists.txt index e08a8e3..40adb4a 100644 --- a/connector/CMakeLists.txt +++ b/connector/CMakeLists.txt @@ -27,6 +27,7 @@ set(SRC_FILES syncthingdev.cpp syncthingconnection.cpp syncthingconnection_requests.cpp + syncthingconnection_capture_replay.cpp syncthingconnectionsettings.cpp syncthingnotifier.cpp syncthingconfig.cpp diff --git a/connector/syncthingconnection.cpp b/connector/syncthingconnection.cpp index 381bc36..6d90edb 100644 --- a/connector/syncthingconnection.cpp +++ b/connector/syncthingconnection.cpp @@ -71,6 +71,8 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt : QObject(parent) , m_syncthingUrl(syncthingUrl) , m_apiKey(apiKey) + , m_captureFile(nullptr) + , m_replayFile(nullptr) , m_status(SyncthingStatus::Disconnected) , m_statusComputionFlags(SyncthingStatusComputionFlags::Default) , m_keepPolling(false) @@ -120,7 +122,7 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt QObject::connect(&m_autoReconnectTimer, &QTimer::timeout, this, &SyncthingConnection::autoReconnect); #ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED - setupTestData(); + setupTestData(); // TODO: remove this in favor of capturing/replay feature #endif #ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS @@ -133,6 +135,8 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt std::cerr << displayNames(devs).join(QStringLiteral(", ")).toStdString() << endl; }); #endif + + initCaptureReplay(); } /*! diff --git a/connector/syncthingconnection.h b/connector/syncthingconnection.h index f73f764..a6f55e8 100644 --- a/connector/syncthingconnection.h +++ b/connector/syncthingconnection.h @@ -304,6 +304,11 @@ private Q_SLOTS: void handleAdditionalRequestCanceled(); void recalculateStatus(); + // capture/replay feature + void initCaptureReplay(); + void captureResponse(std::string_view context, const QByteArray &response); + void replyCapturedResponses(); + private: // internal helper methods QNetworkRequest prepareRequest(const QString &path, const QUrlQuery &query, bool rest = true); @@ -321,6 +326,8 @@ private: QByteArray m_apiKey; QString m_user; QString m_password; + QFile *m_captureFile, *m_replayFile; + CppUtilities::DateTime m_lastReplyEvent; SyncthingStatus m_status; SyncthingStatusComputionFlags m_statusComputionFlags; diff --git a/connector/syncthingconnection_capture_replay.cpp b/connector/syncthingconnection_capture_replay.cpp new file mode 100644 index 0000000..8e17ac9 --- /dev/null +++ b/connector/syncthingconnection_capture_replay.cpp @@ -0,0 +1,146 @@ +#include "./syncthingconnection.h" + +#include +#include + +#include + +namespace Data { + +/// \cond +static QString readEnvVar(const char *name) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + return qEnvironmentVariable(name); +#else + return QString::fromLocal8Bit(qgetenv(name)); +#endif +} +/// \endcond + +/*! + * \brief Initializes capturing/replay if corresponding environment variables are set. + */ +void SyncthingConnection::initCaptureReplay() +{ + const auto captureFilePath = readEnvVar("SYNCTHING_TRAY_CAPTURE_FILE"); + if (!captureFilePath.isEmpty()) { + m_captureFile = new QFile(captureFilePath, this); + m_captureFile->open(QFile::WriteOnly); // TODO: error handling + } + const auto replyFilePath = readEnvVar("SYNCTHING_TRAY_REPLAY_FILE"); + if (!replyFilePath.isEmpty()) { + m_replayFile = new QFile(replyFilePath, this); + m_replayFile->open(QFile::ReadOnly); // TODO: error handling + } +} + + +/*! + * \brief Records the responsive if a capture file is initialized. + */ +void SyncthingConnection::captureResponse(std::string_view context, const QByteArray &response) +{ + if (!m_captureFile) { + return; + } + const char marker[] = "--- "; + const auto space = ' ', newLine = '\n'; + const auto timeStamp = CppUtilities::DateTime::exactGmtNow().toIsoString(); + const auto size = CppUtilities::numberToString(response.size()); + m_captureFile->write(marker, 4); + m_captureFile->write(timeStamp.data(), timeStamp.size()); + m_captureFile->write(&space, 1); + m_captureFile->write(context.data(), context.size()); + m_captureFile->write(&space, 1); + m_captureFile->write(response); + m_captureFile->write(&newLine, 1); + m_captureFile->write(response); + m_captureFile->write(&newLine, 1); + // TODO: error handling +} + +/*! + * \brief Replays the captured responses if a reply file is initialized. + */ +void SyncthingConnection::replyCapturedResponses() +{ + if (!m_replayFile) { + return; + } + enum State { + Marker0, Marker1, Marker2, Marker3, TimeStamp, Size, Context, + } state = Marker0; + auto c = char(); + auto timeStampChars = std::string(); + auto timeStamp = CppUtilities::DateTime(); + auto sizeFactor = 1ul; + auto size = 0ul; + auto context = std::string(); + while (m_replayFile->getChar(&c)) { // TODO: error handling + switch (state) { + case Marker0: + case Marker1: + case Marker2: + switch (c) { + case '-': + state = static_cast(state + 1); + break; + default: + state = Marker0; + } + break; + case Marker3: + switch (c) { + case ' ': + state = TimeStamp; + timeStampChars.clear(); + break; + default: + state = Marker0; + } + break; + case TimeStamp: + switch (c) { + case ' ': + try { + timeStamp = CppUtilities::DateTime::fromIsoStringGmt(timeStampChars.data()); + state = Size; + sizeFactor = 0ul, size = 0ul; + } catch (const CppUtilities::ConversionException &c) { + state = Marker0; // TODO: error handling + } + break; + default: + timeStampChars += c; + } + break; + case Size: + if (c >= '0' && c <= '9') { + size += static_cast(c) * sizeFactor; + sizeFactor *= 10ul; + } else if (c == ' ') { + state = Context; + context.clear(); + } else { + state = Marker0; // TODO: error handling + } + break; + case Context: + switch (c) { + case '\n': + state = Marker0; + m_replayFile->read(size); + // TODO: error handling + // TODO: add to some kind of queue for replays (by context) + // TODO: return if queue has enough items + break; + default: + context += c; + } + break; + } + } +} + +} // namespace Data