Every reply to a peer's search claimed a free upload slot, no speed and an empty queue, whatever the sharer was doing, and searchers rank sources on exactly those fields. A reply now says whether a slot is free, the bytes per second of the last completed upload, and how many uploads are waiting.
213 lines
7.3 KiB
Rust
213 lines
7.3 KiB
Rust
use super::{
|
|
ActiveUpload, Arc, Client, ClientContext, DownloadStatus, PeerMessage,
|
|
RwLock, RwLockExt, collect_failed_tokens, error, next_upload_token, thread,
|
|
};
|
|
use crate::types::UploadStatus;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64};
|
|
|
|
impl Client {
|
|
/// Whether the server listed `username` as privileged. Privileged peers are
|
|
/// served before everyone else.
|
|
#[must_use]
|
|
pub fn is_privileged(&self, username: &str) -> bool {
|
|
self.context
|
|
.read_safe()
|
|
.is_ok_and(|ctx| ctx.is_privileged(username))
|
|
}
|
|
|
|
/// Ask the server how much of our own privilege time is left (code 92). The
|
|
/// answer arrives asynchronously; read it with
|
|
/// [`Client::own_privilege_seconds`].
|
|
///
|
|
/// # Errors
|
|
/// [`crate::error::SoulseekRs::NotConnected`] without a server connection.
|
|
pub fn check_privileges(&self) -> crate::error::Result<()> {
|
|
let Some(handle) = &self.server_handle else {
|
|
return Err(crate::error::SoulseekRs::NotConnected);
|
|
};
|
|
let _ = handle.send(super::ServerMessage::CheckPrivileges);
|
|
Ok(())
|
|
}
|
|
|
|
/// Seconds of our own privileges left, or `None` until the server has
|
|
/// answered a [`Client::check_privileges`].
|
|
#[must_use]
|
|
pub fn own_privilege_seconds(&self) -> Option<u32> {
|
|
self.context
|
|
.read_safe()
|
|
.ok()
|
|
.and_then(|ctx| ctx.own_privileges)
|
|
}
|
|
|
|
/// Queued-upload states recorded since the last call, for a caller that
|
|
/// samples [`Client::uploads`] and would otherwise miss a peer that queued
|
|
/// and was served between two samples.
|
|
#[must_use]
|
|
pub fn take_upload_events(&self) -> Vec<crate::types::UploadInfo> {
|
|
self.context
|
|
.write_safe()
|
|
.map(|mut ctx| ctx.take_upload_events())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// How many uploads run at once. Lowering it does not interrupt transfers
|
|
/// already in flight; the queue simply refills more slowly.
|
|
pub fn set_upload_slots(&self, slots: usize) {
|
|
if let Ok(mut ctx) = self.context.write_safe() {
|
|
ctx.upload_slots = slots.max(1);
|
|
}
|
|
}
|
|
|
|
/// Drop everything `username` was queued or offered, then hand their slot to
|
|
/// whoever is next.
|
|
pub(crate) fn release_upload_slots(
|
|
client_context: &Arc<RwLock<ClientContext>>,
|
|
username: &str,
|
|
) {
|
|
let freed = client_context
|
|
.write_safe()
|
|
.is_ok_and(|mut ctx| ctx.release_upload_slots(username));
|
|
if freed {
|
|
Self::pump_upload_queue(client_context);
|
|
}
|
|
}
|
|
|
|
/// Fill every free upload slot from the queue and send the offers.
|
|
///
|
|
/// Called whenever the queue or the slot count could have changed: a new
|
|
/// request, a finished transfer, or a privileged list that re-ranked who is
|
|
/// waiting.
|
|
pub(crate) fn pump_upload_queue(
|
|
client_context: &Arc<RwLock<ClientContext>>,
|
|
) {
|
|
let (registry, offers) = match client_context.write_safe() {
|
|
Ok(mut ctx) => ctx.pump_uploads(next_upload_token),
|
|
Err(e) => {
|
|
error!("[client] pump_upload_queue write: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
let Some(registry) = registry else {
|
|
return;
|
|
};
|
|
for offer in offers {
|
|
let _ = registry.send_to_peer(
|
|
&offer.requester_key,
|
|
PeerMessage::ServeUpload {
|
|
token: offer.token,
|
|
filename: offer.virtual_path,
|
|
size: offer.size,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Consume the upload job for `token` and stream the file to `host:port`
|
|
/// on a background thread.
|
|
pub(crate) fn spawn_serve(
|
|
client_context: &Arc<RwLock<ClientContext>>,
|
|
own_username: &str,
|
|
token: u32,
|
|
host: String,
|
|
port: u32,
|
|
) {
|
|
let Ok(mut ctx) = client_context.write_safe() else {
|
|
return;
|
|
};
|
|
let Some(job) = ctx.uploads.remove(&token) else {
|
|
return;
|
|
};
|
|
let bytes_sent = Arc::new(AtomicU64::new(0));
|
|
let cancel = Arc::new(AtomicBool::new(false));
|
|
ctx.active_uploads.insert(
|
|
token,
|
|
ActiveUpload {
|
|
username: job.downloader.clone(),
|
|
filename: job.virtual_path.clone(),
|
|
size: job.size,
|
|
bytes_sent: bytes_sent.clone(),
|
|
cancel: cancel.clone(),
|
|
status: UploadStatus::InProgress,
|
|
started: std::time::Instant::now(),
|
|
},
|
|
);
|
|
drop(ctx);
|
|
let own = own_username.to_string();
|
|
let real_path = job.real_path;
|
|
let context = client_context.clone();
|
|
thread::spawn(move || {
|
|
let result = crate::peer::upload_peer::serve_file(
|
|
&host,
|
|
port,
|
|
&own,
|
|
token,
|
|
&real_path,
|
|
&bytes_sent,
|
|
&cancel,
|
|
);
|
|
let streamed = result.as_ref().ok().copied();
|
|
let status = match &result {
|
|
Ok(_) => UploadStatus::Completed,
|
|
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
|
|
UploadStatus::Cancelled
|
|
}
|
|
Err(e) => {
|
|
error!("[client] serve {}: {}", real_path.display(), e);
|
|
UploadStatus::Failed(e.to_string())
|
|
}
|
|
};
|
|
if let Ok(mut ctx) = context.write_safe()
|
|
&& let Some(upload) = ctx.active_uploads.get_mut(&token)
|
|
{
|
|
let secs = upload.started.elapsed().as_secs_f64();
|
|
upload.status = status;
|
|
if let Some(bytes) = streamed
|
|
&& secs > 0.0
|
|
{
|
|
ctx.last_upload_speed = (bytes as f64 / secs) as u32;
|
|
}
|
|
}
|
|
// The slot this transfer held is free now, so whoever is next in
|
|
// line gets it without waiting for another request to arrive.
|
|
Self::pump_upload_queue(&context);
|
|
});
|
|
}
|
|
|
|
pub(crate) fn process_failed_uploads(
|
|
client_context: Arc<RwLock<ClientContext>>,
|
|
username: &str,
|
|
filename: Option<&str>,
|
|
) {
|
|
let failed_tokens = match client_context.read_safe() {
|
|
Ok(context) => {
|
|
collect_failed_tokens(&context.downloads, username, filename)
|
|
}
|
|
Err(e) => {
|
|
error!("[client] process_failed_uploads read: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
if failed_tokens.is_empty() {
|
|
return;
|
|
}
|
|
|
|
match client_context.write_safe() {
|
|
Ok(mut context) => {
|
|
for token in failed_tokens {
|
|
context.downloads.update_status(
|
|
token,
|
|
DownloadStatus::Failed(Some(
|
|
"The upload failed on the other side".to_string(),
|
|
)),
|
|
);
|
|
context.downloads.remove(token);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("[client] process_failed_uploads write: {}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|