Files
soulseek-rs-modded/soulseek-rs-lib/src/peer/download_peer.rs
T
Michel de Graaf d0550b8461 perf(lib): read downloads in 64 KiB slices
Downloads read the peer socket 8 KiB at a time, eight syscalls where one
now does; the reference clients read 50 to 256 KiB. Progress was reported
every fifteen reads, a cadence that stops meaning anything once a read
can be anywhere from a few bytes to 64 KiB, so a report now lands every
128 KiB received instead: the same volume as before for a fast peer, and
a window wide enough for a slow peer's rate to mean something.
2026-09-07 16:17:42 +02:00

842 lines
28 KiB
Rust

use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::net::ToSocketAddrs;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use crate::client::ClientContext;
use crate::message::server::MessageFactory;
use crate::trace;
use crate::types::{Download, DownloadStatus};
use crate::utils::path::{PART_SUFFIX, expand_tilde};
/// Read in 64 KiB slices: an eighth of the syscalls of the 8 KiB reads before,
/// and what a fast peer fills per read. slskd reads 256 KiB, Nicotine+ ~50 KiB.
const READ_BUFFER_SIZE: usize = 64 * 1024;
const CANCEL_POLL: Duration = Duration::from_secs(1);
const STALL_DEADLINE: Duration = Duration::from_secs(30);
/// Bytes between progress reports. Counting bytes rather than reads keeps
/// the report volume flat however big a read is, and gives a slow peer's
/// rate a window wide enough to mean something.
const PROGRESS_INTERVAL_BYTES: usize = 128 * 1024;
#[derive(Debug)]
pub enum DownloadError {
ConnectionFailed(io::Error),
InvalidAddress(String),
HandshakeFailed(io::Error),
StreamReadError(io::Error),
StreamWriteError(io::Error),
TokenNotFound(u32),
DownloadInfoMissing(u32),
FileWriteError(io::Error),
PathResolutionError(String),
InvalidTokenBytes,
LockPoisoned,
Cancelled,
IncompleteDownload { received: usize, expected: usize },
}
impl std::fmt::Display for DownloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConnectionFailed(e) => write!(f, "Connection failed: {e}"),
Self::InvalidAddress(addr) => {
write!(f, "Invalid address: {addr}")
}
Self::HandshakeFailed(e) => write!(f, "Handshake failed: {e}"),
Self::StreamReadError(e) => write!(f, "Stream read error: {e}"),
Self::StreamWriteError(e) => write!(f, "Stream write error: {e}"),
Self::TokenNotFound(token) => {
write!(f, "Token not found: {token}")
}
Self::DownloadInfoMissing(token) => {
write!(f, "Download info missing for token: {token}")
}
Self::FileWriteError(e) => write!(f, "File write error: {e}"),
Self::PathResolutionError(msg) => {
write!(f, "Path resolution error: {msg}")
}
Self::InvalidTokenBytes => {
write!(f, "Invalid token bytes received")
}
Self::LockPoisoned => write!(f, "Lock poisoned"),
Self::Cancelled => write!(f, "Download cancelled"),
Self::IncompleteDownload { received, expected } => write!(
f,
"Incomplete download: received {received} of {expected} bytes"
),
}
}
}
impl std::error::Error for DownloadError {}
fn extract_filename_from_path(full_path: &str) -> &str {
full_path
.split(['/', '\\'])
.next_back()
.unwrap_or(full_path)
}
fn part_path(final_path: &str) -> String {
format!("{final_path}{PART_SUFFIX}")
}
pub fn part_path_of(download: &Download) -> Option<String> {
resolve_download_path(download)
.ok()
.map(|path| part_path(&path))
}
/// Where a download's bytes end up: its configured directory (falling back
/// to the parent if it names a file) joined with the remote basename.
///
/// A directory that does not exist yet stays the target — `PartFile::open`
/// creates it. Treating it as "not a directory" and dodging to the parent
/// made a daemon whose folder was never created download into `~/Downloads`
/// while reporting `~/Downloads/Soulseek`.
fn resolve_download_path(download: &Download) -> Result<String, DownloadError> {
let mut expanded_path = expand_tilde(&download.download_directory);
if expanded_path.is_file() {
expanded_path = expanded_path
.parent()
.ok_or_else(|| {
DownloadError::PathResolutionError(format!(
"Cannot resolve parent directory for: {}",
expanded_path.display()
))
})?
.to_path_buf();
}
let final_path =
expanded_path.join(extract_filename_from_path(&download.filename));
final_path
.to_str()
.ok_or_else(|| {
DownloadError::PathResolutionError(format!(
"Path contains invalid UTF-8: {}",
final_path.display()
))
})
.map(String::from)
}
/// The `.part` file a transfer streams into, so an interrupted transfer leaves
/// behind what it managed to fetch and the next attempt can pick up from there.
struct PartFile {
file: File,
final_path: String,
written: u64,
expected: u64,
}
impl PartFile {
fn open(download: &Download) -> Result<Self, DownloadError> {
let final_path = resolve_download_path(download)?;
let path = PathBuf::from(part_path(&final_path));
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(DownloadError::FileWriteError)?;
}
// A `.part` that already meets or exceeds the expected size is left over
// from a different (or corrupted) transfer, not a resume point.
let on_disk = fs::metadata(&path).map_or(0, |m| m.len());
let written = if on_disk < download.size { on_disk } else { 0 };
let file = if written > 0 {
OpenOptions::new().append(true).open(&path)
} else {
File::create(&path)
}
.map_err(DownloadError::FileWriteError)?;
Ok(Self {
file,
final_path,
written,
expected: download.size,
})
}
/// Peers can coalesce trailing bytes into the last chunk, so overshoot past
/// the expected size is dropped rather than written.
fn write(&mut self, data: &[u8]) -> Result<(), DownloadError> {
let remaining = (self.expected - self.written) as usize;
let data = &data[..data.len().min(remaining)];
self.file
.write_all(data)
.map_err(DownloadError::FileWriteError)?;
self.written += data.len() as u64;
Ok(())
}
const fn is_complete(&self) -> bool {
self.written >= self.expected
}
fn discard(self) {
drop(self.file);
let _ = fs::remove_file(part_path(&self.final_path));
}
/// A short transfer is a failure, and the `.part` stays put so the next
/// attempt can resume it.
fn finish(self) -> Result<String, DownloadError> {
if !self.is_complete() {
return Err(DownloadError::IncompleteDownload {
received: self.written as usize,
expected: self.expected as usize,
});
}
drop(self.file);
fs::rename(part_path(&self.final_path), &self.final_path)
.map_err(DownloadError::FileWriteError)?;
Ok(self.final_path)
}
}
pub struct DownloadPeer {
username: String,
host: String,
port: u32,
#[allow(dead_code)]
own_username: String,
token: u32,
no_pierce: bool,
}
impl DownloadPeer {
#[must_use]
pub const fn new(
username: String,
host: String,
port: u32,
token: u32,
no_pierce: bool,
own_username: String,
) -> Self {
Self {
username,
host,
port,
own_username,
token,
no_pierce,
}
}
fn establish_connection(&self) -> Result<TcpStream, DownloadError> {
let socket_address = format!("{}:{}", self.host, self.port)
.to_socket_addrs()
.map_err(DownloadError::ConnectionFailed)?
.next()
.ok_or_else(|| {
DownloadError::InvalidAddress(format!(
"{}:{}",
self.host, self.port
))
})?;
let stream = TcpStream::connect_timeout(
&socket_address,
Duration::from_secs(20),
)
.map_err(DownloadError::ConnectionFailed)?;
Self::apply_transfer_deadlines(&stream)?;
Ok(stream)
}
/// Bound every stall on a transfer socket. This must cover streams the
/// listener accepted as well as ones we dialled: an accepted F connection
/// used to arrive here with no read timeout at all, so a peer that sent a
/// token and went silent parked the transfer thread — holding the socket
/// and the open `.part` file — for the rest of the session.
fn apply_transfer_deadlines(
stream: &TcpStream,
) -> Result<(), DownloadError> {
stream
.set_read_timeout(Some(STALL_DEADLINE))
.map_err(DownloadError::ConnectionFailed)?;
stream
.set_write_timeout(Some(Duration::from_secs(5)))
.map_err(DownloadError::ConnectionFailed)?;
stream
.set_nodelay(true)
.map_err(DownloadError::ConnectionFailed)?;
Ok(())
}
fn perform_handshake(
&self,
stream: &mut TcpStream,
) -> Result<(), DownloadError> {
trace!(
"[download_peer:{}] performing handshake no_pierce: {}",
self.username, self.no_pierce
);
if !self.no_pierce {
let message =
MessageFactory::build_pierce_firewall_message(self.token);
stream
.write_all(&message.get_buffer())
.map_err(DownloadError::HandshakeFailed)?;
trace!(
"[download_peer:{}] sending pierce firewall message token: {}: {:?}",
self.username,
self.token,
&message.get_buffer()
);
stream.flush().map_err(DownloadError::HandshakeFailed)?;
}
Ok(())
}
/// Soulseek's START_DOWNLOAD is an 8-byte little-endian offset, so anything
/// already in the `.part` file is skipped by asking the peer to start past it.
fn start_transfer(
download: Download,
stream: &mut TcpStream,
client_context: &Arc<RwLock<ClientContext>>,
) -> Result<(Download, PartFile), DownloadError> {
Self::wait_while_paused(client_context, &download)?;
let part = PartFile::open(&download)?;
stream
.write_all(&part.written.to_le_bytes())
.map_err(DownloadError::StreamWriteError)?;
Self::report_progress(client_context, &download, part.written, 0.0);
Ok((download, part))
}
fn begin_pierced_download(
&self,
data: &[u8],
stream: &mut TcpStream,
client_context: &Arc<RwLock<ClientContext>>,
) -> Result<(Download, PartFile), DownloadError> {
let token_bytes =
data.get(0..4).ok_or(DownloadError::InvalidTokenBytes)?;
let token_array: [u8; 4] = token_bytes
.try_into()
.map_err(|_| DownloadError::InvalidTokenBytes)?;
let token_u32 = u32::from_le_bytes(token_array);
trace!(
"[download_peer:{}] got token: {} from data chunk",
self.username, token_u32
);
let client_guard = client_context
.read()
.map_err(|_| DownloadError::LockPoisoned)?;
let download_info =
client_guard.get_download_by_token(token_u32).cloned();
drop(client_guard);
let download =
download_info.ok_or(DownloadError::TokenNotFound(token_u32))?;
Self::start_transfer(download, stream, client_context)
}
fn read_download_stream(
&self,
stream: &mut TcpStream,
client_context: &Arc<RwLock<ClientContext>>,
download: Option<Download>,
) -> Result<(Download, String), DownloadError> {
let mut read_buffer = vec![0u8; READ_BUFFER_SIZE];
let mut bytes_since_last_update = 0usize;
let mut last_update_time = Instant::now();
let mut last_data = Instant::now();
stream
.set_read_timeout(Some(CANCEL_POLL))
.map_err(DownloadError::ConnectionFailed)?;
trace!(
"[download_peer:{}] Starting to read data from peer",
self.username
);
let mut transfer = match download {
Some(dl) => Some(Self::start_transfer(dl, stream, client_context)?),
None => None,
};
loop {
if let Some((dl, _)) = &transfer {
match Self::wait_while_paused(client_context, dl) {
Ok(true) => last_data = Instant::now(),
Ok(false) => {}
Err(e) => {
if matches!(e, DownloadError::Cancelled)
&& let Some((_, part)) = transfer.take()
{
part.discard();
}
return Err(e);
}
}
}
match stream.read(&mut read_buffer) {
Ok(0) => {
trace!(
"[download_peer:{}] connection closed by peer",
self.username
);
break;
}
Ok(bytes_read) => {
last_data = Instant::now();
let data = &read_buffer[..bytes_read];
if transfer.is_none() && !self.no_pierce {
transfer = Some(self.begin_pierced_download(
data,
stream,
client_context,
)?);
continue;
}
let Some((dl, part)) = transfer.as_mut() else {
return Err(DownloadError::DownloadInfoMissing(
self.token,
));
};
part.write(data)?;
bytes_since_last_update += bytes_read;
if bytes_since_last_update >= PROGRESS_INTERVAL_BYTES {
let elapsed = last_update_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 {
bytes_since_last_update as f64 / elapsed
} else {
0.0
};
Self::report_progress(
client_context,
dl,
part.written,
speed,
);
bytes_since_last_update = 0;
last_update_time = Instant::now();
}
if part.is_complete() {
break;
}
}
Err(e)
if matches!(
e.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) && last_data.elapsed() < STALL_DEADLINE => {}
Err(e) => {
return Err(DownloadError::StreamReadError(e));
}
}
}
trace!(
"[download_peer:{}] finished reading data from peer",
self.username
);
let Some((download, part)) = transfer else {
return Err(DownloadError::DownloadInfoMissing(self.token));
};
Ok((download, part.finish()?))
}
fn report_progress(
client_context: &Arc<RwLock<ClientContext>>,
download: &Download,
bytes_downloaded: u64,
speed_bytes_per_sec: f64,
) {
Self::send_download_status(
client_context,
download,
DownloadStatus::InProgress {
bytes_downloaded,
total_bytes: download.size,
speed_bytes_per_sec,
},
);
}
fn send_download_status(
client_context: &Arc<RwLock<ClientContext>>,
download: &Download,
status: DownloadStatus,
) {
let applied = client_context.write().is_ok_and(|mut context| {
context
.downloads
.update_status(download.token, status.clone())
});
if applied {
let _ = download.sender.send(status);
}
}
fn wait_while_paused(
client_context: &Arc<RwLock<ClientContext>>,
download: &Download,
) -> Result<bool, DownloadError> {
let mut waited = false;
loop {
let status = client_context
.read()
.map_err(|_| DownloadError::LockPoisoned)?
.get_download_by_token(download.token)
.map(|download| download.status.clone())
.ok_or(DownloadError::TokenNotFound(download.token))?;
if matches!(status, DownloadStatus::Cancelled) {
return Err(DownloadError::Cancelled);
}
if !matches!(status, DownloadStatus::Paused { .. }) {
return Ok(waited);
}
waited = true;
thread::sleep(Duration::from_millis(200));
}
}
pub fn download_file(
self,
client_context: Arc<RwLock<ClientContext>>,
download: Option<Download>,
stream: Option<TcpStream>,
) -> Result<(Download, String), DownloadError> {
trace!(
"[download_peer:{}] download_file: download is present?: {:?}, stream is present?: {:?}, no_pierce: {}",
self.username,
download.is_some(),
stream.is_some(),
self.no_pierce
);
let mut stream = match stream {
Some(s) => {
Self::apply_transfer_deadlines(&s)?;
s
}
None => self.establish_connection()?,
};
trace!("[download_peer:{}] connected", self.username);
self.perform_handshake(&mut stream)?;
trace!("[download_peer:{}] handshake completed", self.username);
let (download, final_path) =
self.read_download_stream(&mut stream, &client_context, download)?;
trace!(
"[download_peer:{}] download completed successfully: {} bytes, saved to: {}",
self.username, download.size, final_path
);
Ok((download, final_path))
}
}
#[cfg(test)]
mod tests {
use super::{
DownloadError, DownloadPeer, PartFile, extract_filename_from_path,
};
use crate::client::ClientContext;
use crate::types::{Download, DownloadMetadata, DownloadStatus};
use std::sync::{Arc, RwLock, mpsc};
fn scratch_dir(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir()
.join(format!("soulseek-part-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn download_into(dir: &std::path::Path, size: u64) -> Download {
Download {
username: "peer".to_string(),
filename: "song.mp3".to_string(),
token: 1,
size,
download_directory: dir.display().to_string(),
status: DownloadStatus::Queued,
sender: mpsc::channel().0,
queue_position: None,
metadata: DownloadMetadata::default(),
}
}
#[test]
fn a_discarded_part_file_is_removed_from_disk() {
let dir = scratch_dir("discard");
let mut part = PartFile::open(&download_into(&dir, 8)).unwrap();
part.write(b"data").unwrap();
let path = dir.join("song.mp3.part");
assert!(path.exists());
part.discard();
assert!(!path.exists());
assert!(!dir.join("song.mp3").exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_pause_is_reported_so_the_stall_budget_restarts() {
let dir = scratch_dir("paused");
let mut download = download_into(&dir, 8);
download.status = DownloadStatus::Paused {
bytes_downloaded: 0,
total_bytes: 8,
};
let context = Arc::new(RwLock::new(ClientContext::new()));
context.write().unwrap().add_download(download.clone());
let resumer = {
let context = context.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(300));
assert!(
context
.write()
.unwrap()
.downloads
.resume_by_file("peer", "song.mp3")
);
})
};
assert!(matches!(
DownloadPeer::wait_while_paused(&context, &download),
Ok(true)
));
assert!(matches!(
DownloadPeer::wait_while_paused(&context, &download),
Ok(false)
));
let _ = resumer.join();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_cancelled_download_stops_the_transfer_loop() {
let dir = scratch_dir("cancelled");
let mut download = download_into(&dir, 8);
download.status = DownloadStatus::Cancelled;
let context = Arc::new(RwLock::new(ClientContext::new()));
context.write().unwrap().add_download(download.clone());
assert!(matches!(
DownloadPeer::wait_while_paused(&context, &download),
Err(DownloadError::Cancelled)
));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_status_after_cancel_is_not_relayed_on_the_channel() {
let dir = scratch_dir("relay");
let (tx, rx) = mpsc::channel();
let mut download = download_into(&dir, 8);
download.sender = tx;
download.status = DownloadStatus::Cancelled;
let context = Arc::new(RwLock::new(ClientContext::new()));
context.write().unwrap().add_download(download.clone());
DownloadPeer::send_download_status(
&context,
&download,
DownloadStatus::InProgress {
bytes_downloaded: 4,
total_bytes: 8,
speed_bytes_per_sec: 1.0,
},
);
assert!(rx.try_recv().is_err(), "a cancelled row hears nothing more");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn start_transfer_refuses_a_cancelled_download() {
let dir = scratch_dir("refuse");
let mut download = download_into(&dir, 8);
download.status = DownloadStatus::Cancelled;
let context = Arc::new(RwLock::new(ClientContext::new()));
context.write().unwrap().add_download(download.clone());
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let mut stream =
std::net::TcpStream::connect(listener.local_addr().unwrap())
.unwrap();
let _peer = listener.accept().unwrap();
let outcome =
DownloadPeer::start_transfer(download, &mut stream, &context);
assert!(matches!(outcome, Err(DownloadError::Cancelled)));
assert!(!dir.join("song.mp3.part").exists(), "no .part is opened");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_directory_that_does_not_exist_yet_is_created_not_dodged() {
let dir = scratch_dir("fresh").join("Soulseek");
let mut part = PartFile::open(&download_into(&dir, 4)).unwrap();
part.write(b"data").unwrap();
let final_path = part.finish().unwrap();
assert_eq!(
std::path::Path::new(&final_path).parent(),
Some(dir.as_path()),
"the bytes belong in the configured directory, not its parent"
);
assert_eq!(std::fs::read(&final_path).unwrap(), b"data");
let _ = std::fs::remove_dir_all(dir.parent().unwrap());
}
#[test]
fn a_partial_file_sets_the_resume_offset() {
let dir = scratch_dir("resume");
std::fs::write(dir.join("song.mp3.part"), b"0123").unwrap();
let mut part = PartFile::open(&download_into(&dir, 10)).unwrap();
assert_eq!(part.written, 4, "resume from what is already on disk");
part.write(b"456789").unwrap();
let final_path = part.finish().unwrap();
assert_eq!(std::fs::read(&final_path).unwrap(), b"0123456789");
assert!(
!dir.join("song.mp3.part").exists(),
"the .part is renamed, not left behind"
);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn an_oversized_partial_file_restarts_from_zero() {
// Left over from a different transfer — not a valid resume point.
let dir = scratch_dir("stale");
std::fs::write(dir.join("song.mp3.part"), vec![9u8; 40]).unwrap();
let mut part = PartFile::open(&download_into(&dir, 10)).unwrap();
assert_eq!(part.written, 0);
part.write(&(0..10).collect::<Vec<u8>>()).unwrap();
let final_path = part.finish().unwrap();
assert_eq!(
std::fs::read(final_path).unwrap(),
(0..10).collect::<Vec<u8>>()
);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn a_truncated_transfer_fails_and_keeps_the_partial_file() {
let dir = scratch_dir("truncated");
let mut part = PartFile::open(&download_into(&dir, 10)).unwrap();
part.write(b"01234").unwrap();
assert!(matches!(
part.finish(),
Err(DownloadError::IncompleteDownload {
received: 5,
expected: 10
})
));
assert_eq!(
std::fs::read(dir.join("song.mp3.part")).unwrap(),
b"01234",
"the partial stays on disk for the next attempt"
);
assert!(!dir.join("song.mp3").exists());
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn overshoot_past_the_expected_size_is_dropped() {
let dir = scratch_dir("overshoot");
let mut part = PartFile::open(&download_into(&dir, 10)).unwrap();
part.write(&(0..12).collect::<Vec<u8>>()).unwrap();
assert!(part.is_complete());
assert_eq!(
std::fs::read(part.finish().unwrap()).unwrap(),
(0..10).collect::<Vec<u8>>()
);
let _ = std::fs::remove_dir_all(dir);
}
// Streams the listener accepted arrive with no deadlines; without these a
// peer that sends a token and goes silent parks the transfer thread —
// socket and open .part file included — for the rest of the session.
#[test]
fn supplied_streams_get_transfer_deadlines() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let stream =
std::net::TcpStream::connect(listener.local_addr().unwrap())
.unwrap();
DownloadPeer::apply_transfer_deadlines(&stream).unwrap();
let secs = std::time::Duration::from_secs;
assert_eq!(stream.read_timeout().unwrap(), Some(secs(30)));
assert_eq!(stream.write_timeout().unwrap(), Some(secs(5)));
}
#[test]
fn test_establish_connection_invalid_address() {
let download_peer = DownloadPeer::new(
"user".to_string(),
"invalid-host".to_string(),
9999,
123,
false,
"own_user".to_string(),
);
let result = download_peer.establish_connection();
assert!(result.is_err());
}
#[test]
fn test_extract_filename_from_path() {
assert_eq!(extract_filename_from_path("/path/to/file.mp3"), "file.mp3");
assert_eq!(
extract_filename_from_path("C:\\path\\to\\file.mp3"),
"file.mp3"
);
assert_eq!(
extract_filename_from_path(
"@@bhfrv\\Soulseek Downloads\\complete\\Beatport Top Deep House (2021)\\michel test file.mp3"
),
"michel test file.mp3"
);
assert_eq!(extract_filename_from_path("file.mp3"), "file.mp3");
}
}