From 610fe35d334f02d3c32d8ffef4e074ade07e3d05 Mon Sep 17 00:00:00 2001
From: Claude <claude@anthropic.com>
Date: Fri, 6 Mar 2026 02:24:24 +0000
Subject: [PATCH] Add configurable timeout parameters
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adds --timeout and --connect-timeout CLI options to control:
- --timeout: How long to wait for piece downloads (default: 30s)
- --connect-timeout: How long to wait for peer connections (default: 10s)

This allows users to wait longer for rare seeders or slow peers,
which is essential for archival torrents with limited availability.

Examples:
  tornado --timeout 600 file.torrent         # 10min per piece
  tornado --connect-timeout 30 file.torrent  # 30s to connect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
---
 src/bin/cli.ml            | 27 ++++++++++++++++++++-------
 src/lib/torrent_client.ml | 31 ++++++++++++++++++++++---------
 2 files changed, 42 insertions(+), 16 deletions(-)

diff --git a/src/bin/cli.ml b/src/bin/cli.ml
index 80ae3ec..9dcdea1 100644
--- a/src/bin/cli.ml
+++ b/src/bin/cli.ml
@@ -4,9 +4,11 @@ let usage = "Usage: tornado [OPTIONS] <torrent_file>\n\
              \nDownload files from BitTorrent networks.\n\
              \n\
              OPTIONS:\n\
-             \  -o <name>      Set output file/directory name (default: use torrent name)\n\
-             \  --verbose      Output debug information\n\
-             \  --help, -h     Show this help message\n\
+             \  -o <name>              Set output file/directory name (default: use torrent name)\n\
+             \  --timeout <seconds>    Timeout for piece downloads (default: 30.0)\n\
+             \  --connect-timeout <s>  Timeout for peer connections (default: 10.0)\n\
+             \  --verbose              Output debug information\n\
+             \  --help, -h             Show this help message\n\
              \n\
              FEATURES:\n\
              \  • Multi-file torrents - automatically creates directory structure\n\
@@ -15,9 +17,16 @@ let usage = "Usage: tornado [OPTIONS] <torrent_file>\n\
              \  • Seeding - announces as seeder after completing download\n\
              \n\
              EXAMPLES:\n\
-             \  tornado file.torrent              # Download using torrent's name\n\
-             \  tornado file.torrent -o myfile    # Download with custom name\n\
-             \  tornado --verbose file.torrent    # Download with debug output\n\
+             \  tornado file.torrent                      # Download using torrent's name\n\
+             \  tornado file.torrent -o myfile            # Download with custom name\n\
+             \  tornado --verbose file.torrent            # Download with debug output\n\
+             \  tornado --timeout 300 file.torrent        # Wait up to 5 minutes per piece\n\
+             \  tornado --connect-timeout 30 file.torrent # Wait 30s to connect to peers\n\
+             \n\
+             TIMEOUTS:\n\
+             \  For torrents with rare seeders, increase timeouts to wait longer:\n\
+             \    --timeout 600           # 10 minutes per piece\n\
+             \    --connect-timeout 60    # 1 minute to connect\n\
              \n\
              RESUME:\n\
              \  If a download is interrupted, simply run the same command again.\n\
@@ -27,11 +36,15 @@ let verbose = ref false
 let output_file : string option ref = ref None
 let input_file = ref ""
 let show_help = ref false
+let timeout = ref 30.0
+let connect_timeout = ref 10.0
 let apply_output_file str = output_file := Some str
 
 let spec_list =
   [ "--verbose", Arg.Set verbose, ""
   ; "-o", Arg.String apply_output_file, ""
+  ; "--timeout", Arg.Set_float timeout, ""
+  ; "--connect-timeout", Arg.Set_float connect_timeout, ""
   ; "--help", Arg.Set show_help, ""
   ; "-h", Arg.Set show_help, "" ]
 ;;
@@ -53,5 +66,5 @@ let () =
   );
   Log.setup_log (Some (if !verbose then Debug else App));
   let torrent_file = Torrent_file.open_file !input_file in
-  Lwt_main.run (Torrent_client.download_file !output_file torrent_file)
+  Lwt_main.run (Torrent_client.download_file !output_file torrent_file !timeout !connect_timeout)
 ;;
diff --git a/src/lib/torrent_client.ml b/src/lib/torrent_client.ml
index f89f203..78ab70e 100644
--- a/src/lib/torrent_client.ml
+++ b/src/lib/torrent_client.ml
@@ -111,7 +111,7 @@ let with_timeout timeout f =
     ]
 ;;
 
