Saving
This commit is contained in:
parent
0b8580dacd
commit
d7a37c1b48
11 changed files with 337 additions and 168 deletions
|
|
@ -127,14 +127,28 @@ impl HttpApi {
|
||||||
State(state): State<ApiState>,
|
State(state): State<ApiState>,
|
||||||
Path(idx): Path<usize>,
|
Path(idx): Path<usize>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
state.api_torrent_action_pause(idx)
|
state.api_torrent_action_pause(idx).map(axum::Json)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn torrent_action_start(
|
async fn torrent_action_start(
|
||||||
State(state): State<ApiState>,
|
State(state): State<ApiState>,
|
||||||
Path(idx): Path<usize>,
|
Path(idx): Path<usize>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
state.api_torrent_action_start(idx)
|
state.api_torrent_action_start(idx).map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrent_action_forget(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(idx): Path<usize>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
state.api_torrent_action_forget(idx).map(axum::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn torrent_action_delete(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(idx): Path<usize>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
state.api_torrent_action_delete(idx).map(axum::Json)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
|
|
@ -149,7 +163,9 @@ impl HttpApi {
|
||||||
.route("/torrents/:id/stats/v1", get(torrent_stats_v1))
|
.route("/torrents/:id/stats/v1", get(torrent_stats_v1))
|
||||||
.route("/torrents/:id/peer_stats", get(peer_stats))
|
.route("/torrents/:id/peer_stats", get(peer_stats))
|
||||||
.route("/torrents/:id/pause", post(torrent_action_pause))
|
.route("/torrents/:id/pause", post(torrent_action_pause))
|
||||||
.route("/torrents/:id/start", post(torrent_action_start));
|
.route("/torrents/:id/start", post(torrent_action_start))
|
||||||
|
.route("/torrents/:id/forget", post(torrent_action_forget))
|
||||||
|
.route("/torrents/:id/delete", post(torrent_action_delete));
|
||||||
|
|
||||||
#[cfg(feature = "webui")]
|
#[cfg(feature = "webui")]
|
||||||
{
|
{
|
||||||
|
|
@ -244,6 +260,9 @@ pub struct TorrentDetailsResponseFile {
|
||||||
pub included: bool,
|
pub included: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize)]
|
||||||
|
struct EmptyJsonResponse {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct TorrentDetailsResponse {
|
pub struct TorrentDetailsResponse {
|
||||||
pub info_hash: String,
|
pub info_hash: String,
|
||||||
|
|
@ -409,20 +428,32 @@ impl ApiInternal {
|
||||||
.per_peer_stats_snapshot(filter))
|
.per_peer_stats_snapshot(filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<()> {
|
fn api_torrent_action_pause(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
let handle = self.mgr_handle(idx)?;
|
let handle = self.mgr_handle(idx)?;
|
||||||
handle
|
handle
|
||||||
.pause()
|
.pause()
|
||||||
.context("error pausing torrent")
|
.context("error pausing torrent")
|
||||||
.with_error_status_code(StatusCode::BAD_REQUEST)
|
.with_error_status_code(StatusCode::BAD_REQUEST)?;
|
||||||
|
Ok(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_torrent_action_start(&self, idx: TorrentId) -> Result<()> {
|
fn api_torrent_action_start(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
let handle = self.mgr_handle(idx)?;
|
let handle = self.mgr_handle(idx)?;
|
||||||
self.session
|
self.session
|
||||||
.unpause(&handle)
|
.unpause(&handle)
|
||||||
.context("error unpausing torrent")
|
.context("error unpausing torrent")
|
||||||
.with_error_status_code(StatusCode::BAD_REQUEST)
|
.with_error_status_code(StatusCode::BAD_REQUEST)?;
|
||||||
|
Ok(Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_torrent_action_forget(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
|
Err(ApiError::not_implemented("forgetting not implemented yet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_torrent_action_delete(&self, idx: TorrentId) -> Result<EmptyJsonResponse> {
|
||||||
|
Err(ApiError::not_implemented(
|
||||||
|
"deleting torrent not implemented yet",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn api_add_torrent(
|
pub async fn api_add_torrent(
|
||||||
|
|
@ -528,7 +559,7 @@ impl ApiInternal {
|
||||||
ManagedTorrentState::Paused(p) => {
|
ManagedTorrentState::Paused(p) => {
|
||||||
resp.state = "paused";
|
resp.state = "paused";
|
||||||
resp.progress_bytes = p.have_bytes;
|
resp.progress_bytes = p.have_bytes;
|
||||||
resp.finished = p.have_bytes == resp.progress_bytes;
|
resp.finished = p.have_bytes == resp.total_bytes;
|
||||||
}
|
}
|
||||||
ManagedTorrentState::Live(l) => {
|
ManagedTorrentState::Live(l) => {
|
||||||
resp.state = "live";
|
resp.state = "live";
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,14 @@ impl ApiError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn not_implemented(msg: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
status: Some(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
kind: ApiErrorKind::Other(anyhow::anyhow!("{}", msg)),
|
||||||
|
plaintext: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn dht_disabled() -> Self {
|
pub const fn dht_disabled() -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: Some(StatusCode::NOT_FOUND),
|
status: Some(StatusCode::NOT_FOUND),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use crate::spawn_utils::BlockingSpawner;
|
||||||
pub trait PeerConnectionHandler {
|
pub trait PeerConnectionHandler {
|
||||||
fn on_connected(&self, _connection_time: Duration) {}
|
fn on_connected(&self, _connection_time: Duration) {}
|
||||||
fn get_have_bytes(&self) -> u64;
|
fn get_have_bytes(&self) -> u64;
|
||||||
fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec<u8>) -> Option<usize>;
|
fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec<u8>) -> anyhow::Result<usize>;
|
||||||
fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()>;
|
fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()>;
|
||||||
fn on_extended_handshake(
|
fn on_extended_handshake(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -204,15 +204,13 @@ impl<H: PeerConnectionHandler> PeerConnection<H> {
|
||||||
.unwrap_or_else(|| Duration::from_secs(120));
|
.unwrap_or_else(|| Duration::from_secs(120));
|
||||||
|
|
||||||
if self.handler.get_have_bytes() > 0 {
|
if self.handler.get_have_bytes() > 0 {
|
||||||
if let Some(len) = self
|
let len = self
|
||||||
.handler
|
.handler
|
||||||
.serialize_bitfield_message_to_buf(&mut write_buf)
|
.serialize_bitfield_message_to_buf(&mut write_buf)?;
|
||||||
{
|
with_timeout(rwtimeout, write_half.write_all(&write_buf[..len]))
|
||||||
with_timeout(rwtimeout, write_half.write_all(&write_buf[..len]))
|
.await
|
||||||
.await
|
.context("error writing bitfield to peer")?;
|
||||||
.context("error writing bitfield to peer")?;
|
debug!("sent bitfield");
|
||||||
debug!("sent bitfield");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,8 @@ impl PeerConnectionHandler for Handler {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_bitfield_message_to_buf(&self, _buf: &mut Vec<u8>) -> Option<usize> {
|
fn serialize_bitfield_message_to_buf(&self, _buf: &mut Vec<u8>) -> anyhow::Result<usize> {
|
||||||
None
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> {
|
fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use librqbit_core::{
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tracing::{debug, info, trace_span, warn};
|
use tracing::{debug, error_span, info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult},
|
dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult},
|
||||||
|
|
@ -412,13 +412,14 @@ impl Session {
|
||||||
{
|
{
|
||||||
return Ok(AddTorrentResponse::AlreadyManaged(id, handle.clone()));
|
return Ok(AddTorrentResponse::AlreadyManaged(id, handle.clone()));
|
||||||
}
|
}
|
||||||
let managed_torrent = builder.build()?;
|
let next_id = g.torrents.len();
|
||||||
|
let managed_torrent = builder.build(error_span!("torrent", id = next_id))?;
|
||||||
let id = g.add_torrent(managed_torrent.clone());
|
let id = g.add_torrent(managed_torrent.clone());
|
||||||
(managed_torrent, id)
|
(managed_torrent, id)
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
let span = trace_span!("torrent", id = id);
|
let span = managed_torrent.info.span.clone();
|
||||||
let _ = span.enter();
|
let _ = span.enter();
|
||||||
managed_torrent
|
managed_torrent
|
||||||
.start(initial_peers, dht_peer_rx)
|
.start(initial_peers, dht_peer_rx)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use tracing::{debug, error, trace, Instrument};
|
use tracing::{debug, trace, Instrument};
|
||||||
|
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
span: tracing::Span,
|
span: tracing::Span,
|
||||||
fut: impl std::future::Future<Output = anyhow::Result<()>> + Send + 'static,
|
fut: impl std::future::Future<Output = anyhow::Result<()>> + Send + 'static,
|
||||||
) {
|
) -> tokio::task::JoinHandle<()> {
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
trace!("started");
|
trace!("started");
|
||||||
match fut.await {
|
match fut.await {
|
||||||
|
|
@ -11,12 +11,12 @@ pub fn spawn(
|
||||||
debug!("finished");
|
debug!("finished");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{:#}", e)
|
debug!("finished with error: {:#}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.instrument(span.or_current());
|
.instrument(span.or_current());
|
||||||
tokio::spawn(fut);
|
tokio::spawn(fut)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ impl TorrentStateInitializing {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_checked_bytes(&self) -> u64 {
|
||||||
|
self.checked_bytes
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn check(&self) -> anyhow::Result<TorrentStatePaused> {
|
pub async fn check(&self) -> anyhow::Result<TorrentStatePaused> {
|
||||||
let (files, filenames) = {
|
let (files, filenames) = {
|
||||||
let mut files =
|
let mut files =
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ use bencode::from_bytes;
|
||||||
use buffers::{ByteBuf, ByteString};
|
use buffers::{ByteBuf, ByteString};
|
||||||
use clone_to_owned::CloneToOwned;
|
use clone_to_owned::CloneToOwned;
|
||||||
use futures::{stream::FuturesUnordered, StreamExt};
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
|
use itertools::Itertools;
|
||||||
use librqbit_core::{
|
use librqbit_core::{
|
||||||
id20::Id20,
|
id20::Id20,
|
||||||
lengths::{ChunkInfo, Lengths, ValidPieceIndex},
|
lengths::{ChunkInfo, Lengths, ValidPieceIndex},
|
||||||
|
|
@ -79,7 +80,7 @@ use tokio::{
|
||||||
},
|
},
|
||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, info, span, trace, trace_span, warn, Level};
|
use tracing::{debug, error, error_span, info, trace, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -116,15 +117,42 @@ struct InflightPiece {
|
||||||
started: Instant,
|
started: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dummy_file() -> anyhow::Result<std::fs::File> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const DEVNULL: &str = "NUL";
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
const DEVNULL: &str = "/dev/null";
|
||||||
|
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.open(DEVNULL)
|
||||||
|
.with_context(|| format!("error opening {}", DEVNULL))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct TorrentStateLocked {
|
pub(crate) struct TorrentStateLocked {
|
||||||
// What chunks we have and need.
|
// What chunks we have and need.
|
||||||
pub(crate) chunks: ChunkTracker,
|
// If this is None, the torrent was paused, and this live state is useless, and needs to be dropped.
|
||||||
|
pub(crate) chunks: Option<ChunkTracker>,
|
||||||
|
|
||||||
// At a moment in time, we are expecting a piece from only one peer.
|
// At a moment in time, we are expecting a piece from only one peer.
|
||||||
// inflight_pieces stores this information.
|
// inflight_pieces stores this information.
|
||||||
inflight_pieces: HashMap<ValidPieceIndex, InflightPiece>,
|
inflight_pieces: HashMap<ValidPieceIndex, InflightPiece>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TorrentStateLocked {
|
||||||
|
pub(crate) fn get_chunks(&self) -> anyhow::Result<&ChunkTracker> {
|
||||||
|
self.chunks
|
||||||
|
.as_ref()
|
||||||
|
.context("chunk tracker empty, torrent was paused")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_chunks_mut(&mut self) -> anyhow::Result<&mut ChunkTracker> {
|
||||||
|
self.chunks
|
||||||
|
.as_mut()
|
||||||
|
.context("chunk tracker empty, torrent was paused")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TorrentStateOptions {
|
pub struct TorrentStateOptions {
|
||||||
pub peer_connect_timeout: Option<Duration>,
|
pub peer_connect_timeout: Option<Duration>,
|
||||||
|
|
@ -134,7 +162,8 @@ pub struct TorrentStateOptions {
|
||||||
pub struct TorrentStateLive {
|
pub struct TorrentStateLive {
|
||||||
peers: PeerStates,
|
peers: PeerStates,
|
||||||
meta: Arc<ManagedTorrentInfo>,
|
meta: Arc<ManagedTorrentInfo>,
|
||||||
locked: Arc<RwLock<TorrentStateLocked>>,
|
locked: RwLock<TorrentStateLocked>,
|
||||||
|
|
||||||
files: Vec<Arc<Mutex<File>>>,
|
files: Vec<Arc<Mutex<File>>>,
|
||||||
filenames: Vec<PathBuf>,
|
filenames: Vec<PathBuf>,
|
||||||
|
|
||||||
|
|
@ -153,6 +182,9 @@ pub struct TorrentStateLive {
|
||||||
|
|
||||||
finished_notify: Notify,
|
finished_notify: Notify,
|
||||||
|
|
||||||
|
cancel_tx: tokio::sync::watch::Sender<()>,
|
||||||
|
cancel_rx: tokio::sync::watch::Receiver<()>,
|
||||||
|
|
||||||
speed_estimator: SpeedEstimator,
|
speed_estimator: SpeedEstimator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,13 +198,15 @@ impl TorrentStateLive {
|
||||||
let needed_bytes = paused.info.lengths.total_length() - have_bytes;
|
let needed_bytes = paused.info.lengths.total_length() - have_bytes;
|
||||||
let lengths = *paused.chunk_tracker.get_lengths();
|
let lengths = *paused.chunk_tracker.get_lengths();
|
||||||
|
|
||||||
|
let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(());
|
||||||
|
|
||||||
let state = Arc::new(TorrentStateLive {
|
let state = Arc::new(TorrentStateLive {
|
||||||
meta: paused.info.clone(),
|
meta: paused.info.clone(),
|
||||||
peers: Default::default(),
|
peers: Default::default(),
|
||||||
locked: Arc::new(RwLock::new(TorrentStateLocked {
|
locked: RwLock::new(TorrentStateLocked {
|
||||||
chunks: paused.chunk_tracker,
|
chunks: Some(paused.chunk_tracker),
|
||||||
inflight_pieces: Default::default(),
|
inflight_pieces: Default::default(),
|
||||||
})),
|
}),
|
||||||
files: paused.files,
|
files: paused.files,
|
||||||
filenames: paused.filenames,
|
filenames: paused.filenames,
|
||||||
stats: AtomicStats {
|
stats: AtomicStats {
|
||||||
|
|
@ -182,46 +216,71 @@ impl TorrentStateLive {
|
||||||
needed_bytes,
|
needed_bytes,
|
||||||
have_plus_needed_bytes: needed_bytes + have_bytes,
|
have_plus_needed_bytes: needed_bytes + have_bytes,
|
||||||
lengths,
|
lengths,
|
||||||
|
|
||||||
peer_semaphore: Semaphore::new(128),
|
peer_semaphore: Semaphore::new(128),
|
||||||
peer_queue_tx,
|
peer_queue_tx,
|
||||||
finished_notify: Notify::new(),
|
finished_notify: Notify::new(),
|
||||||
speed_estimator,
|
speed_estimator,
|
||||||
|
cancel_rx,
|
||||||
|
cancel_tx,
|
||||||
});
|
});
|
||||||
|
|
||||||
for tracker in state.meta.trackers.iter() {
|
for tracker in state.meta.trackers.iter() {
|
||||||
spawn(
|
state.spawn(
|
||||||
trace_span!("tracker_monitor", url = tracker.to_string()),
|
error_span!(parent: state.meta.span.clone(), "tracker_monitor", url = tracker.to_string()),
|
||||||
state.clone().task_single_tracker_monitor(tracker.clone()),
|
state.clone().task_single_tracker_monitor(tracker.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn(span!(Level::ERROR, "speed_estimator_updater"), {
|
state.spawn(
|
||||||
let state = state.clone();
|
error_span!(parent: state.meta.span.clone(), "speed_estimator_updater"),
|
||||||
async move {
|
{
|
||||||
loop {
|
let state = Arc::downgrade(&state);
|
||||||
let stats = state.stats_snapshot();
|
async move {
|
||||||
let fetched = stats.fetched_bytes;
|
loop {
|
||||||
let needed = state.initially_needed();
|
let state = match state.upgrade() {
|
||||||
// fetched can be too high in theory, so for safety make sure that it doesn't wrap around u64.
|
Some(state) => state,
|
||||||
let remaining = needed
|
None => return Ok(()),
|
||||||
.wrapping_sub(fetched)
|
};
|
||||||
.min(needed - stats.downloaded_and_checked_bytes);
|
let stats = state.stats_snapshot();
|
||||||
state
|
let fetched = stats.fetched_bytes;
|
||||||
.speed_estimator
|
let needed = state.initially_needed();
|
||||||
.add_snapshot(fetched, remaining, Instant::now());
|
// fetched can be too high in theory, so for safety make sure that it doesn't wrap around u64.
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
let remaining = needed
|
||||||
|
.wrapping_sub(fetched)
|
||||||
|
.min(needed - stats.downloaded_and_checked_bytes);
|
||||||
|
state
|
||||||
|
.speed_estimator
|
||||||
|
.add_snapshot(fetched, remaining, Instant::now());
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
spawn(
|
state.spawn(
|
||||||
span!(Level::ERROR, "peer_adder"),
|
error_span!(parent: state.meta.span.clone(), "peer_adder"),
|
||||||
state.clone().task_peer_adder(peer_queue_rx),
|
state.clone().task_peer_adder(peer_queue_rx),
|
||||||
);
|
);
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn(
|
||||||
|
&self,
|
||||||
|
span: tracing::Span,
|
||||||
|
fut: impl std::future::Future<Output = anyhow::Result<()>> + Send + 'static,
|
||||||
|
) {
|
||||||
|
let mut cancel_rx = self.cancel_rx.clone();
|
||||||
|
spawn(span, async move {
|
||||||
|
tokio::select! {
|
||||||
|
r = fut => r,
|
||||||
|
_ = cancel_rx.changed() => {
|
||||||
|
error!("canceled");
|
||||||
|
bail!("canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn speed_estimator(&self) -> &SpeedEstimator {
|
pub fn speed_estimator(&self) -> &SpeedEstimator {
|
||||||
&self.speed_estimator
|
&self.speed_estimator
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +300,7 @@ impl TorrentStateLive {
|
||||||
let response = from_bytes::<TrackerResponse>(&bytes)?;
|
let response = from_bytes::<TrackerResponse>(&bytes)?;
|
||||||
|
|
||||||
for peer in response.peers.iter_sockaddrs() {
|
for peer in response.peers.iter_sockaddrs() {
|
||||||
self.add_peer_if_not_seen(peer);
|
self.add_peer_if_not_seen(peer)?;
|
||||||
}
|
}
|
||||||
Ok(response.interval)
|
Ok(response.interval)
|
||||||
}
|
}
|
||||||
|
|
@ -345,11 +404,11 @@ impl TorrentStateLive {
|
||||||
match res {
|
match res {
|
||||||
// We disconnected the peer ourselves as we don't need it
|
// We disconnected the peer ourselves as we don't need it
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
handler.on_peer_died(None);
|
handler.on_peer_died(None)?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("error managing peer: {:#}", e);
|
debug!("error managing peer: {:#}", e);
|
||||||
handler.on_peer_died(Some(e));
|
handler.on_peer_died(Some(e))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
|
|
@ -361,17 +420,17 @@ impl TorrentStateLive {
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let state = self;
|
let state = self;
|
||||||
loop {
|
loop {
|
||||||
let addr = peer_queue_rx.recv().await.unwrap();
|
let addr = peer_queue_rx.recv().await.context("torrent closed")?;
|
||||||
if state.is_finished() {
|
if state.is_finished() {
|
||||||
debug!("ignoring peer {} as we are finished", addr);
|
debug!("ignoring peer {} as we are finished", addr);
|
||||||
state.peers.mark_peer_not_needed(addr);
|
state.peers.mark_peer_not_needed(addr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let permit = state.peer_semaphore.acquire().await.unwrap();
|
let permit = state.peer_semaphore.acquire().await?;
|
||||||
permit.forget();
|
permit.forget();
|
||||||
spawn(
|
state.spawn(
|
||||||
span!(parent: None, Level::ERROR, "manage_peer", peer = addr.to_string()),
|
error_span!(parent: state.meta.span.clone(), "manage_peer", peer = addr.to_string()),
|
||||||
state.clone().task_manage_peer(addr),
|
state.clone().task_manage_peer(addr),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -406,23 +465,28 @@ impl TorrentStateLive {
|
||||||
TimedExistence::new(timeit(reason, || self.locked.write()), reason)
|
TimedExistence::new(timeit(reason, || self.locked.write()), reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_next_needed_piece(&self, peer_handle: PeerHandle) -> Option<ValidPieceIndex> {
|
fn get_next_needed_piece(
|
||||||
|
&self,
|
||||||
|
peer_handle: PeerHandle,
|
||||||
|
) -> anyhow::Result<Option<ValidPieceIndex>> {
|
||||||
self.peers
|
self.peers
|
||||||
.with_live_mut(peer_handle, "l(get_next_needed_piece)", |live| {
|
.with_live_mut(peer_handle, "l(get_next_needed_piece)", |live| {
|
||||||
let g = self.lock_read("g(get_next_needed_piece)");
|
let g = self.lock_read("g(get_next_needed_piece)");
|
||||||
let bf = &live.bitfield;
|
let bf = &live.bitfield;
|
||||||
for n in g.chunks.iter_needed_pieces() {
|
for n in g.get_chunks()?.iter_needed_pieces() {
|
||||||
if bf.get(n).map(|v| *v) == Some(true) {
|
if bf.get(n).map(|v| *v) == Some(true) {
|
||||||
// in theory it should be safe without validation, but whatever.
|
// in theory it should be safe without validation, but whatever.
|
||||||
return self.lengths.validate_piece_index(n as u32);
|
return Ok(self.lengths.validate_piece_index(n as u32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
Ok(None)
|
||||||
})?
|
})
|
||||||
|
.transpose()
|
||||||
|
.map(|r| r.flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn am_i_interested_in_peer(&self, handle: PeerHandle) -> bool {
|
fn am_i_interested_in_peer(&self, handle: PeerHandle) -> bool {
|
||||||
self.get_next_needed_piece(handle).is_some()
|
matches!(self.get_next_needed_piece(handle), Ok(Some(_)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_peer_live(&self, handle: PeerHandle, h: Handshake) {
|
fn set_peer_live(&self, handle: PeerHandle, h: Handshake) {
|
||||||
|
|
@ -449,6 +513,10 @@ impl TorrentStateLive {
|
||||||
.load(Ordering::Acquire)
|
.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_have_bytes(&self) -> u64 {
|
||||||
|
self.stats.have_bytes.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_finished(&self) -> bool {
|
pub fn is_finished(&self) -> bool {
|
||||||
self.get_left_to_download_bytes() == 0
|
self.get_left_to_download_bytes() == 0
|
||||||
}
|
}
|
||||||
|
|
@ -498,9 +566,11 @@ impl TorrentStateLive {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut unordered: FuturesUnordered<_> = futures.into_iter().collect();
|
let mut unordered: FuturesUnordered<_> = futures.into_iter().collect();
|
||||||
spawn(
|
|
||||||
span!(
|
// We don't want to remember this task as there may be too many.
|
||||||
Level::ERROR,
|
self.spawn(
|
||||||
|
error_span!(
|
||||||
|
parent: self.meta.span.clone(),
|
||||||
"transmit_haves",
|
"transmit_haves",
|
||||||
piece = index.get(),
|
piece = index.get(),
|
||||||
count = unordered.len()
|
count = unordered.len()
|
||||||
|
|
@ -512,14 +582,14 @@ impl TorrentStateLive {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_peer_if_not_seen(&self, addr: SocketAddr) -> bool {
|
pub(crate) fn add_peer_if_not_seen(&self, addr: SocketAddr) -> anyhow::Result<bool> {
|
||||||
match self.peers.add_if_not_seen(addr) {
|
match self.peers.add_if_not_seen(addr) {
|
||||||
Some(handle) => handle,
|
Some(handle) => handle,
|
||||||
None => return false,
|
None => return Ok(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.peer_queue_tx.send(addr).unwrap();
|
self.peer_queue_tx.send(addr)?;
|
||||||
true
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stats_snapshot(&self) -> StatsSnapshot {
|
pub fn stats_snapshot(&self) -> StatsSnapshot {
|
||||||
|
|
@ -560,7 +630,34 @@ impl TorrentStateLive {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause(&self) -> anyhow::Result<TorrentStatePaused> {
|
pub fn pause(&self) -> anyhow::Result<TorrentStatePaused> {
|
||||||
bail!("pause not implemented yet")
|
let _ = self.cancel_tx.send(());
|
||||||
|
|
||||||
|
let mut g = self.locked.write();
|
||||||
|
|
||||||
|
let files = self
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.map(|f| {
|
||||||
|
let mut f = f.lock();
|
||||||
|
let dummy = dummy_file()?;
|
||||||
|
let f = std::mem::replace(&mut *f, dummy);
|
||||||
|
Ok::<_, anyhow::Error>(Arc::new(Mutex::new(f)))
|
||||||
|
})
|
||||||
|
.try_collect()?;
|
||||||
|
|
||||||
|
let filenames = self.filenames.clone();
|
||||||
|
|
||||||
|
// g.chunks;
|
||||||
|
Ok(TorrentStatePaused {
|
||||||
|
info: self.meta.clone(),
|
||||||
|
files,
|
||||||
|
filenames,
|
||||||
|
chunk_tracker: g
|
||||||
|
.chunks
|
||||||
|
.take()
|
||||||
|
.context("bug: pausing already paused torrent")?,
|
||||||
|
have_bytes: self.get_have_bytes(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,16 +730,12 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_have_bytes(&self) -> u64 {
|
fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec<u8>) -> anyhow::Result<usize> {
|
||||||
self.state.stats.have_bytes.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_bitfield_message_to_buf(&self, buf: &mut Vec<u8>) -> Option<usize> {
|
|
||||||
let g = self.state.lock_read("serialize_bitfield_message_to_buf");
|
let g = self.state.lock_read("serialize_bitfield_message_to_buf");
|
||||||
let msg = Message::Bitfield(ByteBuf(g.chunks.get_have_pieces().as_raw_slice()));
|
let msg = Message::Bitfield(ByteBuf(g.get_chunks()?.get_have_pieces().as_raw_slice()));
|
||||||
let len = msg.serialize(buf, None).unwrap();
|
let len = msg.serialize(buf, None)?;
|
||||||
debug!("sending: {:?}, length={}", &msg, len);
|
debug!("sending: {:?}, length={}", &msg, len);
|
||||||
Some(len)
|
Ok(len)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> {
|
fn on_handshake(&self, handshake: Handshake) -> anyhow::Result<()> {
|
||||||
|
|
@ -664,10 +757,14 @@ impl<'a> PeerConnectionHandler for &'a PeerHandler {
|
||||||
fn on_extended_handshake(&self, _: &ExtendedHandshake<ByteBuf>) -> anyhow::Result<()> {
|
fn on_extended_handshake(&self, _: &ExtendedHandshake<ByteBuf>) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_have_bytes(&self) -> u64 {
|
||||||
|
self.state.get_have_bytes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeerHandler {
|
impl PeerHandler {
|
||||||
fn on_peer_died(self, error: Option<anyhow::Error>) {
|
fn on_peer_died(self, error: Option<anyhow::Error>) -> anyhow::Result<()> {
|
||||||
let peers = &self.state.peers;
|
let peers = &self.state.peers;
|
||||||
let pstats = &peers.stats;
|
let pstats = &peers.stats;
|
||||||
let handle = self.addr;
|
let handle = self.addr;
|
||||||
|
|
@ -675,7 +772,7 @@ impl PeerHandler {
|
||||||
Some(peer) => TimedExistence::new(peer, "on_peer_died"),
|
Some(peer) => TimedExistence::new(peer, "on_peer_died"),
|
||||||
None => {
|
None => {
|
||||||
warn!("bug: peer not found in table. Forgetting it forever");
|
warn!("bug: peer not found in table. Forgetting it forever");
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let prev = pe.value_mut().state.take(pstats);
|
let prev = pe.value_mut().state.take(pstats);
|
||||||
|
|
@ -690,20 +787,21 @@ impl PeerHandler {
|
||||||
req.piece.get(),
|
req.piece.get(),
|
||||||
req.chunk
|
req.chunk
|
||||||
);
|
);
|
||||||
g.chunks.mark_chunk_request_cancelled(req.piece, req.chunk);
|
g.get_chunks_mut()?
|
||||||
|
.mark_chunk_request_cancelled(req.piece, req.chunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PeerState::NotNeeded => {
|
PeerState::NotNeeded => {
|
||||||
// Restore it as std::mem::take() replaced it above.
|
// Restore it as std::mem::take() replaced it above.
|
||||||
pe.value_mut().state.set(PeerState::NotNeeded, pstats);
|
pe.value_mut().state.set(PeerState::NotNeeded, pstats);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
s @ PeerState::Queued | s @ PeerState::Dead => {
|
s @ PeerState::Queued | s @ PeerState::Dead => {
|
||||||
warn!("bug: peer was in a wrong state {s:?}, ignoring it forever");
|
warn!("bug: peer was in a wrong state {s:?}, ignoring it forever");
|
||||||
// Prevent deadlocks.
|
// Prevent deadlocks.
|
||||||
drop(pe);
|
drop(pe);
|
||||||
self.state.peers.drop_peer(handle);
|
self.state.peers.drop_peer(handle);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -712,7 +810,7 @@ impl PeerHandler {
|
||||||
None => {
|
None => {
|
||||||
debug!("peer died without errors, not re-queueing");
|
debug!("peer died without errors, not re-queueing");
|
||||||
pe.value_mut().state.set(PeerState::NotNeeded, pstats);
|
pe.value_mut().state.set(PeerState::NotNeeded, pstats);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -721,7 +819,7 @@ impl PeerHandler {
|
||||||
if self.state.is_finished() {
|
if self.state.is_finished() {
|
||||||
debug!("torrent finished, not re-queueing");
|
debug!("torrent finished, not re-queueing");
|
||||||
pe.value_mut().state.set(PeerState::NotNeeded, pstats);
|
pe.value_mut().state.set(PeerState::NotNeeded, pstats);
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
pe.value_mut().state.set(PeerState::Dead, pstats);
|
pe.value_mut().state.set(PeerState::Dead, pstats);
|
||||||
|
|
@ -732,10 +830,9 @@ impl PeerHandler {
|
||||||
drop(pe);
|
drop(pe);
|
||||||
|
|
||||||
if let Some(dur) = backoff {
|
if let Some(dur) = backoff {
|
||||||
spawn(
|
self.state.clone().spawn(
|
||||||
span!(
|
error_span!(
|
||||||
parent: None,
|
parent: self.state.meta.span.clone(),
|
||||||
Level::ERROR,
|
|
||||||
"wait_for_peer",
|
"wait_for_peer",
|
||||||
peer = handle.to_string(),
|
peer = handle.to_string(),
|
||||||
duration = format!("{dur:?}")
|
duration = format!("{dur:?}")
|
||||||
|
|
@ -764,31 +861,35 @@ impl PeerHandler {
|
||||||
} else {
|
} else {
|
||||||
debug!("dropping peer, backoff exhausted");
|
debug!("dropping peer, backoff exhausted");
|
||||||
self.state.peers.drop_peer(handle);
|
self.state.peers.drop_peer(handle);
|
||||||
}
|
};
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reserve_next_needed_piece(&self) -> Option<ValidPieceIndex> {
|
fn reserve_next_needed_piece(&self) -> anyhow::Result<Option<ValidPieceIndex>> {
|
||||||
// TODO: locking one inside the other in different order results in deadlocks.
|
// TODO: locking one inside the other in different order results in deadlocks.
|
||||||
self.state
|
self.state
|
||||||
.peers
|
.peers
|
||||||
.with_live_mut(self.addr, "reserve_next_needed_piece", |live| {
|
.with_live_mut(self.addr, "reserve_next_needed_piece", |live| {
|
||||||
if self.locked.read().i_am_choked {
|
if self.locked.read().i_am_choked {
|
||||||
debug!("we are choked, can't reserve next piece");
|
debug!("we are choked, can't reserve next piece");
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let mut g = self.state.lock_write("reserve_next_needed_piece");
|
let mut g = self.state.lock_write("reserve_next_needed_piece");
|
||||||
|
|
||||||
let n = {
|
let n = {
|
||||||
let mut n_opt = None;
|
let mut n_opt = None;
|
||||||
let bf = &live.bitfield;
|
let bf = &live.bitfield;
|
||||||
for n in g.chunks.iter_needed_pieces() {
|
for n in g.get_chunks()?.iter_needed_pieces() {
|
||||||
if bf.get(n).map(|v| *v) == Some(true) {
|
if bf.get(n).map(|v| *v) == Some(true) {
|
||||||
n_opt = Some(n);
|
n_opt = Some(n);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state.lengths.validate_piece_index(n_opt? as u32)?
|
self.state
|
||||||
|
.lengths
|
||||||
|
.validate_piece_index(n_opt.context("invalid n_opt")? as u32)
|
||||||
|
.context("invalid piece")?
|
||||||
};
|
};
|
||||||
g.inflight_pieces.insert(
|
g.inflight_pieces.insert(
|
||||||
n,
|
n,
|
||||||
|
|
@ -797,10 +898,11 @@ impl PeerHandler {
|
||||||
started: Instant::now(),
|
started: Instant::now(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
g.chunks.reserve_needed_piece(n);
|
g.get_chunks_mut()?.reserve_needed_piece(n);
|
||||||
Some(n)
|
Ok(Some(n))
|
||||||
})
|
})
|
||||||
.flatten()
|
.transpose()
|
||||||
|
.map(|r| r.flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_steal_old_slow_piece(&self, threshold: f64) -> Option<ValidPieceIndex> {
|
fn try_steal_old_slow_piece(&self, threshold: f64) -> Option<ValidPieceIndex> {
|
||||||
|
|
@ -865,7 +967,7 @@ impl PeerHandler {
|
||||||
if !self
|
if !self
|
||||||
.state
|
.state
|
||||||
.lock_read("is_chunk_ready_to_upload")
|
.lock_read("is_chunk_ready_to_upload")
|
||||||
.chunks
|
.get_chunks()?
|
||||||
.is_chunk_ready_to_upload(&chunk_info)
|
.is_chunk_ready_to_upload(&chunk_info)
|
||||||
{
|
{
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
|
|
@ -965,7 +1067,7 @@ impl PeerHandler {
|
||||||
// Afterwards means we are close to completion, try stealing more aggressively.
|
// Afterwards means we are close to completion, try stealing more aggressively.
|
||||||
let next = match self
|
let next = match self
|
||||||
.try_steal_old_slow_piece(10.)
|
.try_steal_old_slow_piece(10.)
|
||||||
.or_else(|| self.reserve_next_needed_piece())
|
.or_else(|| self.reserve_next_needed_piece().ok().flatten())
|
||||||
.or_else(|| self.try_steal_old_slow_piece(2.))
|
.or_else(|| self.try_steal_old_slow_piece(2.))
|
||||||
{
|
{
|
||||||
Some(next) => next,
|
Some(next) => next,
|
||||||
|
|
@ -1037,18 +1139,6 @@ impl PeerHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reopen_read_only(&self) -> anyhow::Result<()> {
|
fn reopen_read_only(&self) -> anyhow::Result<()> {
|
||||||
fn dummy_file() -> anyhow::Result<std::fs::File> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
const DEVNULL: &str = "NUL";
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
const DEVNULL: &str = "/dev/null";
|
|
||||||
|
|
||||||
std::fs::OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.open(DEVNULL)
|
|
||||||
.with_context(|| format!("error opening {}", DEVNULL))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock exclusive just in case to ensure in-flight operations finish.??
|
// Lock exclusive just in case to ensure in-flight operations finish.??
|
||||||
let _guard = self.state.lock_write("reopen_read_only");
|
let _guard = self.state.lock_write("reopen_read_only");
|
||||||
|
|
||||||
|
|
@ -1140,7 +1230,7 @@ impl PeerHandler {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match g.chunks.mark_chunk_downloaded(&piece) {
|
match g.get_chunks_mut()?.mark_chunk_downloaded(&piece) {
|
||||||
Some(ChunkMarkingResult::Completed) => {
|
Some(ChunkMarkingResult::Completed) => {
|
||||||
debug!("piece={} done, will write and checksum", piece.index,);
|
debug!("piece={} done, will write and checksum", piece.index,);
|
||||||
// This will prevent others from stealing it.
|
// This will prevent others from stealing it.
|
||||||
|
|
@ -1204,7 +1294,8 @@ impl PeerHandler {
|
||||||
true => {
|
true => {
|
||||||
{
|
{
|
||||||
let mut g = self.state.lock_write("mark_piece_downloaded");
|
let mut g = self.state.lock_write("mark_piece_downloaded");
|
||||||
g.chunks.mark_piece_downloaded(chunk_info.piece_index);
|
g.get_chunks_mut()?
|
||||||
|
.mark_piece_downloaded(chunk_info.piece_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global piece counters.
|
// Global piece counters.
|
||||||
|
|
@ -1256,7 +1347,7 @@ impl PeerHandler {
|
||||||
warn!("checksum for piece={} did not validate", index,);
|
warn!("checksum for piece={} did not validate", index,);
|
||||||
self.state
|
self.state
|
||||||
.lock_write("mark_piece_broken")
|
.lock_write("mark_piece_broken")
|
||||||
.chunks
|
.get_chunks_mut()?
|
||||||
.mark_piece_broken(chunk_info.piece_index);
|
.mark_piece_broken(chunk_info.piece_index);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub use live::*;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tracing::trace_span;
|
use tracing::error_span;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::chunk_tracker::ChunkTracker;
|
use crate::chunk_tracker::ChunkTracker;
|
||||||
|
|
@ -75,6 +75,7 @@ pub struct ManagedTorrentInfo {
|
||||||
pub trackers: Vec<Url>,
|
pub trackers: Vec<Url>,
|
||||||
pub peer_id: Id20,
|
pub peer_id: Id20,
|
||||||
pub lengths: Lengths,
|
pub lengths: Lengths,
|
||||||
|
pub span: tracing::Span,
|
||||||
pub(crate) options: ManagedTorrentOptions,
|
pub(crate) options: ManagedTorrentOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,6 +90,10 @@ impl ManagedTorrent {
|
||||||
&self.info
|
&self.info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_total_bytes(&self) -> u64 {
|
||||||
|
self.info.lengths.total_length()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn info_hash(&self) -> Id20 {
|
pub fn info_hash(&self) -> Id20 {
|
||||||
self.info.info_hash
|
self.info.info_hash
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +110,7 @@ impl ManagedTorrent {
|
||||||
let g = self.locked.read();
|
let g = self.locked.read();
|
||||||
match &g.state {
|
match &g.state {
|
||||||
ManagedTorrentState::Paused(p) => Ok(f(&p.chunk_tracker)),
|
ManagedTorrentState::Paused(p) => Ok(f(&p.chunk_tracker)),
|
||||||
ManagedTorrentState::Live(l) => Ok(f(&l.lock_read("chunk_tracker").chunks)),
|
ManagedTorrentState::Live(l) => Ok(f(l.lock_read("chunk_tracker").get_chunks()?)),
|
||||||
_ => bail!("no chunk tracker, torrent neither paused nor live"),
|
_ => bail!("no chunk tracker, torrent neither paused nor live"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -131,42 +136,51 @@ impl ManagedTorrent {
|
||||||
ManagedTorrentState::Initializing(init) => {
|
ManagedTorrentState::Initializing(init) => {
|
||||||
let init = init.clone();
|
let init = init.clone();
|
||||||
let t = self.clone();
|
let t = self.clone();
|
||||||
spawn(trace_span!("initialize_and_start"), async move {
|
let span = self.info.span.clone();
|
||||||
match init.check().await {
|
spawn(
|
||||||
Ok(paused) => {
|
error_span!(parent: span.clone(), "initialize_and_start"),
|
||||||
let live = TorrentStateLive::new(paused);
|
async move {
|
||||||
t.locked.write().state = ManagedTorrentState::Live(live.clone());
|
match init.check().await {
|
||||||
|
Ok(paused) => {
|
||||||
|
let live = TorrentStateLive::new(paused);
|
||||||
|
t.locked.write().state = ManagedTorrentState::Live(live.clone());
|
||||||
|
|
||||||
let live = Arc::downgrade(&live);
|
let live = Arc::downgrade(&live);
|
||||||
spawn(trace_span!("peer_adder"), async move {
|
spawn(
|
||||||
{
|
error_span!(parent: span.clone(), "external_peer_adder"),
|
||||||
let live: Arc<TorrentStateLive> =
|
async move {
|
||||||
live.upgrade().context("no longer live")?;
|
{
|
||||||
for peer in initial_peers {
|
let live: Arc<TorrentStateLive> =
|
||||||
live.add_peer_if_not_seen(peer);
|
live.upgrade().context("no longer live")?;
|
||||||
}
|
for peer in initial_peers {
|
||||||
}
|
live.add_peer_if_not_seen(peer)
|
||||||
|
.context("torrent closed")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(mut peer_rx) = peer_rx {
|
if let Some(mut peer_rx) = peer_rx {
|
||||||
while let Some(peer) = peer_rx.next().await {
|
while let Some(peer) = peer_rx.next().await {
|
||||||
live.upgrade()
|
live.upgrade()
|
||||||
.context("no longer live")?
|
.context("no longer live")?
|
||||||
.add_peer_if_not_seen(peer);
|
.add_peer_if_not_seen(peer)
|
||||||
}
|
.context("torrent closed")?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
}
|
||||||
|
Err(err) => {
|
||||||
Ok(())
|
let result = anyhow::anyhow!("{:?}", err);
|
||||||
|
t.locked.write().state = ManagedTorrentState::Error(err);
|
||||||
|
Err(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
},
|
||||||
let result = anyhow::anyhow!("{:?}", err);
|
);
|
||||||
t.locked.write().state = ManagedTorrentState::Error(err);
|
|
||||||
Err(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
ManagedTorrentState::Paused(_) => {
|
ManagedTorrentState::Paused(_) => {
|
||||||
|
|
@ -288,9 +302,10 @@ impl ManagedTorrentBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build(self) -> anyhow::Result<ManagedTorrentHandle> {
|
pub(crate) fn build(self, span: tracing::Span) -> anyhow::Result<ManagedTorrentHandle> {
|
||||||
let lengths = Lengths::from_torrent(&self.info)?;
|
let lengths = Lengths::from_torrent(&self.info)?;
|
||||||
let info = Arc::new(ManagedTorrentInfo {
|
let info = Arc::new(ManagedTorrentInfo {
|
||||||
|
span,
|
||||||
info: self.info,
|
info: self.info,
|
||||||
info_hash: self.info_hash,
|
info_hash: self.info_hash,
|
||||||
out_dir: self.output_folder,
|
out_dir: self.output_folder,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ interface ContextType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppContext = createContext<ContextType>(null);
|
const AppContext = createContext<ContextType>(null);
|
||||||
|
const RefreshTorrentStatsContext = createContext<{ refresh: () => void }>(null);
|
||||||
|
|
||||||
const IconButton: React.FC<{
|
const IconButton: React.FC<{
|
||||||
className: string,
|
className: string,
|
||||||
|
|
@ -39,6 +40,8 @@ const DeleteTorrentModal = ({ id, show, onHide }) => {
|
||||||
const [error, setError] = useState<Error>(null);
|
const [error, setError] = useState<Error>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
const refreshCtx = useContext(RefreshTorrentStatsContext);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
setDeleteFiles(false);
|
setDeleteFiles(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -52,6 +55,7 @@ const DeleteTorrentModal = ({ id, show, onHide }) => {
|
||||||
const call = deleteFiles ? API.delete : API.forget;
|
const call = deleteFiles ? API.delete : API.forget;
|
||||||
|
|
||||||
call(id).then(() => {
|
call(id).then(() => {
|
||||||
|
refreshCtx.refresh();
|
||||||
close();
|
close();
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
setError({
|
setError({
|
||||||
|
|
@ -99,6 +103,8 @@ const TorrentActions: React.FC<{
|
||||||
let [disabled, setDisabled] = useState<boolean>(false);
|
let [disabled, setDisabled] = useState<boolean>(false);
|
||||||
let [deleting, setDeleting] = useState<boolean>(false);
|
let [deleting, setDeleting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
let refreshCtx = useContext(RefreshTorrentStatsContext);
|
||||||
|
|
||||||
const canPause = state == 'live';
|
const canPause = state == 'live';
|
||||||
const canUnpause = state == 'paused';
|
const canUnpause = state == 'paused';
|
||||||
|
|
||||||
|
|
@ -106,22 +112,22 @@ const TorrentActions: React.FC<{
|
||||||
|
|
||||||
const unpause = () => {
|
const unpause = () => {
|
||||||
setDisabled(true);
|
setDisabled(true);
|
||||||
API.start(id).finally(() => setDisabled(false)).catch((e) => {
|
API.start(id).then(() => { refreshCtx.refresh() }, (e) => {
|
||||||
ctx.setCloseableError({
|
ctx.setCloseableError({
|
||||||
text: `Error starting torrent id=${id}`,
|
text: `Error starting torrent id=${id}`,
|
||||||
details: e,
|
details: e,
|
||||||
});
|
});
|
||||||
})
|
}).finally(() => setDisabled(false))
|
||||||
};
|
};
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
setDisabled(true);
|
setDisabled(true);
|
||||||
API.pause(id).finally(() => setDisabled(false)).catch((e) => {
|
API.pause(id).then(() => { refreshCtx.refresh() }, (e) => {
|
||||||
ctx.setCloseableError({
|
ctx.setCloseableError({
|
||||||
text: `Error pausing torrent id=${id}`,
|
text: `Error pausing torrent id=${id}`,
|
||||||
details: e,
|
details: e,
|
||||||
});
|
});
|
||||||
})
|
}).finally(() => setDisabled(false))
|
||||||
};
|
};
|
||||||
|
|
||||||
const startDeleting = () => {
|
const startDeleting = () => {
|
||||||
|
|
@ -241,21 +247,34 @@ const Torrent = ({ id, torrent }) => {
|
||||||
}
|
}
|
||||||
}, [detailsResponse]);
|
}, [detailsResponse]);
|
||||||
|
|
||||||
|
const refreshStats = () => API.getTorrentStats(torrent.id).then((stats) => {
|
||||||
|
updateStatsResponse(stats);
|
||||||
|
return stats;
|
||||||
|
});
|
||||||
|
|
||||||
// Update stats once then forever.
|
// Update stats once then forever.
|
||||||
useEffect(() => customSetInterval((async () => {
|
useEffect(() => customSetInterval((async () => {
|
||||||
const errorInterval = 10000;
|
const errorInterval = 10000;
|
||||||
const liveInterval = 500;
|
const liveInterval = 1000;
|
||||||
const finishedInterval = 5000;
|
const finishedInterval = 10000;
|
||||||
|
const nonLiveInterval = 10000;
|
||||||
|
|
||||||
return API.getTorrentStats(torrent.id).then((stats) => {
|
return refreshStats().then((stats) => {
|
||||||
updateStatsResponse(stats);
|
if (stats.finished) {
|
||||||
return torrentIsDone(stats) ? finishedInterval : liveInterval;
|
return finishedInterval;
|
||||||
|
}
|
||||||
|
if (stats.state == STATE_INITIALIZING || stats.state == STATE_LIVE) {
|
||||||
|
return liveInterval;
|
||||||
|
}
|
||||||
|
return nonLiveInterval;
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
return errorInterval;
|
return errorInterval;
|
||||||
});
|
});
|
||||||
}), 0), []);
|
}), 0), []);
|
||||||
|
|
||||||
return <TorrentRow id={id} detailsResponse={detailsResponse} statsResponse={statsResponse} />
|
return <RefreshTorrentStatsContext.Provider value={{ refresh: refreshStats }}>
|
||||||
|
<TorrentRow id={id} detailsResponse={detailsResponse} statsResponse={statsResponse} />
|
||||||
|
</RefreshTorrentStatsContext.Provider >
|
||||||
}
|
}
|
||||||
|
|
||||||
const TorrentsList = (props: { torrents: Array<TorrentId>, loading: boolean }) => {
|
const TorrentsList = (props: { torrents: Array<TorrentId>, loading: boolean }) => {
|
||||||
|
|
@ -541,10 +560,6 @@ const RootContent = (props: { closeableError: ErrorDetails, otherError: ErrorDet
|
||||||
</Container>
|
</Container>
|
||||||
};
|
};
|
||||||
|
|
||||||
function torrentIsDone(stats: TorrentStats): boolean {
|
|
||||||
return stats.finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,12 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()>
|
||||||
for (idx, torrent) in torrents {
|
for (idx, torrent) in torrents {
|
||||||
let live = torrent.with_state(|s| {
|
let live = torrent.with_state(|s| {
|
||||||
match s {
|
match s {
|
||||||
ManagedTorrentState::Initializing(_) => info!("[{}] initializing", idx),
|
ManagedTorrentState::Initializing(i) => {
|
||||||
|
let total = torrent.get_total_bytes();
|
||||||
|
let progress = i.get_checked_bytes();
|
||||||
|
let pct = (progress as f64 / total as f64) * 100f64;
|
||||||
|
info!("[{}] initializing {:.2}%", idx, pct)
|
||||||
|
},
|
||||||
ManagedTorrentState::Live(h) => return Some(h.clone()),
|
ManagedTorrentState::Live(h) => return Some(h.clone()),
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue