#include "config.h" #include "session/session_manager.h" #include #include #include #include #include #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()) { } 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 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 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 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 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 lock(m_mutex); if (!m_active) throw torrent::internal_error("SessionManager::cleanup() called while not active."); { std::unique_lock 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 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 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 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 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 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 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& 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& lock) { assert(torrent::this_thread::thread() == torrent::main_thread::thread()); auto remove_pending = [this, download]() { std::unique_lock 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