librats 1.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -331
- package/binding.gyp +16 -3
- package/lib/index.d.ts +288 -696
- package/lib/index.js +407 -44
- package/native-src/3rdparty/android/ifaddrs-android.c +1 -0
- package/native-src/3rdparty/android/ifaddrs-android.h +1 -0
- package/native-src/CMakeLists.txt +404 -179
- package/native-src/LICENSE +1 -1
- package/native-src/src/librats/bindings/rats.cpp +762 -0
- package/native-src/src/librats/bindings/rats.h +380 -0
- package/native-src/src/librats/bittorrent/bencode.cpp +437 -0
- package/native-src/src/librats/bittorrent/bencode.h +176 -0
- package/native-src/src/librats/bittorrent/bitfield.cpp +97 -0
- package/native-src/src/librats/bittorrent/bitfield.h +76 -0
- package/native-src/src/librats/bittorrent/byte_io.h +58 -0
- package/native-src/src/librats/bittorrent/choker.cpp +25 -0
- package/native-src/src/librats/bittorrent/choker.h +46 -0
- package/native-src/src/librats/bittorrent/client.cpp +413 -0
- package/native-src/src/librats/bittorrent/client.h +227 -0
- package/native-src/src/librats/bittorrent/disk_io.cpp +209 -0
- package/native-src/src/librats/bittorrent/disk_io.h +150 -0
- package/native-src/src/librats/bittorrent/extensions.cpp +191 -0
- package/native-src/src/librats/bittorrent/extensions.h +94 -0
- package/native-src/src/librats/bittorrent/file_storage.cpp +77 -0
- package/native-src/src/librats/bittorrent/file_storage.h +79 -0
- package/native-src/src/librats/bittorrent/log.h +42 -0
- package/native-src/src/librats/bittorrent/magnet_uri.cpp +98 -0
- package/native-src/src/librats/bittorrent/magnet_uri.h +35 -0
- package/native-src/src/librats/bittorrent/peer_connection.cpp +502 -0
- package/native-src/src/librats/bittorrent/peer_connection.h +194 -0
- package/native-src/src/librats/bittorrent/peer_list.cpp +68 -0
- package/native-src/src/librats/bittorrent/peer_list.h +75 -0
- package/native-src/src/librats/bittorrent/piece_picker.cpp +352 -0
- package/native-src/src/librats/bittorrent/piece_picker.h +201 -0
- package/native-src/src/librats/bittorrent/reactor.cpp +97 -0
- package/native-src/src/librats/bittorrent/reactor.h +89 -0
- package/native-src/src/librats/bittorrent/resume_data.cpp +72 -0
- package/native-src/src/librats/bittorrent/resume_data.h +41 -0
- package/native-src/src/librats/bittorrent/store_buffer.cpp +48 -0
- package/native-src/src/librats/bittorrent/store_buffer.h +47 -0
- package/native-src/src/librats/bittorrent/torrent.cpp +870 -0
- package/native-src/src/librats/bittorrent/torrent.h +260 -0
- package/native-src/src/librats/bittorrent/torrent_creator.cpp +129 -0
- package/native-src/src/librats/bittorrent/torrent_creator.h +58 -0
- package/native-src/src/librats/bittorrent/torrent_info.cpp +314 -0
- package/native-src/src/librats/bittorrent/torrent_info.h +118 -0
- package/native-src/src/librats/bittorrent/tracker.cpp +374 -0
- package/native-src/src/librats/bittorrent/tracker.h +108 -0
- package/native-src/src/librats/bittorrent/types.cpp +206 -0
- package/native-src/src/librats/bittorrent/types.h +86 -0
- package/native-src/src/librats/core/address.cpp +35 -0
- package/native-src/src/librats/core/address.h +78 -0
- package/native-src/src/librats/core/bytes.h +69 -0
- package/native-src/src/librats/core/chained_send_buffer.cpp +172 -0
- package/native-src/src/librats/core/chained_send_buffer.h +183 -0
- package/native-src/src/librats/core/endpoint_parse.cpp +41 -0
- package/native-src/src/librats/core/endpoint_parse.h +31 -0
- package/native-src/src/librats/core/event_bus.h +70 -0
- package/native-src/src/librats/core/host_endpoint.h +56 -0
- package/native-src/src/{io_poller.cpp → librats/core/io_poller.cpp} +520 -65
- package/native-src/src/{io_poller.h → librats/core/io_poller.h} +12 -6
- package/native-src/src/librats/core/ip_address.cpp +120 -0
- package/native-src/src/librats/core/ip_address.h +109 -0
- package/native-src/src/librats/core/mpsc_queue.h +47 -0
- package/native-src/src/librats/core/notifier.h +74 -0
- package/native-src/src/librats/core/receive_buffer.cpp +219 -0
- package/native-src/src/librats/core/receive_buffer.h +171 -0
- package/native-src/src/librats/core/service_registry.h +58 -0
- package/native-src/src/{socket.cpp → librats/core/socket.cpp} +625 -118
- package/native-src/src/librats/core/socket.h +496 -0
- package/native-src/src/librats/core/timer_queue.h +105 -0
- package/native-src/src/librats/core/types.cpp +43 -0
- package/native-src/src/librats/core/types.h +103 -0
- package/native-src/src/librats/core/wakeup_pipe.h +83 -0
- package/native-src/src/{crypto → librats/crypto}/blake2_endian.h +21 -23
- package/native-src/src/{crypto → librats/crypto}/blake2b.c +34 -33
- package/native-src/src/{crypto → librats/crypto}/blake2b.h +7 -6
- package/native-src/src/{crypto → librats/crypto}/blake2s.c +55 -54
- package/native-src/src/{crypto → librats/crypto}/blake2s.h +13 -12
- package/native-src/src/{crypto → librats/crypto}/chacha.c +22 -21
- package/native-src/src/{crypto → librats/crypto}/chacha.h +14 -13
- package/native-src/src/{crypto → librats/crypto}/chachapoly.c +56 -56
- package/native-src/src/{crypto → librats/crypto}/chachapoly.h +24 -17
- package/native-src/src/{crc32.cpp → librats/crypto/crc32.cpp} +1 -1
- package/native-src/src/{crc32.h → librats/crypto/crc32.h} +3 -1
- package/native-src/src/{crypto → librats/crypto}/curve25519.c +6 -4
- package/native-src/src/{crypto → librats/crypto}/curve25519.h +6 -3
- package/native-src/src/librats/crypto/hkdf.c +266 -0
- package/native-src/src/{crypto → librats/crypto}/hkdf.h +19 -19
- package/native-src/src/{noise.cpp → librats/crypto/noise.cpp} +84 -73
- package/native-src/src/{noise.h → librats/crypto/noise.h} +18 -8
- package/native-src/src/{crypto → librats/crypto}/poly1305.c +47 -46
- package/native-src/src/librats/crypto/poly1305.h +37 -0
- package/native-src/src/{sha1.cpp → librats/crypto/sha1.cpp} +33 -1
- package/native-src/src/{sha1.h → librats/crypto/sha1.h} +14 -6
- package/native-src/src/{crypto → librats/crypto}/sha256.c +15 -14
- package/native-src/src/{crypto → librats/crypto}/sha256.h +8 -7
- package/native-src/src/{crypto → librats/crypto}/sha512.c +15 -14
- package/native-src/src/{crypto → librats/crypto}/sha512.h +8 -7
- package/native-src/src/librats/dht/announce.cpp +37 -0
- package/native-src/src/librats/dht/announce.h +41 -0
- package/native-src/src/librats/dht/bep42.cpp +109 -0
- package/native-src/src/librats/dht/bep42.h +48 -0
- package/native-src/src/librats/dht/dht.cpp +501 -0
- package/native-src/src/librats/dht/dht.h +119 -0
- package/native-src/src/librats/dht/dht_runner.cpp +103 -0
- package/native-src/src/librats/dht/dht_runner.h +71 -0
- package/native-src/src/librats/dht/dos_blocker.cpp +42 -0
- package/native-src/src/librats/dht/dos_blocker.h +47 -0
- package/native-src/src/librats/dht/find_peers.cpp +52 -0
- package/native-src/src/librats/dht/find_peers.h +73 -0
- package/native-src/src/librats/dht/id.h +167 -0
- package/native-src/src/{krpc.cpp → librats/dht/krpc.cpp} +32 -81
- package/native-src/src/{krpc.h → librats/dht/krpc.h} +19 -23
- package/native-src/src/librats/dht/log.h +38 -0
- package/native-src/src/librats/dht/node.cpp +473 -0
- package/native-src/src/librats/dht/node.h +164 -0
- package/native-src/src/librats/dht/node_entry.h +81 -0
- package/native-src/src/librats/dht/observer.h +72 -0
- package/native-src/src/librats/dht/persistence.cpp +90 -0
- package/native-src/src/librats/dht/persistence.h +32 -0
- package/native-src/src/librats/dht/routing_table.cpp +559 -0
- package/native-src/src/librats/dht/routing_table.h +185 -0
- package/native-src/src/librats/dht/rpc_manager.cpp +127 -0
- package/native-src/src/librats/dht/rpc_manager.h +77 -0
- package/native-src/src/librats/dht/storage.cpp +92 -0
- package/native-src/src/librats/dht/storage.h +74 -0
- package/native-src/src/librats/dht/transport.h +27 -0
- package/native-src/src/librats/dht/traversal.cpp +326 -0
- package/native-src/src/librats/dht/traversal.h +120 -0
- package/native-src/src/librats/dht/udp_transport.cpp +49 -0
- package/native-src/src/librats/dht/udp_transport.h +51 -0
- package/native-src/src/librats/mdns/log.h +22 -0
- package/native-src/src/{mdns.cpp → librats/mdns/mdns.cpp} +75 -40
- package/native-src/src/{mdns.h → librats/mdns/mdns.h} +9 -8
- package/native-src/src/{natpmp.cpp → librats/nat/natpmp.cpp} +12 -9
- package/native-src/src/{natpmp.h → librats/nat/natpmp.h} +3 -3
- package/native-src/src/{port_mapping.h → librats/nat/port_mapping.h} +3 -2
- package/native-src/src/{stun.cpp → librats/nat/stun.cpp} +4 -4
- package/native-src/src/{stun.h → librats/nat/stun.h} +1 -1
- package/native-src/src/{upnp.cpp → librats/nat/upnp.cpp} +6 -6
- package/native-src/src/{upnp.h → librats/nat/upnp.h} +2 -2
- package/native-src/src/librats/node/circuit_service.h +84 -0
- package/native-src/src/librats/node/config.h +110 -0
- package/native-src/src/librats/node/dial_service.h +54 -0
- package/native-src/src/librats/node/dialer.cpp +264 -0
- package/native-src/src/librats/node/dialer.h +188 -0
- package/native-src/src/librats/node/host_events.h +26 -0
- package/native-src/src/librats/node/identify.cpp +130 -0
- package/native-src/src/librats/node/identify.h +71 -0
- package/native-src/src/librats/node/nat_status.cpp +103 -0
- package/native-src/src/librats/node/nat_status.h +118 -0
- package/native-src/src/librats/node/node.cpp +865 -0
- package/native-src/src/librats/node/node.h +344 -0
- package/native-src/src/librats/node/node_context.h +33 -0
- package/native-src/src/librats/node/peer_network.h +91 -0
- package/native-src/src/librats/peer/peer.h +49 -0
- package/native-src/src/librats/peer/peer_book.cpp +181 -0
- package/native-src/src/librats/peer/peer_book.h +88 -0
- package/native-src/src/librats/peer/peer_id.cpp +72 -0
- package/native-src/src/librats/peer/peer_id.h +62 -0
- package/native-src/src/librats/peer/peer_info.h +37 -0
- package/native-src/src/librats/peer/peer_table.cpp +170 -0
- package/native-src/src/librats/peer/peer_table.h +148 -0
- package/native-src/src/librats/security/handshaker.h +66 -0
- package/native-src/src/librats/security/identity.h +43 -0
- package/native-src/src/librats/security/noise_security.cpp +122 -0
- package/native-src/src/librats/security/noise_security.h +37 -0
- package/native-src/src/librats/security/plaintext_security.h +106 -0
- package/native-src/src/librats/security/session.h +37 -0
- package/native-src/src/{storage.cpp → librats/storage/storage.cpp} +369 -522
- package/native-src/src/{storage.h → librats/storage/storage.h} +135 -299
- package/native-src/src/librats/subsystems/bittorrent.cpp +211 -0
- package/native-src/src/librats/subsystems/bittorrent.h +136 -0
- package/native-src/src/librats/subsystems/dht_discovery.cpp +202 -0
- package/native-src/src/librats/subsystems/dht_discovery.h +123 -0
- package/native-src/src/librats/subsystems/dht_service.h +36 -0
- package/native-src/src/librats/subsystems/file_transfer.cpp +972 -0
- package/native-src/src/librats/subsystems/file_transfer.h +367 -0
- package/native-src/src/librats/subsystems/hole_punch.cpp +605 -0
- package/native-src/src/librats/subsystems/hole_punch.h +290 -0
- package/native-src/src/librats/subsystems/hole_punch_service.h +38 -0
- package/native-src/src/librats/subsystems/mdns_discovery.cpp +66 -0
- package/native-src/src/librats/subsystems/mdns_discovery.h +55 -0
- package/native-src/src/librats/subsystems/message_json.cpp +112 -0
- package/native-src/src/librats/subsystems/message_json.h +88 -0
- package/native-src/src/librats/subsystems/peer_exchange.cpp +241 -0
- package/native-src/src/librats/subsystems/peer_exchange.h +136 -0
- package/native-src/src/librats/subsystems/ping_service.cpp +98 -0
- package/native-src/src/librats/subsystems/ping_service.h +66 -0
- package/native-src/src/librats/subsystems/port_mapping_service.cpp +192 -0
- package/native-src/src/librats/subsystems/port_mapping_service.h +84 -0
- package/native-src/src/librats/subsystems/pubsub.cpp +567 -0
- package/native-src/src/librats/subsystems/pubsub.h +175 -0
- package/native-src/src/librats/subsystems/reconnection.cpp +239 -0
- package/native-src/src/librats/subsystems/reconnection.h +126 -0
- package/native-src/src/librats/subsystems/relay.cpp +1142 -0
- package/native-src/src/librats/subsystems/relay.h +211 -0
- package/native-src/src/librats/subsystems/relay_service.h +46 -0
- package/native-src/src/librats/transport/connection.cpp +343 -0
- package/native-src/src/librats/transport/connection.h +283 -0
- package/native-src/src/librats/transport/link.h +96 -0
- package/native-src/src/librats/transport/reactor.cpp +588 -0
- package/native-src/src/librats/transport/reactor.h +262 -0
- package/native-src/src/librats/transport/reactor_pool.h +81 -0
- package/native-src/src/librats/transport/relay_link.cpp +208 -0
- package/native-src/src/librats/transport/relay_link.h +303 -0
- package/native-src/src/librats/transport/tcp_link.cpp +49 -0
- package/native-src/src/librats/transport/tcp_link.h +43 -0
- package/native-src/src/librats/transport/udp_mux.cpp +617 -0
- package/native-src/src/librats/transport/udp_mux.h +363 -0
- package/native-src/src/librats/transport/udp_packet.cpp +121 -0
- package/native-src/src/librats/transport/udp_packet.h +190 -0
- package/native-src/src/librats/transport/udp_stream.cpp +1194 -0
- package/native-src/src/librats/transport/udp_stream.h +614 -0
- package/native-src/src/librats/util/features.h.in +51 -0
- package/native-src/src/{fs.cpp → librats/util/fs.cpp} +51 -3
- package/native-src/src/librats/util/fs.h +136 -0
- package/native-src/src/librats/util/json.cpp +1002 -0
- package/native-src/src/librats/util/json.h +444 -0
- package/native-src/src/{logger.cpp → librats/util/logger.cpp} +1 -1
- package/native-src/src/{logger.h → librats/util/logger.h} +43 -31
- package/native-src/src/{network_monitor.cpp → librats/util/network_monitor.cpp} +12 -4
- package/native-src/src/{network_monitor.h → librats/util/network_monitor.h} +2 -1
- package/native-src/src/{network_utils.cpp → librats/util/network_utils.cpp} +38 -23
- package/native-src/src/{network_utils.h → librats/util/network_utils.h} +15 -8
- package/native-src/src/{os.cpp → librats/util/os.cpp} +48 -18
- package/native-src/src/librats/util/rats_export.h +69 -0
- package/native-src/src/{version.cpp → librats/util/version.cpp} +2 -2
- package/native-src/src/{version.h.in → librats/util/version.h.in} +1 -1
- package/native-src/src/librats/wire/frame.cpp +76 -0
- package/native-src/src/librats/wire/frame.h +111 -0
- package/native-src/src/librats/wire/message_router.cpp +45 -0
- package/native-src/src/librats/wire/message_router.h +47 -0
- package/package.json +5 -4
- package/scripts/build-librats.js +1 -0
- package/scripts/postinstall.js +3 -3
- package/scripts/prepare-package.js +4 -4
- package/scripts/verify-installation.js +63 -105
- package/src/librats_node.cpp +1067 -1323
- package/native-src/src/bencode.cpp +0 -485
- package/native-src/src/bencode.h +0 -145
- package/native-src/src/bittorrent.cpp +0 -14
- package/native-src/src/bittorrent.h +0 -74
- package/native-src/src/bt_bitfield.cpp +0 -372
- package/native-src/src/bt_bitfield.h +0 -316
- package/native-src/src/bt_choker.cpp +0 -228
- package/native-src/src/bt_choker.h +0 -147
- package/native-src/src/bt_client.cpp +0 -1047
- package/native-src/src/bt_client.h +0 -445
- package/native-src/src/bt_create_torrent.cpp +0 -677
- package/native-src/src/bt_create_torrent.h +0 -473
- package/native-src/src/bt_extension.cpp +0 -469
- package/native-src/src/bt_extension.h +0 -309
- package/native-src/src/bt_file_storage.cpp +0 -261
- package/native-src/src/bt_file_storage.h +0 -298
- package/native-src/src/bt_handshake.cpp +0 -134
- package/native-src/src/bt_handshake.h +0 -157
- package/native-src/src/bt_messages.cpp +0 -364
- package/native-src/src/bt_messages.h +0 -324
- package/native-src/src/bt_network.cpp +0 -1007
- package/native-src/src/bt_network.h +0 -417
- package/native-src/src/bt_peer_connection.cpp +0 -742
- package/native-src/src/bt_peer_connection.h +0 -592
- package/native-src/src/bt_piece_picker.cpp +0 -786
- package/native-src/src/bt_piece_picker.h +0 -473
- package/native-src/src/bt_resume_data.cpp +0 -410
- package/native-src/src/bt_resume_data.h +0 -249
- package/native-src/src/bt_torrent.cpp +0 -2120
- package/native-src/src/bt_torrent.h +0 -641
- package/native-src/src/bt_torrent_info.cpp +0 -659
- package/native-src/src/bt_torrent_info.h +0 -418
- package/native-src/src/bt_types.h +0 -621
- package/native-src/src/chained_send_buffer.cpp +0 -75
- package/native-src/src/chained_send_buffer.h +0 -137
- package/native-src/src/crypto/hkdf.c +0 -266
- package/native-src/src/crypto/poly1305.h +0 -36
- package/native-src/src/dht.cpp +0 -3311
- package/native-src/src/dht.h +0 -717
- package/native-src/src/disk_io.cpp +0 -632
- package/native-src/src/disk_io.h +0 -315
- package/native-src/src/file_transfer.cpp +0 -1415
- package/native-src/src/file_transfer.h +0 -286
- package/native-src/src/fs.h +0 -108
- package/native-src/src/gossipsub.cpp +0 -1139
- package/native-src/src/gossipsub.h +0 -403
- package/native-src/src/ice.cpp +0 -893
- package/native-src/src/ice.h +0 -559
- package/native-src/src/json.hpp +0 -25526
- package/native-src/src/librats.cpp +0 -2378
- package/native-src/src/librats.h +0 -2324
- package/native-src/src/librats_bittorrent.cpp +0 -601
- package/native-src/src/librats_c.cpp +0 -1557
- package/native-src/src/librats_c.h +0 -323
- package/native-src/src/librats_discovery.cpp +0 -402
- package/native-src/src/librats_encryption.cpp +0 -275
- package/native-src/src/librats_file_transfer.cpp +0 -144
- package/native-src/src/librats_gossipsub.cpp +0 -289
- package/native-src/src/librats_ice.cpp +0 -213
- package/native-src/src/librats_log_macros.h +0 -36
- package/native-src/src/librats_logging.cpp +0 -173
- package/native-src/src/librats_mdns.cpp +0 -166
- package/native-src/src/librats_persistence.cpp +0 -796
- package/native-src/src/librats_portmap.cpp +0 -419
- package/native-src/src/librats_reconnection.cpp +0 -218
- package/native-src/src/librats_statistic.cpp +0 -105
- package/native-src/src/librats_storage.cpp +0 -189
- package/native-src/src/rats_export.h +0 -17
- package/native-src/src/receive_buffer.cpp +0 -82
- package/native-src/src/receive_buffer.h +0 -127
- package/native-src/src/socket.h +0 -228
- package/native-src/src/threadmanager.cpp +0 -105
- package/native-src/src/threadmanager.h +0 -53
- package/native-src/src/tracker.cpp +0 -1264
- package/native-src/src/tracker.h +0 -319
- package/native-src/src/turn.cpp +0 -762
- package/native-src/src/turn.h +0 -460
- package/native-src/src/wakeup_pipe.h +0 -60
- /package/native-src/src/{os.h → librats/util/os.h} +0 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
#include "librats/bittorrent/torrent.h"
|
|
2
|
+
|
|
3
|
+
#include "librats/bittorrent/byte_io.h"
|
|
4
|
+
#include "librats/bittorrent/log.h"
|
|
5
|
+
#include "librats/util/fs.h"
|
|
6
|
+
|
|
7
|
+
#include <algorithm>
|
|
8
|
+
#include <chrono>
|
|
9
|
+
#include <cstdlib>
|
|
10
|
+
#include <unordered_map>
|
|
11
|
+
|
|
12
|
+
namespace librats::bittorrent {
|
|
13
|
+
|
|
14
|
+
Torrent::Torrent(Reactor& reactor, TorrentHost& host, TorrentInfo info, std::string save_path)
|
|
15
|
+
: reactor_(reactor)
|
|
16
|
+
, host_(host)
|
|
17
|
+
, info_(std::move(info))
|
|
18
|
+
, save_path_(std::move(save_path))
|
|
19
|
+
, choker_(4) {}
|
|
20
|
+
|
|
21
|
+
Torrent::~Torrent() {
|
|
22
|
+
// Invalidate the liveness token before tearing down: stop() joins the disk /
|
|
23
|
+
// tracker workers, and a completion a worker posts during that join lands in
|
|
24
|
+
// the reactor queue after `this` is gone — the guard in post() drops it.
|
|
25
|
+
alive_->store(false, std::memory_order_release);
|
|
26
|
+
stop();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void Torrent::post(std::function<void()> fn) {
|
|
30
|
+
reactor_.post([alive = alive_, fn = std::move(fn)]() mutable {
|
|
31
|
+
if (alive->load(std::memory_order_acquire)) fn();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void Torrent::start() {
|
|
36
|
+
if (running_) return;
|
|
37
|
+
running_ = true;
|
|
38
|
+
has_metadata_ = info_.has_metadata();
|
|
39
|
+
|
|
40
|
+
if (has_metadata_) {
|
|
41
|
+
picker_ = std::make_unique<PiecePicker>(info_.num_pieces(), info_.piece_length(), info_.total_size());
|
|
42
|
+
disk_ = std::make_unique<ThreadedDiskIo>(
|
|
43
|
+
info_, save_path_,
|
|
44
|
+
[this](std::function<void()> fn) { post(std::move(fn)); });
|
|
45
|
+
state_ = State::Checking;
|
|
46
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " \"" << info_.name() << "\" → Checking ("
|
|
47
|
+
<< info_.num_pieces() << " pieces, " << info_.total_size() << " bytes)");
|
|
48
|
+
// Pieces in resume_have_ are trusted (skip the hash); the rest are verified.
|
|
49
|
+
disk_->async_check_files(resume_have_, nullptr,
|
|
50
|
+
[this](Bitfield have) { on_check_complete(std::move(have)); });
|
|
51
|
+
} else {
|
|
52
|
+
// Magnet: no metadata yet — fetch the info dict from peers (BEP 9) first.
|
|
53
|
+
state_ = State::Metadata;
|
|
54
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " → Metadata (magnet, awaiting info dict)");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Trackers come from the .torrent or the magnet link; announce to find peers
|
|
58
|
+
// (which also seeds the metadata fetch for magnets).
|
|
59
|
+
if (!info_.all_trackers().empty()) {
|
|
60
|
+
trackers_ = std::make_unique<TrackerAnnouncer>(
|
|
61
|
+
info_.all_trackers(), [this](std::function<void()> fn) { post(std::move(fn)); });
|
|
62
|
+
announce_trackers(TrackerEvent::Started);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
schedule_tick();
|
|
66
|
+
try_connect();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void Torrent::stop() {
|
|
70
|
+
if (!running_) return;
|
|
71
|
+
running_ = false;
|
|
72
|
+
paused_ = false;
|
|
73
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " stopped (" << peers_.size() << " peers)");
|
|
74
|
+
if (tick_timer_ != kInvalidTimerId) { reactor_.cancel(tick_timer_); tick_timer_ = kInvalidTimerId; }
|
|
75
|
+
if (trackers_) {
|
|
76
|
+
announce_trackers(TrackerEvent::Stopped); // tell trackers we're leaving (H12)
|
|
77
|
+
trackers_->stop(); // drains the in-flight Stopped announce
|
|
78
|
+
trackers_.reset();
|
|
79
|
+
}
|
|
80
|
+
for (PeerConnection* pc : peers_) pc->close("torrent stopped");
|
|
81
|
+
peers_.clear();
|
|
82
|
+
outstanding_.clear();
|
|
83
|
+
recent_down_.clear();
|
|
84
|
+
seed_peers_.clear();
|
|
85
|
+
if (disk_) disk_->stop();
|
|
86
|
+
disk_.reset();
|
|
87
|
+
picker_.reset();
|
|
88
|
+
state_ = State::Stopped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void Torrent::on_check_complete(Bitfield have) {
|
|
92
|
+
if (!running_) return;
|
|
93
|
+
picker_->set_have_bitfield(have);
|
|
94
|
+
// Account the bytes already on disk toward `left` via verified_bytes_ — NOT
|
|
95
|
+
// bytes_downloaded_, which is the cumulative download counter restored verbatim
|
|
96
|
+
// from resume. Folding on-disk pieces into it too would double-count a resumed
|
|
97
|
+
// torrent (and drive `left` to 0, falsely announcing us as a seed).
|
|
98
|
+
for (std::uint32_t p = 0; p < info_.num_pieces(); ++p)
|
|
99
|
+
if (picker_->have_piece(p)) verified_bytes_ += info_.piece_size(p);
|
|
100
|
+
|
|
101
|
+
if (picker_->is_finished()) { state_ = State::Seeding; completed_announced_ = true; }
|
|
102
|
+
else { state_ = State::Downloading; }
|
|
103
|
+
|
|
104
|
+
// A peer that connected while the check was still running was handed an EMPTY
|
|
105
|
+
// bitfield: it is sent from on_peer_connected, and the protocol pins it to the
|
|
106
|
+
// first message after the handshake, so it cannot be re-sent now that we know
|
|
107
|
+
// what is on disk (our own receiver rejects it too — see piece_state_begun_).
|
|
108
|
+
// Announce the verified pieces with HAVE instead — otherwise such a peer never
|
|
109
|
+
// sees us as interesting, never asks for a block, and the transfer sits at 0%
|
|
110
|
+
// forever. A tracker/DHT answer landing inside the check window hits this every
|
|
111
|
+
// time, which is what BtTracker.DiscoversSeederViaTrackerAndDownloads tripped on.
|
|
112
|
+
//
|
|
113
|
+
// Cost: send_have() flushes per message, so this is one syscall per piece per
|
|
114
|
+
// peer — ~3 us each, i.e. ~50 ms per peer on a 20k-piece torrent (whose check
|
|
115
|
+
// runs for minutes, so peers really do pile up). It is a cold path — once per
|
|
116
|
+
// torrent, never per piece or per block — so the burst is accepted rather than
|
|
117
|
+
// batched; revisit with a batched send_haves() if the stall ever shows up.
|
|
118
|
+
//
|
|
119
|
+
// Iterate a SNAPSHOT: a failing send closes the connection inline (flush ->
|
|
120
|
+
// close -> on_closed -> remove_peer), which erases from peers_ mid-loop. With
|
|
121
|
+
// num_pieces sends per peer, hitting a dead socket in here is likely enough to
|
|
122
|
+
// matter, and iterating the live vector would be UB.
|
|
123
|
+
for (PeerConnection* pc : std::vector<PeerConnection*>(peers_)) {
|
|
124
|
+
if (!alive(pc)) continue; // dropped by an earlier peer's teardown
|
|
125
|
+
for (std::uint32_t p = 0; p < info_.num_pieces(); ++p)
|
|
126
|
+
if (picker_->have_piece(p)) pc->send_have(p);
|
|
127
|
+
if (!alive(pc)) continue; // ...or by its own
|
|
128
|
+
update_interest(*pc); // our own interest is stale too: we may now have it all
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (state_ == State::Seeding)
|
|
132
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " check complete: all "
|
|
133
|
+
<< info_.num_pieces() << " pieces present → Seeding");
|
|
134
|
+
else
|
|
135
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " check complete: " << picker_->num_have()
|
|
136
|
+
<< '/' << info_.num_pieces() << " pieces (" << int(progress() * 100)
|
|
137
|
+
<< "%) → Downloading");
|
|
138
|
+
|
|
139
|
+
try_connect();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void Torrent::pause() {
|
|
143
|
+
if (!running_ || paused_) return;
|
|
144
|
+
paused_ = true;
|
|
145
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " paused (" << peers_.size() << " peers dropped)");
|
|
146
|
+
// Drop all peers and their per-peer request state, but keep picker_/disk_ so a
|
|
147
|
+
// later resume() does not have to re-hash what is already on disk.
|
|
148
|
+
for (PeerConnection* pc : peers_) pc->close("torrent paused");
|
|
149
|
+
peers_.clear();
|
|
150
|
+
outstanding_.clear();
|
|
151
|
+
request_time_.clear();
|
|
152
|
+
recent_down_.clear();
|
|
153
|
+
recent_up_.clear();
|
|
154
|
+
seed_peers_.clear();
|
|
155
|
+
peer_ext_.clear();
|
|
156
|
+
optimistic_ = nullptr;
|
|
157
|
+
// Leave the swarm: tell trackers we're gone so they stop handing our address to
|
|
158
|
+
// others (mirrors stop(), minus tearing down picker_/disk_). tick() is gated on
|
|
159
|
+
// paused_ below, so DHT/tracker re-announces halt too — no more inbound dials.
|
|
160
|
+
announce_trackers(TrackerEvent::Stopped);
|
|
161
|
+
// The 1 s tick keeps running (cheap) but returns early while paused.
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
void Torrent::resume() {
|
|
165
|
+
if (!running_ || !paused_) return;
|
|
166
|
+
paused_ = false;
|
|
167
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " resumed");
|
|
168
|
+
// Rejoin the swarm: a fresh Started announce repopulates the peer list (the DHT
|
|
169
|
+
// get_peers / periodic announces resume with the tick).
|
|
170
|
+
announce_trackers(TrackerEvent::Started);
|
|
171
|
+
try_connect();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
void Torrent::add_peer(const std::string& ip, std::uint16_t port) {
|
|
175
|
+
if (peer_list_.add(ip, port, PeerSource::Tracker) && running_ && !paused_)
|
|
176
|
+
post([this] { try_connect(); });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void Torrent::try_connect() {
|
|
180
|
+
if (!running_ || paused_ || peers_.size() >= kMaxPeers) return;
|
|
181
|
+
auto candidates = peer_list_.connect_candidates(kMaxPeers - peers_.size());
|
|
182
|
+
for (const auto& c : candidates) host_.connect_peer(*this, c.ip, c.port);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
void Torrent::on_connect_failed(const std::string& ip, std::uint16_t port) {
|
|
186
|
+
peer_list_.on_connect_failed(ip, port);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---- scheduling ----
|
|
190
|
+
|
|
191
|
+
void Torrent::schedule_tick() {
|
|
192
|
+
if (!running_) return;
|
|
193
|
+
tick_timer_ = reactor_.schedule(std::chrono::seconds(1), [this] { tick(); });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void Torrent::tick() {
|
|
197
|
+
if (!running_) return;
|
|
198
|
+
++tick_count_;
|
|
199
|
+
|
|
200
|
+
// Paused: no swarm activity — skip DHT/tracker announces, dialing, choking and
|
|
201
|
+
// PEX. Keep the tick alive so resume() picks straight back up. (peers_ is empty
|
|
202
|
+
// while paused, so there is nothing to time out or choke anyway.)
|
|
203
|
+
if (paused_) { schedule_tick(); return; }
|
|
204
|
+
|
|
205
|
+
check_request_timeouts(); // free blocks stuck on stalled peers before anything else
|
|
206
|
+
|
|
207
|
+
// Ask the DHT for fresh peers periodically (every ~30 s).
|
|
208
|
+
if (tick_count_ % 30 == 1) {
|
|
209
|
+
LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → DHT get_peers");
|
|
210
|
+
host_.find_peers_via_dht(info_hash(), [this](const std::string& ip, std::uint16_t port) {
|
|
211
|
+
if (peer_list_.add(ip, port, PeerSource::Dht)) try_connect();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Announce ourselves to the DHT so others can find us — promptly on startup,
|
|
215
|
+
// then every ~15 min per BEP 5 (H15). Was previously never done → undiscoverable.
|
|
216
|
+
if (tick_count_ % 900 == 5) {
|
|
217
|
+
LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → DHT announce_peer port "
|
|
218
|
+
<< host_.listen_port());
|
|
219
|
+
host_.announce_to_dht(info_hash(), host_.listen_port());
|
|
220
|
+
}
|
|
221
|
+
// Re-announce when the tracker's requested interval has elapsed (H13).
|
|
222
|
+
if (tick_count_ >= next_announce_tick_) {
|
|
223
|
+
next_announce_tick_ = tick_count_ + 300; // fallback until the response updates it
|
|
224
|
+
announce_trackers(TrackerEvent::None);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try_connect();
|
|
228
|
+
send_pex();
|
|
229
|
+
|
|
230
|
+
// Choking runs on a coarse cadence, not every tick: recompute the unchoke set
|
|
231
|
+
// every ~10 s and rotate the optimistic slot every ~30 s. Recomputing every
|
|
232
|
+
// second (with a 1 s scoring window) made peers near the slot boundary flip
|
|
233
|
+
// choke/unchoke constantly (H4). The tit-for-tat window therefore accumulates
|
|
234
|
+
// over the whole 10 s and is only reset when we recompute.
|
|
235
|
+
if (tick_count_ % 30 == 0) rotate_optimistic();
|
|
236
|
+
if (tick_count_ % 10 == 0) {
|
|
237
|
+
recompute_choker();
|
|
238
|
+
for (auto& [pc, score] : recent_down_) score = 0;
|
|
239
|
+
for (auto& [pc, score] : recent_up_) score = 0;
|
|
240
|
+
}
|
|
241
|
+
schedule_tick();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- peer event handling ----
|
|
245
|
+
|
|
246
|
+
void Torrent::on_handshake(PeerConnection& pc, const InfoHash&, const PeerId&) {
|
|
247
|
+
// Reject inbound peers while paused — pause() means "no swarm activity".
|
|
248
|
+
if (paused_) { pc.close("torrent paused"); return; }
|
|
249
|
+
// Cap peers per torrent. Outbound dials are already gated by try_connect(), but
|
|
250
|
+
// inbound peers reach us straight through the handshake, so enforce it here too
|
|
251
|
+
// (H3) — otherwise a flood of incoming handshakes grows peers_ without bound.
|
|
252
|
+
if (peers_.size() >= kMaxPeers) { pc.close("too many peers"); return; }
|
|
253
|
+
peers_.push_back(&pc);
|
|
254
|
+
outstanding_[&pc] = 0;
|
|
255
|
+
recent_down_[&pc] = 0;
|
|
256
|
+
peer_list_.set_connected(pc.remote_ip(), pc.remote_port(), true);
|
|
257
|
+
// Milestone: the first peer on a torrent is worth an INFO line; the rest are
|
|
258
|
+
// routine (each peer's handshake is already logged at DEBUG in bt.peer).
|
|
259
|
+
if (peers_.size() == 1)
|
|
260
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " first peer connected "
|
|
261
|
+
<< pc.remote_ip() << ':' << pc.remote_port());
|
|
262
|
+
|
|
263
|
+
if (pc.peer_supports_extensions()) send_extended_handshake(pc);
|
|
264
|
+
// Only announce a bitfield once we know the piece count (i.e. have metadata).
|
|
265
|
+
if (has_metadata_ && picker_) pc.send_bitfield(picker_->have_bitfield());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
void Torrent::on_bitfield(PeerConnection& pc, const Bitfield& bf) {
|
|
269
|
+
if (!picker_) return;
|
|
270
|
+
// A full bitfield is a seed: count it as one O(1) seed instead of bumping every
|
|
271
|
+
// piece's availability. Remember which peers we counted this way so remove_peer
|
|
272
|
+
// undoes it symmetrically. The insert() guard also makes a (spurious) repeat
|
|
273
|
+
// full bitfield idempotent for the seed count.
|
|
274
|
+
const std::uint32_t n = info_.num_pieces();
|
|
275
|
+
if (n > 0 && bf.size() == n && bf.count() == n) {
|
|
276
|
+
if (seed_peers_.insert(&pc).second) picker_->inc_availability_all();
|
|
277
|
+
} else {
|
|
278
|
+
picker_->inc_availability(bf);
|
|
279
|
+
}
|
|
280
|
+
update_interest(pc);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
void Torrent::on_have(PeerConnection& pc, std::uint32_t piece) {
|
|
284
|
+
if (!picker_ || piece >= info_.num_pieces()) return;
|
|
285
|
+
picker_->peer_has_piece(piece);
|
|
286
|
+
// A single new piece can only *gain* us interest, never lose it, and the piece
|
|
287
|
+
// the peer just announced is enough to decide — so this stays O(1) instead of
|
|
288
|
+
// rescanning the peer's whole bitfield via is_interesting() on every HAVE.
|
|
289
|
+
if (!pc.am_interested() && picker_->piece_interesting(piece)) pc.send_interested();
|
|
290
|
+
if (pc.am_interested() && !pc.peer_choking()) refill(pc);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
void Torrent::on_choke(PeerConnection& pc, bool peer_choking) {
|
|
294
|
+
if (!picker_) return;
|
|
295
|
+
if (peer_choking) {
|
|
296
|
+
picker_->cancel_peer(&pc); // they dropped our outstanding requests
|
|
297
|
+
outstanding_[&pc] = 0;
|
|
298
|
+
request_time_.erase(&pc);
|
|
299
|
+
} else {
|
|
300
|
+
refill(pc);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
void Torrent::on_interest(PeerConnection&, bool) {
|
|
305
|
+
recompute_choker(); // a peer's interest in us changed → re-evaluate upload slots
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
void Torrent::update_interest(PeerConnection& pc) {
|
|
309
|
+
const bool want = picker_->is_interesting(pc.peer_bitfield());
|
|
310
|
+
if (want && !pc.am_interested()) pc.send_interested();
|
|
311
|
+
else if (!want && pc.am_interested()) pc.send_not_interested();
|
|
312
|
+
if (want && !pc.peer_choking()) refill(pc);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
void Torrent::refill(PeerConnection& pc) {
|
|
316
|
+
if (!running_ || !picker_ || pc.peer_choking() || !pc.am_interested()) return;
|
|
317
|
+
// Disk backpressure: while too many writes are still pending, stop requesting
|
|
318
|
+
// new blocks so the write queue can drain (D-2). on_block_written() resumes
|
|
319
|
+
// requesting once it falls back under the watermark.
|
|
320
|
+
if (disk_ && disk_->queued_write_bytes() > kDiskWriteHighWater) { write_stalled_ = true; return; }
|
|
321
|
+
int budget = kPipelineDepth - outstanding_[&pc];
|
|
322
|
+
if (budget <= 0) return;
|
|
323
|
+
|
|
324
|
+
const int before = outstanding_[&pc];
|
|
325
|
+
auto blocks = picker_->pick_blocks(pc.peer_bitfield(), budget, &pc);
|
|
326
|
+
for (const PieceBlock& b : blocks) {
|
|
327
|
+
picker_->mark_requested(b, &pc);
|
|
328
|
+
pc.send_request(b.piece, b.block * kBlockSize, picker_->block_size(b.piece, b.block));
|
|
329
|
+
++outstanding_[&pc];
|
|
330
|
+
}
|
|
331
|
+
if (!blocks.empty())
|
|
332
|
+
LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " → request " << blocks.size()
|
|
333
|
+
<< " block(s) from " << pc.remote_ip() << ':' << pc.remote_port()
|
|
334
|
+
<< " (" << outstanding_[&pc] << " in flight)");
|
|
335
|
+
// Start the stall clock when a previously-idle peer gets a fresh batch. If it
|
|
336
|
+
// already had requests in flight, keep the older timestamp so a peer that
|
|
337
|
+
// stops delivering can't hide behind newly-added requests.
|
|
338
|
+
if (before == 0 && outstanding_[&pc] > 0)
|
|
339
|
+
request_time_[&pc] = std::chrono::steady_clock::now();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
void Torrent::on_piece(PeerConnection& pc, std::uint32_t piece, std::uint32_t offset, ByteView data) {
|
|
343
|
+
if (!picker_ || piece >= info_.num_pieces()) return;
|
|
344
|
+
|
|
345
|
+
// Reject a malformed or unsolicited block before it can index the picker's
|
|
346
|
+
// block vector out of bounds: the offset must be block-aligned and inside the
|
|
347
|
+
// piece, and the payload must be exactly that block's size. A hostile peer
|
|
348
|
+
// could otherwise drive an out-of-bounds access via mark_writing()/mark_finished().
|
|
349
|
+
const std::uint32_t piece_bytes = info_.piece_size(piece);
|
|
350
|
+
if (offset % kBlockSize != 0 || offset >= piece_bytes) {
|
|
351
|
+
LOG_WARN("bt.torrent", short_hash(info_hash()) << " ✗ bad block from " << pc.remote_ip()
|
|
352
|
+
<< ':' << pc.remote_port() << " piece " << piece << " off " << offset
|
|
353
|
+
<< " (misaligned/out of range), dropped");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (data.size() != picker_->block_size(piece, offset / kBlockSize)) {
|
|
357
|
+
LOG_WARN("bt.torrent", short_hash(info_hash()) << " ✗ bad block from " << pc.remote_ip()
|
|
358
|
+
<< ':' << pc.remote_port() << " piece " << piece
|
|
359
|
+
<< " wrong size " << data.size() << ", dropped");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (outstanding_[&pc] > 0) --outstanding_[&pc];
|
|
364
|
+
// Progress: this peer just delivered, so reset its stall clock — any remaining
|
|
365
|
+
// outstanding blocks get a fresh timeout window.
|
|
366
|
+
request_time_[&pc] = std::chrono::steady_clock::now();
|
|
367
|
+
|
|
368
|
+
// We already completed this piece — an end-game duplicate that crossed our
|
|
369
|
+
// CANCEL, or a block arriving after the piece verified. Discard it: writing it
|
|
370
|
+
// again would be wasted I/O and would resurrect a stale picker entry. The
|
|
371
|
+
// request slot is already freed, so just keep this peer's pipeline full.
|
|
372
|
+
if (picker_->have_piece(piece)) { refill(pc); return; }
|
|
373
|
+
|
|
374
|
+
const PieceBlock block{piece, offset / kBlockSize};
|
|
375
|
+
bytes_downloaded_ += data.size();
|
|
376
|
+
recent_down_[&pc] += data.size();
|
|
377
|
+
|
|
378
|
+
// End-game: this block may have been requested from several peers at once. Now
|
|
379
|
+
// that it has arrived, CANCEL the duplicate requests still outstanding on the
|
|
380
|
+
// *other* peers so we don't download the same block again from each of them.
|
|
381
|
+
const std::vector<const void*> others = picker_->mark_writing(block, &pc);
|
|
382
|
+
for (const void* o : others) {
|
|
383
|
+
auto* opc = static_cast<PeerConnection*>(const_cast<void*>(o));
|
|
384
|
+
if (!alive(opc)) continue;
|
|
385
|
+
auto it = outstanding_.find(opc);
|
|
386
|
+
if (it != outstanding_.end() && it->second > 0) --it->second; // freed a slot
|
|
387
|
+
opc->send_cancel(piece, block.block * kBlockSize, picker_->block_size(piece, block.block));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
disk_->async_write(piece, offset, data.to_bytes(),
|
|
391
|
+
[this, block](bool ok) { on_block_written(block, ok); });
|
|
392
|
+
|
|
393
|
+
refill(pc); // keep the pipeline full while the write is in flight
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
void Torrent::check_request_timeouts() {
|
|
397
|
+
if (!picker_) return;
|
|
398
|
+
const auto now = std::chrono::steady_clock::now();
|
|
399
|
+
|
|
400
|
+
// Find peers that hold outstanding requests but have made no progress within
|
|
401
|
+
// the timeout. A peer sending only keep-alives (which refresh the 120 s idle
|
|
402
|
+
// deadline) would otherwise sit on its blocks forever, stalling those pieces.
|
|
403
|
+
std::vector<PeerConnection*> stalled;
|
|
404
|
+
for (const auto& [pc, n] : outstanding_) {
|
|
405
|
+
if (n <= 0) continue;
|
|
406
|
+
auto it = request_time_.find(pc);
|
|
407
|
+
if (it != request_time_.end() && now - it->second > request_timeout_)
|
|
408
|
+
stalled.push_back(pc);
|
|
409
|
+
}
|
|
410
|
+
if (stalled.empty()) return;
|
|
411
|
+
|
|
412
|
+
// Release each stalled peer's blocks back to the picker so another peer can
|
|
413
|
+
// pick them, and leave the peer idle (no re-request this round) so we don't
|
|
414
|
+
// immediately hand the same blocks back to the peer that just stalled.
|
|
415
|
+
for (PeerConnection* pc : stalled) {
|
|
416
|
+
picker_->cancel_peer(pc);
|
|
417
|
+
outstanding_[pc] = 0;
|
|
418
|
+
request_time_.erase(pc);
|
|
419
|
+
}
|
|
420
|
+
LOG_WARN("bt.torrent", short_hash(info_hash()) << " snubbed " << stalled.size()
|
|
421
|
+
<< " stalled peer(s), freed their blocks for re-request");
|
|
422
|
+
|
|
423
|
+
// Re-request the freed blocks from peers that are actually delivering.
|
|
424
|
+
for (PeerConnection* pc : peers_) {
|
|
425
|
+
if (std::find(stalled.begin(), stalled.end(), pc) != stalled.end()) continue;
|
|
426
|
+
if (pc->am_interested() && !pc->peer_choking()) refill(*pc);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
void Torrent::on_block_written(PieceBlock block, bool ok) {
|
|
431
|
+
if (!running_ || !picker_) return;
|
|
432
|
+
if (!ok) {
|
|
433
|
+
LOG_ERROR("bt.torrent", short_hash(info_hash()) << " ✗ disk write failed for piece "
|
|
434
|
+
<< block.piece << ", will refetch");
|
|
435
|
+
picker_->restore_piece(block.piece);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (picker_->mark_finished(block)) verify_piece(block.piece);
|
|
439
|
+
|
|
440
|
+
// A write just drained: if backpressure had paused requesting and the queue is
|
|
441
|
+
// back under the watermark, resume filling peers' pipelines (D-2).
|
|
442
|
+
if (write_stalled_ && (!disk_ || disk_->queued_write_bytes() <= kDiskWriteHighWater)) {
|
|
443
|
+
write_stalled_ = false;
|
|
444
|
+
refill_all();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
void Torrent::refill_all() {
|
|
449
|
+
for (PeerConnection* pc : peers_)
|
|
450
|
+
if (pc->am_interested() && !pc->peer_choking()) refill(*pc);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
void Torrent::verify_piece(std::uint32_t piece) {
|
|
454
|
+
disk_->async_hash(piece, [this, piece](bool ok, std::array<std::uint8_t, 20> hash) {
|
|
455
|
+
on_piece_hashed(piece, ok, hash);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
void Torrent::on_piece_hashed(std::uint32_t piece, bool ok, std::array<std::uint8_t, 20> hash) {
|
|
460
|
+
if (!running_ || !picker_) return;
|
|
461
|
+
if (!ok || hash != info_.piece_hash(piece)) {
|
|
462
|
+
LOG_WARN("bt.torrent", short_hash(info_hash()) << " ✗ piece " << piece
|
|
463
|
+
<< " hash mismatch, refetch");
|
|
464
|
+
picker_->restore_piece(piece); // corrupt — fetch it again
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
picker_->we_have(piece);
|
|
469
|
+
verified_bytes_ += info_.piece_size(piece); // one more piece is now on disk → shrinks `left`
|
|
470
|
+
for (PeerConnection* pc : peers_) {
|
|
471
|
+
pc->send_have(piece);
|
|
472
|
+
update_interest(*pc); // we may no longer need some peers
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
LOG_DEBUG("bt.torrent", short_hash(info_hash()) << " ✓ piece " << piece << " verified ("
|
|
476
|
+
<< picker_->num_have() << '/' << info_.num_pieces() << ')');
|
|
477
|
+
// Progress milestone: log once per 10% crossed, not once per piece (H: keep INFO
|
|
478
|
+
// readable as a clean per-torrent story instead of thousands of lines).
|
|
479
|
+
const int pct = int(progress() * 100);
|
|
480
|
+
if (pct / 10 > progress_logged_ && pct < 100) {
|
|
481
|
+
progress_logged_ = pct / 10;
|
|
482
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " progress " << (progress_logged_ * 10) << '%');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (picker_->is_finished() && !completed_announced_) {
|
|
486
|
+
completed_announced_ = true;
|
|
487
|
+
state_ = State::Seeding;
|
|
488
|
+
LOG_INFO("bt.torrent", short_hash(info_hash()) << " ✓ download complete → Seeding");
|
|
489
|
+
announce_trackers(TrackerEvent::Completed); // tell trackers we're now a seed (H12)
|
|
490
|
+
if (on_complete_) on_complete_();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
void Torrent::on_request(PeerConnection& pc, std::uint32_t piece, std::uint32_t offset, std::uint32_t length) {
|
|
495
|
+
if (!picker_ || pc.am_choking()) return; // we are not serving this peer
|
|
496
|
+
if (piece >= info_.num_pieces() || !picker_->have_piece(piece)) return;
|
|
497
|
+
if (length == 0 || length > kMaxBlockSize) return;
|
|
498
|
+
// The requested range must lie wholly within the piece, else the disk read
|
|
499
|
+
// would spill into an adjacent piece's file region and serve unrelated bytes.
|
|
500
|
+
const std::uint32_t piece_bytes = info_.piece_size(piece);
|
|
501
|
+
if (offset >= piece_bytes || length > piece_bytes - offset) return;
|
|
502
|
+
|
|
503
|
+
PeerConnection* peer = &pc;
|
|
504
|
+
disk_->async_read(piece, offset, length, [this, peer, piece, offset](bool ok, Bytes data) {
|
|
505
|
+
if (ok && alive(peer)) {
|
|
506
|
+
const std::size_t sent = data.size(); // data is about to be moved away
|
|
507
|
+
peer->send_piece(piece, offset, std::move(data));
|
|
508
|
+
bytes_uploaded_ += sent;
|
|
509
|
+
recent_up_[peer] += sent; // seed-choking score for this window
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
void Torrent::on_closed(PeerConnection& pc, const std::string&) {
|
|
515
|
+
peer_list_.set_connected(pc.remote_ip(), pc.remote_port(), false);
|
|
516
|
+
pex_sent_.erase(&pc);
|
|
517
|
+
remove_peer(&pc);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ---- choking ----
|
|
521
|
+
|
|
522
|
+
void Torrent::recompute_choker() {
|
|
523
|
+
if (!running_) return;
|
|
524
|
+
// Score peers by what they've done for us this window: while downloading that
|
|
525
|
+
// is bytes received (tit-for-tat); while seeding we receive nothing, so score
|
|
526
|
+
// by bytes we served them instead — otherwise every peer scores 0 and a seed
|
|
527
|
+
// would serve only whichever peers happened to connect first, forever (H2).
|
|
528
|
+
const bool seeding = picker_ && picker_->is_finished();
|
|
529
|
+
std::vector<Choker::Candidate> candidates;
|
|
530
|
+
for (PeerConnection* pc : peers_)
|
|
531
|
+
if (pc->peer_interested())
|
|
532
|
+
candidates.push_back(Choker::Candidate{pc, seeding ? recent_up_[pc] : recent_down_[pc]});
|
|
533
|
+
|
|
534
|
+
auto unchoke = choker_.select(std::move(candidates), optimistic_);
|
|
535
|
+
for (PeerConnection* pc : peers_) {
|
|
536
|
+
const bool should_unchoke =
|
|
537
|
+
std::find(unchoke.begin(), unchoke.end(), pc) != unchoke.end();
|
|
538
|
+
if (should_unchoke && pc->am_choking()) pc->send_unchoke();
|
|
539
|
+
else if (!should_unchoke && !pc->am_choking()) pc->send_choke();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
void Torrent::rotate_optimistic() {
|
|
544
|
+
// Round-robin the optimistic slot through the interested-but-choked peers so a
|
|
545
|
+
// newcomer that hasn't earned a tit-for-tat slot still gets a chance to prove
|
|
546
|
+
// itself (H1). Advance from just after the current optimistic in peer order.
|
|
547
|
+
std::vector<PeerConnection*> candidates;
|
|
548
|
+
for (PeerConnection* pc : peers_)
|
|
549
|
+
if (pc->peer_interested()) candidates.push_back(pc);
|
|
550
|
+
if (candidates.empty()) { optimistic_ = nullptr; return; }
|
|
551
|
+
|
|
552
|
+
auto it = std::find(candidates.begin(), candidates.end(), optimistic_);
|
|
553
|
+
const std::size_t next = (it == candidates.end()) ? 0
|
|
554
|
+
: (std::size_t(it - candidates.begin()) + 1) % candidates.size();
|
|
555
|
+
optimistic_ = candidates[next];
|
|
556
|
+
recompute_choker(); // apply the new optimistic slot immediately
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ---- helpers ----
|
|
560
|
+
|
|
561
|
+
bool Torrent::alive(PeerConnection* pc) const {
|
|
562
|
+
return std::find(peers_.begin(), peers_.end(), pc) != peers_.end();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
void Torrent::remove_peer(PeerConnection* pc) {
|
|
566
|
+
auto it = std::find(peers_.begin(), peers_.end(), pc);
|
|
567
|
+
if (it == peers_.end()) return;
|
|
568
|
+
if (picker_) {
|
|
569
|
+
// Undo the availability accounting the same way it was applied: a seed via
|
|
570
|
+
// the seed counter, anyone else per-piece from their bitfield.
|
|
571
|
+
if (seed_peers_.erase(pc)) picker_->dec_availability_all();
|
|
572
|
+
else picker_->dec_availability(pc->peer_bitfield());
|
|
573
|
+
picker_->cancel_peer(pc);
|
|
574
|
+
}
|
|
575
|
+
if (optimistic_ == pc) optimistic_ = nullptr;
|
|
576
|
+
peers_.erase(it);
|
|
577
|
+
outstanding_.erase(pc);
|
|
578
|
+
request_time_.erase(pc);
|
|
579
|
+
recent_down_.erase(pc);
|
|
580
|
+
recent_up_.erase(pc);
|
|
581
|
+
peer_ext_.erase(pc);
|
|
582
|
+
pex_last_tick_.erase(pc);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
std::size_t Torrent::num_outstanding_requests() const noexcept {
|
|
586
|
+
std::size_t n = 0;
|
|
587
|
+
for (const auto& [pc, count] : outstanding_) if (count > 0) n += std::size_t(count);
|
|
588
|
+
return n;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
double Torrent::progress() const {
|
|
592
|
+
if (!picker_ || info_.num_pieces() == 0) return 0.0;
|
|
593
|
+
return double(picker_->num_have()) / double(info_.num_pieces());
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---- extension protocol / metadata (BEP 10 / BEP 9) ----
|
|
597
|
+
|
|
598
|
+
void Torrent::on_extended(PeerConnection& pc, std::uint8_t ext_id, ByteView payload) {
|
|
599
|
+
if (ext_id == 0) handle_ext_handshake(pc, payload);
|
|
600
|
+
else if (ext_id == ext::kUtMetadataLocalId) handle_ut_metadata(pc, payload);
|
|
601
|
+
else if (ext_id == ext::kUtPexLocalId) handle_pex(pc, payload);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
void Torrent::send_extended_handshake(PeerConnection& pc) {
|
|
605
|
+
const std::uint32_t ms = has_metadata_ ? std::uint32_t(info_.info_dict_bytes().size()) : 0;
|
|
606
|
+
const Bytes hs = ext::encode_handshake(ms, host_.listen_port());
|
|
607
|
+
pc.send_extended(0, ByteView(hs));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
void Torrent::handle_ext_handshake(PeerConnection& pc, ByteView payload) {
|
|
611
|
+
auto pe = ext::decode_handshake(payload);
|
|
612
|
+
if (!pe) return;
|
|
613
|
+
peer_ext_[&pc] = *pe;
|
|
614
|
+
if (!has_metadata_ && pe->metadata_size > 0) {
|
|
615
|
+
ensure_metadata_buffer(pe->metadata_size);
|
|
616
|
+
request_metadata(pc);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
void Torrent::ensure_metadata_buffer(std::uint32_t total_size) {
|
|
621
|
+
if (metadata_size_ != 0 || total_size == 0) return; // already sized, or unknown
|
|
622
|
+
// The size is self-reported by a peer (extended handshake or ut_metadata data
|
|
623
|
+
// message), so reject an implausible value before allocating — otherwise a
|
|
624
|
+
// hostile peer advertising ~4 GiB drives an out-of-memory allocation (C3).
|
|
625
|
+
if (total_size > kMaxMetadataSize) {
|
|
626
|
+
LOG_WARN("bt.meta", short_hash(info_hash()) << " ✗ rejected oversize metadata "
|
|
627
|
+
<< total_size << " bytes (> " << kMaxMetadataSize << ')');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
metadata_size_ = total_size;
|
|
631
|
+
metadata_pieces_ = (total_size + kMetadataPieceSize - 1) / kMetadataPieceSize;
|
|
632
|
+
metadata_buf_.assign(total_size, 0);
|
|
633
|
+
metadata_have_.assign(metadata_pieces_, false);
|
|
634
|
+
metadata_received_ = 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
void Torrent::request_metadata(PeerConnection& pc) {
|
|
638
|
+
if (has_metadata_ || metadata_pieces_ == 0) return;
|
|
639
|
+
auto it = peer_ext_.find(&pc);
|
|
640
|
+
if (it == peer_ext_.end() || it->second.ut_metadata_id == 0) return;
|
|
641
|
+
for (std::uint32_t p = 0; p < metadata_pieces_; ++p)
|
|
642
|
+
if (!metadata_have_[p])
|
|
643
|
+
pc.send_extended(it->second.ut_metadata_id, ByteView(ext::encode_metadata_request(p)));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
void Torrent::handle_ut_metadata(PeerConnection& pc, ByteView payload) {
|
|
647
|
+
auto msg = ext::decode_metadata(payload);
|
|
648
|
+
if (!msg) return;
|
|
649
|
+
|
|
650
|
+
if (msg->type == ext::MetadataType::Request) {
|
|
651
|
+
auto it = peer_ext_.find(&pc);
|
|
652
|
+
const std::uint8_t id = (it != peer_ext_.end()) ? it->second.ut_metadata_id : 0;
|
|
653
|
+
if (id == 0) return;
|
|
654
|
+
if (has_metadata_) {
|
|
655
|
+
const Bytes& info = info_.info_dict_bytes();
|
|
656
|
+
const std::uint32_t total = std::uint32_t(info.size());
|
|
657
|
+
const std::uint32_t pieces = (total + kMetadataPieceSize - 1) / kMetadataPieceSize;
|
|
658
|
+
if (msg->piece < pieces) {
|
|
659
|
+
const std::uint32_t off = msg->piece * kMetadataPieceSize;
|
|
660
|
+
const std::uint32_t len = (std::min)(kMetadataPieceSize, total - off);
|
|
661
|
+
pc.send_extended(id, ByteView(ext::encode_metadata_data(
|
|
662
|
+
msg->piece, total, ByteView(info.data() + off, len))));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
pc.send_extended(id, ByteView(ext::encode_metadata_reject(msg->piece)));
|
|
667
|
+
} else if (msg->type == ext::MetadataType::Data) {
|
|
668
|
+
on_metadata_piece(msg->piece, msg->total_size, ByteView(msg->block));
|
|
669
|
+
}
|
|
670
|
+
// Reject: leave the piece unmarked so it can be re-requested from another peer.
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
void Torrent::on_metadata_piece(std::uint32_t piece, std::uint32_t total_size, ByteView block) {
|
|
674
|
+
if (has_metadata_) return;
|
|
675
|
+
ensure_metadata_buffer(total_size);
|
|
676
|
+
if (metadata_size_ == 0 || piece >= metadata_pieces_ || metadata_have_[piece]) return;
|
|
677
|
+
|
|
678
|
+
const std::uint32_t off = piece * kMetadataPieceSize;
|
|
679
|
+
const std::uint32_t expect = (std::min)(kMetadataPieceSize, metadata_size_ - off);
|
|
680
|
+
if (block.size() != expect) return; // malformed slice — ignore
|
|
681
|
+
|
|
682
|
+
std::copy(block.begin(), block.end(), metadata_buf_.begin() + std::ptrdiff_t(off));
|
|
683
|
+
metadata_have_[piece] = true;
|
|
684
|
+
if (++metadata_received_ == metadata_pieces_) try_complete_metadata();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
void Torrent::try_complete_metadata() {
|
|
688
|
+
if (info_.set_metadata(metadata_buf_)) { // verifies SHA-1 == info-hash
|
|
689
|
+
has_metadata_ = true;
|
|
690
|
+
LOG_INFO("bt.meta", short_hash(info_hash()) << " ✓ metadata resolved: \"" << info_.name()
|
|
691
|
+
<< "\" " << info_.num_pieces() << " pieces, " << info_.total_size() << " bytes");
|
|
692
|
+
if (on_metadata_) on_metadata_(info_);
|
|
693
|
+
promote_to_downloading();
|
|
694
|
+
} else {
|
|
695
|
+
// Verification failed — discard and re-request from every peer.
|
|
696
|
+
LOG_WARN("bt.meta", short_hash(info_hash()) << " ✗ metadata verification failed, re-requesting");
|
|
697
|
+
std::fill(metadata_have_.begin(), metadata_have_.end(), false);
|
|
698
|
+
metadata_received_ = 0;
|
|
699
|
+
for (PeerConnection* pc : peers_) request_metadata(*pc);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
void Torrent::promote_to_downloading() {
|
|
704
|
+
picker_ = std::make_unique<PiecePicker>(info_.num_pieces(), info_.piece_length(), info_.total_size());
|
|
705
|
+
disk_ = std::make_unique<ThreadedDiskIo>(
|
|
706
|
+
info_, save_path_, [this](std::function<void()> fn) { post(std::move(fn)); });
|
|
707
|
+
state_ = State::Downloading;
|
|
708
|
+
|
|
709
|
+
// Adopt the bitfields we received while metadata-less and (re)evaluate interest.
|
|
710
|
+
for (PeerConnection* pc : peers_) {
|
|
711
|
+
picker_->inc_availability(pc->peer_bitfield());
|
|
712
|
+
update_interest(*pc);
|
|
713
|
+
}
|
|
714
|
+
disk_->async_check_files(Bitfield{}, nullptr,
|
|
715
|
+
[this](Bitfield have) { on_check_complete(std::move(have)); });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ---- peer exchange (BEP 11) ----
|
|
719
|
+
|
|
720
|
+
void Torrent::handle_pex(PeerConnection& pc, ByteView payload) {
|
|
721
|
+
auto msg = ext::decode_pex(payload);
|
|
722
|
+
if (!msg) return;
|
|
723
|
+
bool added_any = false;
|
|
724
|
+
for (const auto& p : msg->added)
|
|
725
|
+
if (peer_list_.add(p.ip, p.port, PeerSource::Pex)) added_any = true;
|
|
726
|
+
if (added_any) try_connect();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
std::optional<ext::PexPeer> Torrent::dialable(PeerConnection& pc) const {
|
|
730
|
+
if (pc.remote_ip().empty()) return std::nullopt;
|
|
731
|
+
if (pc.outgoing()) // we dialed its listen port directly
|
|
732
|
+
return ext::PexPeer{pc.remote_ip(), pc.remote_port()};
|
|
733
|
+
// Incoming: the source port is ephemeral; use the listen port it advertised.
|
|
734
|
+
auto it = peer_ext_.find(const_cast<PeerConnection*>(&pc));
|
|
735
|
+
if (it == peer_ext_.end() || it->second.listen_port == 0) return std::nullopt;
|
|
736
|
+
return ext::PexPeer{pc.remote_ip(), it->second.listen_port};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
void Torrent::send_pex() {
|
|
740
|
+
for (PeerConnection* pc : peers_) {
|
|
741
|
+
auto ext_it = peer_ext_.find(pc);
|
|
742
|
+
if (ext_it == peer_ext_.end() || ext_it->second.ut_pex_id == 0) continue;
|
|
743
|
+
|
|
744
|
+
// BEP 11: the first PEX message to a peer may go out promptly, but
|
|
745
|
+
// subsequent ones must be at least 60 s apart or the peer treats it as
|
|
746
|
+
// abuse and disconnects (too_frequent_pex) (H14). Gate per peer.
|
|
747
|
+
auto tick_it = pex_last_tick_.find(pc);
|
|
748
|
+
const bool first = (tick_it == pex_last_tick_.end());
|
|
749
|
+
if (!first && tick_count_ - tick_it->second < 60) continue;
|
|
750
|
+
|
|
751
|
+
// Diff the set of dialable peers we've told this one about against the
|
|
752
|
+
// current set: newly-seen peers go in `added`, departed ones in `dropped`.
|
|
753
|
+
std::unordered_map<std::string, ext::PexPeer> current;
|
|
754
|
+
for (PeerConnection* other : peers_) {
|
|
755
|
+
if (other == pc) continue;
|
|
756
|
+
if (auto ep = dialable(*other))
|
|
757
|
+
current.emplace(ep->ip + ":" + std::to_string(ep->port), *ep);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
auto& sent = pex_sent_[pc];
|
|
761
|
+
std::vector<ext::PexPeer> added, dropped;
|
|
762
|
+
for (const auto& [key, ep] : current)
|
|
763
|
+
if (!sent.count(key)) added.push_back(ep);
|
|
764
|
+
for (const std::string& key : sent) {
|
|
765
|
+
if (current.count(key)) continue;
|
|
766
|
+
const std::size_t c = key.rfind(':'); // reconstruct the departed peer's endpoint
|
|
767
|
+
if (c != std::string::npos)
|
|
768
|
+
dropped.push_back(ext::PexPeer{key.substr(0, c),
|
|
769
|
+
std::uint16_t(std::atoi(key.c_str() + c + 1))});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (!added.empty() || !dropped.empty()) {
|
|
773
|
+
pc->send_extended(ext_it->second.ut_pex_id, ByteView(ext::encode_pex(added, dropped)));
|
|
774
|
+
pex_last_tick_[pc] = tick_count_;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
sent.clear();
|
|
778
|
+
for (const auto& [key, ep] : current) sent.insert(key);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ---- trackers ----
|
|
783
|
+
|
|
784
|
+
TrackerRequest Torrent::make_tracker_request(TrackerEvent event) const {
|
|
785
|
+
TrackerRequest r;
|
|
786
|
+
r.info_hash = info_hash();
|
|
787
|
+
r.peer_id = host_.peer_id();
|
|
788
|
+
r.port = host_.listen_port();
|
|
789
|
+
r.uploaded = bytes_uploaded_;
|
|
790
|
+
r.downloaded = bytes_downloaded_;
|
|
791
|
+
// `left` is what remains to fetch = total size minus what is verified on disk.
|
|
792
|
+
// Must use verified_bytes_, not the cumulative bytes_downloaded_ (which can
|
|
793
|
+
// exceed on-disk bytes after re-downloads and would understate `left`).
|
|
794
|
+
r.left = (has_metadata_ && info_.total_size() > std::int64_t(verified_bytes_))
|
|
795
|
+
? std::uint64_t(info_.total_size()) - verified_bytes_
|
|
796
|
+
: 0;
|
|
797
|
+
r.event = event;
|
|
798
|
+
r.numwant = 50;
|
|
799
|
+
return r;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
void Torrent::announce_trackers(TrackerEvent event) {
|
|
803
|
+
if (!trackers_) return;
|
|
804
|
+
trackers_->announce(make_tracker_request(event),
|
|
805
|
+
[this](const std::vector<Address>& peers, std::uint32_t interval) {
|
|
806
|
+
// Schedule the next periodic announce off the tracker's requested interval
|
|
807
|
+
// rather than a fixed cadence (H13). tick_count_ advances ~1/s.
|
|
808
|
+
next_announce_tick_ = tick_count_ + int(interval);
|
|
809
|
+
int added = 0;
|
|
810
|
+
for (const Address& a : peers)
|
|
811
|
+
if (peer_list_.add(a.ip.to_string(), a.port, PeerSource::Tracker)) ++added;
|
|
812
|
+
LOG_INFO("bt.tracker", short_hash(info_hash()) << " ← " << peers.size() << " peers (+"
|
|
813
|
+
<< added << " new), next announce in " << interval << "s");
|
|
814
|
+
if (added) try_connect();
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ---- fast resume ----
|
|
819
|
+
|
|
820
|
+
void Torrent::load_resume_data(const ResumeData& rd) {
|
|
821
|
+
if (running_) return; // must be applied before start()
|
|
822
|
+
// Recover metadata for a magnet torrent from the embedded info section.
|
|
823
|
+
if (!info_.has_metadata() && !rd.info_dict.empty()) info_.set_metadata(rd.info_dict);
|
|
824
|
+
if (!is_all_zero(rd.info_hash) && rd.info_hash != info_hash()) return; // not ours
|
|
825
|
+
resume_have_ = rd.have;
|
|
826
|
+
bytes_uploaded_ = rd.total_uploaded;
|
|
827
|
+
bytes_downloaded_ = rd.total_downloaded;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
bool Torrent::try_load_resume_data() {
|
|
831
|
+
if (running_) return false; // must be applied before start()
|
|
832
|
+
std::size_t size = 0;
|
|
833
|
+
void* raw = read_file_binary(default_resume_path().c_str(), &size);
|
|
834
|
+
if (!raw) return false;
|
|
835
|
+
Bytes data(static_cast<std::uint8_t*>(raw), static_cast<std::uint8_t*>(raw) + size);
|
|
836
|
+
std::free(raw);
|
|
837
|
+
auto rd = ResumeData::decode(data);
|
|
838
|
+
if (!rd) return false;
|
|
839
|
+
load_resume_data(*rd);
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
ResumeData Torrent::generate_resume_data() const {
|
|
844
|
+
ResumeData rd;
|
|
845
|
+
rd.info_hash = info_hash();
|
|
846
|
+
rd.name = info_.name();
|
|
847
|
+
rd.save_path = save_path_;
|
|
848
|
+
rd.have = picker_ ? picker_->have_bitfield() : Bitfield{};
|
|
849
|
+
rd.total_uploaded = bytes_uploaded_;
|
|
850
|
+
rd.total_downloaded = bytes_downloaded_;
|
|
851
|
+
if (info_.has_metadata()) rd.info_dict = info_.info_dict_bytes();
|
|
852
|
+
return rd;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
std::string Torrent::default_resume_path() const {
|
|
856
|
+
return combine_paths(combine_paths(save_path_, ".resume"), info_.info_hash_hex() + ".resume");
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
bool Torrent::save_resume_data() const {
|
|
860
|
+
return save_resume_data(default_resume_path());
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
bool Torrent::save_resume_data(const std::string& path) const {
|
|
864
|
+
const std::string parent = get_parent_directory(path.c_str());
|
|
865
|
+
if (!parent.empty() && !directory_exists(parent.c_str())) create_directories(parent.c_str());
|
|
866
|
+
const Bytes data = generate_resume_data().encode();
|
|
867
|
+
return create_file_binary(path.c_str(), data.data(), data.size());
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
} // namespace librats::bittorrent
|