/* Copyright (c) 2003-2022, Arvid Norberg Copyright (c) 2015, Mike Tzou Copyright (c) 2016, 2018-2020, Alden Torres Copyright (c) 2016, Andrei Kurushin Copyright (c) 2017, AllSeeingEyeTolledEweSew Copyright (c) 2017-2018, Steven Siloti Copyright (c) 2019, Pavel Pimenov All rights reserved. You may use, distribute and modify this code under the terms of the BSD license, see LICENSE file. */ #include // for snprintf #include // for atoi #include #include #include #include #include #include // for min()/max() #include "libtorrent/config.hpp" #ifdef TORRENT_WINDOWS #include // for _mkdir and _getcwd #include // for _stat #include #endif #ifdef TORRENT_UTP_LOG_ENABLE #include "libtorrent/utp_stream.hpp" #endif #include "libtorrent/torrent_info.hpp" #include "libtorrent/announce_entry.hpp" #include "libtorrent/entry.hpp" #include "libtorrent/bencode.hpp" #include "libtorrent/session.hpp" #include "libtorrent/session_params.hpp" #include "libtorrent/identify_client.hpp" #include "libtorrent/alert_types.hpp" #include "libtorrent/ip_filter.hpp" #include "libtorrent/magnet_uri.hpp" #include "libtorrent/peer_info.hpp" #include "libtorrent/bdecode.hpp" #include "libtorrent/add_torrent_params.hpp" #include "libtorrent/time.hpp" #include "libtorrent/read_resume_data.hpp" #include "libtorrent/write_resume_data.hpp" #include "libtorrent/string_view.hpp" #include "libtorrent/disk_interface.hpp" // for open_file_state #include "libtorrent/load_torrent.hpp" #include "libtorrent/mmap_disk_io.hpp" #include "libtorrent/posix_disk_io.hpp" #include "libtorrent/pread_disk_io.hpp" #include "libtorrent/disabled_disk_io.hpp" #include "torrent_view.hpp" #include "session_view.hpp" #include "print.hpp" #ifdef _WIN32 #include #include #else #include #include #include #include #include #endif namespace { using lt::total_milliseconds; using lt::alert; using lt::piece_index_t; using lt::file_index_t; using lt::torrent_handle; using lt::add_torrent_params; using lt::total_seconds; using lt::torrent_flags_t; using lt::seconds; using lt::operator ""_sv; using lt::address_v4; using lt::address_v6; using lt::make_address_v6; using lt::make_address_v4; using lt::make_address; using std::chrono::duration_cast; using std::stoi; #ifdef _WIN32 bool sleep_and_input(int* c, lt::time_duration const sleep) { for (int i = 0; i < 2; ++i) { if (_kbhit()) { *c = _getch(); return true; } std::this_thread::sleep_for(sleep / 2); } return false; } #else struct set_keypress { enum terminal_mode { echo = 1, canonical = 2 }; explicit set_keypress(std::uint8_t const mode = 0) { using ul = unsigned long; termios new_settings; tcgetattr(0, &stored_settings); new_settings = stored_settings; // Disable canonical mode, and set buffer size to 1 byte // and disable echo if (mode & echo) new_settings.c_lflag |= ECHO; else new_settings.c_lflag &= ul(~ECHO); if (mode & canonical) new_settings.c_lflag |= ICANON; else new_settings.c_lflag &= ul(~ICANON); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); } ~set_keypress() { tcsetattr(0, TCSANOW, &stored_settings); } private: termios stored_settings; }; bool sleep_and_input(int* c, lt::time_duration const sleep) { lt::time_point const done = lt::clock_type::now() + sleep; int ret = 0; retry: fd_set set; FD_ZERO(&set); FD_SET(0, &set); auto const delay = total_milliseconds(done - lt::clock_type::now()); timeval tv = {int(delay / 1000), int((delay % 1000) * 1000) }; ret = select(1, &set, nullptr, nullptr, &tv); if (ret > 0) { *c = getc(stdin); return true; } if (errno == EINTR) { if (lt::clock_type::now() < done) goto retry; return false; } if (ret < 0 && errno != 0 && errno != ETIMEDOUT) { std::fprintf(stderr, "select failed: %s\n", strerror(errno)); std::this_thread::sleep_for(lt::milliseconds(500)); } return false; } #endif bool print_trackers = false; bool print_peers = false; bool print_peers_legend = false; bool print_connecting_peers = false; bool print_log = false; int print_downloads = 0; bool print_matrix = false; bool print_file_progress = false; bool print_piece_availability = false; bool show_pad_files = false; bool show_dht_status = false; bool print_ip = true; bool print_peaks = false; bool print_local_ip = false; bool print_timers = false; bool print_block = false; bool print_fails = false; bool print_send_bufs = true; bool print_disk_stats = false; // the number of times we've asked to save resume data // without having received a response (successful or failure) int num_outstanding_resume_data = 0; #ifndef TORRENT_DISABLE_DHT std::vector dht_active_requests; std::vector dht_routing_table; #endif std::string to_hex(lt::sha1_hash const& s) { std::stringstream ret; ret << s; return ret.str(); } bool load_file(std::string const& filename, std::vector& v , int limit = 8000000) { std::fstream f(filename, std::ios_base::in | std::ios_base::binary); f.seekg(0, std::ios_base::end); auto const s = f.tellg(); if (s > limit || s < 0) return false; f.seekg(0, std::ios_base::beg); v.resize(static_cast(s)); if (s == std::fstream::pos_type(0)) return !f.fail(); f.read(v.data(), int(v.size())); return !f.fail(); } bool is_absolute_path(std::string const& f) { if (f.empty()) return false; #if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) int i = 0; // match the xx:\ or xx:/ form while (f[i] && strchr("abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ", f[i])) ++i; if (i < int(f.size()-1) && f[i] == ':' && (f[i+1] == '\\' || f[i+1] == '/')) return true; // match the \\ form if (int(f.size()) >= 2 && f[0] == '\\' && f[1] == '\\') return true; return false; #else if (f[0] == '/') return true; return false; #endif } std::string path_append(std::string const& lhs, std::string const& rhs) { if (lhs.empty() || lhs == ".") return rhs; if (rhs.empty() || rhs == ".") return lhs; #if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2) #define TORRENT_SEPARATOR "\\" bool need_sep = lhs[lhs.size()-1] != '\\' && lhs[lhs.size()-1] != '/'; #else #define TORRENT_SEPARATOR "/" bool need_sep = lhs[lhs.size()-1] != '/'; #endif return lhs + (need_sep?TORRENT_SEPARATOR:"") + rhs; } std::string make_absolute_path(std::string const& p) { if (is_absolute_path(p)) return p; std::string ret; #if defined TORRENT_WINDOWS char* cwd = ::_getcwd(nullptr, 0); ret = path_append(cwd, p); std::free(cwd); #else char* cwd = ::getcwd(nullptr, 0); ret = path_append(cwd, p); std::free(cwd); #endif return ret; } std::string print_endpoint(lt::tcp::endpoint const& ep) { using namespace lt; char buf[200]; address const& addr = ep.address(); if (addr.is_v6()) std::snprintf(buf, sizeof(buf), "[%s]:%d", addr.to_string().c_str(), ep.port()); else std::snprintf(buf, sizeof(buf), "%s:%d", addr.to_string().c_str(), ep.port()); return buf; } using lt::torrent_status; auto const first_ts = lt::clock_type::now(); FILE* g_log_file = nullptr; FILE* g_stats_log_file = nullptr; int peer_index(lt::tcp::endpoint addr, std::vector const& peers) { using namespace lt; auto i = std::find_if(peers.begin(), peers.end() , [&addr](peer_info const& pi) { return pi.remote_endpoint() == addr; }); if (i == peers.end()) return -1; return int(i - peers.begin()); } #if TORRENT_USE_I2P void base32encode_i2p(lt::sha256_hash const& s, std::string& out, int limit) { static char const base32_table[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '2', '3', '4', '5', '6', '7' }; static std::array const input_output_mapping{{0, 2, 4, 5, 7, 8}}; std::array inbuf; std::array outbuf; TORRENT_ASSERT(s.size() % 5 ); for (auto i = s.begin(); i != s.end();) { int const available_input = std::min(int(inbuf.size()), int(s.end() - i)); // clear input buffer inbuf.fill(0); // read a chunk of input into inbuf std::copy(i, i + available_input, inbuf.begin()); i += available_input; // encode inbuf to outbuf outbuf[0] = (inbuf[0] & 0xf8) >> 3; outbuf[1] = (((inbuf[0] & 0x07) << 2) | ((inbuf[1] & 0xc0) >> 6)) & 0xff; outbuf[2] = ((inbuf[1] & 0x3e) >> 1); outbuf[3] = (((inbuf[1] & 0x01) << 4) | ((inbuf[2] & 0xf0) >> 4)) & 0xff; outbuf[4] = (((inbuf[2] & 0x0f) << 1) | ((inbuf[3] & 0x80) >> 7)) & 0xff; outbuf[5] = ((inbuf[3] & 0x7c) >> 2); outbuf[6] = (((inbuf[3] & 0x03) << 3) | ((inbuf[4] & 0xe0) >> 5)) & 0xff; outbuf[7] = inbuf[4] & 0x1f; // write output int const num_out = input_output_mapping[std::size_t(available_input)]; for (int j = 0; j < num_out; ++j) { out += base32_table[outbuf[std::size_t(j)]]; --limit; if (limit <= 0) return; } } } #endif // returns the number of lines printed int print_peer_info(std::string& out , std::vector const& peers, int max_lines) { using namespace lt; int pos = 0; if (print_ip) out += "IP "; if (print_local_ip) out += "local IP "; out += "progress down (total"; if (print_peaks) out += " | peak "; out += " ) up (total"; if (print_peaks) out += " | peak "; out += " ) sent-req tmo bsy rcv flags dn up source "; if (print_fails) out += "fail hshf "; if (print_send_bufs) out += " rq sndb (recvb |alloc | wmrk ) q-bytes "; if (print_timers) out += "inactive wait timeout q-time "; out += " v disk ^ rtt "; if (print_block) out += "block-progress "; out += "client \x1b[K\n"; ++pos; char str[500]; for (std::vector::const_iterator i = peers.begin(); i != peers.end(); ++i) { if ((i->flags & (peer_info::handshake | peer_info::connecting) && !print_connecting_peers)) { continue; } if (print_ip) { #if TORRENT_USE_I2P if (i->flags & peer_info::i2p_socket) { base32encode_i2p(i->i2p_destination(), out, 31); } else #endif { std::snprintf(str, sizeof(str), "%-30s ", ::print_endpoint(i->remote_endpoint()).c_str()); out += str; } } if (print_local_ip) { #if TORRENT_USE_I2P if (i->flags & peer_info::i2p_socket) out += " "; else #endif { std::snprintf(str, sizeof(str), "%-30s ", ::print_endpoint(i->local_endpoint()).c_str()); out += str; } } char temp[10]; std::snprintf(temp, sizeof(temp), "%d/%d" , i->download_queue_length , i->target_dl_queue_length); temp[7] = 0; char peer_progress[10]; std::snprintf(peer_progress, sizeof(peer_progress), "%.1f%%", i->progress_ppm / 10000.0); std::snprintf(str, sizeof(str) , "%s %s%s (%s%s) %s%s (%s%s) %s%7s %4d%4d%4d %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s %s%s%s %s%s%s %s%s%s%s%s%s " , progress_bar(i->progress_ppm / 1000, 15, col_green, '#', '-', peer_progress).c_str() , esc("32"), add_suffix(i->down_speed, "/s").c_str() , add_suffix(i->total_download).c_str() , print_peaks ? ("|" + add_suffix(i->download_rate_peak, "/s")).c_str() : "" , esc("31"), add_suffix(i->up_speed, "/s").c_str(), add_suffix(i->total_upload).c_str() , print_peaks ? ("|" + add_suffix(i->upload_rate_peak, "/s")).c_str() : "" , esc("0") , temp // sent requests and target number of outstanding reqs. , i->timed_out_requests , i->busy_requests , i->upload_queue_length , color("I", (i->flags & peer_info::interesting)?col_white:col_blue).c_str() , color("C", (i->flags & peer_info::choked)?col_white:col_blue).c_str() , color("i", (i->flags & peer_info::remote_interested)?col_white:col_blue).c_str() , color("c", (i->flags & peer_info::remote_choked)?col_white:col_blue).c_str() , color("x", (i->flags & peer_info::supports_extensions)?col_white:col_blue).c_str() , color("o", (i->flags & peer_info::outgoing_connection)?col_white:col_blue).c_str() , color("p", (i->flags & peer_info::on_parole)?col_white:col_blue).c_str() , color("O", (i->flags & peer_info::optimistic_unchoke)?col_white:col_blue).c_str() , color("S", (i->flags & peer_info::snubbed)?col_white:col_blue).c_str() , color("U", (i->flags & peer_info::upload_only)?col_white:col_blue).c_str() , color("e", (i->flags & peer_info::endgame_mode)?col_white:col_blue).c_str() , color("E", (i->flags & peer_info::rc4_encrypted)?col_white:(i->flags & peer_info::plaintext_encrypted)?col_cyan:col_blue).c_str() , color("h", (i->flags & peer_info::holepunched)?col_white:col_blue).c_str() , color("s", (i->flags & peer_info::seed)?col_white:col_blue).c_str() , color("u", (i->flags & peer_info::utp_socket)?col_white:col_blue).c_str() , color("I", (i->flags & peer_info::i2p_socket)?col_white:col_blue).c_str() , color("d", (i->read_state & peer_info::bw_disk)?col_white:col_blue).c_str() , color("l", (i->read_state & peer_info::bw_limit)?col_white:col_blue).c_str() , color("n", (i->read_state & peer_info::bw_network)?col_white:col_blue).c_str() , color("d", (i->write_state & peer_info::bw_disk)?col_white:col_blue).c_str() , color("l", (i->write_state & peer_info::bw_limit)?col_white:col_blue).c_str() , color("n", (i->write_state & peer_info::bw_network)?col_white:col_blue).c_str() , color("t", (i->source & peer_info::tracker)?col_white:col_blue).c_str() , color("p", (i->source & peer_info::pex)?col_white:col_blue).c_str() , color("d", (i->source & peer_info::dht)?col_white:col_blue).c_str() , color("l", (i->source & peer_info::lsd)?col_white:col_blue).c_str() , color("r", (i->source & peer_info::resume_data)?col_white:col_blue).c_str() , color("i", (i->source & peer_info::incoming)?col_white:col_blue).c_str()); out += str; if (print_fails) { std::snprintf(str, sizeof(str), "%4d %4d " , i->failcount, i->num_hashfails); out += str; } if (print_send_bufs) { std::snprintf(str, sizeof(str), "%3d %6s %6s|%6s|%6s%7s " , i->requests_in_buffer , add_suffix(i->used_send_buffer).c_str() , add_suffix(i->used_receive_buffer).c_str() , add_suffix(i->receive_buffer_size).c_str() , add_suffix(i->receive_buffer_watermark).c_str() , add_suffix(i->queue_bytes).c_str()); out += str; } if (print_timers) { char req_timeout[20] = "-"; // timeout is only meaningful if there is at least one outstanding // request to the peer if (i->download_queue_length > 0) std::snprintf(req_timeout, sizeof(req_timeout), "%d", i->request_timeout); std::snprintf(str, sizeof(str), "%8d %4d %7s %6d " , int(total_seconds(i->last_active)) , int(total_seconds(i->last_request)) , req_timeout , int(total_seconds(i->download_queue_time))); out += str; } std::snprintf(str, sizeof(str), "%s|%s %5d " , add_suffix(i->pending_disk_bytes).c_str() , add_suffix(i->pending_disk_read_bytes).c_str() , i->rtt); out += str; if (print_block) { if (i->downloading_piece_index >= piece_index_t(0)) { char buf[50]; std::snprintf(buf, sizeof(buf), "%d:%d" , static_cast(i->downloading_piece_index), i->downloading_block_index); out += progress_bar( i->downloading_progress * 1000 / i->downloading_total, 14, col_green, '-', '#', buf); } else { out += progress_bar(0, 14); } } out += " "; if (i->flags & lt::peer_info::handshake) { out += esc("31"); out += " waiting for handshake"; out += esc("0"); } else if (i->flags & lt::peer_info::connecting) { out += esc("31"); out += " connecting to peer"; out += esc("0"); } else { out += " "; out += i->client; } out += "\x1b[K\n"; ++pos; if (pos >= max_lines) break; } return pos; } // returns the number of lines printed int print_peer_legend(std::string& out, int max_lines) { #ifdef _MSC_VER #pragma warning(push, 1) // warning C4566: character represented by universal-character-name '\u256F' // cannot be represented in the current code page (1252) #pragma warning(disable: 4566) #endif std::array lines{{ " we are interested \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2502\u2570\u2500\u2500\u2500 incoming\x1b[K\n", " we have choked \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502\u2502\u2570\u2500\u2500\u2500 resume data\x1b[K\n", "remote is interested \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502\u2570\u2500\u2500\u2500 local peer discovery\x1b[K\n", " remote has choked \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2570\u2500\u2500\u2500 DHT\x1b[K\n", " supports extensions \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2570\u2500\u2500\u2500 peer exchange\x1b[K\n", " outgoing connection \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2502 \u2502\u2502\u2502 \u2570\u2500\u2500\u2500 tracker\x1b[K\n", " on parole \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2502\u2570\u2500\u253c\u253c\u2534\u2500\u2500\u2500 network\x1b[K\n", " optimistic unchoke \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2502\u2570\u2500\u2500\u253c\u2534\u2500\u2500\u2500 rate limit\x1b[K\n", " snubbed \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2502\u2502 \u2570\u2500\u2500\u2500\u2534\u2500\u2500\u2500 disk\x1b[K\n", " upload only \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2502\u2502\u2570\u2500\u2500\u2500 i2p\x1b[K\n", " end-game mode \u2500\u2500\u2500\u256f\u2502\u2502\u2502\u2570\u2500\u2500\u2500 uTP\x1b[K\n", " obfuscation level \u2500\u2500\u2500\u256f\u2502\u2570\u2500\u2500\u2500 seed\x1b[K\n", " hole-punched \u2500\u2500\u2500\u256f\x1b[K\n", }}; #ifdef _MSC_VER #pragma warning(pop) #endif char const* ip = " "; char const* indentation = " "; char const* peaks = " "; int ret = 0; for (auto const& l : lines) { if (max_lines <= 0) break; ++ret; out += indentation; if (print_ip) out += ip; if (print_local_ip) out += ip; if (print_peaks) out += peaks; out += l; } return ret; } lt::storage_mode_t allocation_mode = lt::storage_mode_sparse; std::string save_path("."); int torrent_upload_limit = 0; int torrent_download_limit = 0; std::string monitor_dir; int poll_interval = 5; int max_connections_per_torrent = 50; bool seed_mode = false; bool exit_on_finish = false; bool share_mode = false; bool quit = false; #ifndef _WIN32 void signal_handler(int) { // make the main loop terminate quit = true; } #endif // if non-empty, a peer that will be added to all torrents std::string peer; void print_settings(int const start, int const num , char const* const type) { for (int i = start; i < start + num; ++i) { char const* name = lt::name_for_setting(i); if (!name || name[0] == '\0') continue; std::printf("%s=<%s>\n", name, type); } } void assign_setting(lt::settings_pack& settings, std::string const& key, char const* value) { int const sett_name = lt::setting_by_name(key); if (sett_name < 0) { std::fprintf(stderr, "unknown setting: \"%s\"\n", key.c_str()); std::exit(1); } using lt::settings_pack; switch (sett_name & settings_pack::type_mask) { case settings_pack::string_type_base: settings.set_str(sett_name, value); break; case settings_pack::bool_type_base: if (value == "1"_sv || value == "on"_sv || value == "true"_sv) { settings.set_bool(sett_name, true); } else if (value == "0"_sv || value == "off"_sv || value == "false"_sv) { settings.set_bool(sett_name, false); } else { std::fprintf(stderr, "invalid value for \"%s\". expected 0 or 1\n" , key.c_str()); std::exit(1); } break; case settings_pack::int_type_base: using namespace lt::literals; static std::map const enums = { {"no_piece_suggestions"_sv, settings_pack::no_piece_suggestions}, {"suggest_read_cache"_sv, settings_pack::suggest_read_cache}, {"fixed_slots_choker"_sv, settings_pack::fixed_slots_choker}, {"rate_based_choker"_sv, settings_pack::rate_based_choker}, {"round_robin"_sv, settings_pack::round_robin}, {"fastest_upload"_sv, settings_pack::fastest_upload}, {"anti_leech"_sv, settings_pack::anti_leech}, {"enable_os_cache"_sv, settings_pack::enable_os_cache}, {"disable_os_cache"_sv, settings_pack::disable_os_cache}, {"write_through"_sv, settings_pack::write_through}, {"prefer_tcp"_sv, settings_pack::prefer_tcp}, {"peer_proportional"_sv, settings_pack::peer_proportional}, {"pe_forced"_sv, settings_pack::pe_forced}, {"pe_enabled"_sv, settings_pack::pe_enabled}, {"pe_disabled"_sv, settings_pack::pe_disabled}, {"pe_plaintext"_sv, settings_pack::pe_plaintext}, {"pe_rc4"_sv, settings_pack::pe_rc4}, {"pe_both"_sv, settings_pack::pe_both}, {"none"_sv, settings_pack::none}, {"socks4"_sv, settings_pack::socks4}, {"socks5"_sv, settings_pack::socks5}, {"socks5_pw"_sv, settings_pack::socks5_pw}, {"http"_sv, settings_pack::http}, {"http_pw"_sv, settings_pack::http_pw}, }; { auto const it = enums.find(lt::string_view(value)); if (it != enums.end()) { settings.set_int(sett_name, it->second); break; } } static std::map const alert_categories = { {"error"_sv, lt::alert_category::error}, {"peer"_sv, lt::alert_category::peer}, {"port_mapping"_sv, lt::alert_category::port_mapping}, {"storage"_sv, lt::alert_category::storage}, {"tracker"_sv, lt::alert_category::tracker}, {"connect"_sv, lt::alert_category::connect}, {"status"_sv, lt::alert_category::status}, {"ip_block"_sv, lt::alert_category::ip_block}, {"performance_warning"_sv, lt::alert_category::performance_warning}, {"dht"_sv, lt::alert_category::dht}, {"session_log"_sv, lt::alert_category::session_log}, {"torrent_log"_sv, lt::alert_category::torrent_log}, {"peer_log"_sv, lt::alert_category::peer_log}, {"incoming_request"_sv, lt::alert_category::incoming_request}, {"dht_log"_sv, lt::alert_category::dht_log}, {"dht_operation"_sv, lt::alert_category::dht_operation}, {"port_mapping_log"_sv, lt::alert_category::port_mapping_log}, {"picker_log"_sv, lt::alert_category::picker_log}, {"file_progress"_sv, lt::alert_category::file_progress}, {"piece_progress"_sv, lt::alert_category::piece_progress}, {"upload"_sv, lt::alert_category::upload}, {"block_progress"_sv, lt::alert_category::block_progress}, {"all"_sv, lt::alert_category::all}, }; std::stringstream flags(value); std::string f; lt::alert_category_t val; while (std::getline(flags, f, ',')) try { auto const it = alert_categories.find(f); if (it == alert_categories.end()) val |= lt::alert_category_t{unsigned(std::stoi(f))}; else val |= it->second; } catch (std::invalid_argument const&) { std::fprintf(stderr, "invalid value for \"%s\". expected integer or enum value\n" , key.c_str()); std::exit(1); } settings.set_int(sett_name, val); break; } } std::string resume_file(lt::info_hash_t const& info_hash) { return path_append(save_path, path_append(".resume" , to_hex(info_hash.get_best()) + ".resume")); } void set_torrent_params(lt::add_torrent_params& p) { p.max_connections = max_connections_per_torrent; p.max_uploads = -1; p.upload_limit = torrent_upload_limit; p.download_limit = torrent_download_limit; if (seed_mode) p.flags |= lt::torrent_flags::seed_mode; if (share_mode) p.flags |= lt::torrent_flags::share_mode; p.save_path = save_path; p.storage_mode = allocation_mode; } void add_magnet(lt::session& ses, lt::string_view uri) { lt::error_code ec; lt::add_torrent_params p = lt::parse_magnet_uri(uri, ec); if (ec) { std::printf("invalid magnet link \"%s\": %s\n" , std::string(uri).c_str(), ec.message().c_str()); return; } std::vector resume_data; if (load_file(resume_file(p.info_hashes), resume_data)) { p = lt::read_resume_data(resume_data, ec); if (ec) std::printf(" failed to load resume data: %s\n", ec.message().c_str()); } set_torrent_params(p); std::printf("adding magnet: %s\n", std::string(uri).c_str()); ses.async_add_torrent(std::move(p)); } // return false on failure bool add_torrent(lt::session& ses, std::string const torrent) try { using lt::storage_mode_t; static int counter = 0; std::printf("[%d] %s\n", counter++, torrent.c_str()); lt::error_code ec; lt::add_torrent_params atp = lt::load_torrent_file(torrent); std::vector resume_data; if (load_file(resume_file(atp.info_hashes), resume_data)) { lt::add_torrent_params rd = lt::read_resume_data(resume_data, ec); if (ec) std::printf(" failed to load resume data: %s\n", ec.message().c_str()); else atp = rd; } set_torrent_params(atp); atp.flags &= ~lt::torrent_flags::duplicate_is_error; ses.async_add_torrent(std::move(atp)); return true; } catch (lt::system_error const& e) { std::printf("failed to load torrent \"%s\": %s\n" , torrent.c_str(), e.code().message().c_str()); return false; } std::vector list_dir(std::string path , bool (*filter_fun)(lt::string_view) , lt::error_code& ec) { std::vector ret; #ifdef TORRENT_WINDOWS if (!path.empty() && path[path.size()-1] != '\\') path += "\\*"; else path += "*"; WIN32_FIND_DATAA fd; HANDLE handle = FindFirstFileA(path.c_str(), &fd); if (handle == INVALID_HANDLE_VALUE) { ec.assign(GetLastError(), boost::system::system_category()); return ret; } do { lt::string_view const p = fd.cFileName; if (filter_fun(p)) ret.emplace_back(p); } while (FindNextFileA(handle, &fd)); FindClose(handle); #else if (!path.empty() && path[path.size()-1] == '/') path.resize(path.size()-1); DIR* handle = opendir(path.c_str()); if (handle == nullptr) { ec.assign(errno, boost::system::system_category()); return ret; } struct dirent* de; while ((de = readdir(handle))) { lt::string_view const p(de->d_name); if (filter_fun(p)) ret.emplace_back(p); } closedir(handle); #endif return ret; } void scan_dir(std::string const& dir_path, lt::session& ses) { using namespace lt; error_code ec; std::vector ents = list_dir(dir_path , [](lt::string_view p) { return p.size() > 8 && p.substr(p.size() - 8) == ".torrent"; }, ec); if (ec) { std::fprintf(stderr, "failed to list directory: (%s : %d) %s\n" , ec.category().name(), ec.value(), ec.message().c_str()); return; } for (auto const& e : ents) { std::string const file = path_append(dir_path, e); // there's a new file in the monitor directory, load it up if (add_torrent(ses, file)) { if (::remove(file.c_str()) < 0) { std::fprintf(stderr, "failed to remove torrent file: \"%s\"\n" , file.c_str()); } } } } char const* timestamp() { time_t t = std::time(nullptr); #ifdef TORRENT_WINDOWS std::tm const* timeinfo = localtime(&t); #else std::tm buf; std::tm const* timeinfo = localtime_r(&t, &buf); #endif static char str[200]; std::strftime(str, 200, "%b %d %X", timeinfo); return str; } void print_alert(lt::alert const* a, std::string& str) { using namespace lt; if (a->category() & alert_category::error) { str += esc("31"); } else if (a->category() & (alert_category::peer | alert_category::storage)) { str += esc("33"); } str += "["; str += timestamp(); str += "] "; str += a->message(); str += esc("0"); if (g_log_file) std::fprintf(g_log_file, "[%" PRId64 "] %s\n" , std::int64_t(duration_cast(a->timestamp() - first_ts).count()) , a->message().c_str()); } int save_file(std::string const& filename, std::vector const& v) { std::fstream f(filename, std::ios_base::trunc | std::ios_base::out | std::ios_base::binary); f.write(v.data(), int(v.size())); return !f.fail(); } struct client_state_t { torrent_view& view; session_view& ses_view; std::deque events; std::vector peers; std::vector file_progress; std::vector file_prio; std::vector file_state; std::vector download_queue; std::vector download_queue_block_info; std::vector piece_availability; std::vector trackers; void clear() { peers.clear(); file_progress.clear(); file_prio.clear(); file_state.clear(); download_queue.clear(); download_queue_block_info.clear(); piece_availability.clear(); trackers.clear(); } }; // returns true if the alert was handled (and should not be printed to the log) // returns false if the alert was not handled bool handle_alert(client_state_t& client_state, lt::alert* a) { using namespace lt; if (session_stats_alert* s = alert_cast(a)) { client_state.ses_view.update_counters(s->counters(), s->timestamp()); } if (alert_cast(a) || alert_cast(a)) { if (g_stats_log_file != nullptr) { std::fprintf(g_stats_log_file, "[%" PRId64 "] %s\n" , std::int64_t(duration_cast(a->timestamp() - first_ts).count()) , a->message().c_str()); } return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) client_state.peers = std::move(p->peer_info); return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) client_state.file_progress = std::move(p->files); return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) client_state.file_prio = std::move(p->priorities); return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) client_state.file_state = std::move(p->state); return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) { client_state.download_queue = std::move(p->piece_info); client_state.download_queue_block_info = std::move(p->block_data); } return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) client_state.piece_availability = std::move(p->piece_availability); return true; } if (auto* p = alert_cast(a)) { if (client_state.view.get_active_torrent().handle == p->handle) client_state.trackers = std::move(p->trackers); return true; } #ifndef TORRENT_DISABLE_DHT if (dht_stats_alert* p = alert_cast(a)) { dht_active_requests = p->active_requests; dht_routing_table = p->routing_table; return true; } #endif #ifdef TORRENT_SSL_PEERS if (torrent_need_cert_alert* p = alert_cast(a)) { torrent_handle h = p->handle; std::string base_name = path_append("certificates", to_hex(h.info_hash())); std::string cert = base_name + ".pem"; std::string priv = base_name + "_key.pem"; #ifdef TORRENT_WINDOWS struct ::_stat st; int ret = ::_stat(cert.c_str(), &st); if (ret < 0 || (st.st_mode & _S_IFREG) == 0) #else struct ::stat st; int ret = ::stat(cert.c_str(), &st); if (ret < 0 || (st.st_mode & S_IFREG) == 0) #endif { char msg[256]; std::snprintf(msg, sizeof(msg), "ERROR. could not load certificate %s: %s\n" , cert.c_str(), std::strerror(errno)); if (g_log_file) std::fprintf(g_log_file, "[%s] %s\n", timestamp(), msg); return true; } #ifdef TORRENT_WINDOWS ret = ::_stat(priv.c_str(), &st); if (ret < 0 || (st.st_mode & _S_IFREG) == 0) #else ret = ::stat(priv.c_str(), &st); if (ret < 0 || (st.st_mode & S_IFREG) == 0) #endif { char msg[256]; std::snprintf(msg, sizeof(msg), "ERROR. could not load private key %s: %s\n" , priv.c_str(), std::strerror(errno)); if (g_log_file) std::fprintf(g_log_file, "[%s] %s\n", timestamp(), msg); return true; } char msg[256]; std::snprintf(msg, sizeof(msg), "loaded certificate %s and key %s\n", cert.c_str(), priv.c_str()); if (g_log_file) std::fprintf(g_log_file, "[%s] %s\n", timestamp(), msg); h.set_ssl_certificate(cert, priv, "certificates/dhparams.pem", "1234"); h.resume(); } #endif // don't log every peer we try to connect to if (alert_cast(a)) return true; if (peer_disconnected_alert* pd = alert_cast(a)) { // ignore failures to connect and peers not responding with a // handshake. The peers that we successfully connect to and then // disconnect is more interesting. if (pd->op == operation_t::connect || pd->error == errors::timed_out_no_handshake) return true; } #ifdef _MSC_VER // it seems msvc makes the definitions of 'p' escape the if-statement here #pragma warning(push) #pragma warning(disable: 4456) #endif if (metadata_received_alert* p = alert_cast(a)) { torrent_handle h = p->handle; h.save_resume_data(torrent_handle::save_info_dict); ++num_outstanding_resume_data; } if (add_torrent_alert* p = alert_cast(a)) { if (p->error) { std::fprintf(stderr, "failed to add torrent: %s %s\n" , p->params.ti ? p->params.ti->name().c_str() : p->params.name.c_str() , p->error.message().c_str()); } else { torrent_handle h = p->handle; h.save_resume_data(torrent_handle::save_info_dict | torrent_handle::if_metadata_changed); ++num_outstanding_resume_data; // if we have a peer specified, connect to it if (!peer.empty()) { auto port = peer.find_last_of(':'); if (port != std::string::npos) { peer[port++] = '\0'; char const* ip = peer.data(); int const peer_port = atoi(peer.data() + port); error_code ec; if (peer_port > 0) h.connect_peer(tcp::endpoint(make_address(ip, ec), std::uint16_t(peer_port))); } } } } if (torrent_finished_alert* p = alert_cast(a)) { p->handle.set_max_connections(max_connections_per_torrent / 2); // write resume data for the finished torrent // the alert handler for save_resume_data_alert // will save it to disk torrent_handle h = p->handle; h.save_resume_data(torrent_handle::save_info_dict | torrent_handle::if_download_progress); ++num_outstanding_resume_data; if (exit_on_finish) quit = true; } if (save_resume_data_alert* p = alert_cast(a)) { --num_outstanding_resume_data; auto const buf = write_resume_data_buf(p->params); save_file(resume_file(p->params.info_hashes), buf); } if (save_resume_data_failed_alert* p = alert_cast(a)) { --num_outstanding_resume_data; // don't print the error if it was just that we didn't need to save resume // data. Returning true means "handled" and not printed to the log return p->error == lt::errors::resume_data_not_modified; } if (torrent_paused_alert* p = alert_cast(a)) { if (!quit) { // write resume data for the finished torrent // the alert handler for save_resume_data_alert // will save it to disk torrent_handle h = p->handle; h.save_resume_data(torrent_handle::save_info_dict); ++num_outstanding_resume_data; } } if (state_update_alert* p = alert_cast(a)) { lt::torrent_handle const prev = client_state.view.get_active_handle(); client_state.view.update_torrents(std::move(p->status)); // when the active torrent changes, we need to clear the peers, trackers, files, etc. if (client_state.view.get_active_handle() != prev) client_state.clear(); return true; } if (torrent_removed_alert* p = alert_cast(a)) { client_state.view.remove_torrent(std::move(p->handle)); } return false; #ifdef _MSC_VER #pragma warning(pop) #endif } void pop_alerts(client_state_t& client_state, lt::session& ses) { std::vector alerts; ses.pop_alerts(&alerts); for (auto a : alerts) { if (::handle_alert(client_state, a)) continue; // if we didn't handle the alert, print it to the log std::string event_string; print_alert(a, event_string); client_state.events.push_back(event_string); if (client_state.events.size() >= 20) client_state.events.pop_front(); } } void print_compact_piece(lt::partial_piece_info const& pp, std::string& out) { using namespace lt; char str[50]; int const piece = static_cast(pp.piece_index); int const num_blocks = pp.blocks_in_piece; std::snprintf(str, sizeof(str), "%5d:[", piece); out += str; out += esc("32"); lt::bitfield blocks(num_blocks); for (int j = 0; j < num_blocks; ++j) if (pp.blocks[j].state >= block_info::writing) blocks.set_bit(j); int height = 0; out += piece_matrix(blocks, num_blocks / 8, &height); out += esc("0"); out += "]"; } void print_piece(lt::partial_piece_info const& pp , std::vector const& peers , int const rows , std::string& out) { using namespace lt; char str[1024]; int const piece = static_cast(pp.piece_index); int const num_blocks = pp.blocks_in_piece; std::snprintf(str, sizeof(str), "%5d:[", piece); out += str; string_view last_color; int line_len = num_blocks / rows; for (int j = 0; j < num_blocks; ++j) { char const* color = ""; if (line_len == 0) { line_len = num_blocks / rows; color = esc("0"); if (last_color != color) { out += color; last_color = color; } out += "]\n ["; } int const index = peer_index(pp.blocks[j].peer(), peers) % 36; bool const snubbed = index >= 0 ? bool(peers[std::size_t(index)].flags & lt::peer_info::snubbed) : false; char const* chr = " "; if (pp.blocks[j].bytes_progress > 0 && pp.blocks[j].state == block_info::requested) { if (pp.blocks[j].num_peers > 1) color = esc("0;1"); else color = snubbed ? esc("0;35") : esc("0;33"); #ifndef TORRENT_WINDOWS static char const* const progress[] = { "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588" }; chr = progress[pp.blocks[j].bytes_progress * 8 / pp.blocks[j].block_size]; #else static char const* const progress[] = { "\xb0", "\xb1", "\xb2" }; chr = progress[pp.blocks[j].bytes_progress * 3 / pp.blocks[j].block_size]; #endif } else if (pp.blocks[j].state == block_info::finished) color = esc("32;7"); else if (pp.blocks[j].state == block_info::writing) color = esc("36;7"); else if (pp.blocks[j].state == block_info::requested) { color = snubbed ? esc("0;35") : esc("0"); chr = "="; } else { color = esc("0"); chr = " "; } if (last_color != color) { out += color; last_color = color; } out += chr; line_len -= 1; } out += esc("0"); out += "]"; } bool is_resume_file(std::string const& s) { static std::string const hex_digit = "0123456789abcdef"; if (s.size() != 40 + 7) return false; if (s.substr(40) != ".resume") return false; for (char const c : s.substr(0, 40)) { if (hex_digit.find(c) == std::string::npos) return false; } return true; } void print_usage() { std::fprintf(stderr, R"(usage: client_test [OPTIONS] [TORRENT|MAGNETURL] OPTIONS: CLIENT OPTIONS -h print this message -f logs all events to the given file -s sets the save path for downloads. This also determines the resume data save directory. Torrents from the resume directory are automatically added to the session on startup. -m sets the .torrent monitor directory. torrent files dropped in the directory are added the session and the resume data directory, and removed from the monitor dir. -t sets the scan interval of the monitor dir -F sets the UI refresh rate. This is the number of milliseconds between screen refreshes. -k enable high performance settings. This overwrites any other previous command line options, so be sure to specify this first -G Add torrents in seed-mode (i.e. assume all pieces are present and check hashes on-demand) -e exit client after the specified number of iterations through the main loop -O print session stats counters to the specified log file -1 exit on first torrent completing (useful for benchmarks) -i specify which disk I/O back-end to use. One of: mmap, posix, pread, disabled )" #ifdef TORRENT_UTP_LOG_ENABLE R"( -q Enable uTP transport-level verbose logging )" #endif R"( LIBTORRENT SETTINGS --= set the libtorrent setting to --list-settings print all libtorrent settings and exit BITTORRENT OPTIONS -T sets the max number of connections per torrent -U sets per-torrent upload rate -D sets per-torrent download rate -Q enables share mode. Share mode attempts to maximize share ratio rather than downloading -r connect to specified peer NETWORK OPTIONS -x loads an emule IP-filter file -Y Rate limit local peers DISK OPTIONS -a sets the allocation mode. [sparse|allocate] -0 disable disk I/O, read garbage and don't flush to disk TORRENT is a path to a .torrent file MAGNETURL is a magnet link alert mask flags: error peer port_mapping storage tracker connect status ip_block performance_warning dht session_log torrent_log peer_log incoming_request dht_log dht_operation port_mapping_log picker_log file_progress piece_progress upload block_progress all examples: --alert_mask=error,port_mapping,tracker,connect,session_log --alert_mask=error,session_log,torrent_log,peer_log --alert_mask=error,dht,dht_log,dht_operation --alert_mask=all )") ; } } // anonymous namespace int main(int argc, char* argv[]) { #ifndef _WIN32 // sets the terminal to single-character mode // and resets when destructed set_keypress s_; #endif if (argc == 1) { print_usage(); return 0; } using lt::settings_pack; using lt::session_handle; torrent_view view; session_view ses_view; lt::session_params params; #ifndef TORRENT_DISABLE_DHT std::vector in; if (load_file(".ses_state", in)) params = read_session_params(in, session_handle::save_dht_state); #endif auto& settings = params.settings; settings.set_str(settings_pack::user_agent, "client_test/" LIBTORRENT_VERSION); settings.set_int(settings_pack::alert_mask , lt::alert_category::error | lt::alert_category::peer | lt::alert_category::port_mapping | lt::alert_category::storage | lt::alert_category::tracker | lt::alert_category::connect | lt::alert_category::status | lt::alert_category::ip_block | lt::alert_category::performance_warning | lt::alert_category::dht | lt::alert_category::incoming_request | lt::alert_category::dht_operation | lt::alert_category::port_mapping_log | lt::alert_category::file_progress); lt::time_duration refresh_delay = lt::milliseconds(500); bool rate_limit_locals = false; client_state_t client_state{ view, ses_view, {}, {}, {}, {}, {}, {}, {}, {}, {} }; int loop_limit = -1; lt::time_point next_dir_scan = lt::clock_type::now(); // load the torrents given on the commandline std::vector torrents; lt::ip_filter loaded_ip_filter; for (int i = 1; i < argc; ++i) { if (argv[i][0] != '-') { torrents.push_back(argv[i]); continue; } if (argv[i] == "--list-settings"_sv) { // print all libtorrent settings and exit print_settings(settings_pack::string_type_base , settings_pack::num_string_settings, "string"); print_settings(settings_pack::bool_type_base , settings_pack::num_bool_settings, "bool"); print_settings(settings_pack::int_type_base , settings_pack::num_int_settings, "int"); return 0; } // maybe this is an assignment of a libtorrent setting if (argv[i][1] == '-' && strchr(argv[i], '=') != nullptr) { char const* equal = strchr(argv[i], '='); char const* start = argv[i]+2; // +2 is to skip the -- std::string const key(start, std::size_t(equal - start)); char const* value = equal + 1; assign_setting(settings, key, value); continue; } // command line switches that don't take an argument switch (argv[i][1]) { case 'k': settings = lt::high_performance_seed(); continue; case 'G': seed_mode = true; continue; case '1': exit_on_finish = true; continue; #ifdef TORRENT_UTP_LOG_ENABLE case 'q': lt::set_utp_stream_logging(true); continue; #endif case 'Q': share_mode = true; continue; case 'Y': rate_limit_locals = true; continue; case '0': params.disk_io_constructor = lt::disabled_disk_io_constructor; continue; case 'h': print_usage(); return 0; } // if there's a flag but no argument following, ignore it if (argc == i + 1) { std::fprintf(stderr, "invalid command line argument or missing parameter: %s\n", argv[i]); return 1; } char const* arg = argv[i+1]; if (arg == nullptr) arg = ""; switch (argv[i][1]) { case 'O': g_stats_log_file = std::fopen(arg, "w+"); continue; case 'f': g_log_file = std::fopen(arg, "w+"); break; case 's': save_path = make_absolute_path(arg); break; case 'U': torrent_upload_limit = atoi(arg) * 1000; break; case 'D': torrent_download_limit = atoi(arg) * 1000; break; case 'm': monitor_dir = make_absolute_path(arg); break; case 't': poll_interval = atoi(arg); break; case 'F': refresh_delay = lt::milliseconds(atoi(arg)); break; case 'a': allocation_mode = (arg == std::string("sparse")) ? lt::storage_mode_sparse : lt::storage_mode_allocate; break; case 'x': { std::fstream filter(arg, std::ios_base::in); if (!filter.fail()) { std::regex regex(R"(^\s*([0-9\.]+)\s*-\s*([0-9\.]+)\s+([0-9]+)$)"); std::string line; while (std::getline(filter, line)) { std::smatch m; if (std::regex_match(line, m, regex)) { address_v4 start = make_address_v4(m[1]); address_v4 last = make_address_v4(m[2]); loaded_ip_filter.add_rule(start, last, stoi(m[3]) <= 127 ? lt::ip_filter::blocked : 0); } } } } break; case 'T': max_connections_per_torrent = atoi(arg); break; case 'r': peer = arg; break; case 'i': { #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE if (arg == "mmap"_sv) params.disk_io_constructor = lt::mmap_disk_io_constructor; else #endif if (arg == "posix"_sv) params.disk_io_constructor = lt::posix_disk_io_constructor; #if TORRENT_HAVE_PREAD || defined TORRENT_WINDOWS else if (arg == "pread"_sv) params.disk_io_constructor = lt::pread_disk_io_constructor; #endif else if (arg == "disabled"_sv) params.disk_io_constructor = lt::disabled_disk_io_constructor; else { std::fprintf(stderr, "unknown disk-io subsystem: \"%s\"\n", arg); std::exit(1); } break; } case 'e': { loop_limit = atoi(arg); break; } } ++i; // skip the argument } // create directory for resume files #ifdef TORRENT_WINDOWS int mkdir_ret = _mkdir(path_append(save_path, ".resume").c_str()); #else int mkdir_ret = mkdir(path_append(save_path, ".resume").c_str(), 0777); #endif if (mkdir_ret < 0 && errno != EEXIST) { std::fprintf(stderr, "failed to create resume file directory: (%d) %s\n" , errno, strerror(errno)); } lt::session ses(std::move(params)); if (rate_limit_locals) { lt::ip_filter pcf; pcf.add_rule(make_address_v4("0.0.0.0") , make_address_v4("255.255.255.255") , 1 << static_cast(lt::session::global_peer_class_id)); pcf.add_rule(make_address_v6("::") , make_address_v6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), 1); ses.set_peer_class_filter(pcf); } ses.set_ip_filter(loaded_ip_filter); for (auto const& i : torrents) { if (i.substr(0, 7) == "magnet:") add_magnet(ses, i); else add_torrent(ses, std::string(i)); } std::thread resume_data_loader([&ses] { // load resume files lt::error_code ec; std::string const resume_dir = path_append(save_path, ".resume"); std::vector ents = list_dir(resume_dir , [](lt::string_view p) { return p.size() > 7 && p.substr(p.size() - 7) == ".resume"; }, ec); if (ec) { std::fprintf(stderr, "failed to list resume directory \"%s\": (%s : %d) %s\n" , resume_dir.c_str(), ec.category().name(), ec.value(), ec.message().c_str()); } else { for (auto const& e : ents) { // only load resume files of the form .resume if (!is_resume_file(e)) continue; std::string const file = path_append(resume_dir, e); std::vector resume_data; if (!load_file(file, resume_data)) { std::printf(" failed to load resume file \"%s\": %s\n" , file.c_str(), ec.message().c_str()); continue; } add_torrent_params p = lt::read_resume_data(resume_data, ec); if (ec) { std::printf(" failed to parse resume data \"%s\": %s\n" , file.c_str(), ec.message().c_str()); continue; } ses.async_add_torrent(std::move(p)); } } }); // main loop #ifndef _WIN32 signal(SIGTERM, signal_handler); signal(SIGINT, signal_handler); #endif while (!quit && loop_limit != 0) { if (loop_limit > 0) --loop_limit; ses.post_torrent_updates(); ses.post_session_stats(); ses.post_dht_stats(); auto const [terminal_width, terminal_height] = terminal_size(); // the ratio of torrent-list and details below depend on the number of // torrents we have in the session int const height = std::min(terminal_height / 2 , std::max(5, view.num_visible_torrents() + 2)); view.set_size(terminal_width, height); ses_view.set_pos(height); ses_view.set_width(terminal_width); int c = 0; if (sleep_and_input(&c, refresh_delay)) { #ifdef _WIN32 constexpr int escape_seq = 224; constexpr int left_arrow = 75; constexpr int right_arrow = 77; constexpr int up_arrow = 72; constexpr int down_arrow = 80; #else constexpr int escape_seq = 27; constexpr int left_arrow = 68; constexpr int right_arrow = 67; constexpr int up_arrow = 65; constexpr int down_arrow = 66; #endif torrent_handle h = view.get_active_handle(); if (c == EOF) { quit = true; break; } do { if (c == escape_seq) { // escape code, read another character #ifdef _WIN32 int c2 = _getch(); #else int c2 = getc(stdin); if (c2 == EOF) { quit = true; break; } if (c2 != '[') continue; c2 = getc(stdin); #endif if (c2 == EOF) { quit = true; break; } if (c2 == left_arrow) { int const filter = view.filter(); if (filter > 0) { client_state.clear(); view.set_filter(filter - 1); h = view.get_active_handle(); } } else if (c2 == right_arrow) { int const filter = view.filter(); if (filter < torrent_view::torrents_max - 1) { client_state.clear(); view.set_filter(filter + 1); h = view.get_active_handle(); } } else if (c2 == up_arrow) { client_state.clear(); view.arrow_up(); h = view.get_active_handle(); } else if (c2 == down_arrow) { client_state.clear(); view.arrow_down(); h = view.get_active_handle(); } } if (c == '<') { int const order = view.sort_order(); if (order > 0) view.set_sort_order(order - 1); } if (c == '>') { int const order = view.sort_order(); if (order < 2) view.set_sort_order(order + 1); } if (c == '[' && h.is_valid()) { h.queue_position_up(); } if (c == ']' && h.is_valid()) { h.queue_position_down(); } // add magnet link if (c == 'm') { char url[4096]; url[0] = '\0'; puts("Enter magnet link:\n"); #ifndef _WIN32 // enable terminal echo temporarily set_keypress echo_(set_keypress::echo | set_keypress::canonical); #endif if (std::scanf("%4095s", url) == 1) add_magnet(ses, url); else std::printf("failed to read magnet link\n"); } if (c == 'q') { quit = true; break; } if (c == 'W' && h.is_valid()) { std::set seeds = h.url_seeds(); for (auto const& s : seeds) h.remove_url_seed(s); } if (c == 'D' && h.is_valid()) { torrent_status const& st = view.get_active_torrent(); std::printf("\n\nARE YOU SURE YOU WANT TO DELETE THE FILES FOR '%s'. THIS OPERATION CANNOT BE UNDONE. (y/N)" , st.name.c_str()); #ifndef _WIN32 // enable terminal echo temporarily set_keypress echo_(set_keypress::echo | set_keypress::canonical); #endif char response = 'n'; int scan_ret = std::scanf("%c", &response); if (scan_ret == 1 && response == 'y') { // also delete the resume file std::string const rpath = resume_file(st.info_hashes); if (::remove(rpath.c_str()) < 0) std::printf("failed to delete resume file (\"%s\")\n" , rpath.c_str()); if (st.handle.is_valid()) { ses.remove_torrent(st.handle, lt::session::delete_files); } else { std::printf("failed to delete torrent, invalid handle: %s\n" , st.name.c_str()); } client_state.clear(); } } if (c == 'j' && h.is_valid()) { h.force_recheck(); } if (c == 'r' && h.is_valid()) { h.force_reannounce(); } if (c == 's' && h.is_valid()) { torrent_status const& ts = view.get_active_torrent(); h.set_flags(~ts.flags, lt::torrent_flags::sequential_download); } if (c == 'R') { // save resume data for all torrents view.for_each_torrent([&](lt::torrent_status const& st) { st.handle.save_resume_data(torrent_handle::save_info_dict); ++num_outstanding_resume_data; }); } if (c == 'o' && h.is_valid()) { torrent_status const& ts = view.get_active_torrent(); int num_pieces = ts.num_pieces; if (num_pieces > 300) num_pieces = 300; for (piece_index_t i(0); i < piece_index_t(num_pieces); ++i) { h.set_piece_deadline(i, (static_cast(i)+5) * 1000 , torrent_handle::alert_when_available); } } if (c == 'v' && h.is_valid()) { h.scrape_tracker(); } if (c == 'p' && h.is_valid()) { torrent_status const& ts = view.get_active_torrent(); if ((ts.flags & (lt::torrent_flags::auto_managed | lt::torrent_flags::paused)) == lt::torrent_flags::paused) { h.set_flags(lt::torrent_flags::auto_managed); } else { h.unset_flags(lt::torrent_flags::auto_managed); h.pause(torrent_handle::graceful_pause); } } // toggle force-start if (c == 'k' && h.is_valid()) { torrent_status const& ts = view.get_active_torrent(); h.set_flags( ~(ts.flags & lt::torrent_flags::auto_managed), lt::torrent_flags::auto_managed); if ((ts.flags & lt::torrent_flags::auto_managed) && (ts.flags & lt::torrent_flags::paused)) { h.resume(); } } if (c == 'c' && h.is_valid()) { h.clear_error(); } // toggle displays if (c == 't') print_trackers = !print_trackers; if (c == 'i') print_peers = !print_peers; if (c == 'I') print_peers_legend = !print_peers_legend; if (c == 'l') print_log = !print_log; if (c == 'd') print_downloads = (print_downloads + 1) % 3; if (c == 'y') print_matrix = !print_matrix; if (c == 'f') print_file_progress = !print_file_progress; if (c == 'a') print_piece_availability = !print_piece_availability; if (c == 'P') show_pad_files = !show_pad_files; if (c == 'g') show_dht_status = !show_dht_status; if (c == 'x') print_disk_stats = !print_disk_stats; // toggle columns if (c == '1') print_ip = !print_ip; if (c == '2') print_connecting_peers = !print_connecting_peers; if (c == '3') print_timers = !print_timers; if (c == '4') print_block = !print_block; if (c == '5') print_peaks = !print_peaks; if (c == '6') print_fails = !print_fails; if (c == '7') print_send_bufs = !print_send_bufs; if (c == '8') print_local_ip = !print_local_ip; if (c == 'h') { clear_screen(); set_cursor_pos(0,0); print( R"(HELP SCREEN (press any key to dismiss) CLIENT OPTIONS [q] quit client [m] add magnet link TORRENT ACTIONS [p] pause/resume selected torrent [W] remove all web seeds [s] toggle sequential download [j] force recheck [space] toggle session pause [c] clear error [v] scrape [D] delete torrent and data [r] force reannounce [R] save resume data for all torrents [o] set piece deadlines (sequential dl) [P] toggle auto-managed [k] toggle force-started [W] remove all web seeds [ move queue position closer to beginning ] move queue position closer to end DISPLAY OPTIONS left/right arrow keys: select torrent filter up/down arrow keys: select torrent [i] toggle show peers [d] toggle show downloading pieces [P] show pad files (in file list) [f] toggle show files [g] show DHT [x] toggle disk cache stats [t] show trackers [l] toggle show log [y] toggle show piece matrix [I] toggle show peer flag legend [a] toggle show piece availability COLUMN OPTIONS [1] toggle IP column [2] toggle show peer connection attempts [3] toggle timers column [4] toggle block progress column [5] toggle print peak rates [6] toggle failures column [7] toggle send buffers column [8] toggle local IP column )"); int tmp; while (sleep_and_input(&tmp, lt::milliseconds(500)) == false); } } while (sleep_and_input(&c, lt::milliseconds(0))); if (c == 'q') { quit = true; break; } } pop_alerts(client_state, ses); std::string out; char str[500]; int pos = view.height() + ses_view.height(); set_cursor_pos(0, pos); torrent_handle h = view.get_active_handle(); #ifndef TORRENT_DISABLE_DHT if (show_dht_status) { // TODO: 3 expose these counters as performance counters /* std::snprintf(str, sizeof(str), "DHT nodes: %d DHT cached nodes: %d " "total DHT size: %" PRId64 " total observers: %d\n" , sess_stat.dht_nodes, sess_stat.dht_node_cache, sess_stat.dht_global_nodes , sess_stat.dht_total_allocations); out += str; */ int bucket = 0; for (lt::dht_routing_bucket const& n : dht_routing_table) { char const* progress_bar = "################################" "################################" "################################" "################################"; char const* short_progress_bar = "--------"; std::snprintf(str, sizeof(str) , "%3d [%3d, %d] %s%s\x1b[K\n" , bucket, n.num_nodes, n.num_replacements , progress_bar + (128 - n.num_nodes) , short_progress_bar + (8 - std::min(8, n.num_replacements))); out += str; pos += 1; ++bucket; } for (lt::dht_lookup const& l : dht_active_requests) { std::snprintf(str, sizeof(str) , " %10s target: %s " "[limit: %2d] " "in-flight: %-2d " "left: %-3d " "1st-timeout: %-2d " "timeouts: %-2d " "responses: %-2d " "last_sent: %-2d " "\x1b[K\n" , l.type , to_hex(l.target).c_str() , l.branch_factor , l.outstanding_requests , l.nodes_left , l.first_timeout , l.timeouts , l.responses , l.last_sent); out += str; pos += 1; } } #endif lt::time_point const now = lt::clock_type::now(); if (h.is_valid()) { torrent_status const& s = view.get_active_torrent(); if (!print_matrix) { print((piece_bar(s.pieces, terminal_width - 2) + "\x1b[K\n").c_str()); pos += 1; } if ((print_downloads > 0 && s.state != torrent_status::seeding) || print_peers) h.post_peer_info(); auto& peers = client_state.peers; if (print_peers && !peers.empty()) { using lt::peer_info; // sort connecting towards the bottom of the list, and by peer_id // otherwise, to keep the list as stable as possible std::sort(peers.begin(), peers.end() , [](peer_info const& lhs, peer_info const& rhs) { { bool const l = bool(lhs.flags & peer_info::connecting); bool const r = bool(rhs.flags & peer_info::connecting); if (l != r) return l < r; } { bool const l = bool(lhs.flags & peer_info::handshake); bool const r = bool(rhs.flags & peer_info::handshake); if (l != r) return l < r; } return lhs.pid < rhs.pid; }); pos += print_peer_info(out, peers, terminal_height - pos - 2); if (print_peers_legend) { pos += print_peer_legend(out, terminal_height - pos - 2); } } if (print_trackers) { snprintf(str, sizeof(str), "next_announce: %4" PRId64 " | current tracker: %s\x1b[K\n" , std::int64_t(duration_cast(s.next_announce).count()) , s.current_tracker.c_str()); out += str; pos += 1; h.post_trackers(); for (lt::announce_entry const& ae : client_state.trackers) { std::snprintf(str, sizeof(str), "%2d %-55s %s\x1b[K\n" , ae.tier, ae.url.c_str(), ae.verified?"OK ":"- "); out += str; pos += 1; int idx = 0; for (auto const& ep : ae.endpoints) { ++idx; if (pos + 1 >= terminal_height) break; if (!ep.enabled) continue; for (lt::protocol_version const v : {lt::protocol_version::V1, lt::protocol_version::V2}) { if (!s.info_hashes.has(v)) continue; auto const& av = ep.info_hashes[v]; std::snprintf(str, sizeof(str), " [%2d] %s fails: %-3d (%-3d) %s %5d \"%s\" %s\x1b[K\n" , idx , v == lt::protocol_version::V1 ? "v1" : "v2" , av.fails, ae.fail_limit , to_string(int(total_seconds(av.next_announce - now)), 8).c_str() , av.min_announce > now ? int(total_seconds(av.min_announce - now)) : 0 , av.last_error ? av.last_error.message().c_str() : "" , av.message.c_str()); out += str; pos += 1; // we only need to show this error once, not for every // endpoint if (av.last_error == boost::asio::error::host_not_found) goto done; } } done: if (pos + 1 >= terminal_height) break; } } if (print_matrix) { int height_out = 0; print(piece_matrix(s.pieces, terminal_width, &height_out).c_str()); print("\n"); pos += height_out; } if (print_piece_availability) { h.post_piece_availability(); if (!client_state.piece_availability.empty()) { int const num_pieces = int(client_state.piece_availability.size()); lt::bitfield avail(num_pieces); for (int idx = 0; idx != num_pieces; ++idx) { if (client_state.piece_availability[std::size_t(idx)] > 0) avail.set_bit(idx); } int height_out = 0; print(piece_matrix(avail, terminal_width, &height_out).c_str()); print("\n"); pos += height_out; } } if (print_downloads > 0) { h.post_download_queue(); int p = 0; // this is horizontal position for (lt::partial_piece_info const& i : client_state.download_queue) { if (pos + 3 >= terminal_height) break; int const num_blocks = i.blocks_in_piece; if (print_downloads == 2) { p += num_blocks / 4 + 8; print_compact_piece(i, out); } else { p += num_blocks + 8; int const width = std::max((terminal_width - 8), 8); int const rows = std::max(1, (num_blocks + width - 1) / width); print_piece(i, peers, rows, out); } if (p + num_blocks + 8 > terminal_width) { out += "\x1b[K\n"; pos += 1; p = 0; } } if (p != 0) { out += "\x1b[K\n"; pos += 1; } std::snprintf(str, sizeof(str), "%s %s downloading | %s %s writing | %s %s flushed | %s %s snubbed | = requested\x1b[K\n" , esc("33;7"), esc("0") // downloading , esc("36;7"), esc("0") // writing , esc("32;7"), esc("0") // flushed , esc("35;7"), esc("0") // snubbed ); out += str; pos += 1; } if (print_file_progress && s.has_metadata && h.is_valid()) { h.post_file_progress({}); h.post_file_priorities(); h.post_file_status(); std::shared_ptr ti = s.torrent_file.lock(); // TODO: ti may be nullptr here, we should check auto const& file_progress = client_state.file_progress; auto const& file_prio = client_state.file_prio; auto const& file_status = client_state.file_state; auto f = file_status.begin(); // if there are a lot of files in the torrent, the less space we use to print each file lt::file_storage const& fs = ti->layout(); int const num_files = fs.num_files(); int const file_width = num_files < 10 ? 75 : num_files < 20 ? 65 : num_files < 30 ? 55 : num_files < 40 ? 45 : 35; int p = 0; // this is horizontal position for (file_index_t const i : fs.file_range()) { auto const idx = std::size_t(static_cast(i)); if (pos + 1 >= terminal_height) break; bool const pad_file = fs.pad_file_at(i); if (pad_file && !show_pad_files) continue; if (idx >= file_progress.size()) break; // 0-sized files are always fully downloaded int const progress = fs.file_size(i) > 0 ? int(file_progress[idx] * 1000 / fs.file_size(i)) : 1000; TORRENT_ASSERT(file_progress[idx] <= fs.file_size(i)); bool const complete = file_progress[idx] == fs.file_size(i); std::string title{fs.file_name(i)}; if (!complete) { std::snprintf(str, sizeof(str), " (%.1f%%)", progress / 10.0); title += str; } if (f != file_status.end() && f->file_index == i) { title += " [ "; if ((f->open_mode & lt::file_open_mode::rw_mask) == lt::file_open_mode::read_write) title += "rw "; else if ((f->open_mode & lt::file_open_mode::rw_mask) == lt::file_open_mode::read_only) title += "r "; else if ((f->open_mode & lt::file_open_mode::rw_mask) == lt::file_open_mode::write_only) title += "w "; if (!(f->open_mode & lt::file_open_mode::random_access)) title += "seq "; if (!(f->open_mode & lt::file_open_mode::sparse)) title += "alloc "; if (f->open_mode & lt::file_open_mode::mmapped) title += "mm "; title += "]"; ++f; } const int file_progress_width = pad_file ? 10 : file_width; // do we need to line-break? if (p + file_progress_width + 14 > terminal_width) { out += "\x1b[K\n"; pos += 1; p = 0; if (pos + 1 >= terminal_height) break; } std::snprintf(str, sizeof(str), "%s %7s p: %d ", progress_bar(progress, file_progress_width , pad_file ? col_blue : complete ? col_green : col_yellow , '-', '#', title.c_str()).c_str() , add_suffix(file_progress[idx]).c_str() , static_cast(file_prio[idx])); p += file_progress_width + 14; out += str; } if (p != 0) { out += "\x1b[K\n"; pos += 1; } } } if (print_log) { for (auto const& e : client_state.events) { if (pos + 1 >= terminal_height) break; out += e; out += "\x1b[K\n"; pos += 1; } } // clear rest of screen out += "\x1b[J"; print(out.c_str()); std::fflush(stdout); if (!monitor_dir.empty() && next_dir_scan < now) { scan_dir(monitor_dir, ses); next_dir_scan = now + seconds(poll_interval); } } resume_data_loader.join(); quit = true; ses.pause(); std::printf("saving resume data\n"); // get all the torrent handles that we need to save resume data for int idx = 0; view.for_each_torrent([&](lt::torrent_status const& st) { // save_resume_data will generate an alert when it's done st.handle.save_resume_data(torrent_handle::save_info_dict); ++num_outstanding_resume_data; ++idx; if ((idx % 32) == 0) { std::printf("\r%d ", num_outstanding_resume_data); pop_alerts(client_state, ses); } }); std::printf("\nwaiting for resume data [%d]\n", num_outstanding_resume_data); while (num_outstanding_resume_data > 0) { alert const* a = ses.wait_for_alert(seconds(10)); if (a == nullptr) continue; pop_alerts(client_state, ses); } if (g_log_file) std::fclose(g_log_file); if (g_stats_log_file) std::fclose(g_stats_log_file); // we're just saving the DHT state #ifndef TORRENT_DISABLE_DHT std::printf("\nsaving session state\n"); { std::vector out = write_session_params_buf(ses.session_state(lt::session::save_dht_state)); save_file(".ses_state", out); } #endif std::printf("closing session\n"); return 0; }