-let try_download_piece (client : Client.t) (pw : piece_work) torrent =
+let try_download_piece timeout (client : Client.t) (pw : piece_work) torrent =
   let state =
     { requested = ref 0
     ; downloaded = ref 0
@@ -124,7 +124,7 @@ let try_download_piece (client : Client.t) (pw : piece_work) torrent =
       "Try to download piece %d of %d"
       (pw.index + 1)
       (Array.length torrent.piece_hashes));
-  with_timeout 30.0 (fun () ->
+  with_timeout timeout (fun () ->
     download_piece client pw torrent state >>= fun piece_buf ->
     Logs.info (fun m ->
       m
@@ -143,13 +143,14 @@ let check_integrity (pw : piece_work) buf =
 ;;
 
 let download_piece_task
+  timeout
   client
   torrent
   (push_work : piece_work option -> unit)
   (push_result : piece_result option -> unit)
   pw
   =
-  try_download_piece client pw torrent >>= fun piece_buf ->
+  try_download_piece timeout client pw torrent >>= fun piece_buf ->
   let integrity_result = check_integrity pw piece_buf in
   match integrity_result with
   | Error (`Error e) ->
@@ -170,6 +171,7 @@ let download_piece_task
 ;;
 
 let rec download_torrent_worker
+  timeout
   (torrent : t)
   (client : Client.t)
   (work_stream : piece_work Lwt_stream.t)
@@ -194,6 +196,7 @@ let rec download_torrent_worker
           Lwt.return_unit)
         else
           download_piece_task
+            timeout
             client
             torrent
             push_work
@@ -206,27 +209,31 @@ let rec download_torrent_worker
         Logs.err (fun m -> m "%a" Fmt.exn ex);
         Lwt.return_unit)
     >>= fun () ->
-    download_torrent_worker torrent client work_stream push_work push_result
+    download_torrent_worker timeout torrent client work_stream push_work push_result
 ;;
 
 let connect_and_download_torrent
+  timeout
+  connect_timeout
   torrent
   peer
   (work_stream : piece_work Lwt_stream.t)
   (push_work : piece_work option -> unit)
   (push_result : piece_result option -> unit)
   =
-  with_timeout 4.0 (fun () -> Client.connect peer torrent.info_hash torrent.peer_id)
+  with_timeout connect_timeout (fun () -> Client.connect peer torrent.info_hash torrent.peer_id)
   >>= fun client_result ->
   let client = Result.get_ok client_result in
   Logs.debug (fun m ->
     m "Completed handshake with %s\n" (Ipaddr.V4.to_string peer.ip));
   Client.send_unchoke client >>= fun () ->
   Client.send_interested client >>= fun () ->
-  download_torrent_worker torrent client work_stream push_work push_result
+  download_torrent_worker timeout torrent client work_stream push_work push_result
 ;;
 
 let rec download_torrent_by_peers
+  timeout
+  connect_timeout
   torrent
   (peers : Peers.t list)
   (work_stream : piece_work Lwt_stream.t)
@@ -240,6 +247,8 @@ let rec download_torrent_by_peers
       Lwt.catch
         (fun () ->
           connect_and_download_torrent
+            timeout
+            connect_timeout
             torrent
             peers_head
             work_stream
@@ -247,6 +256,8 @@ let rec download_torrent_by_peers
             push_result)
         (fun _ex -> Lwt.return_unit));
     download_torrent_by_peers
+      timeout
+      connect_timeout
       torrent
       peers_tail
       work_stream
@@ -309,7 +320,7 @@ let remove_state_file output_file =
     Unix.unlink path
 ;;
 
-let download_torrent (torrent : t) file_name completed_pieces =
+let download_torrent timeout connect_timeout (torrent : t) file_name completed_pieces =
   let open Lwt.Infix in
   let final_buf = Bytes.create torrent.length in
   let pieces_hashes_len = Array.length torrent.piece_hashes in
@@ -337,6 +348,8 @@ let download_torrent (torrent : t) file_name completed_pieces =
   (* Start peer download tasks *)
   Lwt.async (fun () ->
     download_torrent_by_peers
+      timeout
+      connect_timeout
       torrent
       torrent.peers
       work_stream
@@ -408,7 +421,7 @@ let write_multifile base_dir files final_buf =
   write_files 0 files
 ;;
 
-let download_file output_file torrent_file =
+let download_file output_file torrent_file timeout connect_timeout =
   let open Torrent_file in
   let base_name =
     match output_file, torrent_file.name with
@@ -443,7 +456,7 @@ let download_file output_file torrent_file =
       (torrent_file.piece_length |> Int64.to_int)
       total_len
   in
-  download_torrent torrent base_name completed_pieces >>= fun final_buf ->
+  download_torrent timeout connect_timeout torrent base_name completed_pieces >>= fun final_buf ->
   (* Write File(s) *)
   (match torrent_file.file_mode with
   | SingleFile _ ->
-- 
2.43.0

