""" ctypes bindings for the multi-peer engine ABI (include/engine.h). The engine owns a pool of event-loop threads; torrents are pinned to a loop and every connection of a torrent lives there. Each loop has its own arena, so a delivered block names both the loop and the slot. We wrap each loop's arena once as a zero-copy ``memoryview`` and slice per block; returning the slot via ``release()`` is what lets the engine issue new requests (credit-based flow control). """ from __future__ import annotations import ctypes as C import os # peer_state / peer_error mirrors of include/engine.h STATE_IDLE, STATE_CONNECTING, STATE_HANDSHAKE, STATE_CHOKED, \ STATE_RUNNING, STATE_STOPPED, STATE_ERROR = range(7) STATE_NAMES = ["IDLE", "CONNECTING", "HANDSHAKE", "CHOKED", "RUNNING", "STOPPED", "ERROR"] ERROR_NAMES = ["OK", "CONNECT", "HANDSHAKE", "CLOSED", "PROTOCOL", "IO", "NOMEM"] BLOCK_SIZE = 16384 class EngineConfig(C.Structure): _fields_ = [ ("loop_count", C.c_uint32), ("slots_per_loop", C.c_uint32), ("max_pipeline", C.c_uint32), ("request_timeout_ms", C.c_uint32), ("recv_buffer_bytes", C.c_uint32), ("encryption", C.c_uint32), ("utp", C.c_uint32), ("connect_timeout_ms", C.c_uint32), ("fallback", C.c_uint32), ] class EngineBlock(C.Structure): _fields_ = [ ("torrent", C.c_uint32), ("piece", C.c_uint32), ("begin", C.c_uint32), ("len", C.c_uint32), ("loop", C.c_uint32), ("slot", C.c_uint32), ] class TorrentStatus(C.Structure): _fields_ = [ ("state", C.c_int32), ("error", C.c_int32), ("bytes_received", C.c_uint64), ("blocks_received", C.c_uint64), ("peers", C.c_uint32), ("peers_connected", C.c_uint32), ("peers_failed", C.c_uint32), ("outstanding", C.c_uint32), ("free_slots", C.c_uint32), ("pipeline_target", C.c_uint32), ("rate_bps", C.c_double), ("rtt_min_ms", C.c_double), ] def _default_lib_path() -> str: here = os.path.dirname(os.path.abspath(__file__)) cand = [ os.path.join(here, "..", "build", "libtorrentpeer.so"), os.path.join(here, "..", "build", "lib", "libtorrentpeer.so"), ] for p in cand: if os.path.exists(p): return os.path.abspath(p) return os.path.abspath(cand[0]) def _load(lib_path: str | None) -> C.CDLL: lib = C.CDLL(lib_path or _default_lib_path()) lib.engine_create.restype = C.c_void_p lib.engine_create.argtypes = [C.POINTER(EngineConfig)] lib.engine_destroy.restype = None lib.engine_destroy.argtypes = [C.c_void_p] lib.engine_add_torrent.restype = C.c_int32 lib.engine_add_torrent.argtypes = [ C.c_void_p, C.POINTER(C.c_uint8), C.POINTER(C.c_uint8), C.c_uint64, C.c_uint64, C.c_uint32, ] lib.engine_add_peer.restype = C.c_int lib.engine_add_peer.argtypes = [C.c_void_p, C.c_uint32, C.c_char_p, C.c_uint16] lib.engine_set_priorities.restype = C.c_int lib.engine_set_priorities.argtypes = [ C.c_void_p, C.c_uint32, C.POINTER(C.c_uint8), C.c_uint32] lib.engine_set_priority.restype = C.c_int lib.engine_set_priority.argtypes = [C.c_void_p, C.c_uint32, C.c_uint32, C.c_uint8] lib.engine_request_piece.restype = C.c_int lib.engine_request_piece.argtypes = [C.c_void_p, C.c_uint32, C.c_uint32] lib.engine_poll_ready.restype = C.c_uint32 lib.engine_poll_ready.argtypes = [C.c_void_p, C.POINTER(EngineBlock), C.c_uint32] lib.engine_release_slot.restype = None lib.engine_release_slot.argtypes = [C.c_void_p, C.c_uint32, C.c_uint32] lib.engine_wait.restype = C.c_int lib.engine_wait.argtypes = [C.c_void_p, C.c_int] lib.engine_arena_base.restype = C.c_void_p lib.engine_arena_base.argtypes = [C.c_void_p, C.c_uint32] lib.engine_arena_bytes.restype = C.c_uint64 lib.engine_arena_bytes.argtypes = [C.c_void_p, C.c_uint32] lib.engine_loop_count.restype = C.c_uint32 lib.engine_loop_count.argtypes = [C.c_void_p] lib.engine_torrent_status.restype = None lib.engine_torrent_status.argtypes = [C.c_void_p, C.c_uint32, C.POINTER(TorrentStatus)] return lib class Engine: """Pythonic wrapper around one engine instance (a pool of loops).""" def __init__(self, cfg: EngineConfig | None = None, lib_path: str | None = None, poll_batch: int = 1024): self._lib = _load(lib_path) self._e = self._lib.engine_create(C.byref(cfg) if cfg else None) if not self._e: raise RuntimeError("engine_create failed") # One zero-copy memoryview per loop arena. self.nloops = self._lib.engine_loop_count(self._e) self._arenas = [] self.arenas = [] for i in range(self.nloops): base = self._lib.engine_arena_base(self._e, i) nbytes = self._lib.engine_arena_bytes(self._e, i) buf = (C.c_char * nbytes).from_address(base) self._arenas.append(buf) self.arenas.append(memoryview(buf).cast("B")) self._batch = poll_batch self._blocks = (EngineBlock * poll_batch)() def add_torrent(self, info_hash: bytes, peer_id: bytes, piece_length: int, total_size: int, num_pieces: int) -> int: ih = (C.c_uint8 * 20).from_buffer_copy(info_hash) pid = (C.c_uint8 * 20).from_buffer_copy(peer_id) tid = self._lib.engine_add_torrent(self._e, ih, pid, piece_length, total_size, num_pieces) if tid < 0: raise RuntimeError("engine_add_torrent failed") return tid def add_peer(self, torrent_id: int, ip: str, port: int) -> None: if self._lib.engine_add_peer(self._e, torrent_id, ip.encode(), port) != 0: raise RuntimeError("engine_add_peer failed") def set_priorities(self, torrent_id: int, priorities) -> None: buf = bytes(priorities) arr = (C.c_uint8 * len(buf)).from_buffer_copy(buf) if self._lib.engine_set_priorities(self._e, torrent_id, arr, len(buf)) != 0: raise ValueError("set_priorities: length must equal num_pieces") def set_priority(self, torrent_id: int, piece_index: int, priority: int) -> None: if self._lib.engine_set_priority(self._e, torrent_id, piece_index, priority) != 0: raise ValueError(f"set_priority({piece_index}) out of range") def request_piece(self, torrent_id: int, piece_index: int) -> None: if self._lib.engine_request_piece(self._e, torrent_id, piece_index) != 0: raise ValueError(f"request_piece({piece_index}) out of range") def poll_ready(self): """Return a list of EngineBlock for completed blocks (may be empty).""" n = self._lib.engine_poll_ready(self._e, self._blocks, self._batch) return [self._blocks[i] for i in range(n)] def block_data(self, loop: int, slot: int, length: int) -> memoryview: off = slot * BLOCK_SIZE return self.arenas[loop][off:off + length] def release(self, loop: int, slot: int) -> None: self._lib.engine_release_slot(self._e, loop, slot) def wait(self, timeout_ms: int) -> int: return self._lib.engine_wait(self._e, timeout_ms) def status(self, torrent_id: int) -> TorrentStatus: st = TorrentStatus() self._lib.engine_torrent_status(self._e, torrent_id, C.byref(st)) return st def close(self) -> None: if self._e: for mv in self.arenas: mv.release() self.arenas = [] self._arenas = [] self._lib.engine_destroy(self._e) self._e = None def __enter__(self): return self def __exit__(self, *exc): self.close()