Restore BitTorrent session asynchronously

Reduce the total startup time of the application and maintain sufficient responsiveness of the UI during startup due to the following:
1. Load resume data from disk asynchronously in separate thread;
2. Split handling of loaded resume data in chunks;
3. Reduce the number of emitting signals.

PR #16840.
This commit is contained in:
Vladimir Golovnev
2022-07-04 12:48:21 +03:00
committed by GitHub
parent ec1d2cba40
commit be7cfb78de
25 changed files with 927 additions and 553 deletions

View File

@@ -124,6 +124,7 @@ add_library(qbt_base STATIC
bittorrent/peeraddress.cpp
bittorrent/peerinfo.cpp
bittorrent/portforwarderimpl.cpp
bittorrent/resumedatastorage.cpp
bittorrent/session.cpp
bittorrent/speedmonitor.cpp
bittorrent/statistics.cpp

View File

@@ -124,6 +124,7 @@ SOURCES += \
$$PWD/bittorrent/peeraddress.cpp \
$$PWD/bittorrent/peerinfo.cpp \
$$PWD/bittorrent/portforwarderimpl.cpp \
$$PWD/bittorrent/resumedatastorage.cpp \
$$PWD/bittorrent/session.cpp \
$$PWD/bittorrent/speedmonitor.cpp \
$$PWD/bittorrent/statistics.cpp \

View File

@@ -90,21 +90,20 @@ namespace
}
BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path, QObject *parent)
: ResumeDataStorage {parent}
, m_resumeDataPath {path}
, m_ioThread {new QThread {this}}
, m_asyncWorker {new Worker(m_resumeDataPath)}
: ResumeDataStorage(path, parent)
, m_ioThread {new QThread(this)}
, m_asyncWorker {new Worker(path)}
{
Q_ASSERT(path.isAbsolute());
if (!m_resumeDataPath.exists() && !Utils::Fs::mkpath(m_resumeDataPath))
if (!path.exists() && !Utils::Fs::mkpath(path))
{
throw RuntimeError(tr("Cannot create torrent resume folder: \"%1\"")
.arg(m_resumeDataPath.toString()));
.arg(path.toString()));
}
const QRegularExpression filenamePattern {u"^([A-Fa-f0-9]{40})\\.fastresume$"_qs};
const QStringList filenames = QDir(m_resumeDataPath.data()).entryList(QStringList(u"*.fastresume"_qs), QDir::Files, QDir::Unsorted);
const QStringList filenames = QDir(path.data()).entryList(QStringList(u"*.fastresume"_qs), QDir::Files, QDir::Unsorted);
m_registeredTorrents.reserve(filenames.size());
for (const QString &filename : filenames)
@@ -114,7 +113,7 @@ BitTorrent::BencodeResumeDataStorage::BencodeResumeDataStorage(const Path &path,
m_registeredTorrents.append(TorrentID::fromString(rxMatch.captured(1)));
}
loadQueue(m_resumeDataPath / Path(u"queue"_qs));
loadQueue(path / Path(u"queue"_qs));
qDebug() << "Registered torrents count: " << m_registeredTorrents.size();
@@ -134,25 +133,19 @@ QVector<BitTorrent::TorrentID> BitTorrent::BencodeResumeDataStorage::registeredT
return m_registeredTorrents;
}
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const
BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(const TorrentID &id) const
{
const QString idString = id.toString();
const Path fastresumePath = m_resumeDataPath / Path(idString + u".fastresume");
const Path torrentFilePath = m_resumeDataPath / Path(idString + u".torrent");
const Path fastresumePath = path() / Path(idString + u".fastresume");
const Path torrentFilePath = path() / Path(idString + u".torrent");
QFile resumeDataFile {fastresumePath.data()};
if (!resumeDataFile.open(QIODevice::ReadOnly))
{
LogMsg(tr("Cannot read file %1: %2").arg(fastresumePath.toString(), resumeDataFile.errorString()), Log::WARNING);
return std::nullopt;
}
return nonstd::make_unexpected(tr("Cannot read file %1: %2").arg(fastresumePath.toString(), resumeDataFile.errorString()));
QFile metadataFile {torrentFilePath.data()};
if (metadataFile.exists() && !metadataFile.open(QIODevice::ReadOnly))
{
LogMsg(tr("Cannot read file %1: %2").arg(torrentFilePath.toString(), metadataFile.errorString()), Log::WARNING);
return std::nullopt;
}
return nonstd::make_unexpected(tr("Cannot read file %1: %2").arg(torrentFilePath.toString(), metadataFile.errorString()));
const QByteArray data = resumeDataFile.readAll();
const QByteArray metadata = (metadataFile.isOpen() ? metadataFile.readAll() : "");
@@ -160,16 +153,64 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorag
return loadTorrentResumeData(data, metadata);
}
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData(
const QByteArray &data, const QByteArray &metadata) const
void BitTorrent::BencodeResumeDataStorage::doLoadAll() const
{
qDebug() << "Loading torrents count: " << m_registeredTorrents.size();
emit const_cast<BencodeResumeDataStorage *>(this)->loadStarted(m_registeredTorrents);
for (const TorrentID &torrentID : asConst(m_registeredTorrents))
onResumeDataLoaded(torrentID, load(torrentID));
emit const_cast<BencodeResumeDataStorage *>(this)->loadFinished();
}
void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
{
QFile queueFile {queueFilename.data()};
if (!queueFile.exists())
return;
if (!queueFile.open(QFile::ReadOnly))
{
LogMsg(tr("Couldn't load torrents queue: %1").arg(queueFile.errorString()), Log::WARNING);
return;
}
const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_qs};
int start = 0;
while (true)
{
const auto line = QString::fromLatin1(queueFile.readLine().trimmed());
if (line.isEmpty())
break;
const QRegularExpressionMatch rxMatch = hashPattern.match(line);
if (rxMatch.hasMatch())
{
const auto torrentID = BitTorrent::TorrentID::fromString(rxMatch.captured(1));
const int pos = m_registeredTorrents.indexOf(torrentID, start);
if (pos != -1)
{
std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
++start;
}
}
}
}
BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const
{
const QByteArray allData = ((metadata.isEmpty() || data.isEmpty())
? data : (data.chopped(1) + metadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
if (ec || (root.type() != lt::bdecode_node::dict_t))
return std::nullopt;
if (ec)
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
if (root.type() != lt::bdecode_node::dict_t)
return nonstd::make_unexpected(tr("Cannot parse resume data: invalid foormat"));
LoadTorrentParams torrentParams;
torrentParams.restored = true;
@@ -247,7 +288,7 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::BencodeResumeDataStorag
const bool hasMetadata = (p.ti && p.ti->is_valid());
if (!hasMetadata && !root.dict_find("info-hash"))
return std::nullopt;
return nonstd::make_unexpected(tr("Resume data is invalid: neither metadata nor info-hash was found"));
return torrentParams;
}
@@ -276,39 +317,6 @@ void BitTorrent::BencodeResumeDataStorage::storeQueue(const QVector<TorrentID> &
});
}
void BitTorrent::BencodeResumeDataStorage::loadQueue(const Path &queueFilename)
{
QFile queueFile {queueFilename.data()};
if (!queueFile.exists())
return;
if (queueFile.open(QFile::ReadOnly))
{
const QRegularExpression hashPattern {u"^([A-Fa-f0-9]{40})$"_qs};
QString line;
int start = 0;
while (!(line = QString::fromLatin1(queueFile.readLine().trimmed())).isEmpty())
{
const QRegularExpressionMatch rxMatch = hashPattern.match(line);
if (rxMatch.hasMatch())
{
const auto torrentID = TorrentID::fromString(rxMatch.captured(1));
const int pos = m_registeredTorrents.indexOf(torrentID, start);
if (pos != -1)
{
std::swap(m_registeredTorrents[start], m_registeredTorrents[pos]);
++start;
}
}
}
}
else
{
LogMsg(tr("Couldn't load torrents queue from '%1'. Error: %2")
.arg(queueFile.fileName(), queueFile.errorString()), Log::WARNING);
}
}
BitTorrent::BencodeResumeDataStorage::Worker::Worker(const Path &resumeDataDir)
: m_resumeDataDir {resumeDataDir}
{

View File

@@ -31,7 +31,7 @@
#include <QDir>
#include <QVector>
#include "base/path.h"
#include "base/pathfwd.h"
#include "resumedatastorage.h"
class QByteArray;
@@ -49,16 +49,16 @@ namespace BitTorrent
~BencodeResumeDataStorage() override;
QVector<TorrentID> registeredTorrents() const override;
std::optional<LoadTorrentParams> load(const TorrentID &id) const override;
LoadResumeDataResult load(const TorrentID &id) const override;
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
void remove(const TorrentID &id) const override;
void storeQueue(const QVector<TorrentID> &queue) const override;
private:
void doLoadAll() const override;
void loadQueue(const Path &queueFilename);
std::optional<LoadTorrentParams> loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const;
LoadResumeDataResult loadTorrentResumeData(const QByteArray &data, const QByteArray &metadata) const;
const Path m_resumeDataPath;
QVector<TorrentID> m_registeredTorrents;
QThread *m_ioThread = nullptr;

View File

@@ -175,7 +175,7 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(Worker)
public:
Worker(const Path &dbPath, const QString &dbConnectionName);
Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock);
void openDatabase() const;
void closeDatabase() const;
@@ -187,11 +187,64 @@ namespace BitTorrent
private:
const Path m_path;
const QString m_connectionName;
QReadWriteLock &m_dbLock;
};
namespace
{
LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
{
LoadTorrentParams resumeData;
resumeData.restored = true;
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
if (!tagsData.isEmpty())
{
const QStringList tagList = tagsData.split(u',');
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
}
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
}
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
? bencodedResumeData
: (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(root, ec);
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString();
return resumeData;
}
}
}
BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject *parent)
: ResumeDataStorage {parent}
: ResumeDataStorage(dbPath, parent)
, m_ioThread {new QThread(this)}
{
const bool needCreateDB = !dbPath.exists();
@@ -212,7 +265,7 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject
updateDBFromVersion1();
}
m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs);
m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs, m_dbLock);
m_asyncWorker->moveToThread(m_ioThread);
connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater);
m_ioThread->start();
@@ -262,7 +315,7 @@ QVector<BitTorrent::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorren
return registeredTorrents;
}
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
{
const QString selectTorrentStatement = u"SELECT * FROM %1 WHERE %2 = %3;"_qs
.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
@@ -283,56 +336,11 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::lo
}
catch (const RuntimeError &err)
{
LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2")
.arg(id.toString(), err.message()), Log::CRITICAL);
return std::nullopt;
return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2")
.arg(id.toString(), err.message()));
}
LoadTorrentParams resumeData;
resumeData.restored = true;
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
if (!tagsData.isEmpty())
{
const QStringList tagList = tagsData.split(u',');
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
}
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
}
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
? bencodedResumeData
: (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(root, ec);
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString();
return resumeData;
return parseQueryResultRow(query);
}
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
@@ -359,6 +367,49 @@ void BitTorrent::DBResumeDataStorage::storeQueue(const QVector<TorrentID> &queue
});
}
void BitTorrent::DBResumeDataStorage::doLoadAll() const
{
const QString connectionName = u"ResumeDataStorageLoadAll"_qs;
{
auto db = QSqlDatabase::addDatabase(u"QSQLITE"_qs, connectionName);
db.setDatabaseName(path().data());
if (!db.open())
throw RuntimeError(db.lastError().text());
QSqlQuery query {db};
const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_qs
.arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
const QReadLocker locker {&m_dbLock};
if (!query.exec(selectTorrentIDStatement))
throw RuntimeError(query.lastError().text());
QVector<TorrentID> registeredTorrents;
registeredTorrents.reserve(query.size());
while (query.next())
registeredTorrents.append(TorrentID::fromString(query.value(0).toString()));
emit const_cast<DBResumeDataStorage *>(this)->loadStarted(registeredTorrents);
const auto selectStatement = u"SELECT * FROM %1 ORDER BY %2;"_qs.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
if (!query.exec(selectStatement))
throw RuntimeError(query.lastError().text());
while (query.next())
{
const auto torrentID = TorrentID::fromString(query.value(DB_COLUMN_TORRENT_ID.name).toString());
onResumeDataLoaded(torrentID, parseQueryResultRow(query));
}
}
emit const_cast<DBResumeDataStorage *>(this)->loadFinished();
QSqlDatabase::removeDatabase(connectionName);
}
int BitTorrent::DBResumeDataStorage::currentDBVersion() const
{
const auto selectDBVersionStatement = u"SELECT %1 FROM %2 WHERE %3 = %4;"_qs
@@ -372,6 +423,8 @@ int BitTorrent::DBResumeDataStorage::currentDBVersion() const
query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
const QReadLocker locker {&m_dbLock};
if (!query.exec())
throw RuntimeError(query.lastError().text());
@@ -390,6 +443,8 @@ void BitTorrent::DBResumeDataStorage::createDB() const
{
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock};
if (!db.transaction())
throw RuntimeError(db.lastError().text());
@@ -453,6 +508,8 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
{
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock};
if (!db.transaction())
throw RuntimeError(db.lastError().text());
@@ -485,9 +542,10 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
}
}
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, const QString &dbConnectionName)
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock)
: m_path {dbPath}
, m_connectionName {dbConnectionName}
, m_dbLock {dbLock}
{
}
@@ -612,6 +670,7 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L
if (!bencodedMetadata.isEmpty())
query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
const QWriteLocker locker {&m_dbLock};
if (!query.exec())
throw RuntimeError(query.lastError().text());
}
@@ -636,6 +695,8 @@ void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id) const
throw RuntimeError(query.lastError().text());
query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
const QWriteLocker locker {&m_dbLock};
if (!query.exec())
throw RuntimeError(query.lastError().text());
}
@@ -656,6 +717,8 @@ void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector<TorrentID
try
{
const QWriteLocker locker {&m_dbLock};
if (!db.transaction())
throw RuntimeError(db.lastError().text());

View File

@@ -28,6 +28,8 @@
#pragma once
#include <QReadWriteLock>
#include "base/pathfwd.h"
#include "resumedatastorage.h"
@@ -45,12 +47,14 @@ namespace BitTorrent
~DBResumeDataStorage() override;
QVector<TorrentID> registeredTorrents() const override;
std::optional<LoadTorrentParams> load(const TorrentID &id) const override;
LoadResumeDataResult load(const TorrentID &id) const override;
void store(const TorrentID &id, const LoadTorrentParams &resumeData) const override;
void remove(const TorrentID &id) const override;
void storeQueue(const QVector<TorrentID> &queue) const override;
private:
void doLoadAll() const override;
int currentDBVersion() const;
void createDB() const;
void updateDBFromVersion1() const;
@@ -59,5 +63,7 @@ namespace BitTorrent
class Worker;
Worker *m_asyncWorker = nullptr;
mutable QReadWriteLock m_dbLock;
};
}

