mod observation;
mod options;
mod runtime;
pub(in crate::engine) mod shell;
pub mod telemetry;
use crate::engine::{
profile::Profile,
provisioning::FileStorage,
storage::{Checkpoint, writer::Writer},
};
use crate::protocol::metainfo::OwnedMetainfo;
use serde::{Serialize, Serializer};
use std::time::Instant;
use std::{future::Future, io, net::SocketAddr, sync::Arc, time::Duration};
use tokio::{sync::watch, task::JoinHandle};
use tracing::Instrument;
pub struct TorrentOptions {
pub(in crate::engine) settings: crate::engine::settings::TorrentSettings,
pub(in crate::engine) metainfo: OwnedMetainfo,
pub(in crate::engine) profile: Profile,
storage: Option<FileStorage>,
resume: Option<Vec<u8>>,
checkpoint: Checkpoint,
completed_announce: Option<bool>,
announce_checkpoint: Arc<dyn Fn(bool) -> io::Result<()> + Send + Sync>,
pub(in crate::engine) peers: Vec<SocketAddr>,
pub(in crate::engine) dht: Option<crate::engine::session::dht::Handle>,
pub(in crate::engine) run: RunPolicy,
pub(in crate::engine) log_peer_endpoints: bool,
pub(in crate::engine) no_progress_timeout: Duration,
pub(in crate::engine) diagnostics: bool,
}
pub(in crate::engine) enum Networking {
Online(Network),
Recheck,
}
pub(in crate::engine) struct Network {
pub(in crate::engine) advertised_port: watch::Receiver<u16>,
pub(in crate::engine) sockets: tokio::sync::mpsc::Receiver<crate::engine::session::Inbound>,
pub(in crate::engine) dial_queue: crate::engine::session::dial::Queue,
pub(in crate::engine) tracker_http: reqwest::Client,
pub(in crate::engine) tracker_udp: crate::engine::session::udp::Transport,
pub(in crate::engine) allow_local_network: bool,
}
pub(in crate::engine) struct RuntimeOptions {
pub(in crate::engine) reannounce: watch::Receiver<()>,
pub(in crate::engine) completed_announce: Option<bool>,
pub(in crate::engine) announce_checkpoint: Arc<dyn Fn(bool) -> io::Result<()> + Send + Sync>,
pub(in crate::engine) settings: crate::engine::settings::TorrentSettings,
pub(in crate::engine) metainfo: OwnedMetainfo,
pub(in crate::engine) profile: Profile,
pub(in crate::engine) peers: Vec<SocketAddr>,
pub(in crate::engine) dht: Option<crate::engine::session::dht::Handle>,
run: RunPolicy,
log_peer_endpoints: bool,
no_progress_timeout: Duration,
diagnostics: bool,
selection: crate::engine::selection::FileSelection,
pub(in crate::engine) peer_id: [u8; 20],
bandwidth: crate::engine::bandwidth::Throttle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RunPolicy {
Recheck,
Persistent,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TorrentPhase {
#[default]
Starting,
Recovering,
Downloading,
Seeding,
Stopping,
Stopped,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TorrentExit {
StopRequested,
NoProgress,
Complete,
Unavailable,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum TorrentRetryStatus {
#[default]
Idle,
Backoff {
attempt: u32,
#[serde(rename = "retry_in_ms", serialize_with = "serialize_retry_delay")]
retry_at: Instant,
},
Retrying {
attempt: u32,
},
}
fn serialize_retry_delay<S: Serializer>(
deadline: &Instant,
serializer: S,
) -> Result<S::Ok, S::Error> {
let millis = deadline
.saturating_duration_since(Instant::now())
.as_millis();
serializer.serialize_u64(millis.min(u128::from(u64::MAX)) as u64)
}
fn serialize_piece_bitfield<S: Serializer>(
pieces: &[bool],
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut bytes = vec![0u8; pieces.len().div_ceil(8)];
for (index, &verified) in pieces.iter().enumerate() {
if verified {
bytes[index / 8] |= 0x80 >> (index % 8);
}
}
bytes.serialize(serializer)
}
fn piece_map_empty(pieces: &std::sync::Arc<[bool]>) -> bool {
pieces.is_empty()
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TorrentStatus {
pub progress: TorrentProgress,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Arc<TorrentDiagnostics>>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TorrentDiagnostics {
pub utp_retransmits: u64,
pub pending_contacts: usize,
pub writer_free_slots: usize,
pub upload_reads_in_flight: usize,
pub upload_reads_finished: usize,
pub peers: Vec<telemetry::PeerStatus>,
pub trackers: Vec<telemetry::TrackerStatus>,
pub web_seeds: Vec<telemetry::WebSeedStatus>,
pub dht: Option<telemetry::DhtStatus>,
pub retry: TorrentRetryStatus,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TorrentProgress {
pub phase: TorrentPhase,
pub verified_bytes: u64,
pub total_bytes: u64,
pub wanted_bytes: u64,
pub wanted_verified_bytes: u64,
pub processed_payload_bytes: u64,
pub hash_failed_bytes: u64,
pub hash_failed_pieces: u64,
pub written_payload_bytes: u64,
pub download_bytes_per_second: f64,
pub upload_bytes_per_second: f64,
pub completed: bool,
pub ready: bool,
pub verified_pieces: u32,
#[serde(
rename = "verified_piece_bitfield",
serialize_with = "serialize_piece_bitfield",
skip_serializing_if = "piece_map_empty"
)]
pub verified_piece_map: std::sync::Arc<[bool]>,
pub active_peers: usize,
pub exit: Option<TorrentExit>,
pub joined: bool,
}
pub(in crate::engine) struct TorrentHandle {
reannounce: watch::Sender<()>,
stop: watch::Sender<bool>,
status: watch::Receiver<TorrentStatus>,
task: Option<JoinHandle<io::Result<()>>>,
}
#[derive(Clone)]
pub struct TorrentObserver {
reannounce: watch::Sender<()>,
stop: watch::Sender<bool>,
status: watch::Receiver<TorrentStatus>,
}
impl TorrentObserver {
pub fn reannounce(&self) {
self.reannounce.send_replace(());
}
pub fn request_stop(&self) {
self.stop.send_replace(true);
}
pub fn snapshot(&self) -> TorrentStatus {
self.status.borrow().clone()
}
pub async fn changed(&mut self) -> bool {
self.status.changed().await.is_ok()
}
}
impl TorrentHandle {
pub(in crate::engine) fn observer(&self) -> TorrentObserver {
TorrentObserver {
reannounce: self.reannounce.clone(),
stop: self.stop.clone(),
status: self.status.clone(),
}
}
pub(in crate::engine) fn spawn_storage(
reannounce: watch::Sender<()>,
writer: Writer,
options: RuntimeOptions,
networking: Networking,
) -> Self {
let initial = TorrentStatus {
progress: TorrentProgress {
total_bytes: options.metainfo.layout().total_length(),
..Default::default()
},
diagnostics: options
.diagnostics
.then(|| Arc::new(TorrentDiagnostics::default())),
};
Self::spawn_task(initial, reannounce, move |stop, status| {
runtime::run(writer, options, networking, stop, status)
})
}
fn spawn_task<F: Future<Output = io::Result<()>> + Send + 'static>(
initial: TorrentStatus,
reannounce: watch::Sender<()>,
run: impl FnOnce(watch::Receiver<bool>, watch::Sender<TorrentStatus>) -> F,
) -> Self {
let (stop, stopping) = watch::channel(false);
let (status, observer) = watch::channel(initial);
let task = tokio::spawn(run(stopping, status).instrument(tracing::Span::current()));
Self {
reannounce,
stop,
status: observer,
task: Some(task),
}
}
pub(in crate::engine) fn request_stop(&self) {
self.stop.send_replace(true);
}
pub(in crate::engine) async fn join(&mut self) -> io::Result<()> {
let task = self
.task
.as_mut()
.ok_or_else(|| io::Error::other("torrent task already joined"))?;
let result = task.await.map_err(io::Error::other);
self.task = None;
result?
}
}
impl Drop for TorrentHandle {
fn drop(&mut self) {
self.request_stop();
}
}