mirror of
https://github.com/rakshasa/rtorrent.git
synced 2026-08-06 18:22:32 +00:00
572 lines
16 KiB
C++
572 lines
16 KiB
C++
#include "config.h"
|
|
|
|
#include "session/session_manager.h"
|
|
|
|
#include <cassert>
|
|
#include <cstdlib>
|
|
#include <torrent/exceptions.h>
|
|
#include <torrent/system/callbacks.h>
|
|
#include <torrent/utils/log.h>
|
|
|
|
#include "globals.h"
|
|
#include "session/download_storer.h"
|
|
#include "utils/lockfile.h"
|
|
|
|
#define LT_LOG(log_fmt, ...) \
|
|
lt_log_print(torrent::LOG_SESSION_EVENTS, "session-events: " log_fmt, __VA_ARGS__);
|
|
|
|
// TODO: Properly handle errors.
|
|
//
|
|
// TODO: If no more sockets can be opened, wait for a job to finish. if all is finished, use a
|
|
// timeout and try again.
|
|
|
|
namespace session {
|
|
|
|
SessionManager::SessionManager(torrent::system::Thread* thread)
|
|
: m_thread(thread),
|
|
m_callback_id(torrent::system::make_callback_id()),
|
|
m_lockfile(std::make_unique<utils::Lockfile>()) {
|
|
}
|
|
|
|
SessionManager::~SessionManager() = default;
|
|
|
|
void
|
|
SessionManager::set_path(std::string path) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
if (m_freeze_info)
|
|
throw torrent::input_error("Session path cannot be changed after startup.");
|
|
|
|
path = expand_path(path);
|
|
|
|
if (path.empty() || path.back() == '/')
|
|
m_path = path;
|
|
else
|
|
m_path = path + '/';
|
|
}
|
|
|
|
void
|
|
SessionManager::set_use_fsyncdisk(bool use_fsyncdisk) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
if (m_freeze_info)
|
|
throw torrent::input_error("Session fsyncdisk option cannot be changed after startup.");
|
|
|
|
m_use_fsyncdisk = use_fsyncdisk;
|
|
}
|
|
|
|
void
|
|
SessionManager::set_use_lock(bool use_lock) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
if (m_freeze_info)
|
|
throw torrent::input_error("Session lock option cannot be changed after startup.");
|
|
|
|
m_use_lock = use_lock;
|
|
}
|
|
|
|
void
|
|
SessionManager::save_resume_download(core::Download* download) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
if (m_path.empty())
|
|
return;
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_pending_builds_mutex);
|
|
|
|
// TODO: This is under the wrong lock.
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::save_resume_download() called while not active.");
|
|
|
|
if (std::find(m_pending_builds.begin(), m_pending_builds.end(), download) != m_pending_builds.end()) {
|
|
// TODO: Do we need to make sure we're getting processed?
|
|
LT_LOG("download already in pending build of resume save : download:%p", download);
|
|
return;
|
|
}
|
|
|
|
m_pending_builds.push_back(download);
|
|
|
|
callback_pending_builds();
|
|
|
|
LT_LOG("build of resume save data queued : download:%p", download);
|
|
}
|
|
}
|
|
|
|
void
|
|
SessionManager::save_full_download(core::Download* download) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
if (m_path.empty())
|
|
return;
|
|
|
|
DownloadStorer storer(download);
|
|
|
|
storer.build_full_streams();
|
|
|
|
auto save_request = SaveRequest{
|
|
download,
|
|
storer.build_path(m_path),
|
|
storer.torrent_stream(),
|
|
storer.rtorrent_stream(),
|
|
storer.libtorrent_stream()
|
|
};
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
LT_LOG("requesting save : download:%p path:%s", download, save_request.path.c_str());
|
|
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::save_download() called while not active.");
|
|
|
|
if (replace_save_request_unsafe(save_request)) {
|
|
LT_LOG("updated pending save request with full save data : download:%p", download);
|
|
return;
|
|
}
|
|
|
|
m_save_requests.push_back(std::move(save_request));
|
|
m_save_request_counter = m_save_requests.size();
|
|
|
|
LT_LOG("queued new full save request : download:%p", download);
|
|
}
|
|
|
|
callback_save_request();
|
|
}
|
|
|
|
void
|
|
SessionManager::remove_download(core::Download* download) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
if (m_path.empty())
|
|
return;
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::remove_download() called while not active.");
|
|
|
|
if (remove_completely_unsafe(download, lock))
|
|
LT_LOG("canceled pending save request : download:%p", download);
|
|
|
|
DownloadStorer(download).unlink_files(m_path);
|
|
|
|
LT_LOG("removed session files : download:%p", download);
|
|
}
|
|
|
|
void
|
|
SessionManager::start() {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
if (m_active || m_freeze_info)
|
|
throw torrent::internal_error("SessionManager::start() called while already started.");
|
|
|
|
m_active = true;
|
|
m_freeze_info = true;
|
|
|
|
if (m_path.empty()) {
|
|
LT_LOG("session manager started with empty path, disabling session management", 0);
|
|
return;
|
|
}
|
|
|
|
LT_LOG("starting session manager with path: %s", m_path.c_str());
|
|
|
|
if (m_use_lock) {
|
|
m_lockfile->set_path(m_path + "rtorrent.lock");
|
|
|
|
if (!m_lockfile->try_lock()) {
|
|
if (errno == ENOENT || errno == ENOTDIR || errno == EACCES)
|
|
throw torrent::input_error("Could not lock session directory: " + std::string(std::strerror(errno)) + " : " + m_path);
|
|
else
|
|
throw torrent::input_error("Could not lock session directory, held by: " + m_lockfile->locked_by_as_string() + " : " + m_path);
|
|
}
|
|
|
|
LT_LOG("locked session directory: %s", m_path.c_str());
|
|
}
|
|
}
|
|
|
|
void
|
|
SessionManager::cleanup() {
|
|
assert(m_thread == torrent::this_thread::thread());
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::cleanup() called while not active.");
|
|
|
|
{
|
|
std::unique_lock<std::mutex> pending_lock(m_pending_builds_mutex);
|
|
|
|
if (!m_pending_builds.empty())
|
|
throw torrent::internal_error("SessionManager::cleanup() called with pending builds.");
|
|
}
|
|
|
|
m_active = false;
|
|
|
|
if (m_path.empty()) {
|
|
LT_LOG("session manager cleanup called with empty path, skipping", 0);
|
|
return;
|
|
}
|
|
|
|
LT_LOG("cleaning up session manager with path: %s", m_path.c_str());
|
|
|
|
flush_all_and_wait_unsafe(lock);
|
|
|
|
if (m_use_lock) {
|
|
if (!m_lockfile->unlock())
|
|
LT_LOG("could not unlock session directory: %s", m_path.c_str());
|
|
|
|
LT_LOG("unlocked session directory: %s", m_path.c_str());
|
|
}
|
|
|
|
LT_LOG("session manager cleaned up", 0);
|
|
|
|
torrent::system::cancel_callback_and_wait(m_callback_id, session_thread::thread(), torrent::main_thread::thread());
|
|
}
|
|
|
|
void
|
|
SessionManager::callback_pending_builds() {
|
|
if (m_save_request_counter >= max_concurrent_requests)
|
|
return;
|
|
|
|
if (m_callback_scheduled_process_pending_builds.exchange(true))
|
|
return;
|
|
|
|
torrent::main_thread::callback(m_callback_id, [this]() {
|
|
process_pending_builds(false);
|
|
});
|
|
}
|
|
|
|
void
|
|
SessionManager::callback_save_request() {
|
|
if (m_save_request_counter == 0)
|
|
return;
|
|
|
|
if (m_processing_save_counter >= max_concurrent_processing)
|
|
return;
|
|
|
|
if (m_callback_scheduled_process_saves_request.exchange(true))
|
|
return;
|
|
|
|
session_thread::callback(m_callback_id, [this]() {
|
|
process_save_request();
|
|
});
|
|
}
|
|
|
|
void
|
|
SessionManager::callback_finished_saves() {
|
|
if (m_callback_scheduled_process_finished_saves.exchange(true))
|
|
return;
|
|
|
|
session_thread::callback(m_callback_id, [this]() {
|
|
process_finished_saves();
|
|
});
|
|
}
|
|
|
|
void
|
|
SessionManager::process_pending_builds(bool is_flushing) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
std::vector<SaveRequest> requests;
|
|
|
|
// Only main thread is allowed to process pending builds or remove downloads, as such it is safe
|
|
// to unlock before adding them to save requests.
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_pending_builds_mutex);
|
|
|
|
m_callback_scheduled_process_pending_builds = false;
|
|
|
|
while (!m_pending_builds.empty()) {
|
|
if (!is_flushing && m_save_request_counter + requests.size() >= max_concurrent_requests)
|
|
break;
|
|
|
|
auto* download = m_pending_builds.front();
|
|
m_pending_builds.pop_front();
|
|
|
|
LT_LOG("processing pending resume save : download:%p", download);
|
|
|
|
DownloadStorer storer(download);
|
|
|
|
storer.build_resume_streams();
|
|
|
|
auto save_request = SaveRequest{
|
|
download,
|
|
storer.build_path(m_path),
|
|
nullptr,
|
|
storer.rtorrent_stream(),
|
|
storer.libtorrent_stream()
|
|
};
|
|
|
|
requests.push_back(std::move(save_request));
|
|
}
|
|
}
|
|
|
|
if (requests.empty())
|
|
return;
|
|
|
|
{
|
|
std::unique_lock<std::mutex> save_lock(m_mutex);
|
|
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::process_pending_builds() called while not active.");
|
|
|
|
for (auto& save_request : requests) {
|
|
if (replace_save_request_unsafe(save_request)) {
|
|
LT_LOG("updated pending resume save request : download:%p", save_request.download);
|
|
continue;
|
|
}
|
|
|
|
m_save_requests.push_back(std::move(save_request));
|
|
m_save_request_counter = m_save_requests.size();
|
|
|
|
LT_LOG("queued new resume save request : download:%p", save_request.download);
|
|
}
|
|
}
|
|
|
|
if (!is_flushing)
|
|
callback_save_request();
|
|
}
|
|
|
|
void
|
|
SessionManager::process_save_request() {
|
|
assert(m_thread == torrent::this_thread::thread());
|
|
|
|
if (m_path.empty())
|
|
throw torrent::internal_error("SessionManager::process_save_request() called with empty path.");
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
m_callback_scheduled_process_saves_request = false;
|
|
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::process_save_request() called while not active.");
|
|
|
|
while (!m_save_requests.empty()) {
|
|
if (m_processing_saves.size() >= max_concurrent_processing)
|
|
break;
|
|
|
|
process_next_save_request_unsafe();
|
|
}
|
|
}
|
|
|
|
callback_pending_builds();
|
|
}
|
|
|
|
void
|
|
SessionManager::process_next_save_request_unsafe() {
|
|
auto request = std::move(m_save_requests.front());
|
|
|
|
m_save_requests.pop_front();
|
|
m_save_request_counter = m_save_requests.size();
|
|
|
|
auto itr = m_processing_saves.insert(m_processing_saves.end(), ProcessingSave{});
|
|
m_processing_save_counter = m_processing_saves.size();
|
|
|
|
itr->second = std::move(request);
|
|
itr->first = std::async(std::launch::async, [this, itr]() {
|
|
auto cleanup_fn = [this, itr]() {
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
callback_finished_saves();
|
|
|
|
m_finished_saves.push_back(std::move(*itr));
|
|
m_finished_condition.notify_all();
|
|
|
|
m_processing_saves.erase(itr);
|
|
m_processing_save_counter = m_processing_saves.size();
|
|
};
|
|
|
|
try {
|
|
DownloadStorer::save_and_move_streams(itr->second.path, m_use_fsyncdisk,
|
|
itr->second.torrent_stream.get(),
|
|
itr->second.rtorrent_stream.get(),
|
|
itr->second.libtorrent_stream.get());
|
|
} catch (...) {
|
|
cleanup_fn();
|
|
throw;
|
|
}
|
|
|
|
cleanup_fn();
|
|
});
|
|
|
|
LT_LOG("started save of download : download:%p path:%s", itr->second.download, itr->second.path.c_str());
|
|
}
|
|
|
|
void
|
|
SessionManager::process_finished_saves() {
|
|
assert(m_thread == torrent::this_thread::thread());
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
|
|
m_callback_scheduled_process_finished_saves = false;
|
|
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::process_finished_saves() called while not active.");
|
|
|
|
for (auto& request : m_finished_saves) {
|
|
try {
|
|
request.first.get();
|
|
|
|
} catch (torrent::storage_error& e) {
|
|
LT_LOG("error saving download : storage error :download:%p path:%s : %s", request.second.download, request.second.path.c_str(), e.what());
|
|
|
|
if (m_last_storage_error_message + std::chrono::minutes(5) > torrent::this_thread::cached_time()) {
|
|
m_ignored_storage_error_count++;
|
|
continue;
|
|
}
|
|
|
|
lt_log_print(torrent::LOG_ERROR, "Storage errors saving session data for download: ignored:%u : %s", m_ignored_storage_error_count, e.what());
|
|
|
|
m_last_storage_error_message = torrent::this_thread::cached_time();
|
|
m_ignored_storage_error_count = 0;
|
|
|
|
continue;
|
|
|
|
} catch (torrent::internal_error& e) {
|
|
LT_LOG("error saving download : internal error : download:%p path:%s : %s", request.second.download, request.second.path.c_str(), e.what());
|
|
throw;
|
|
|
|
} catch (...) {
|
|
LT_LOG("error saving download : unknown error : download:%p path:%s", request.second.download, request.second.path.c_str());
|
|
throw;
|
|
}
|
|
|
|
LT_LOG("finished saving download : download:%p path:%s", request.second.download, request.second.path.c_str());
|
|
}
|
|
|
|
m_finished_saves.clear();
|
|
|
|
callback_save_request();
|
|
}
|
|
|
|
void
|
|
SessionManager::flush_all_and_wait_unsafe(std::unique_lock<std::mutex>& lock) {
|
|
LT_LOG("flushing all pending saves", 0);
|
|
|
|
// Caller already ensured pending builds are empty.
|
|
|
|
while (!m_save_requests.empty()) {
|
|
if (m_processing_save_counter >= max_cleanup_processing) {
|
|
m_finished_condition.wait(lock);
|
|
continue;
|
|
}
|
|
|
|
process_next_save_request_unsafe();
|
|
}
|
|
|
|
while (!m_processing_saves.empty())
|
|
m_finished_condition.wait(lock);
|
|
|
|
for (auto& request : m_finished_saves)
|
|
LT_LOG("finished saving download : download:%p path:%s", request.second.download, request.second.path.c_str());
|
|
|
|
m_finished_saves.clear();
|
|
|
|
LT_LOG("flushed all pending saves", 0);
|
|
}
|
|
|
|
bool
|
|
SessionManager::replace_save_request_unsafe(SaveRequest& save_request) {
|
|
// Can be run in any thread.
|
|
|
|
auto itr = std::find_if(m_save_requests.begin(), m_save_requests.end(), [download = save_request.download](auto& req) {
|
|
return req.download == download;
|
|
});
|
|
|
|
if (itr == m_save_requests.end())
|
|
return false;
|
|
|
|
if (itr->path != save_request.path)
|
|
throw torrent::internal_error("SessionManager::replace_save_request_unsafe() path mismatch on replace: " + itr->path + " != " + save_request.path);
|
|
|
|
// If the existing request is a full save (created during torrent initialization),
|
|
// keep its torrent_stream but update the resume streams with newer data.
|
|
//
|
|
// Otherwise, replace all streams.
|
|
|
|
if (itr->torrent_stream != nullptr) {
|
|
// Keep existing full-save torrent stream, only update resume streams
|
|
if (save_request.rtorrent_stream != nullptr)
|
|
itr->rtorrent_stream = std::move(save_request.rtorrent_stream);
|
|
|
|
if (save_request.libtorrent_stream != nullptr)
|
|
itr->libtorrent_stream = std::move(save_request.libtorrent_stream);
|
|
|
|
} else {
|
|
// No full save pending, replace everything
|
|
itr->torrent_stream = std::move(save_request.torrent_stream);
|
|
itr->rtorrent_stream = std::move(save_request.rtorrent_stream);
|
|
itr->libtorrent_stream = std::move(save_request.libtorrent_stream);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
SessionManager::remove_completely_unsafe(core::Download* download, std::unique_lock<std::mutex>& lock) {
|
|
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
|
|
|
|
auto remove_pending = [this, download]() {
|
|
std::unique_lock<std::mutex> pending_lock(m_pending_builds_mutex);
|
|
|
|
auto itr = std::remove_if(m_pending_builds.begin(), m_pending_builds.end(), [download](auto* req) {
|
|
return req == download;
|
|
});
|
|
|
|
if (itr == m_pending_builds.end())
|
|
return false;
|
|
|
|
m_pending_builds.erase(itr, m_pending_builds.end());
|
|
return true;
|
|
};
|
|
|
|
auto remove_requests = [this, download]() {
|
|
auto itr = std::remove_if(m_save_requests.begin(), m_save_requests.end(), [download](auto& req) {
|
|
return req.download == download;
|
|
});
|
|
|
|
if (itr == m_save_requests.end())
|
|
return false;
|
|
|
|
m_save_requests.erase(itr, m_save_requests.end());
|
|
m_save_request_counter = m_save_requests.size();
|
|
return true;
|
|
};
|
|
|
|
bool removed_something = false;
|
|
|
|
if (remove_pending())
|
|
removed_something = true;
|
|
|
|
if (remove_requests()) {
|
|
removed_something = true;
|
|
callback_pending_builds();
|
|
}
|
|
|
|
// This may block for a relatively long time if fdatasync is in use, however this is necessary.
|
|
while (true) {
|
|
auto itr = std::find_if(m_processing_saves.begin(), m_processing_saves.end(), [download](auto& req) {
|
|
return req.second.download == download;
|
|
});
|
|
|
|
if (itr == m_processing_saves.end())
|
|
break;
|
|
|
|
m_finished_condition.wait(lock);
|
|
}
|
|
|
|
// Since we're the main thread, no more requests for this download can be added.
|
|
|
|
// Checking active after remove_save_request_unsafe to ensure we're calling this after shutdown.
|
|
if (!m_active)
|
|
throw torrent::internal_error("SessionManager::remove_completely_unsafe() called while not active.");
|
|
|
|
return removed_something;
|
|
}
|
|
|
|
} // namespace session
|
|
|