View File

@@ -0,0 +1,76 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2022 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "resumedatastorage.h"
#include <utility>
#include <QMetaObject>
#include <QMutexLocker>
#include <QThread>
const int TORRENTIDLIST_TYPEID = qRegisterMetaType<QVector<BitTorrent::TorrentID>>();
BitTorrent::ResumeDataStorage::ResumeDataStorage(const Path &path, QObject *parent)
: QObject(parent)
, m_path {path}
{
}
Path BitTorrent::ResumeDataStorage::path() const
{
return m_path;
}
void BitTorrent::ResumeDataStorage::loadAll() const
{
m_loadedResumeData.reserve(1024);
auto *loadingThread = QThread::create([this]()
{
doLoadAll();
});
connect(loadingThread, &QThread::finished, loadingThread, &QObject::deleteLater);
loadingThread->start();
}
QVector<BitTorrent::LoadedResumeData> BitTorrent::ResumeDataStorage::fetchLoadedResumeData() const
{
const QMutexLocker locker {&m_loadedResumeDataMutex};
const QVector<BitTorrent::LoadedResumeData> loadedResumeData = m_loadedResumeData;
m_loadedResumeData.clear();
return loadedResumeData;
}
void BitTorrent::ResumeDataStorage::onResumeDataLoaded(const TorrentID &torrentID, const LoadResumeDataResult &loadResumeDataResult) const
{
const QMutexLocker locker {&m_loadedResumeDataMutex};
m_loadedResumeData.append({torrentID, loadResumeDataResult});
}

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2022 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -28,15 +28,25 @@
#pragma once
#include <optional>
#include <QtContainerFwd>
#include <QMutex>
#include <QObject>
#include <QVector>
#include "base/3rdparty/expected.hpp"
#include "base/path.h"
#include "infohash.h"
#include "loadtorrentparams.h"
namespace BitTorrent
{
class TorrentID;
struct LoadTorrentParams;
using LoadResumeDataResult = nonstd::expected<LoadTorrentParams, QString>;
struct LoadedResumeData
{
TorrentID torrentID;
LoadResumeDataResult result;
};
class ResumeDataStorage : public QObject
{
@@ -44,12 +54,31 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(ResumeDataStorage)
public:
using QObject::QObject;
explicit ResumeDataStorage(const Path &path, QObject *parent = nullptr);
Path path() const;
virtual QVector<TorrentID> registeredTorrents() const = 0;
virtual std::optional<LoadTorrentParams> load(const TorrentID &id) const = 0;
virtual LoadResumeDataResult load(const TorrentID &id) const = 0;
virtual void store(const TorrentID &id, const LoadTorrentParams &resumeData) const = 0;
virtual void remove(const TorrentID &id) const = 0;
virtual void storeQueue(const QVector<TorrentID> &queue) const = 0;
void loadAll() const;
QVector<LoadedResumeData> fetchLoadedResumeData() const;
signals:
void loadStarted(const QVector<BitTorrent::TorrentID> &torrents);
void loadFinished();
protected:
void onResumeDataLoaded(const TorrentID &torrentID, const LoadResumeDataResult &loadResumeDataResult) const;
private:
virtual void doLoadAll() const = 0;
const Path m_path;
mutable QVector<LoadedResumeData> m_loadedResumeData;
mutable QMutex m_loadedResumeDataMutex;
};
}

View File

@@ -104,6 +104,7 @@
#include "magneturi.h"
#include "nativesessionextension.h"
#include "portforwarderimpl.h"
#include "resumedatastorage.h"
#include "statistics.h"
#include "torrentimpl.h"
#include "tracker.h"
@@ -112,6 +113,7 @@ using namespace std::chrono_literals;
using namespace BitTorrent;
const Path CATEGORIES_FILE_NAME {u"categories.json"_qs};
const int MAX_PROCESSING_RESUMEDATA_COUNT = 50;
namespace
{
@@ -297,6 +299,23 @@ namespace
#endif
}
struct BitTorrent::Session::ResumeSessionContext final : public QObject
{
using QObject::QObject;
ResumeDataStorage *startupStorage = nullptr;
ResumeDataStorageType currentStorageType = ResumeDataStorageType::Legacy;
QVector<LoadedResumeData> loadedResumeData;
int processingResumeDataCount = 0;
bool isLoadFinished = false;
bool isLoadedResumeDataHandlingEnqueued = false;
QSet<QString> recoveredCategories;
#ifdef QBT_USES_LIBTORRENT2
QSet<TorrentID> indexedTorrents;
QSet<TorrentID> skippedIDs;
#endif
};
const int addTorrentParamsId = qRegisterMetaType<AddTorrentParams>();
// Session
@@ -467,7 +486,6 @@ Session::Session(QObject *parent)
const QStringList storedTags = m_storedTags.get();
m_tags = {storedTags.cbegin(), storedTags.cend()};
enqueueRefresh();
updateSeedingLimitTimer();
populateAdditionalTrackers();
if (isExcludedFileNamesEnabled())
@@ -494,19 +512,12 @@ Session::Session(QObject *parent)
m_ioThread->start();
// Regular saving of fastresume data
connect(m_resumeDataTimer, &QTimer::timeout, this, [this]() { generateResumeData(); });
const int saveInterval = saveResumeDataInterval();
if (saveInterval > 0)
{
m_resumeDataTimer->setInterval(std::chrono::minutes(saveInterval));
m_resumeDataTimer->start();
}
// initialize PortForwarder instance
new PortForwarderImpl {m_nativeSession};
new PortForwarderImpl(m_nativeSession);
initMetrics();
prepareStartup();
}
bool Session::isDHTEnabled() const
@@ -1062,6 +1073,285 @@ void Session::configureComponents()
#endif
}
void Session::prepareStartup()
{
qDebug("Initializing torrents resume data storage...");
const Path dbPath = specialFolderLocation(SpecialFolder::Data) / Path(u"torrents.db"_qs);
const bool dbStorageExists = dbPath.exists();
auto *context = new ResumeSessionContext(this);
context->currentStorageType = resumeDataStorageType();
if (context->currentStorageType == ResumeDataStorageType::SQLite)
{
m_resumeDataStorage = new DBResumeDataStorage(dbPath, this);
if (!dbStorageExists)
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
context->startupStorage = new BencodeResumeDataStorage(dataPath, this);
}
}
else
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this);
if (dbStorageExists)
context->startupStorage = new DBResumeDataStorage(dbPath, this);
}
if (!context->startupStorage)
context->startupStorage = m_resumeDataStorage;
connect(context->startupStorage, &ResumeDataStorage::loadStarted, context
, [this, context](const QVector<TorrentID> &torrents)
{
#ifdef QBT_USES_LIBTORRENT2
context->indexedTorrents = QSet<TorrentID>(torrents.cbegin(), torrents.cend());
#endif
handleLoadedResumeData(context);
});
connect(context->startupStorage, &ResumeDataStorage::loadFinished, context, [context]()
{
context->isLoadFinished = true;
});
connect(this, &Session::torrentsLoaded, context, [this, context](const QVector<Torrent *> &torrents)
{
context->processingResumeDataCount -= torrents.count();
if (!context->isLoadedResumeDataHandlingEnqueued)
{
QMetaObject::invokeMethod(this, [this, context]() { handleLoadedResumeData(context); }, Qt::QueuedConnection);
context->isLoadedResumeDataHandlingEnqueued = true;
}
m_nativeSession->post_torrent_updates();
m_refreshEnqueued = true;
});
context->startupStorage->loadAll();
}
void Session::handleLoadedResumeData(ResumeSessionContext *context)
{
context->isLoadedResumeDataHandlingEnqueued = false;
while (context->processingResumeDataCount < MAX_PROCESSING_RESUMEDATA_COUNT)
{
if (context->loadedResumeData.isEmpty())
context->loadedResumeData = context->startupStorage->fetchLoadedResumeData();
if (context->loadedResumeData.isEmpty())
{
if (context->processingResumeDataCount == 0)
{
if (context->isLoadFinished)
{
endStartup(context);
}
else if (!context->isLoadedResumeDataHandlingEnqueued)
{
QMetaObject::invokeMethod(this, [this, context]() { handleLoadedResumeData(context); }, Qt::QueuedConnection);
context->isLoadedResumeDataHandlingEnqueued = true;
}
}
break;
}
processNextResumeData(context);
}
}
void Session::processNextResumeData(ResumeSessionContext *context)
{
const LoadedResumeData loadedResumeDataItem = context->loadedResumeData.takeFirst();
TorrentID torrentID = loadedResumeDataItem.torrentID;
#ifdef QBT_USES_LIBTORRENT2
if (context->skippedIDs.contains(torrentID))
return;
#endif
const nonstd::expected<LoadTorrentParams, QString> &loadResumeDataResult = loadedResumeDataItem.result;
if (!loadResumeDataResult)
{
LogMsg(tr("Failed to resume torrent. Torrent: \"%1\". Reason: \"%2\"")
.arg(torrentID.toString(), loadResumeDataResult.error()), Log::CRITICAL);
return;
}
LoadTorrentParams resumeData = *loadResumeDataResult;
bool needStore = false;
#ifdef QBT_USES_LIBTORRENT2
const lt::info_hash_t infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hashes()
: resumeData.ltAddTorrentParams.info_hashes);
const bool isHybrid = infoHash.has_v1() && infoHash.has_v2();
const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash);
const auto torrentIDv1 = TorrentID::fromInfoHash(lt::info_hash_t(infoHash.v1));
if (torrentID == torrentIDv2)
{
if (isHybrid && context->indexedTorrents.contains(torrentIDv1))
{
// if we don't have metadata, try to find it in alternative "resume data"
if (!resumeData.ltAddTorrentParams.ti)
{
const nonstd::expected<LoadTorrentParams, QString> loadAltResumeDataResult = context->startupStorage->load(torrentIDv1);
if (loadAltResumeDataResult)
resumeData.ltAddTorrentParams.ti = loadAltResumeDataResult->ltAddTorrentParams.ti;
}
// remove alternative "resume data" and skip the attempt to load it
m_resumeDataStorage->remove(torrentIDv1);
context->skippedIDs.insert(torrentIDv1);
}
}
else if (torrentID == torrentIDv1)
{
torrentID = torrentIDv2;
needStore = true;
m_resumeDataStorage->remove(torrentIDv1);
if (context->indexedTorrents.contains(torrentID))
{
context->skippedIDs.insert(torrentID);
const nonstd::expected<LoadTorrentParams, QString> loadPreferredResumeDataResult = context->startupStorage->load(torrentID);
if (loadPreferredResumeDataResult)
{
std::shared_ptr<lt::torrent_info> ti = resumeData.ltAddTorrentParams.ti;
resumeData = *loadPreferredResumeDataResult;
if (!resumeData.ltAddTorrentParams.ti)
resumeData.ltAddTorrentParams.ti = ti;
}
}
}
else
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
return;
}
#else
const lt::sha1_hash infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hash()
: resumeData.ltAddTorrentParams.info_hash);
if (torrentID != TorrentID::fromInfoHash(infoHash))
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
return;
}
#endif
if (m_resumeDataStorage != context->startupStorage)
needStore = true;
// TODO: Remove the following upgrade code in v4.6
// == BEGIN UPGRADE CODE ==
if (!needStore)
{
if (m_needUpgradeDownloadPath && isDownloadPathEnabled() && !resumeData.useAutoTMM)
{
resumeData.downloadPath = downloadPath();
needStore = true;
}
}
// == END UPGRADE CODE ==
if (needStore)
m_resumeDataStorage->store(torrentID, resumeData);
const QString category = resumeData.category;
bool isCategoryRecovered = context->recoveredCategories.contains(category);
if (!category.isEmpty() && (isCategoryRecovered || !m_categories.contains(category)))
{
if (!isCategoryRecovered)
{
if (addCategory(category))
{
context->recoveredCategories.insert(category);
isCategoryRecovered = true;
LogMsg(tr("Detected inconsistent data: category is missing from the configuration file."
" Category will be recovered but its settings will be reset to default."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
else
{
resumeData.category.clear();
LogMsg(tr("Detected inconsistent data: invalid category. Torrent: \"%1\". Category: \"%2\"")
.arg(torrentID.toString(), category), Log::WARNING);
}
}
// We should check isCategoryRecovered again since the category
// can be just recovered by the code above
if (isCategoryRecovered && resumeData.useAutoTMM)
{
const Path storageLocation {resumeData.ltAddTorrentParams.save_path};
if ((storageLocation != categorySavePath(resumeData.category)) && (storageLocation != categoryDownloadPath(resumeData.category)))
{
resumeData.useAutoTMM = false;
resumeData.savePath = storageLocation;
resumeData.downloadPath = {};
LogMsg(tr("Detected mismatch between the save paths of the recovered category and the current save path of the torrent."
" Torrent is now switched to Manual mode."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
}
}
resumeData.ltAddTorrentParams.userdata = LTClientData(new ExtensionData);
#ifndef QBT_USES_LIBTORRENT2
resumeData.ltAddTorrentParams.storage = customStorageConstructor;
#endif
qDebug() << "Starting up torrent" << torrentID.toString() << "...";
m_loadingTorrents.insert(torrentID, resumeData);
m_nativeSession->async_add_torrent(resumeData.ltAddTorrentParams);
++context->processingResumeDataCount;
}
void Session::endStartup(ResumeSessionContext *context)
{
if (m_resumeDataStorage != context->startupStorage)
{
if (isQueueingSystemEnabled())
saveTorrentsQueue();
const Path dbPath = context->startupStorage->path();
delete context->startupStorage;
if (context->currentStorageType == ResumeDataStorageType::Legacy)
Utils::Fs::removeFile(dbPath);
}
context->deleteLater();
m_nativeSession->resume();
if (m_refreshEnqueued)
m_refreshEnqueued = false;
else
enqueueRefresh();
// Regular saving of fastresume data
connect(m_resumeDataTimer, &QTimer::timeout, this, &Session::generateResumeData);
const int saveInterval = saveResumeDataInterval();
if (saveInterval > 0)
{
m_resumeDataTimer->setInterval(std::chrono::minutes(saveInterval));
m_resumeDataTimer->start();
}
m_isRestored = true;
emit restored();
}
void Session::initializeNativeSession()
{
const std::string peerId = lt::generate_fingerprint(PEER_ID, QBT_VERSION_MAJOR, QBT_VERSION_MINOR, QBT_VERSION_BUGFIX, QBT_VERSION_BUILD);
@@ -1100,7 +1390,7 @@ void Session::initializeNativeSession()
break;
}
#endif
m_nativeSession = new lt::session {sessionParams};
m_nativeSession = new lt::session(sessionParams, lt::session::paused);
LogMsg(tr("Peer ID: \"%1\"").arg(QString::fromStdString(peerId)), Log::INFO);
LogMsg(tr("HTTP User-Agent: \"%1\"").arg(USER_AGENT), Log::INFO);
@@ -1764,9 +2054,7 @@ void Session::fileSearchFinished(const TorrentID &id, const Path &savePath, cons
const auto loadingTorrentsIter = m_loadingTorrents.find(id);
if (loadingTorrentsIter != m_loadingTorrents.end())
{
LoadTorrentParams params = loadingTorrentsIter.value();
m_loadingTorrents.erase(loadingTorrentsIter);
LoadTorrentParams &params = loadingTorrentsIter.value();
lt::add_torrent_params &p = params.ltAddTorrentParams;
p.save_path = savePath.toString().toStdString();
@@ -1775,7 +2063,7 @@ void Session::fileSearchFinished(const TorrentID &id, const Path &savePath, cons
for (int i = 0; i < fileNames.size(); ++i)
p.renamed_files[nativeIndexes[i]] = fileNames[i].toString().toStdString();
loadTorrent(params);
m_nativeSession->async_add_torrent(p);
}
}
@@ -2054,6 +2342,9 @@ bool Session::addTorrent(const QString &source, const AddTorrentParams &params)
{
// `source`: .torrent file path/url or magnet uri
if (!isRestored())
return false;
if (Net::DownloadManager::hasSupportedScheme(source))
{
LogMsg(tr("Downloading torrent, please wait... Source: \"%1\"").arg(source));
@@ -2083,13 +2374,20 @@ bool Session::addTorrent(const QString &source, const AddTorrentParams &params)
bool Session::addTorrent(const MagnetUri &magnetUri, const AddTorrentParams &params)
{
if (!magnetUri.isValid()) return false;
if (!isRestored())
return false;
if (!magnetUri.isValid())
return false;
return addTorrent_impl(magnetUri, params);
}
bool Session::addTorrent(const TorrentInfo &torrentInfo, const AddTorrentParams &params)
{
if (!isRestored())
return false;
return addTorrent_impl(torrentInfo, params);
}
@@ -2152,6 +2450,8 @@ LoadTorrentParams Session::initLoadTorrentParams(const AddTorrentParams &addTorr
// Add a torrent to the BitTorrent session
bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams)
{
Q_ASSERT(isRestored());
const bool hasMetadata = std::holds_alternative<TorrentInfo>(source);
const auto id = TorrentID::fromInfoHash(hasMetadata ? std::get<TorrentInfo>(source).infoHash() : std::get<MagnetUri>(source).infoHash());
@@ -2332,36 +2632,18 @@ bool Session::addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source
p.added_time = std::time(nullptr);
if (!isFindingIncompleteFiles)
return loadTorrent(loadTorrentParams);
m_loadingTorrents.insert(id, loadTorrentParams);
return true;
}
// Add a torrent to the BitTorrent session
bool Session::loadTorrent(LoadTorrentParams params)
{
lt::add_torrent_params &p = params.ltAddTorrentParams;
// Limits
p.max_connections = maxConnectionsPerTorrent();
p.max_uploads = maxUploadsPerTorrent();
p.userdata = LTClientData(new ExtensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
// Limits
p.max_connections = maxConnectionsPerTorrent();
p.max_uploads = maxUploadsPerTorrent();
const bool hasMetadata = (p.ti && p.ti->is_valid());
#ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(hasMetadata ? p.ti->info_hashes() : p.info_hashes);
#else
const auto id = TorrentID::fromInfoHash(hasMetadata ? p.ti->info_hash() : p.info_hash);
#endif
m_loadingTorrents.insert(id, params);
// Adding torrent to BitTorrent session
m_nativeSession->async_add_torrent(p);
m_loadingTorrents.insert(id, loadTorrentParams);
if (!isFindingIncompleteFiles)
m_nativeSession->async_add_torrent(p);
return true;
}
@@ -3207,6 +3489,11 @@ QStringList Session::bannedIPs() const
return m_bannedIPs;
}
bool Session::isRestored() const
{
return m_isRestored;
}
#if defined(Q_OS_WIN)
OSMemoryPriority Session::getOSMemoryPriority() const
{
@@ -4537,206 +4824,6 @@ const CacheStatus &Session::cacheStatus() const
return m_cacheStatus;
}
void Session::startUpTorrents()
{
qDebug("Initializing torrents resume data storage...");
const Path dbPath = specialFolderLocation(SpecialFolder::Data) / Path(u"torrents.db"_qs);
const bool dbStorageExists = dbPath.exists();
ResumeDataStorage *startupStorage = nullptr;
if (resumeDataStorageType() == ResumeDataStorageType::SQLite)
{
m_resumeDataStorage = new DBResumeDataStorage(dbPath, this);
if (!dbStorageExists)
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
startupStorage = new BencodeResumeDataStorage(dataPath, this);
}
}
else
{
const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_qs);
m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this);
if (dbStorageExists)
startupStorage = new DBResumeDataStorage(dbPath, this);
}
if (!startupStorage)
startupStorage = m_resumeDataStorage;
qDebug("Starting up torrents...");
const QVector<TorrentID> torrents = startupStorage->registeredTorrents();
int resumedTorrentsCount = 0;
QVector<TorrentID> queue;
QSet<QString> recoveredCategories;
#ifdef QBT_USES_LIBTORRENT2
const QSet<TorrentID> indexedTorrents {torrents.cbegin(), torrents.cend()};
QSet<TorrentID> skippedIDs;
#endif
for (TorrentID torrentID : torrents)
{
#ifdef QBT_USES_LIBTORRENT2
if (skippedIDs.contains(torrentID))
continue;
#endif
const std::optional<LoadTorrentParams> loadResumeDataResult = startupStorage->load(torrentID);
if (!loadResumeDataResult)
{
LogMsg(tr("Failed to resume torrent. Torrent: \"%1\"").arg(torrentID.toString()), Log::CRITICAL);
continue;
}
LoadTorrentParams resumeData = *loadResumeDataResult;
bool needStore = false;
#ifdef QBT_USES_LIBTORRENT2
const lt::info_hash_t infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hashes()
: resumeData.ltAddTorrentParams.info_hashes);
const bool isHybrid = infoHash.has_v1() && infoHash.has_v2();
const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash);
const auto torrentIDv1 = TorrentID::fromInfoHash(lt::info_hash_t(infoHash.v1));
if (torrentID == torrentIDv2)
{
if (isHybrid && indexedTorrents.contains(torrentIDv1))
{
// if we don't have metadata, try to find it in alternative "resume data"
if (!resumeData.ltAddTorrentParams.ti)
{
const std::optional<LoadTorrentParams> loadAltResumeDataResult = startupStorage->load(torrentIDv1);
if (loadAltResumeDataResult)
resumeData.ltAddTorrentParams.ti = loadAltResumeDataResult->ltAddTorrentParams.ti;
}
// remove alternative "resume data" and skip the attempt to load it
m_resumeDataStorage->remove(torrentIDv1);
skippedIDs.insert(torrentIDv1);
}
}
else if (torrentID == torrentIDv1)
{
torrentID = torrentIDv2;
needStore = true;
m_resumeDataStorage->remove(torrentIDv1);
if (indexedTorrents.contains(torrentID))
{
skippedIDs.insert(torrentID);
const std::optional<LoadTorrentParams> loadPreferredResumeDataResult = startupStorage->load(torrentID);
if (loadPreferredResumeDataResult)
{
std::shared_ptr<lt::torrent_info> ti = resumeData.ltAddTorrentParams.ti;
resumeData = *loadPreferredResumeDataResult;
if (!resumeData.ltAddTorrentParams.ti)
resumeData.ltAddTorrentParams.ti = ti;
}
}
}
else
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
continue;
}
#else
const lt::sha1_hash infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hash()
: resumeData.ltAddTorrentParams.info_hash);
if (torrentID != TorrentID::fromInfoHash(infoHash))
{
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
.arg(torrentID.toString()), Log::WARNING);
continue;
}
#endif
if (m_resumeDataStorage != startupStorage)
{
needStore = true;
if (isQueueingSystemEnabled() && !resumeData.hasSeedStatus)
queue.append(torrentID);
}
// TODO: Remove the following upgrade code in v4.6
// == BEGIN UPGRADE CODE ==
if (m_needUpgradeDownloadPath && isDownloadPathEnabled())
{
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = downloadPath();
needStore = true;
}
}
// == END UPGRADE CODE ==
if (needStore)
m_resumeDataStorage->store(torrentID, resumeData);
const QString category = resumeData.category;
bool isCategoryRecovered = recoveredCategories.contains(category);
if (!category.isEmpty() && (isCategoryRecovered || !m_categories.contains(category)))
{
if (!isCategoryRecovered)
{
if (addCategory(category))
{
recoveredCategories.insert(category);
isCategoryRecovered = true;
LogMsg(tr("Detected inconsistent data: category is missing from the configuration file."
" Category will be recovered but its settings will be reset to default."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
else
{
resumeData.category.clear();
LogMsg(tr("Detected inconsistent data: invalid category. Torrent: \"%1\". Category: \"%2\"")
.arg(torrentID.toString(), category), Log::WARNING);
}
}
// We should check isCategoryRecovered again since the category
// can be just recovered by the code above
if (isCategoryRecovered && resumeData.useAutoTMM)
{
const Path storageLocation {resumeData.ltAddTorrentParams.save_path};
if ((storageLocation != categorySavePath(resumeData.category)) && (storageLocation != categoryDownloadPath(resumeData.category)))
{
resumeData.useAutoTMM = false;
resumeData.savePath = storageLocation;
resumeData.downloadPath = {};
LogMsg(tr("Detected mismatch between the save paths of the recovered category and the current save path of the torrent."
" Torrent is now switched to Manual mode."
" Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
}
}
}
qDebug() << "Starting up torrent" << torrentID.toString() << "...";
if (!loadTorrent(resumeData))
LogMsg(tr("Failed to resume torrent. Torrent: \"%1\"").arg(torrentID.toString()), Log::CRITICAL);
// process add torrent messages before message queue overflow
if ((resumedTorrentsCount % 100) == 0) readAlerts();
++resumedTorrentsCount;
}
if (m_resumeDataStorage != startupStorage)
{
delete startupStorage;
if (resumeDataStorageType() == ResumeDataStorageType::Legacy)
Utils::Fs::removeFile(dbPath);
if (isQueueingSystemEnabled())
m_resumeDataStorage->storeQueue(queue);
}
}
qint64 Session::getAlltimeDL() const
{
@@ -4807,12 +4894,63 @@ void Session::setTorrentContentLayout(const TorrentContentLayout value)
void Session::readAlerts()
{
const std::vector<lt::alert *> alerts = getPendingAlerts();
handleAddTorrentAlerts(alerts);
for (const lt::alert *a : alerts)
handleAlert(a);
processTrackerStatuses();
}
void Session::handleAddTorrentAlerts(const std::vector<lt::alert *> &alerts)
{
QVector<Torrent *> loadedTorrents;
if (!isRestored())
loadedTorrents.reserve(MAX_PROCESSING_RESUMEDATA_COUNT);
for (const lt::alert *a : alerts)
{
if (a->type() != lt::add_torrent_alert::alert_type)
continue;
auto alert = static_cast<const lt::add_torrent_alert *>(a);
if (alert->error)
{
const QString msg = QString::fromStdString(alert->message());
LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
emit loadTorrentFailed(msg);
const lt::add_torrent_params &params = alert->params;
const bool hasMetadata = (params.ti && params.ti->is_valid());
#ifdef QBT_USES_LIBTORRENT2
const auto torrentID = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hashes() : params.info_hashes);
#else
const auto torrentID = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hash() : params.info_hash);
#endif
m_loadingTorrents.remove(torrentID);
return;
}
#ifdef QBT_USES_LIBTORRENT2
const auto torrentID = TorrentID::fromInfoHash(alert->handle.info_hashes());
#else
const auto torrentID = TorrentID::fromInfoHash(alert->handle.info_hash());
#endif
const auto loadingTorrentsIter = m_loadingTorrents.find(torrentID);
if (loadingTorrentsIter != m_loadingTorrents.end())
{
LoadTorrentParams params = loadingTorrentsIter.value();
m_loadingTorrents.erase(loadingTorrentsIter);
Torrent *torrent = createTorrent(alert->handle, params);
loadedTorrents.append(torrent);
}
}
if (!loadedTorrents.isEmpty())
emit torrentsLoaded(loadedTorrents);
}
void Session::handleAlert(const lt::alert *a)
{
try
@@ -4852,7 +4990,7 @@ void Session::handleAlert(const lt::alert *a)
handleFileErrorAlert(static_cast<const lt::file_error_alert*>(a));
break;
case lt::add_torrent_alert::alert_type:
handleAddTorrentAlert(static_cast<const lt::add_torrent_alert*>(a));
// handled separately
break;
case lt::torrent_removed_alert::alert_type:
handleTorrentRemovedAlert(static_cast<const lt::torrent_removed_alert*>(a));
@@ -4924,7 +5062,7 @@ void Session::dispatchTorrentAlert(const lt::alert *a)
}
}
void Session::createTorrent(const lt::torrent_handle &nativeHandle)
TorrentImpl *Session::createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
{
#ifdef QBT_USES_LIBTORRENT2
const auto torrentID = TorrentID::fromInfoHash(nativeHandle.info_hashes());
@@ -4932,21 +5070,15 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
const auto torrentID = TorrentID::fromInfoHash(nativeHandle.info_hash());
#endif
Q_ASSERT(m_loadingTorrents.contains(torrentID));
const LoadTorrentParams params = m_loadingTorrents.take(torrentID);
auto *const torrent = new TorrentImpl(this, m_nativeSession, nativeHandle, params);
m_torrents.insert(torrent->id(), torrent);
const bool hasMetadata = torrent->hasMetadata();
if (!params.restored)
{
m_resumeDataStorage->store(torrent->id(), params);
// The following is useless for newly added magnet
if (hasMetadata)
if (torrent->hasMetadata())
{
if (!torrentExportDirectory().isEmpty())
exportTorrentFile(torrent, torrentExportDirectory());
@@ -4959,9 +5091,6 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
m_seedingLimitTimer->start();
}
// Send torrent addition signal
emit torrentLoaded(torrent);
// Send new torrent signal
if (params.restored)
{
LogMsg(tr("Restored torrent. Torrent: \"%1\"").arg(torrent->name()));
@@ -4975,29 +5104,8 @@ void Session::createTorrent(const lt::torrent_handle &nativeHandle)
// Torrent could have error just after adding to libtorrent
if (torrent->hasError())
LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING);
}
void Session::handleAddTorrentAlert(const lt::add_torrent_alert *p)
{
if (p->error)
{
const QString msg = QString::fromStdString(p->message());
LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
emit loadTorrentFailed(msg);
const lt::add_torrent_params &params = p->params;
const bool hasMetadata = (params.ti && params.ti->is_valid());
#ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hashes() : params.info_hashes);
#else
const auto id = TorrentID::fromInfoHash(hasMetadata ? params.ti->info_hash() : params.info_hash);
#endif
m_loadingTorrents.remove(id);
}
else if (m_loadingTorrents.contains(p->handle.info_hash()))
{
createTorrent(p->handle);
}
return torrent;
}
void Session::handleTorrentRemovedAlert(const lt::torrent_removed_alert *p)

View File

@@ -469,7 +469,8 @@ namespace BitTorrent
void setOSMemoryPriority(OSMemoryPriority priority);
#endif
void startUpTorrents();
bool isRestored() const;
Torrent *findTorrent(const TorrentID &id) const;
QVector<Torrent *> torrents() const;
qsizetype torrentsCount() const;
@@ -539,6 +540,7 @@ namespace BitTorrent
void loadTorrentFailed(const QString &error);
void metadataDownloaded(const TorrentInfo &info);
void recursiveTorrentDownloadPossible(Torrent *torrent);
void restored();
void speedLimitModeChanged(bool alternative);
void statsUpdated();
void subcategoriesSupportChanged();
@@ -549,12 +551,12 @@ namespace BitTorrent
void torrentCategoryChanged(Torrent *torrent, const QString &oldCategory);
void torrentFinished(Torrent *torrent);
void torrentFinishedChecking(Torrent *torrent);
void torrentLoaded(Torrent *torrent);
void torrentMetadataReceived(Torrent *torrent);
void torrentPaused(Torrent *torrent);
void torrentResumed(Torrent *torrent);
void torrentSavePathChanged(Torrent *torrent);
void torrentSavingModeChanged(Torrent *torrent);
void torrentsLoaded(const QVector<Torrent *> &torrents);
void torrentsUpdated(const QVector<Torrent *> &torrents);
void torrentTagAdded(Torrent *torrent, const QString &tag);
void torrentTagRemoved(Torrent *torrent, const QString &tag);
@@ -585,6 +587,8 @@ namespace BitTorrent
#endif
private:
struct ResumeSessionContext;
struct MoveStorageJob
{
lt::torrent_handle torrentHandle;
@@ -630,8 +634,11 @@ namespace BitTorrent
#endif
void processTrackerStatuses();
void populateExcludedFileNamesRegExpList();
void prepareStartup();
void handleLoadedResumeData(ResumeSessionContext *context);
void processNextResumeData(ResumeSessionContext *context);
void endStartup(ResumeSessionContext *context);
bool loadTorrent(LoadTorrentParams params);
LoadTorrentParams initLoadTorrentParams(const AddTorrentParams &addTorrentParams);
bool addTorrent_impl(const std::variant<MagnetUri, TorrentInfo> &source, const AddTorrentParams &addTorrentParams);
@@ -639,8 +646,8 @@ namespace BitTorrent
void exportTorrentFile(const Torrent *torrent, const Path &folderPath);
void handleAlert(const lt::alert *a);
void handleAddTorrentAlerts(const std::vector<lt::alert *> &alerts);
void dispatchTorrentAlert(const lt::alert *a);
void handleAddTorrentAlert(const lt::add_torrent_alert *p);
void handleStateUpdateAlert(const lt::state_update_alert *p);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *p);
void handleFileErrorAlert(const lt::file_error_alert *p);
@@ -662,7 +669,7 @@ namespace BitTorrent
void handleSocks5Alert(const lt::socks5_alert *p) const;
void handleTrackerAlert(const lt::tracker_alert *a);
void createTorrent(const lt::torrent_handle &nativeHandle);
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
void saveResumeData();
void saveTorrentsQueue() const;
@@ -792,6 +799,8 @@ namespace BitTorrent
CachedSettingValue<OSMemoryPriority> m_OSMemoryPriority;
#endif
bool m_isRestored = false;
// Order is important. This needs to be declared after its CachedSettingsValue
// counterpart, because it uses it for initialization in the constructor
// initialization list.

View File

@@ -138,8 +138,20 @@ AutoDownloader::AutoDownloader()
m_processingTimer->setSingleShot(true);
connect(m_processingTimer, &QTimer::timeout, this, &AutoDownloader::process);
if (isProcessingEnabled())
startProcessing();
const auto *btSession = BitTorrent::Session::instance();
if (btSession->isRestored())
{
if (isProcessingEnabled())
startProcessing();
}
else
{
connect(btSession, &BitTorrent::Session::restored, this, [this]()
{
if (isProcessingEnabled())
startProcessing();
});
}
}
AutoDownloader::~AutoDownloader()
@@ -506,7 +518,8 @@ void AutoDownloader::setProcessingEnabled(const bool enabled)
m_storeProcessingEnabled = enabled;
if (enabled)
{
startProcessing();
if (BitTorrent::Session::instance()->isRestored())
startProcessing();
}
else
{

View File

@@ -255,13 +255,12 @@ TorrentFilesWatcher *TorrentFilesWatcher::instance()
TorrentFilesWatcher::TorrentFilesWatcher(QObject *parent)
: QObject {parent}
, m_ioThread {new QThread(this)}
, m_asyncWorker {new TorrentFilesWatcher::Worker}
{
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::magnetFound, this, &TorrentFilesWatcher::onMagnetFound);
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::torrentFound, this, &TorrentFilesWatcher::onTorrentFound);
m_asyncWorker->moveToThread(m_ioThread);
m_ioThread->start();
const auto *btSession = BitTorrent::Session::instance();
if (btSession->isRestored())
initWorker();
else
connect(btSession, &BitTorrent::Session::restored, this, &TorrentFilesWatcher::initWorker);
load();
}
@@ -273,6 +272,27 @@ TorrentFilesWatcher::~TorrentFilesWatcher()
delete m_asyncWorker;
}
void TorrentFilesWatcher::initWorker()
{
Q_ASSERT(!m_asyncWorker);
m_asyncWorker = new TorrentFilesWatcher::Worker;
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::magnetFound, this, &TorrentFilesWatcher::onMagnetFound);
connect(m_asyncWorker, &TorrentFilesWatcher::Worker::torrentFound, this, &TorrentFilesWatcher::onTorrentFound);
m_asyncWorker->moveToThread(m_ioThread);
m_ioThread->start();
for (auto it = m_watchedFolders.cbegin(); it != m_watchedFolders.cend(); ++it)
{
QMetaObject::invokeMethod(m_asyncWorker, [this, path = it.key(), options = it.value()]()
{
m_asyncWorker->setWatchedFolder(path, options);
});
}
}
void TorrentFilesWatcher::load()
{
QFile confFile {(specialFolderLocation(SpecialFolder::Config) / Path(CONF_FILE_NAME)).data()};
@@ -399,10 +419,13 @@ void TorrentFilesWatcher::doSetWatchedFolder(const Path &path, const WatchedFold
m_watchedFolders[path] = options;
QMetaObject::invokeMethod(m_asyncWorker, [this, path, options]()
if (m_asyncWorker)
{
m_asyncWorker->setWatchedFolder(path, options);
});
QMetaObject::invokeMethod(m_asyncWorker, [this, path, options]()
{
m_asyncWorker->setWatchedFolder(path, options);
});
}
emit watchedFolderSet(path, options);
}
@@ -411,10 +434,13 @@ void TorrentFilesWatcher::removeWatchedFolder(const Path &path)
{
if (m_watchedFolders.remove(path))
{
QMetaObject::invokeMethod(m_asyncWorker, [this, path]()
if (m_asyncWorker)
{
m_asyncWorker->removeWatchedFolder(path);
});
QMetaObject::invokeMethod(m_asyncWorker, [this, path]()
{
m_asyncWorker->removeWatchedFolder(path);
});
}
emit watchedFolderRemoved(path);

View File

@@ -78,6 +78,7 @@ private:
explicit TorrentFilesWatcher(QObject *parent = nullptr);
~TorrentFilesWatcher() override;
void initWorker();
void load();
void loadLegacy();
void store() const;