169 lines · 6.5 KB · TypeScript
RawDownload
/** * Proves torlink's seeding actually transfers bytes (not just a UI label), and * that pause and resume really stop/restart it. * * Offline-deterministic: the leechers run with dht/tracker/lsd OFF and are * wired straight to our seeder via addPeer("127.0.0.1:<port>"), so the ONLY * possible source of data is the TorrentEngine under test. * *   npx tsx scripts/verify-seeding.ts */import WebTorrent from "webtorrent";import { promises as fs } from "node:fs";import os from "node:os";import path from "node:path";import { TorrentEngine } from "../src/download/engine"; const FILE_BYTES = 4 * 1024 * 1024; // 4 MBconst PULL_TIMEOUT = 20_000;const PAUSE_WINDOW = 6_000; function log(msg: string): void {  process.stdout.write(`${msg}\n`);} function randomBytes(n: number): Buffer {  // Deterministic-ish filler; content doesn't matter, only that it transfers.  const buf = Buffer.allocUnsafe(n);  for (let i = 0; i < n; i++) buf[i] = (i * 31 + 7) & 0xff;  return buf;} // Spin a leecher wired directly to the seeder; resolve with bytes downloaded.// `source` is the .torrent path (metadata) so the leecher knows the pieces and// the only open question is whether our seeder actually serves them.function leech(  source: string,  port: number,  dir: string,  timeout: number,): Promise<{ done: boolean; downloaded: number }> {  return new Promise((resolve) => {    const client = new WebTorrent({ dht: false, tracker: false, lsd: false });    const torrent = client.add(source, { path: dir, announce: [] });    let settled = false;    let rewire: ReturnType<typeof setInterval> | null = null;    const timer = setTimeout(() => finish(false), timeout);    const finish = (done: boolean): void => {      if (settled) return;      settled = true;      const downloaded = client.torrents[0]?.downloaded ?? 0;      clearTimeout(timer);      if (rewire) clearInterval(rewire);      client.destroy(() => resolve({ done, downloaded }));    };    // A magnet has no metadata yet, so the leecher must learn it FROM this peer;    // addPeer is only legal after the infoHash event. webtorrent's server binds    // IPv6 (::), so use the v6 loopback (v4 too for good measure). Re-wire on a    // timer: with discovery off, a dropped connection won't be re-found, so we    // keep nudging the peer back in until the transfer completes.    const wire = (): void => {      torrent.addPeer(`[::1]:${port}`);      torrent.addPeer(`127.0.0.1:${port}`);    };    torrent.on("infoHash", () => {      wire();      rewire = setInterval(wire, 1000);    });    torrent.on("done", () => finish(true));  });} // Wait until the engine reports the torrent fully verified (progress 1), so we// only start leeching once there is something complete to pull.async function waitSeederReady(  engine: TorrentEngine,  id: string,  timeout: number,): Promise<{ progress: number; peers: number }> {  const start = Date.now();  for (;;) {    const s = engine.stats(id);    if (s && s.progress >= 1) return { progress: s.progress, peers: s.peers };    if (Date.now() - start > timeout) return { progress: s?.progress ?? 0, peers: s?.peers ?? 0 };    await new Promise((r) => setTimeout(r, 200));  }} async function main(): Promise<void> {  const root = await fs.mkdtemp(path.join(os.tmpdir(), "torlink-seedcheck-"));  const seedDir = path.join(root, "seed");  await fs.mkdir(seedDir, { recursive: true });  const filePath = path.join(seedDir, "payload.bin");  await fs.writeFile(filePath, randomBytes(FILE_BYTES));  log(`payload: ${filePath} (${FILE_BYTES} bytes)`);   // Mint the .torrent metadata for the on-disk file via a throwaway client, then  // drop it. This is what torlink now captures at download time and seeds from.  const minter = new WebTorrent({ dht: false, tracker: false, lsd: false });  const { meta, infoHash } = await new Promise<{ meta: Uint8Array; infoHash: string }>(    (resolve) => {      minter.seed(filePath, { announce: [] }, (t) =>        resolve({ meta: Uint8Array.from(t.torrentFile), infoHash: t.infoHash }),      );    },  );  await new Promise<void>((r) => minter.destroy(() => r()));  const torrentPath = path.join(root, "meta.torrent");  await fs.writeFile(torrentPath, meta);  log(`metadata minted: ${infoHash}`);   const engine = new TorrentEngine();  let failures = 0;  const check = (label: string, ok: boolean, detail: string): void => {    log(`${ok ? "PASS" : "FAIL"}  ${label}  ${detail}`);    if (!ok) failures++;  };   // 1) SEED — engine verifies the on-disk file from metadata and uploads it.  engine.add(infoHash, torrentPath, seedDir, {    onError: (m) => log(`seed onError: ${m}`),  });  const ready1 = await waitSeederReady(engine, infoHash, 10_000);  const port1 = engine.listenPort();  log(`engine seeding: progress=${ready1.progress} port=${port1}`);  const r1 = await leech(torrentPath, port1 ?? 0, path.join(root, "leech1"), PULL_TIMEOUT);  const s1 = engine.stats(infoHash);  check(    "seed uploads to a peer",    r1.done && r1.downloaded === FILE_BYTES,    `downloaded=${r1.downloaded}/${FILE_BYTES} done=${r1.done} engineUploaded=${s1?.uploaded ?? 0} peers=${s1?.peers ?? 0}`,  );   // 2) PAUSE — engine.remove tears the torrent down; a fresh leecher gets nothing.  engine.remove(infoHash);  await new Promise((r) => setTimeout(r, 500));  const r2 = await leech(torrentPath, port1 ?? 0, path.join(root, "leech2"), PAUSE_WINDOW);  check(    "pause stops seeding",    !r2.done && r2.downloaded === 0,    `downloaded=${r2.downloaded} done=${r2.done} (expected 0 / false)`,  );   // 3) RESUME — re-add and a fresh leecher completes again.  engine.add(infoHash, torrentPath, seedDir, {    onError: (m) => log(`resume onError: ${m}`),  });  const ready3 = await waitSeederReady(engine, infoHash, 10_000);  const port3 = engine.listenPort();  log(`engine re-seeding: progress=${ready3.progress} port=${port3}`);  const r3 = await leech(torrentPath, port3 ?? 0, path.join(root, "leech3"), PULL_TIMEOUT);  const s3 = engine.stats(infoHash);  check(    "resume re-seeds",    r3.done && r3.downloaded === FILE_BYTES,    `downloaded=${r3.downloaded}/${FILE_BYTES} done=${r3.done} engineUploaded=${s3?.uploaded ?? 0}`,  );   engine.destroy();  await fs.rm(root, { recursive: true, force: true }).catch(() => {});   log("");  log(failures === 0 ? "RESULT: seeding is REAL ✓ (seed/pause/resume verified)" : `RESULT: ${failures} check(s) FAILED`);  process.exit(failures === 0 ? 0 : 1);} main().catch((e) => {  log(`harness error: ${e instanceof Error ? e.stack : String(e)}`);  process.exit(1);});