Initial commit: multi-peer torrent download engine
Reactor/loop-pool engine with TCP/µTP/MSE transports, per-connection pipelining, priority-driven piece selection with endgame, and the Python FFI test harness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
d8208685a2
55 changed files with 9989 additions and 0 deletions
109
interop/seed_transmission.py
Normal file
109
interop/seed_transmission.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from common import touch_ready, wait_for_tcp
|
||||
|
||||
|
||||
def run_remote(rpc_port: int, *args: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
["transmission-remote", f"127.0.0.1:{rpc_port}", *args],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
check=check,
|
||||
)
|
||||
|
||||
|
||||
def write_settings(config_dir: str, peer_port: int, rpc_port: int, data_dir: str) -> None:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
settings = {
|
||||
"download-dir": data_dir,
|
||||
"incomplete-dir-enabled": False,
|
||||
"dht-enabled": False,
|
||||
"pex-enabled": False,
|
||||
"lpd-enabled": False,
|
||||
"utp-enabled": False,
|
||||
"port-forwarding-enabled": False,
|
||||
"peer-port": peer_port,
|
||||
"peer-port-random-on-start": False,
|
||||
"rpc-enabled": True,
|
||||
"rpc-bind-address": "127.0.0.1",
|
||||
"rpc-port": rpc_port,
|
||||
"rpc-whitelist-enabled": False,
|
||||
"start-added-torrents": True,
|
||||
"trash-original-torrent-files": False,
|
||||
}
|
||||
with open(os.path.join(config_dir, "settings.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(settings, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Seed a fixture with Transmission.")
|
||||
ap.add_argument("--torrent", required=True)
|
||||
ap.add_argument("--data", required=True)
|
||||
ap.add_argument("--peer-port", type=int, required=True)
|
||||
ap.add_argument("--rpc-port", type=int, required=True)
|
||||
ap.add_argument("--utp", action="store_true")
|
||||
ap.add_argument("--encryption", choices=["tolerated", "preferred", "required"],
|
||||
default="tolerated")
|
||||
args = ap.parse_args()
|
||||
|
||||
config_dir = "/tmp/transmission-config"
|
||||
shutil.rmtree(config_dir, ignore_errors=True)
|
||||
write_settings(config_dir, args.peer_port, args.rpc_port, args.data)
|
||||
|
||||
proc = subprocess.Popen(["transmission-daemon", "-f", "-g", config_dir])
|
||||
stop = False
|
||||
|
||||
def _stop(signum, frame):
|
||||
nonlocal stop
|
||||
stop = True
|
||||
proc.terminate()
|
||||
|
||||
signal.signal(signal.SIGTERM, _stop)
|
||||
signal.signal(signal.SIGINT, _stop)
|
||||
|
||||
try:
|
||||
wait_for_tcp("127.0.0.1", args.rpc_port, timeout=30)
|
||||
run_remote(args.rpc_port, "--no-dht", "--no-pex", "--no-lpd", "--no-portmap")
|
||||
run_remote(args.rpc_port, "--utp" if args.utp else "--no-utp")
|
||||
run_remote(args.rpc_port, {
|
||||
"tolerated": "--encryption-tolerated",
|
||||
"preferred": "--encryption-preferred",
|
||||
"required": "--encryption-required",
|
||||
}[args.encryption])
|
||||
run_remote(args.rpc_port, "-a", args.torrent, "-w", args.data)
|
||||
run_remote(args.rpc_port, "-t", "all", "--start")
|
||||
wait_for_tcp("127.0.0.1", args.peer_port, timeout=30)
|
||||
deadline = time.time() + 60
|
||||
while time.time() < deadline:
|
||||
info = run_remote(args.rpc_port, "-t", "all", "-i", check=False).stdout
|
||||
if "Percent Done: 100%" in info or "Seeding" in info:
|
||||
touch_ready()
|
||||
print(f"transmission seeding on {args.peer_port}", flush=True)
|
||||
break
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise TimeoutError("Transmission did not report a complete seed")
|
||||
|
||||
while not stop and proc.poll() is None:
|
||||
time.sleep(1)
|
||||
return proc.returncode or 0
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue