diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,7 @@ qt-job.cpp ${CMAKE_CURRENT_BINARY_DIR}/kazv-version.cpp matrix-sdk.cpp + kazv-session-lock-guard.cpp matrix-room.cpp matrix-room-list.cpp matrix-room-timeline.cpp diff --git a/src/kazv-session-lock-guard.hpp b/src/kazv-session-lock-guard.hpp new file mode 100644 --- /dev/null +++ b/src/kazv-session-lock-guard.hpp @@ -0,0 +1,21 @@ +/* + * This file is part of kazv. + * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#pragma once +#include <kazv-defs.hpp> +#include <memory> +#include <filesystem> + +class KazvSessionLockGuard +{ +public: + KazvSessionLockGuard(const std::filesystem::path &sessionDir); + ~KazvSessionLockGuard(); + +private: + struct Private; + std::unique_ptr<Private> m_d; +}; diff --git a/src/kazv-session-lock-guard.cpp b/src/kazv-session-lock-guard.cpp new file mode 100644 --- /dev/null +++ b/src/kazv-session-lock-guard.cpp @@ -0,0 +1,64 @@ +/* + * This file is part of kazv. + * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <kazv-defs.hpp> +#include <kazv-platform.hpp> +#include <cstring> +#if !KAZV_IS_WINDOWS +#include <fcntl.h> +#include <unistd.h> +#endif +#include "kazv-session-lock-guard.hpp" +#include "kazv-log.hpp" + +struct KazvSessionLockGuard::Private +{ + std::string lockFilePath; + int fd{-1}; + + void cleanup() + { +#if !KAZV_IS_WINDOWS + qCDebug(kazvLog) << "KazvSessionLockGuard::Private::cleanup" << QString::fromStdString(lockFilePath); + if (fd != -1) { + qCDebug(kazvLog) << "unlocking session"; + lockf(fd, F_ULOCK, 0); + close(fd); + fd = -1; + } +#endif + } + + Private(const std::filesystem::path &sessionDir) + : lockFilePath((sessionDir / "lock").string()) + { +#if !KAZV_IS_WINDOWS + qCDebug(kazvLog) << "KazvSessionLockGuard::Private::Private" << QString::fromStdString(lockFilePath); + fd = open(lockFilePath.data(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); + if (fd == -1) { + throw std::runtime_error{std::string("Cannot open lock file: ") + std::strerror(errno)}; + } + + auto res = lockf(fd, F_TLOCK, 0); + if (res == -1) { + cleanup(); + throw std::runtime_error{std::string("Cannot grab lock file: ") + std::strerror(errno)}; + } +#endif + } + + ~Private() + { + cleanup(); + } +}; + +KazvSessionLockGuard::KazvSessionLockGuard(const std::filesystem::path &sessionDir) + : m_d(std::make_unique<Private>(sessionDir)) +{ +} + +KazvSessionLockGuard::~KazvSessionLockGuard() = default; diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp --- a/src/matrix-sdk.hpp +++ b/src/matrix-sdk.hpp @@ -43,6 +43,7 @@ Q_OBJECT QML_ELEMENT + QString m_userDataDir; std::unique_ptr<MatrixSdkPrivate> m_d; /// @param d A dynamically allocated d-pointer, whose ownership @@ -78,7 +79,7 @@ private: // Replaces the store with another one - void emplace(std::optional<Kazv::SdkModel> model); + void emplace(std::optional<Kazv::SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard); Q_SIGNALS: void trigger(Kazv::KazvEvent e); diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp --- a/src/matrix-sdk.cpp +++ b/src/matrix-sdk.cpp @@ -44,6 +44,7 @@ #include "matrix-user-given-attrs-map.hpp" #include "kazv-log.hpp" #include "matrix-utils.hpp" +#include "kazv-session-lock-guard.hpp" using namespace Kazv; // Sdk with qt event loop, identity transform and no enhancers @@ -78,10 +79,11 @@ struct MatrixSdkPrivate { - MatrixSdkPrivate(MatrixSdk *q, bool testing); - MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model); + MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard); + MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard); bool testing; std::string userDataDir; + std::unique_ptr<KazvSessionLockGuard> lockGuard; RandomInterface randomGenerator; QThread *thread; QObject *obj; @@ -126,25 +128,25 @@ explicit CleanupHelper(std::unique_ptr<MatrixSdkPrivate> d) : oldD(std::move(d)) { - // After the thread's event loop is finished, we can delete the root object - connect(oldD->thread, &QThread::finished, oldD->obj, &QObject::deleteLater); - connect(oldD->obj, &QObject::destroyed, this, &QObject::deleteLater); } - std::shared_ptr<MatrixSdkPrivate> oldD; + std::unique_ptr<MatrixSdkPrivate> oldD; void cleanup() { qCInfo(kazvLog) << "start to clean up everything"; oldD->clientOnSecondaryRoot.stopSyncing() - .then([oldD=oldD](auto &&) { + .then([obj=oldD->obj, thread=oldD->thread](auto &&) { qCDebug(kazvLog) << "stopped syncing"; - QMetaObject::invokeMethod(oldD->obj, [thread=oldD->thread]() { + QMetaObject::invokeMethod(obj, [thread]() { thread->quit(); }); }); oldD->thread->wait(); oldD->thread->deleteLater(); + // After the thread's event loop is finished, we can delete the root object + oldD->obj->deleteLater(); + this->deleteLater(); qCInfo(kazvLog) << "thread is done"; } }; @@ -178,6 +180,15 @@ return; } + if (!lockGuard) { + try { + lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir); + } catch (const std::runtime_error &e) { + qCWarning(kazvLog) << "Error locking session: " << e.what(); + return; + } + } + { auto storeStream = std::ofstream(storeFile); if (! storeStream) { @@ -199,9 +210,10 @@ } } -MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing) +MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, std::unique_ptr<KazvSessionLockGuard> lockGuard) : testing(testing) , userDataDir{kazvUserDataDir().toStdString()} + , lockGuard(std::move(lockGuard)) , randomGenerator(QtRandAdapter{}) , thread(new QThread()) , obj(new QObject()) @@ -222,9 +234,10 @@ obj->moveToThread(thread); } -MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model) +MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, bool testing, SdkModel model, std::unique_ptr<KazvSessionLockGuard> lockGuard) : testing(testing) , userDataDir{kazvUserDataDir().toStdString()} + , lockGuard(std::move(lockGuard)) , randomGenerator(QtRandAdapter{}) , thread(new QThread()) , obj(new QObject()) @@ -247,6 +260,7 @@ MatrixSdk::MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent) : QObject(parent) + , m_userDataDir(kazvUserDataDir()) , m_d(std::move(d)) { init(); @@ -300,12 +314,12 @@ } MatrixSdk::MatrixSdk(QObject *parent) - : MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, /* testing = */ false), parent) + : MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, /* testing = */ false, std::unique_ptr<KazvSessionLockGuard>()), parent) { } MatrixSdk::MatrixSdk(SdkModel model, bool testing, QObject *parent) - : MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model)), parent) + : MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model), std::unique_ptr<KazvSessionLockGuard>()), parent) { } @@ -371,7 +385,7 @@ return new MatrixRoomList(m_d->clientOnSecondaryRoot); } -void MatrixSdk::emplace(std::optional<SdkModel> model) +void MatrixSdk::emplace(std::optional<SdkModel> model, std::unique_ptr<KazvSessionLockGuard> lockGuard) { auto testing = m_d->testing; auto userDataDir = m_d->userDataDir; @@ -381,8 +395,8 @@ } m_d = (model.has_value() - ? std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model.value())) - : std::make_unique<MatrixSdkPrivate>(this, testing)); + ? std::make_unique<MatrixSdkPrivate>(this, testing, std::move(model.value()), std::move(lockGuard)) + : std::make_unique<MatrixSdkPrivate>(this, testing, std::move(lockGuard))); m_d->userDataDir = userDataDir; // Re-initialize lager-qt cursors and watchable connections @@ -436,7 +450,8 @@ bool MatrixSdk::loadSession(QString sessionName) { using StdPath = std::filesystem::path; - auto loadFromSession = [this, sessionName](StdPath sessionDir) { + std::unique_ptr<KazvSessionLockGuard> lockGuard; + auto loadFromSession = [this, sessionName, &lockGuard](StdPath sessionDir) { auto storeFile = sessionDir / "store"; auto metadataFile = sessionDir / "metadata"; @@ -488,7 +503,7 @@ qDebug() << "Error when loading session:" << QString::fromStdString(e.what()); return false; } - emplace(std::move(model)); + emplace(std::move(model), std::move(lockGuard)); return true; }; @@ -500,6 +515,12 @@ auto sessionId = parts[1].toStdString(); auto encodedUserId = encodeBase64(userId, Base64Opts::urlSafe); auto sessionDir = userDataDir / "sessions" / encodedUserId / sessionId; + try { + lockGuard = std::make_unique<KazvSessionLockGuard>(sessionDir); + } catch (const std::runtime_error &e) { + qCWarning(kazvLog) << "Error locking session: " << e.what(); + return false; + } return loadFromSession(sessionDir); } qDebug(kazvLog) << "no session found for" << sessionName; @@ -508,7 +529,7 @@ bool MatrixSdk::startNewSession() { - emplace(std::nullopt); + emplace(std::nullopt, std::unique_ptr<KazvSessionLockGuard>()); return true; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -9,6 +9,10 @@ target_include_directories(kazvtestlib PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(kazvtestlib PUBLIC kazvprivlib libkazv::kazvtestfixtures) +add_executable(matrix-sdk-session-loader matrix-sdk-session-loader.cpp matrix-sdk-sessions-test.cpp) +target_compile_definitions(matrix-sdk-session-loader PRIVATE MATRIX_SDK_SESSIONS_TEST_NO_MAIN) +target_link_libraries(matrix-sdk-session-loader Qt${QT_MAJOR_VERSION}::Test kazvtestlib) + ecm_add_tests( qt-job-handler-test.cpp qt-promise-handler-test.cpp diff --git a/src/tests/matrix-sdk-session-loader.cpp b/src/tests/matrix-sdk-session-loader.cpp new file mode 100644 --- /dev/null +++ b/src/tests/matrix-sdk-session-loader.cpp @@ -0,0 +1,26 @@ +/* + * This file is part of kazv. + * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <kazv-defs.hpp> +#include <iostream> +#include <QString> +#include "matrix-sdk-sessions-test.hpp" + +int main([[maybe_unused]] int argc, char *argv[]) +{ + std::string userDataDir = argv[1]; + std::string sessionName = argv[2]; + auto sdk = MatrixSdkSessionsTest::makeMatrixSdkImpl(userDataDir); + auto res = sdk->loadSession(QString::fromStdString(sessionName)); + if (!res) { + std::cout << "cannot load session" << std::endl; + return 1; + } + + std::cout << "loaded session" << std::endl; + while (1) {} + return 0; +} diff --git a/src/tests/matrix-sdk-sessions-test.hpp b/src/tests/matrix-sdk-sessions-test.hpp new file mode 100644 --- /dev/null +++ b/src/tests/matrix-sdk-sessions-test.hpp @@ -0,0 +1,47 @@ +/* + * This file is part of kazv. + * SPDX-FileCopyrightText: 2024 tusooa <tusooa@kazv.moe> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#pragma once +#include <kazv-defs.hpp> +#include <memory> +#include <QObject> +#include <matrix-sdk.hpp> + +class MatrixSdkSessionsTest : public QObject +{ + Q_OBJECT + +private: + std::string m_userDataDir; + + void clearDir(); + void createSession(std::string userId, std::string deviceId); + +public: + template<class ...Args> + static std::unique_ptr<MatrixSdk> makeMatrixSdkImpl(std::string userDataDir, Args &&...args) + { + std::unique_ptr<MatrixSdk> sdk(new MatrixSdk(std::forward<Args>(args)...)); + sdk->setUserDataDir(userDataDir); + return sdk; + } + + template<class ...Args> + std::unique_ptr<MatrixSdk> makeMatrixSdk(Args &&...args) + { + return makeMatrixSdkImpl(m_userDataDir, std::forward<Args>(args)...); + } + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testListSessions(); + void testListLegacySessions(); + void testLoadSession(); + void testSessionLock(); +}; diff --git a/src/tests/matrix-sdk-sessions-test.cpp b/src/tests/matrix-sdk-sessions-test.cpp --- a/src/tests/matrix-sdk-sessions-test.cpp +++ b/src/tests/matrix-sdk-sessions-test.cpp @@ -5,47 +5,22 @@ */ #include <kazv-defs.hpp> +#include <kazv-platform.hpp> #include <memory> #include <filesystem> #include <fstream> #include <QObject> #include <QtTest> +#include <QProcess> +#include <QCoreApplication> #include <crypto/base64.hpp> -#include <matrix-sdk.hpp> #include "test-temp.hpp" +#include "matrix-sdk-sessions-test.hpp" using namespace Kazv; namespace Fs = std::filesystem; using StdPath = Fs::path; -class MatrixSdkSessionsTest : public QObject -{ - Q_OBJECT - -private: - std::string m_userDataDir; - - void clearDir(); - void createSession(std::string userId, std::string deviceId); - - template<class ...Args> - std::unique_ptr<MatrixSdk> makeMatrixSdk(Args &&...args) - { - std::unique_ptr<MatrixSdk> sdk(new MatrixSdk(std::forward<Args>(args)...)); - sdk->setUserDataDir(m_userDataDir); - return sdk; - } - -private Q_SLOTS: - void initTestCase(); - void init(); - void cleanup(); - - void testListSessions(); - void testListLegacySessions(); - void testLoadSession(); -}; - void MatrixSdkSessionsTest::initTestCase() { auto dir = StdPath(kazvTestTempDir()) / "sessions-test"; @@ -70,8 +45,13 @@ SdkModel model; model.client.userId = userId; model.client.deviceId = deviceId; - auto sdk = makeMatrixSdk(model, /* testing = */ false); - sdk->serializeToFile(); + { + auto sdk = makeMatrixSdk(model, /* testing = */ false); + sdk->serializeToFile(); + } + // Not ideal, but this is the only way to wait for + // MatrixSdkPravite to be destroyed. + QTest::qWait(100); } void MatrixSdkSessionsTest::testListSessions() @@ -117,15 +97,44 @@ void MatrixSdkSessionsTest::testLoadSession() { - createSession("@mew:example.com", "device1"); + createSession("@mew:example.com", "device4"); auto sdk = makeMatrixSdk(); - auto res = sdk->loadSession("@mew:example.com/device1"); + auto res = sdk->loadSession("@mew:example.com/device4"); QVERIFY(res); QCOMPARE(sdk->userId(), QStringLiteral("@mew:example.com")); - QCOMPARE(sdk->deviceId(), QStringLiteral("device1")); + QCOMPARE(sdk->deviceId(), QStringLiteral("device4")); } -QTEST_MAIN(MatrixSdkSessionsTest) +void MatrixSdkSessionsTest::testSessionLock() +{ +#if KAZV_IS_WINDOWS + QSKIP("Skipping because session lock is not yet supported on Windows"); +#else + createSession("@mew:example.com", "device5"); + + auto program = QCoreApplication::applicationDirPath() + QStringLiteral("/matrix-sdk-session-loader"); + QProcess proc1; + QStringList args{ + QString::fromStdString(m_userDataDir), + QStringLiteral("@mew:example.com/device5"), + }; + proc1.start(program, args); + proc1.waitForReadyRead(); + auto line = proc1.readLine(); + QCOMPARE(line, QByteArray("loaded session\n")); + + QProcess proc2; + proc2.start(program, args); + auto res = proc2.waitForFinished(); + QVERIFY(res); + QCOMPARE(proc2.exitStatus(), QProcess::NormalExit); + QCOMPARE(proc2.exitCode(), 1); -#include "matrix-sdk-sessions-test.moc" + proc1.kill(); +#endif +} + +#ifndef MATRIX_SDK_SESSIONS_TEST_NO_MAIN +QTEST_MAIN(MatrixSdkSessionsTest) +#endif