Skip to content

Commit 3a9bc57

Browse files
committed
adds solutions
2 parents b3a3692 + d2dcb7d commit 3a9bc57

8 files changed

Lines changed: 21 additions & 398 deletions

File tree

AzuriteConfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"instaceID":"7d61017f-125f-488f-b15d-143f1a2fc570"}

__azurite_db_table__.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"filename":"c:\\Users\\MeMyself\\bittorrentclient\\__azurite_db_table__.json","collections":[{"name":"$TABLES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"account":{"name":"account","dirty":false,"values":[]},"table":{"name":"table","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$TABLES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}

ccbt/peer/async_peer_connection.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13945,6 +13945,7 @@ async def _handle_ut_metadata_request(
1394513945
"Rejected ut_metadata request for piece %d from %s because metadata is not available locally",
1394613946
piece_index,
1394713947
connection.peer_info,
13948+
num_pieces,
1394813949
)
1394913950
return
1395013951

ccbt/piece/async_piece_manager.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3570,9 +3570,18 @@ async def handle_piece_block(
35703570
piece_index, piece
35713571
)
35723572

3573-
# Schedule hash verification and keep a strong reference
3574-
_task = asyncio.create_task(
3575-
self._verify_piece_hash(piece_index, piece),
3573+
# Select pieces within window
3574+
window_pieces = [
3575+
idx for idx in missing_pieces if window_start <= idx < window_end
3576+
]
3577+
3578+
if window_pieces:
3579+
# CRITICAL FIX: Check if we have any peers with bitfields before requesting pieces
3580+
if self._peer_manager:
3581+
active_peers = (
3582+
self._peer_manager.get_active_peers()
3583+
if hasattr(self._peer_manager, "get_active_peers")
3584+
else []
35763585
)
35773586
self._background_tasks.add(_task)
35783587
_task.add_done_callback(self._background_tasks.discard)

docs/en/bep_xet.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ The Xet protocol extension is fully implemented in ccBitTorrent:
6767
- ✅ Configuration management
6868
- ✅ Folder session/runtime management
6969
- ✅ Best-effort folder synchronization runtime
70-
- ✅ Tonic file format (`.tonic`), `tonic?:` link parsing, and remote .tonic URL (http/https) ingestion
71-
- ✅ Cold tonic link: joining from a link only (no .tonic file); discovery uses DHT and optional trackers/source peers from the link; metadata is fetched via XET metadata exchange from discovered peers
70+
- ✅ Tonic file format (`.tonic`) and `tonic?:` link parsing
7271
- ✅ Imported metadata bootstrap without empty-workspace overwrite
7372
- ✅ Materialization of joined workspaces into an explicit output directory
7473
- ✅ Workspace-scoped update routing inside the active session/daemon runtime

test.magnet.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
magnet:?xt=urn:btih:4CD8F32A4A8AADA4A24838FF67DE9ED3522D45A7&dn=CIA.2026.S01E04.1080p.x265-ELiTE&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.fnix.net%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.qu.ax%3A6969%2Fannounce&tr=http%3A%2F%2Ftracker.renfei.net%3A8080%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce

tests/unit/session/test_magnet_startup_regressions.py

Lines changed: 4 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -121,72 +121,6 @@ async def test_tracker_metadata_exchange_still_runs_with_low_active_count() -> N
121121
session.handle_magnet_metadata_exchange.assert_awaited_once()
122122

123123

124-
@pytest.mark.asyncio
125-
async def test_tracker_metadata_exchange_uses_productive_summary_not_raw_active_count() -> None:
126-
"""Non-productive active connections must not suppress standalone tracker metadata fetch."""
127-
from ccbt.session.announce import AnnounceLoop
128-
from ccbt.session.session import AsyncTorrentSession
129-
130-
td = {
131-
"name": "magnet-test",
132-
"info_hash": b"1" * 20,
133-
"announce": "http://tracker.example.com/announce",
134-
"_metadata_incomplete": True,
135-
"pieces_info": None,
136-
"file_info": None,
137-
}
138-
session = AsyncTorrentSession(td, ".")
139-
session.handle_magnet_metadata_exchange = AsyncMock(return_value=True)
140-
141-
loop = AnnounceLoop(session)
142-
await loop._maybe_trigger_tracker_metadata_exchange(
143-
[{"ip": "192.0.2.1", "port": 6881, "peer_source": "tracker"}],
144-
active_count=25,
145-
connection_summary={
146-
"active_connections": 25,
147-
"productive_connections": 0,
148-
"metadata_capable_connections": 0,
149-
"metadata_exchange_active": 0,
150-
"peers_with_piece_info": 0,
151-
},
152-
)
153-
154-
session.handle_magnet_metadata_exchange.assert_awaited_once()
155-
156-
157-
@pytest.mark.asyncio
158-
async def test_tracker_metadata_exchange_skips_when_live_exchange_already_active() -> None:
159-
"""Standalone tracker metadata fetch should not duplicate an already-running live exchange."""
160-
from ccbt.session.announce import AnnounceLoop
161-
from ccbt.session.session import AsyncTorrentSession
162-
163-
td = {
164-
"name": "magnet-test",
165-
"info_hash": b"1" * 20,
166-
"announce": "http://tracker.example.com/announce",
167-
"_metadata_incomplete": True,
168-
"pieces_info": None,
169-
"file_info": None,
170-
}
171-
session = AsyncTorrentSession(td, ".")
172-
session.handle_magnet_metadata_exchange = AsyncMock(return_value=True)
173-
174-
loop = AnnounceLoop(session)
175-
await loop._maybe_trigger_tracker_metadata_exchange(
176-
[{"ip": "192.0.2.1", "port": 6881, "peer_source": "tracker"}],
177-
active_count=1,
178-
connection_summary={
179-
"active_connections": 1,
180-
"productive_connections": 1,
181-
"metadata_capable_connections": 1,
182-
"metadata_exchange_active": 1,
183-
"peers_with_piece_info": 0,
184-
},
185-
)
186-
187-
session.handle_magnet_metadata_exchange.assert_not_awaited()
188-
189-
190124
@pytest.mark.asyncio
191125
async def test_immediate_tracker_connection_schedules_metadata_fallback(
192126
monkeypatch,
@@ -372,66 +306,13 @@ async def test_magnet_bitfield_does_not_promote_incomplete_metadata() -> None:
372306
assert len(piece_manager.peer_availability["198.51.100.10:6881"].pieces) == 8
373307

374308

375-
@pytest.mark.asyncio
376-
async def test_magnet_checkpoint_restore_waits_for_metadata_geometry() -> None:
377-
"""Checkpoint piece states should wait for final metadata before rebuilding layout."""
378-
torrent_data = {
379-
"info_hash": b"m" * 20,
380-
"name": "magnet-checkpoint-test",
381-
"announce": "http://tracker.example.com/announce",
382-
"_metadata_incomplete": True,
383-
"file_info": None,
384-
"pieces_info": None,
385-
}
386-
387-
piece_manager = AsyncPieceManager(torrent_data)
388-
checkpoint = TorrentCheckpoint(
389-
info_hash=b"m" * 20,
390-
torrent_name="magnet-checkpoint-test",
391-
total_pieces=2,
392-
piece_length=16384,
393-
total_length=32768,
394-
verified_pieces=[0],
395-
piece_states={0: CheckpointPieceState.VERIFIED},
396-
download_stats=DownloadStats(bytes_downloaded=16384),
397-
output_dir=".",
398-
)
399-
400-
await piece_manager.restore_from_checkpoint(checkpoint)
401-
402-
assert piece_manager._deferred_checkpoint is not None
403-
assert piece_manager.pieces == []
404-
405-
await piece_manager.update_from_metadata(
406-
{
407-
"info_hash": b"m" * 20,
408-
"name": "magnet-checkpoint-test",
409-
"announce": "http://tracker.example.com/announce",
410-
"_metadata_incomplete": False,
411-
"file_info": {
412-
"name": "magnet-checkpoint-test",
413-
"type": "single",
414-
"total_length": 524288,
415-
},
416-
"pieces_info": {
417-
"num_pieces": 2,
418-
"piece_length": 262144,
419-
"piece_hashes": [b"a" * 20, b"b" * 20],
420-
"total_length": 524288,
421-
},
422-
}
423-
)
424-
425-
assert piece_manager._deferred_checkpoint is None
426-
assert piece_manager.pieces[0].length == 262144
427-
assert piece_manager.pieces[0].state == PieceState.MISSING
428-
429-
430309
@pytest.mark.asyncio
431310
async def test_selector_premarked_piece_still_issues_initial_request(
432311
monkeypatch,
433312
) -> None:
434313
"""Selector pre-marking must not suppress the first real piece request."""
314+
import time
315+
435316
from ccbt.piece.async_piece_manager import AsyncPieceManager, PieceState
436317

437318
torrent_data = {
@@ -453,7 +334,7 @@ async def test_selector_premarked_piece_still_issues_initial_request(
453334
piece = piece_manager.pieces[0]
454335
piece.state = PieceState.REQUESTED
455336
piece.request_count = 1
456-
piece.last_request_time = 0.0
337+
piece.last_request_time = time.time()
457338
piece_manager._pending_piece_requests.add(0)
458339
piece_manager.peer_availability["198.51.100.10:6881"] = SimpleNamespace(
459340
pieces={0}
@@ -471,10 +352,9 @@ async def fake_request_blocks_normal(
471352
missing_blocks: list[object],
472353
available_peers: list[object],
473354
_peer_manager: object,
474-
) -> int:
355+
) -> None:
475356
_ = missing_blocks, _peer_manager
476357
request_calls.append((piece_index, len(available_peers)))
477-
return 1
478358

479359
monkeypatch.setattr(piece_manager, "_get_peers_for_piece", fake_get_peers_for_piece)
480360
monkeypatch.setattr(
@@ -488,7 +368,6 @@ async def fake_request_blocks_normal(
488368
assert request_calls == [(0, 1)]
489369
assert 0 not in piece_manager._pending_piece_requests
490370
assert piece_manager.pieces[0].state == PieceState.DOWNLOADING
491-
assert piece_manager.pieces[0].last_request_time > 0.0
492371

493372

494373
@pytest.mark.asyncio
@@ -531,88 +410,3 @@ async def test_emergency_tracker_path_attempts_metadata_exchange() -> None:
531410

532411
session.handle_magnet_metadata_exchange.assert_awaited()
533412
peer_manager.connect_to_peers.assert_awaited()
534-
535-
536-
@pytest.mark.asyncio
537-
async def test_piece_stays_requested_when_no_block_requests_are_sent(monkeypatch) -> None:
538-
"""Pieces should remain retryable when peer selection produced no actual block requests."""
539-
from ccbt.piece.async_piece_manager import AsyncPieceManager, PieceState
540-
541-
torrent_data = {
542-
"info_hash": b"6" * 20,
543-
"name": "no-op-request-test",
544-
"announce": "http://tracker.example.com/announce",
545-
"file_info": {"type": "single", "length": 16384, "name": "x", "total_length": 16384},
546-
"pieces_info": {
547-
"piece_length": 16384,
548-
"num_pieces": 1,
549-
"piece_hashes": [b"x" * 20],
550-
"total_length": 16384,
551-
},
552-
}
553-
554-
piece_manager = AsyncPieceManager(torrent_data)
555-
await piece_manager.update_from_metadata(torrent_data)
556-
piece_manager.peer_availability["198.51.100.10:6881"] = SimpleNamespace(pieces={0})
557-
558-
async def fake_get_peers_for_piece(piece_index: int, _peer_manager: object):
559-
_ = piece_index, _peer_manager
560-
return [SimpleNamespace()]
561-
562-
async def fake_request_blocks_normal(
563-
piece_index: int,
564-
missing_blocks: list[object],
565-
available_peers: list[object],
566-
_peer_manager: object,
567-
) -> int:
568-
_ = piece_index, missing_blocks, available_peers, _peer_manager
569-
return 0
570-
571-
monkeypatch.setattr(piece_manager, "_get_peers_for_piece", fake_get_peers_for_piece)
572-
monkeypatch.setattr(
573-
piece_manager,
574-
"_request_blocks_normal",
575-
fake_request_blocks_normal,
576-
)
577-
578-
await piece_manager.request_piece_from_peers(0, SimpleNamespace(get_active_peers=lambda: []))
579-
580-
assert piece_manager.pieces[0].state == PieceState.REQUESTED
581-
assert piece_manager.pieces[0].last_request_time == 0.0
582-
583-
584-
@pytest.mark.asyncio
585-
async def test_handle_peer_choked_requeues_piece_without_inflight_requests() -> None:
586-
"""Choked peers should return inert pieces to a retryable state immediately."""
587-
from ccbt.piece.async_piece_manager import AsyncPieceManager, PieceState
588-
589-
torrent_data = {
590-
"info_hash": b"7" * 20,
591-
"name": "choke-requeue-test",
592-
"announce": "http://tracker.example.com/announce",
593-
"file_info": {"type": "single", "length": 16384, "name": "x", "total_length": 16384},
594-
"pieces_info": {
595-
"piece_length": 16384,
596-
"num_pieces": 1,
597-
"piece_hashes": [b"x" * 20],
598-
"total_length": 16384,
599-
},
600-
}
601-
602-
piece_manager = AsyncPieceManager(torrent_data)
603-
await piece_manager.update_from_metadata(torrent_data)
604-
piece = piece_manager.pieces[0]
605-
piece.state = PieceState.DOWNLOADING
606-
piece.last_request_time = 123.0
607-
piece.blocks[0].requested_from.add("198.51.100.10:6881")
608-
piece_manager._requested_pieces_per_peer["198.51.100.10:6881"] = {0}
609-
piece_manager._active_block_requests[0] = {"198.51.100.10:6881": [(0, 16384, 100.0)]}
610-
611-
peer = SimpleNamespace(peer_info=SimpleNamespace(ip="198.51.100.10", port=6881))
612-
await piece_manager.handle_peer_choked(peer)
613-
614-
assert piece.state == PieceState.MISSING
615-
assert piece.last_request_time == 0.0
616-
assert piece.blocks[0].requested_from == set()
617-
assert "198.51.100.10:6881" not in piece_manager._requested_pieces_per_peer
618-
assert 0 not in piece_manager._active_block_requests

0 commit comments

Comments
 (0)