refactor: resolve all lint findings

- remove 34 trivial restating comments (auto-fix)
- replace all 20 production .unwrap() calls with graceful handling:
  ?-propagation via LockPoisoned, unwrap_or_default on infallible
  conversions, let _ = on actor-channel sends, and a downloads_sender()
  helper in the TUI that also dedupes three channel-init blocks
- collapse nested if into match guard (clippy::collapsible_match)
- split client.rs (2275 lines) into a client/ module: connection,
  search, downloads, uploads, rooms, operations
- split ui/main_tui.rs (2027 lines) into a main_tui/ module: render,
  input, browse, rooms, downloads, search
- extract oversized functions: peer_actor/server_actor handle_message
  into per-message helpers, main() into init_logging/run_default_tui,
  download_selector and main_tui render into per-section helpers

Mechanical only, zero behavior change. Verified: clippy clean
(pedantic+nursery), fmt clean, 143 unit tests + 16 e2e tests against
soulfind pass, lint clean.
This commit is contained in:
Michel de Graaf
2026-07-20 10:53:57 +02:00
parent 17864ac9b0
commit 2918b0ce75
35 changed files with 4113 additions and 3983 deletions
-2
View File
@@ -74,7 +74,6 @@ impl ActorSystem {
let (sender, receiver) = channel::<ActorMessage<A::Message>>();
let handle = ActorHandle { sender };
// Start the actor event loop on the thread pool
self.thread_pool.execute(move || {
actor.on_start();
Self::run_actor_loop(&mut actor, receiver);
@@ -198,7 +197,6 @@ mod tests {
assert_eq!(count.load(Ordering::SeqCst), 6);
// Stop the actor
handle.stop().unwrap();
// Give actor time to process the stop message
+189 -154
View File
@@ -199,130 +199,23 @@ impl PeerActor {
self.send_message(message);
}
PeerMessage::FileSearchResult(file_search) => {
if let Err(e) = self
.client_channel
.send(ClientOperation::SearchResult(file_search))
{
error!(
"[peer:{}] failed to forward search result: {}",
self.peer_username(),
e
);
}
self.handle_file_search_result(file_search);
}
PeerMessage::TransferRequest(transfer) => {
let username = self.peer_username();
debug!(
"[peer:{}] TransferRequest for {}",
username, transfer.token
);
if let Err(e) = self.client_channel.send(
ClientOperation::UpdateDownloadTokens(
transfer.clone(),
username.clone(),
),
) {
error!(
"[peer:{}] failed to send UpdateDownloadTokens: {}",
username, e
);
}
let transfer_response =
MessageFactory::build_transfer_response_message(transfer);
if let Some(ref handle) = self.self_handle
&& let Err(e) =
handle.send(PeerMessage::SendMessage(transfer_response))
{
error!(
"[peer:{}] Failed to send TransferResponse message: {}",
username, e
);
}
self.handle_transfer_request(transfer);
}
PeerMessage::TransferResponse {
token,
allowed,
reason,
} => {
let username = self.peer_username();
debug!(
"[peer:{}] transfer response token: {} allowed: {}",
username, token, allowed
);
// If this token is one of our uploads, the peer just accepted
// our offer — start streaming. This leaves the download path
// (every other token) byte-for-byte unchanged.
if self.serving_tokens.remove(&token) {
if allowed {
let _ = self
.client_channel
.send(ClientOperation::StartUpload { token });
}
return;
}
if allowed {
debug!(
"[peer:{}] Transfer allowed, ready to connect with token {:}",
username, token
);
let Some(peer_snapshot) = self.peer_snapshot() else {
return;
};
if let Err(e) = self.client_channel.send(
ClientOperation::DownloadFromPeer(
token,
peer_snapshot,
allowed,
),
) {
error!(
"[peer:{}] failed to send DownloadFromPeer: {}",
username, e
);
}
} else if let Some(reason_text) = reason {
debug!(
"[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...",
username, reason_text, token
);
}
self.handle_transfer_response(token, allowed, reason);
}
PeerMessage::PlaceInQueueResponse { filename, place } => {
let username = self.peer_username();
debug!(
"[peer:{}] Place in queue response - file: {}, place: {}",
username, filename, place
);
if let Err(e) = self.client_channel.send(
ClientOperation::PlaceInQueueUpdate {
username: username.clone(),
filename,
place,
},
) {
error!(
"[peer:{}] failed to forward PlaceInQueueUpdate: {}",
username, e
);
}
self.handle_place_in_queue_response(filename, place);
}
PeerMessage::SetUsername(username) => {
trace!(
"[peer:{}] SetUsername: {}",
self.peer_username(),
username
);
match self.peer.write_safe() {
Ok(mut p) => p.username = username,
Err(e) => {
error!("[peer_actor] SetUsername write: {}", e);
}
}
self.handle_set_username(username);
}
PeerMessage::QueueUpload(filename) => {
let message =
@@ -330,48 +223,20 @@ impl PeerActor {
self.send_message(message);
}
PeerMessage::IncomingQueueUpload(filename) => {
// A peer wants to download one of our shared files. Ask the
// client (which owns the shares) to prepare the upload.
let requester_key = self.peer_username();
if let Err(e) =
self.client_channel.send(ClientOperation::QueueUpload {
requester_key,
filename,
})
{
error!("[peer_actor] forward IncomingQueueUpload: {}", e);
}
self.handle_incoming_queue_upload(filename);
}
PeerMessage::ServeUpload {
token,
filename,
size,
} => {
self.serving_tokens.insert(token);
let message = MessageFactory::build_upload_transfer_request(
&filename, token, size,
);
self.send_message(message);
self.handle_serve_upload(token, filename, size);
}
PeerMessage::ShareListRequested => {
let requester_key = self.peer_username();
if let Err(e) = self
.client_channel
.send(ClientOperation::ShareListRequested { requester_key })
{
error!("[peer_actor] forward ShareListRequested: {}", e);
}
self.handle_share_list_requested();
}
PeerMessage::ShareListReceived(directories) => {
let username = self.peer_username();
if let Err(e) =
self.client_channel.send(ClientOperation::BrowseResult {
username,
directories,
})
{
error!("[peer_actor] forward BrowseResult: {}", e);
}
self.handle_share_list_received(directories);
}
PeerMessage::RequestTransfer(download) => {
let message = MessageFactory::build_transfer_request_message(
@@ -384,19 +249,189 @@ impl PeerActor {
self.process_read();
}
PeerMessage::UploadFailed(username, filename) => {
if let Err(e) = self
.client_channel
.send(ClientOperation::UploadFailed(username, filename))
{
error!(
"[peer_actor] failed to forward UploadFailed: {}",
e
);
}
self.handle_upload_failed(username, filename);
}
}
}
fn handle_file_search_result(&self, file_search: SearchResult) {
if let Err(e) = self
.client_channel
.send(ClientOperation::SearchResult(file_search))
{
error!(
"[peer:{}] failed to forward search result: {}",
self.peer_username(),
e
);
}
}
fn handle_transfer_request(&self, transfer: Transfer) {
let username = self.peer_username();
debug!("[peer:{}] TransferRequest for {}", username, transfer.token);
if let Err(e) =
self.client_channel
.send(ClientOperation::UpdateDownloadTokens(
transfer.clone(),
username.clone(),
))
{
error!(
"[peer:{}] failed to send UpdateDownloadTokens: {}",
username, e
);
}
let transfer_response =
MessageFactory::build_transfer_response_message(transfer);
if let Some(ref handle) = self.self_handle
&& let Err(e) =
handle.send(PeerMessage::SendMessage(transfer_response))
{
error!(
"[peer:{}] Failed to send TransferResponse message: {}",
username, e
);
}
}
fn handle_transfer_response(
&mut self,
token: u32,
allowed: bool,
reason: Option<String>,
) {
let username = self.peer_username();
debug!(
"[peer:{}] transfer response token: {} allowed: {}",
username, token, allowed
);
// If this token is one of our uploads, the peer just accepted
// our offer — start streaming. This leaves the download path
// (every other token) byte-for-byte unchanged.
if self.serving_tokens.remove(&token) {
if allowed {
let _ = self
.client_channel
.send(ClientOperation::StartUpload { token });
}
return;
}
if allowed {
debug!(
"[peer:{}] Transfer allowed, ready to connect with token {:}",
username, token
);
let Some(peer_snapshot) = self.peer_snapshot() else {
return;
};
if let Err(e) =
self.client_channel.send(ClientOperation::DownloadFromPeer(
token,
peer_snapshot,
allowed,
))
{
error!(
"[peer:{}] failed to send DownloadFromPeer: {}",
username, e
);
}
} else if let Some(reason_text) = reason {
debug!(
"[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...",
username, reason_text, token
);
}
}
fn handle_place_in_queue_response(&self, filename: String, place: u32) {
let username = self.peer_username();
debug!(
"[peer:{}] Place in queue response - file: {}, place: {}",
username, filename, place
);
if let Err(e) =
self.client_channel
.send(ClientOperation::PlaceInQueueUpdate {
username: username.clone(),
filename,
place,
})
{
error!(
"[peer:{}] failed to forward PlaceInQueueUpdate: {}",
username, e
);
}
}
fn handle_set_username(&self, username: String) {
trace!("[peer:{}] SetUsername: {}", self.peer_username(), username);
match self.peer.write_safe() {
Ok(mut p) => p.username = username,
Err(e) => {
error!("[peer_actor] SetUsername write: {}", e);
}
}
}
fn handle_incoming_queue_upload(&self, filename: String) {
// A peer wants to download one of our shared files. Ask the
// client (which owns the shares) to prepare the upload.
let requester_key = self.peer_username();
if let Err(e) = self.client_channel.send(ClientOperation::QueueUpload {
requester_key,
filename,
}) {
error!("[peer_actor] forward IncomingQueueUpload: {}", e);
}
}
fn handle_serve_upload(&mut self, token: u32, filename: String, size: u64) {
self.serving_tokens.insert(token);
let message = MessageFactory::build_upload_transfer_request(
&filename, token, size,
);
self.send_message(message);
}
fn handle_share_list_requested(&self) {
let requester_key = self.peer_username();
if let Err(e) = self
.client_channel
.send(ClientOperation::ShareListRequested { requester_key })
{
error!("[peer_actor] forward ShareListRequested: {}", e);
}
}
fn handle_share_list_received(&self, directories: Vec<SharedDirectory>) {
let username = self.peer_username();
if let Err(e) =
self.client_channel.send(ClientOperation::BrowseResult {
username,
directories,
})
{
error!("[peer_actor] forward BrowseResult: {}", e);
}
}
fn handle_upload_failed(&self, username: String, filename: String) {
if let Err(e) = self
.client_channel
.send(ClientOperation::UploadFailed(username, filename))
{
error!("[peer_actor] failed to forward UploadFailed: {}", e);
}
}
fn process_read(&mut self) {
if self.reader.buffer_len() > 0 {
self.extract_and_process_messages();
@@ -489,7 +524,7 @@ impl PeerActor {
.get_message_name(
MessageType::Peer,
u32::from_le_bytes(
message.get_slice(0, 4).try_into().unwrap()
message.get_slice(0, 4).try_into().unwrap_or_default()
)
)
.map_err(|e| e.to_string())
+150 -105
View File
@@ -413,37 +413,10 @@ impl ServerActor {
match msg {
ServerMessage::ConnectToPeer(peer) => {
if let Some(op) = match peer.connection_type {
ConnectionType::P | ConnectionType::F => {
Some(ClientOperation::ConnectToPeer(peer))
}
ConnectionType::D => None,
} && let Err(e) = self.client_channel.send(op)
{
error!("[server] failed to send ConnectToPeer: {}", e);
}
self.handle_connect_to_peer(peer);
}
ServerMessage::LoginStatus(message) => {
match self.context.write_safe() {
Ok(mut ctx) => ctx.logged_in = Some(message),
Err(e) => {
error!("[server] LoginStatus write: {}", e);
}
}
// Send the post-login handshake exactly once, only on success,
// on the live path (the old ServerActor::login did this but was
// never called). Advertises real shared counts and, when
// listening, the port peers must connect to.
if message {
for msg in post_login_messages(
self.enable_listen,
self.listen_port,
self.shared_folder_count,
self.shared_file_count,
) {
self.send_message(msg);
}
}
self.handle_login_status(message);
}
ServerMessage::PierceFirewall(token) => {
self.send_message(
@@ -465,40 +438,16 @@ impl ServerActor {
obfuscation_type,
obfuscated_port,
} => {
debug!(
"[server] Received GetPeerAddress response for {}: {}:{} (obf_type: {}, obf_port: {})",
username, host, port, obfuscation_type, obfuscated_port
self.handle_get_peer_address_response(
username,
host,
port,
obfuscation_type,
obfuscated_port,
);
if let Err(e) = self.client_channel.send(
ClientOperation::GetPeerAddressResponse {
username,
host,
port,
obfuscation_type,
obfuscated_port,
},
) {
error!(
"[server] Error forwarding GetPeerAddress response to client: {}",
e
);
}
}
ServerMessage::PrivateMessageReceived(user_message) => {
debug!(
"[server] Private message from {}",
user_message.username()
);
if let Err(e) = self
.client_channel
.send(ClientOperation::PrivateMessageReceived(user_message))
{
error!(
"[server] Error forwarding private message to client: {}",
e
);
}
self.handle_private_message_received(user_message);
}
ServerMessage::RoomListReceived(rooms) => {
self.forward_room_event(RoomEvent::List(rooms));
@@ -537,41 +486,7 @@ impl ServerActor {
password,
response,
} => {
self.queue_message(MessageFactory::build_login_message(
&username, &password,
));
let start = std::time::Instant::now();
let timeout = Duration::from_secs(5);
let context = self.context.clone();
std::thread::spawn(move || {
loop {
if start.elapsed() >= timeout {
let _ = response.send(Err(SoulseekRs::Timeout));
break;
}
let logged_in = match context.read_safe() {
Ok(ctx) => ctx.logged_in,
Err(e) => {
let _ = response.send(Err(e));
break;
}
};
if let Some(logged_in) = logged_in {
let result = if logged_in {
Ok(true)
} else {
Err(SoulseekRs::AuthenticationFailed)
};
let _ = response.send(result);
break;
}
std::thread::sleep(Duration::from_millis(100));
}
});
self.handle_login(username, password, response);
}
ServerMessage::FileSearch { token, query } => {
self.file_search(token, &query);
@@ -581,19 +496,149 @@ impl ServerActor {
token,
query,
} => {
if let Err(e) =
self.client_channel.send(ClientOperation::IncomingSearch {
username,
token,
query,
})
{
error!("[server] forward IncomingSearch: {}", e);
}
self.handle_file_search_request(username, token, query);
}
}
}
fn handle_connect_to_peer(&self, peer: Peer) {
if let Some(op) = match peer.connection_type {
ConnectionType::P | ConnectionType::F => {
Some(ClientOperation::ConnectToPeer(peer))
}
ConnectionType::D => None,
} && let Err(e) = self.client_channel.send(op)
{
error!("[server] failed to send ConnectToPeer: {}", e);
}
}
fn handle_login_status(&mut self, message: bool) {
match self.context.write_safe() {
Ok(mut ctx) => ctx.logged_in = Some(message),
Err(e) => {
error!("[server] LoginStatus write: {}", e);
}
}
// Send the post-login handshake exactly once, only on success,
// on the live path (the old ServerActor::login did this but was
// never called). Advertises real shared counts and, when
// listening, the port peers must connect to.
if message {
for msg in post_login_messages(
self.enable_listen,
self.listen_port,
self.shared_folder_count,
self.shared_file_count,
) {
self.send_message(msg);
}
}
}
fn handle_get_peer_address_response(
&self,
username: String,
host: String,
port: u32,
obfuscation_type: u32,
obfuscated_port: u16,
) {
debug!(
"[server] Received GetPeerAddress response for {}: {}:{} (obf_type: {}, obf_port: {})",
username, host, port, obfuscation_type, obfuscated_port
);
if let Err(e) =
self.client_channel
.send(ClientOperation::GetPeerAddressResponse {
username,
host,
port,
obfuscation_type,
obfuscated_port,
})
{
error!(
"[server] Error forwarding GetPeerAddress response to client: {}",
e
);
}
}
fn handle_private_message_received(&self, user_message: UserMessage) {
debug!("[server] Private message from {}", user_message.username());
if let Err(e) = self
.client_channel
.send(ClientOperation::PrivateMessageReceived(user_message))
{
error!(
"[server] Error forwarding private message to client: {}",
e
);
}
}
fn handle_login(
&mut self,
username: String,
password: String,
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
) {
self.queue_message(MessageFactory::build_login_message(
&username, &password,
));
let start = std::time::Instant::now();
let timeout = Duration::from_secs(5);
let context = self.context.clone();
std::thread::spawn(move || {
loop {
if start.elapsed() >= timeout {
let _ = response.send(Err(SoulseekRs::Timeout));
break;
}
let logged_in = match context.read_safe() {
Ok(ctx) => ctx.logged_in,
Err(e) => {
let _ = response.send(Err(e));
break;
}
};
if let Some(logged_in) = logged_in {
let result = if logged_in {
Ok(true)
} else {
Err(SoulseekRs::AuthenticationFailed)
};
let _ = response.send(result);
break;
}
std::thread::sleep(Duration::from_millis(100));
}
});
}
fn handle_file_search_request(
&self,
username: String,
token: u32,
query: String,
) {
if let Err(e) =
self.client_channel.send(ClientOperation::IncomingSearch {
username,
token,
query,
})
{
error!("[server] forward IncomingSearch: {}", e);
}
}
fn process_read(&mut self) {
if self.reader.buffer_len() > 0 {
self.extract_and_process_messages();
@@ -696,7 +741,7 @@ impl ServerActor {
.get_message_name(
MessageType::Server,
u32::from_le_bytes(
message.get_slice(0, 4).try_into().unwrap()
message.get_slice(0, 4).try_into().unwrap_or_default()
)
)
.map_err(|e| e.to_string())
+270
View File
@@ -0,0 +1,270 @@
use super::{
Arc, Client, ClientContext, ClientOperation, ConnectionType, DownloadPeer,
DownloadStatus, Listen, Peer, PeerRegistry, Receiver, Result, RwLock,
RwLockExt, Sender, ServerActor, ServerMessage, Shares, SoulseekRs,
TcpStream, debug, error, info, mpsc, thread, trace, warn,
};
impl Client {
pub fn connect(&mut self) -> Result<()> {
let (sender, message_reader): (
Sender<ClientOperation>,
Receiver<ClientOperation>,
) = mpsc::channel();
let mut ctx = self.context.write_safe()?;
ctx.sender = Some(sender.clone());
let peer_registry = PeerRegistry::new(
ctx.actor_system.clone(),
sender.clone(),
self.username.clone(),
);
ctx.peer_registry = Some(peer_registry);
let listen_sender = sender.clone();
// Scan the shared directory once into the read-only index, and report
// the real folder/file counts to the server on login.
let shares = match self.shared_directory.as_deref() {
Some(dir) if !dir.trim().is_empty() => {
match Shares::scan(std::path::Path::new(dir)) {
Ok(scanned) => {
info!(
"Sharing {} files in {} folders from {}",
scanned.file_count(),
scanned.folder_count(),
dir
);
Arc::new(scanned)
}
Err(e) => {
warn!("Failed to scan shared directory {}: {}", dir, e);
Arc::new(Shares::empty())
}
}
}
_ => Arc::new(Shares::empty()),
};
let shared_folder_count = shares.folder_count();
let shared_file_count = shares.file_count();
ctx.shares = shares;
let server_actor = ServerActor::new(
self.address.clone(),
sender,
self.listen_port,
self.enable_listen,
shared_folder_count,
shared_file_count,
);
self.server_handle = Some(ctx.actor_system.spawn_with_handle(
server_actor,
|actor, handle| {
actor.set_self_handle(handle);
},
));
if self.enable_listen {
let listen_port = self.listen_port;
let client_sender = listen_sender;
let context = self.context.clone();
let own_username = self.username.clone();
thread::spawn(move || {
Listen::start(
listen_port,
client_sender,
context,
own_username,
);
});
}
Self::listen_to_client_operations(
message_reader,
self.context.clone(),
self.username.clone(),
);
Ok(())
}
pub fn login(&self) -> Result<bool> {
info!("Logging in as {}", self.username);
if let Some(handle) = &self.server_handle {
let (tx, rx) = std::sync::mpsc::channel();
let _ = handle.send(ServerMessage::Login {
username: self.username.clone(),
password: self.password.clone(),
response: tx,
});
match rx.recv() {
Ok(result) => result,
Err(_) => Err(SoulseekRs::Timeout),
}
} else {
Err(SoulseekRs::NotConnected)
}
}
/// Ask the server for a peer's address and open a direct control
/// connection to it. Downloads queued for that peer are sent automatically
/// once the connection is established.
///
/// # Errors
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
pub fn connect_peer(&self, username: &str) -> Result<()> {
let handle = self
.server_handle
.as_ref()
.ok_or(SoulseekRs::NotConnected)?;
handle
.send(ServerMessage::GetPeerAddress(username.to_string()))
.map_err(|_| SoulseekRs::NotConnected)?;
Ok(())
}
#[allow(dead_code)]
pub fn remove_peer(&self, username: &str) {
let context = match self.context.read_safe() {
Ok(c) => c,
Err(e) => {
error!("[client] remove_peer: {}", e);
return;
}
};
if let Some(ref registry) = context.peer_registry
&& let Some(handle) = registry.remove_peer(username)
{
let _ = handle.stop();
}
}
pub(crate) fn connect_to_peer(
peer: Peer,
client_context: Arc<RwLock<ClientContext>>,
own_username: String,
stream: Option<TcpStream>,
) {
let client_context = client_context;
let peer_clone = peer.clone();
trace!(
"[client] connecting to {}, with connection_type: {}, and token {:?}",
peer.username, peer.connection_type, peer.token
);
match peer.connection_type {
ConnectionType::P => {
let username = peer.username;
let context = match client_context.read_safe() {
Ok(c) => c,
Err(e) => {
error!("[client] connect_to_peer read: {}", e);
return;
}
};
if let Some(ref registry) = context.peer_registry {
match registry.register_peer(peer_clone, stream, None) {
Ok(_) => (),
Err(e) => {
trace!(
"Failed to spawn peer actor for {:?}: {:?}",
username, e
);
}
}
} else {
trace!("PeerRegistry not initialized");
}
}
ConnectionType::F => {
trace!(
"[client] downloading from: {}, {:?}",
peer.username, peer.token
);
let Some(token) = peer.token else {
error!(
"[client] F connection from {} without token",
peer.username
);
return;
};
let download_peer = DownloadPeer::new(
peer.username,
peer.host,
peer.port,
token,
false,
own_username,
);
match download_peer.download_file(
client_context.clone(),
None,
None,
) {
Ok((download, filename)) => {
trace!(
"[client] downloaded {} bytes {:?} ",
filename, download.size
);
let _ = download.sender.send(DownloadStatus::Completed);
match client_context.write_safe() {
Ok(mut ctx) => ctx.update_download_with_status(
download.token,
DownloadStatus::Completed,
),
Err(e) => error!(
"[client] connect_to_peer F write: {}",
e
),
}
}
Err(e) => {
trace!("[client] failed to download: {}", e);
}
}
}
ConnectionType::D => {
error!("ConnectionType::D not implemented");
}
}
}
pub(crate) fn pierce_firewall(
peer: Peer,
client_context: Arc<RwLock<ClientContext>>,
own_username: String,
) {
debug!("Piercing firewall for peer: {:?}", peer);
let context = match client_context.read_safe() {
Ok(c) => c,
Err(e) => {
error!("[client] pierce_firewall read: {}", e);
return;
}
};
if let Some(server_sender) = &context.server_sender {
if let Some(token) = peer.token {
match server_sender.send(ServerMessage::PierceFirewall(token)) {
Ok(()) => (),
Err(e) => {
error!("Failed to send PierceFirewall message: {}", e);
}
}
} else {
error!("No token available for PierceFirewall");
}
} else {
error!("No server sender available for PierceFirewall");
}
drop(context);
Self::connect_to_peer(peer, client_context, own_username, None);
}
}
+191
View File
@@ -0,0 +1,191 @@
use super::{
Arc, Client, ClientContext, Download, DownloadMetadata, DownloadStatus,
Receiver, Result, RwLock, RwLockExt, Sender, error, info, md5, mpsc,
};
impl Client {
#[must_use]
pub fn get_all_downloads(&self) -> Vec<Download> {
self.context
.read_safe()
.map(|ctx| ctx.get_downloads().clone())
.unwrap_or_default()
}
#[must_use]
pub fn pause_download(&self, username: &str, filename: &str) -> bool {
match self.context.write_safe() {
Ok(mut ctx) => ctx.downloads.pause_by_file(username, filename),
Err(e) => {
error!("[client] pause_download: {}", e);
false
}
}
}
#[must_use]
pub fn resume_download(&self, username: &str, filename: &str) -> bool {
match self.context.write_safe() {
Ok(mut ctx) => ctx.downloads.resume_by_file(username, filename),
Err(e) => {
error!("[client] resume_download: {}", e);
false
}
}
}
#[must_use]
pub fn remove_queued_download(
&self,
username: &str,
filename: &str,
) -> bool {
match self.context.write_safe() {
Ok(mut ctx) => {
ctx.downloads.remove_queued_by_file(username, filename)
}
Err(e) => {
error!("[client] remove_queued_download: {}", e);
false
}
}
}
/// Remove every download for `username`/`filename` regardless of status.
/// Call this before re-issuing [`Client::download`] for a failed download,
/// otherwise the stale entry (whose md5-derived token equals the retry's)
/// shadows the fresh one and its completion is misrouted.
///
/// Returns whether anything was removed.
#[must_use]
pub fn remove_download(&self, username: &str, filename: &str) -> bool {
match self.context.write_safe() {
Ok(mut ctx) => ctx.downloads.remove_by_file(username, filename),
Err(e) => {
error!("[client] remove_download: {}", e);
false
}
}
}
pub fn download(
&self,
filename: String,
username: String,
size: u64,
download_directory: String,
) -> Result<(Download, Receiver<DownloadStatus>)> {
self.download_with_metadata(
filename,
username,
size,
download_directory,
DownloadMetadata::default(),
)
}
pub fn download_with_metadata(
&self,
filename: String,
username: String,
size: u64,
download_directory: String,
metadata: DownloadMetadata,
) -> Result<(Download, Receiver<DownloadStatus>)> {
info!("[client] Downloading {} from {}", filename, username);
let hash = md5::md5(&filename);
let token = u32::from_str_radix(&hash[0..5], 16)?;
let (download_sender, download_receiver): (
Sender<DownloadStatus>,
Receiver<DownloadStatus>,
) = mpsc::channel();
let download = Download {
username: username.clone(),
filename,
token,
size,
download_directory,
status: DownloadStatus::Queued,
sender: download_sender,
queue_position: None,
metadata,
};
let mut context = self.context.write_safe()?;
context.add_download(download.clone());
// If we already have a control connection to this peer, queue the
// upload immediately. Otherwise open one directly (server GetPeerAddress
// → outbound PeerInit → PeerConnected → the queued upload is flushed).
let peer_registered = context
.peer_registry
.as_ref()
.is_some_and(|r| r.contains(&username));
let queued_now = peer_registered
&& context.peer_registry.as_ref().is_some_and(|r| {
r.queue_upload(&username, download.filename.clone()).is_ok()
});
drop(context);
let failed = if peer_registered {
!queued_now
} else {
// No existing connection: initiate one. Only a genuinely
// unconnected client (no server handle) fails outright here.
self.connect_peer(&username).is_err()
};
if failed {
let reason = if peer_registered {
"The user declined the download"
} else {
"Could not connect to the user"
};
let _ = download
.sender
.send(DownloadStatus::Failed(Some(reason.to_string())));
self.context.write_safe()?.update_download_with_status(
token,
DownloadStatus::Failed(Some(reason.to_string())),
);
}
Ok((download, download_receiver))
}
/// Fail every still-`Queued` download for `username`, both on the caller's
/// status channel (so a blocked `Receiver` unblocks) and in the store.
pub(crate) fn fail_queued_downloads(
client_context: &Arc<RwLock<ClientContext>>,
username: &str,
) {
let mut context = match client_context.write_safe() {
Ok(c) => c,
Err(e) => {
error!("[client] fail_queued_downloads write: {}", e);
return;
}
};
let doomed: Vec<(u32, Sender<DownloadStatus>)> = context
.get_downloads()
.iter()
.filter(|d| {
d.username == username
&& matches!(d.status, DownloadStatus::Queued)
})
.map(|d| (d.token, d.sender.clone()))
.collect();
for (token, sender) in doomed {
let reason = Some("The user went offline".to_string());
let _ = sender.send(DownloadStatus::Failed(reason.clone()));
context.update_download_with_status(
token,
DownloadStatus::Failed(reason),
);
}
}
}
+660
View File
@@ -0,0 +1,660 @@
use crate::actor::ActorHandle;
use crate::actor::server_actor::{
PeerAddress, ServerActor, ServerMessage, UserMessage,
};
use crate::download_store::{DownloadStore, collect_failed_tokens};
use crate::types::{DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo};
use crate::utils::logger;
use crate::{
Transfer,
actor::{ActorSystem, peer_registry::PeerRegistry},
error::{Result, SoulseekRs},
message::peer::{FileEntry, SharedDirectory, build_file_search_response},
peer::{
ConnectionType, DownloadPeer, NewPeer, Peer, PeerMessage,
listen::Listen,
},
shares::Shares,
types::{Download, Search, SearchResult},
utils::{lock::RwLockExt, md5, thread_pool::ThreadPool},
};
use std::{
collections::HashMap,
net::TcpStream,
sync::{
RwLock,
atomic::{AtomicBool, AtomicU32, Ordering},
mpsc::{Receiver, Sender},
},
thread::{self, sleep},
};
use std::{
sync::{Arc, mpsc},
time::{Duration, Instant},
};
use crate::{debug, error, info, trace, warn};
const DEFAULT_LISTEN_PORT: u16 = 2234;
/// How long to wait for a server-brokered (firewalled) peer to connect back
/// before giving up and failing the download. Matches the direct-dial timeout.
const BROKER_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
/// Source of non-zero correlation tokens for server-brokered connections.
static NEXT_CONNECT_TOKEN: AtomicU32 = AtomicU32::new(1);
fn next_connect_token() -> u32 {
NEXT_CONNECT_TOKEN.fetch_add(1, Ordering::Relaxed).max(1)
}
/// Upload tokens are minted in the high half of the space so they never collide
/// with download tokens (md5-derived, always < 2^20).
static NEXT_UPLOAD_TOKEN: AtomicU32 = AtomicU32::new(0x8000_0000);
fn next_upload_token() -> u32 {
NEXT_UPLOAD_TOKEN.fetch_add(1, Ordering::Relaxed)
}
/// A file we have agreed to serve to a peer, awaiting their TransferResponse.
struct UploadJob {
downloader: String,
real_path: std::path::PathBuf,
}
/// Build a `FileSearchResponse` for `query` against `shares`, or `None` if
/// nothing matches. `own_username` is the name the searcher will download from.
fn build_search_response(
shares: &Shares,
own_username: &str,
token: u32,
query: &str,
) -> Option<crate::message::Message> {
let matches = shares.search(query);
if matches.is_empty() {
return None;
}
let entries: Vec<FileEntry> = matches
.iter()
.map(|f| FileEntry {
name: &f.virtual_path,
size: f.size,
attribs: &f.attributes,
})
.collect();
Some(build_file_search_response(
own_username,
token,
&entries,
1,
0,
))
}
#[derive(Debug, Clone)]
pub struct ClientSettings {
pub username: String,
pub password: String,
pub server_address: PeerAddress,
pub enable_listen: bool,
pub listen_port: u16,
/// Directory whose files are shared with (uploaded to) other peers.
/// `None` means nothing is shared.
pub shared_directory: Option<String>,
}
impl ClientSettings {
pub fn new(
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
Self {
username: username.into(),
password: password.into(),
..Default::default()
}
}
}
impl Default for ClientSettings {
fn default() -> Self {
Self {
username: String::new(),
password: String::new(),
server_address: PeerAddress::new(
"server.slsknet.org".to_string(),
2416,
),
enable_listen: true,
listen_port: DEFAULT_LISTEN_PORT,
shared_directory: None,
}
}
}
#[derive(Debug)]
pub enum ClientOperation {
NewPeer(NewPeer),
ConnectToPeer(Peer),
SearchResult(SearchResult),
PeerDisconnected(u64, String, Option<SoulseekRs>),
PierceFireWall(Peer),
DownloadFromPeer(u32, Peer, bool),
UpdateDownloadTokens(Transfer, String),
GetPeerAddressResponse {
username: String,
host: String,
port: u32,
obfuscation_type: u32,
obfuscated_port: u16,
},
UploadFailed(String, String),
PlaceInQueueUpdate {
username: String,
filename: String,
place: u32,
},
SetServerSender(Sender<ServerMessage>),
PrivateMessageReceived(UserMessage),
PeerConnected(String),
/// A search distributed to us by the server; reply if our shares match.
IncomingSearch {
username: String,
token: u32,
query: String,
},
/// A peer queued one of our shared files; `requester_key` is the registry
/// key of the peer actor (may carry a `:direct` suffix).
QueueUpload {
requester_key: String,
filename: String,
},
/// The peer accepted our upload offer for `token`; start streaming.
StartUpload {
token: u32,
},
/// A peer asked to browse our shared files; send our SharedFileListResponse.
ShareListRequested {
requester_key: String,
},
/// A peer we are browsing returned their shared-file listing.
BrowseResult {
username: String,
directories: Vec<SharedDirectory>,
},
/// A direct outbound connection to this peer failed before it was
/// established — the peer is likely firewalled, so fall back to asking the
/// server to broker the connection. Carries the reporting actor's id.
PeerConnectFailed(u64, String),
/// Something happened in the chat-room subsystem (list refreshed, a room
/// joined/left, a message said, a member joined/left).
RoomEvent(RoomEvent),
}
pub struct ClientContext {
pub peer_registry: Option<PeerRegistry>,
pub downloads: DownloadStore,
sender: Option<Sender<ClientOperation>>,
server_sender: Option<Sender<ServerMessage>>,
searches: HashMap<String, Search>,
private_messages: Vec<UserMessage>,
/// Correlation tokens for server-brokered (firewalled) connections, mapping
/// a token we sent in a ConnectToPeer to the peer we expect back.
pending_connect_tokens: HashMap<u32, String>,
/// Files we share with peers (read-only after connect).
pub shares: Arc<Shares>,
/// Peer listen addresses learned from GetPeerAddress responses.
peer_addresses: HashMap<String, (String, u32)>,
/// Peer messages waiting for a control connection to that peer.
pending_peer_messages: HashMap<String, Vec<crate::message::Message>>,
/// Uploads we have offered, keyed by our transfer token.
uploads: HashMap<u32, UploadJob>,
/// Upload tokens waiting for the downloader's address to be resolved.
pending_serves: HashMap<String, Vec<u32>>,
/// Shared-file listings received from peers we browsed.
browse_results: HashMap<String, Vec<SharedDirectory>>,
/// Latest snapshot of the public chat-room list (from `RoomList`, code 64).
room_list: Vec<RoomInfo>,
/// Chat-room events awaiting consumption by the client/UI.
room_events: Vec<RoomEvent>,
actor_system: Arc<ActorSystem>,
}
impl Default for ClientContext {
fn default() -> Self {
Self::new()
}
}
// Thin delegating shims so existing callers (peer/listen, peer/download_peer,
// tests) keep working while download state lives in DownloadStore.
impl ClientContext {
pub fn add_download(&mut self, download: Download) {
self.downloads.add(download);
}
pub fn remove_download(&mut self, token: u32) {
self.downloads.remove(token);
}
#[must_use]
pub fn get_download_by_token(&self, token: u32) -> Option<&Download> {
self.downloads.get_by_token(token)
}
pub fn get_download_by_token_mut(
&mut self,
token: u32,
) -> Option<&mut Download> {
self.downloads.get_by_token_mut(token)
}
pub fn get_download_by_file_mut(
&mut self,
username: &str,
filename: &str,
) -> Option<&mut Download> {
self.downloads.get_by_file_mut(username, filename)
}
#[must_use]
pub fn get_download_tokens(&self) -> Vec<u32> {
self.downloads.tokens()
}
#[must_use]
pub const fn get_downloads(&self) -> &Vec<Download> {
self.downloads.list()
}
pub fn update_download_with_status(
&mut self,
token: u32,
status: DownloadStatus,
) {
self.downloads.update_status(token, status);
}
pub fn remove_queued_download_by_file(
&mut self,
username: &str,
filename: &str,
) -> bool {
self.downloads.remove_queued_by_file(username, filename)
}
}
#[test]
fn test_client_context_downloads() {
let mut context = ClientContext::new();
let token = 123;
let new_token = 1234;
let download = Download {
username: "test".to_string(),
filename: "test.txt".to_string(),
token,
size: 100,
download_directory: "test".to_string(),
status: DownloadStatus::Queued,
sender: mpsc::channel().0,
queue_position: None,
metadata: DownloadMetadata::default(),
};
context.add_download(download);
assert!(context.get_download_by_token(123).is_some());
assert_eq!(context.get_download_tokens(), vec![123]);
assert_eq!(context.get_downloads().len(), 1);
if let Some(download) = context.get_download_by_token_mut(token) {
assert_eq!(download.token, token);
download.token = new_token;
}
assert!(context.get_download_by_token(new_token).is_some());
assert_eq!(context.get_download_tokens(), vec![new_token]);
context.remove_download(new_token);
assert_eq!(context.get_downloads().len(), 0);
assert!(context.get_download_by_token(1234).is_none());
}
#[test]
fn test_client_pause_and_resume_download() {
let client = Client::new("test-user", "test-password");
let (download_sender, download_receiver) = mpsc::channel();
let download = Download {
username: "peer".to_string(),
filename: "song.mp3".to_string(),
token: 123,
size: 100,
download_directory: "test".to_string(),
status: DownloadStatus::InProgress {
bytes_downloaded: 25,
total_bytes: 100,
speed_bytes_per_sec: 10.0,
},
sender: download_sender,
queue_position: None,
metadata: DownloadMetadata::default(),
};
client.context.write().unwrap().add_download(download);
assert!(client.pause_download("peer", "song.mp3"));
assert!(matches!(
client
.context
.read()
.unwrap()
.get_download_by_token(123)
.unwrap()
.status,
DownloadStatus::Paused {
bytes_downloaded: 25,
total_bytes: 100
}
));
assert!(matches!(
download_receiver.try_recv().unwrap(),
DownloadStatus::Paused {
bytes_downloaded: 25,
total_bytes: 100
}
));
assert!(client.resume_download("peer", "song.mp3"));
assert!(matches!(
client
.context
.read()
.unwrap()
.get_download_by_token(123)
.unwrap()
.status,
DownloadStatus::InProgress {
bytes_downloaded: 25,
total_bytes: 100,
speed_bytes_per_sec: 0.0
}
));
}
#[test]
fn download_without_a_connection_resolves_failed() {
// A client that never connected has no server handle and no peer registry,
// so it cannot open a connection to the peer: the download must resolve to
// Failed rather than hang Queued forever.
let client = Client::new("test-user", "test-password");
let (_download, receiver) = client
.download(
"song.mp3".to_string(),
"peer".to_string(),
100,
"test".to_string(),
)
.expect("download() should return a handle");
assert!(matches!(
receiver.recv_timeout(Duration::from_secs(1)),
Ok(DownloadStatus::Failed(_))
));
}
#[test]
fn fail_queued_downloads_notifies_receiver_and_store() {
// When a brokered connect times out, every Queued download for the peer
// must resolve to Failed both on its channel and in the store.
let client = Client::new("u", "p");
let (sender, receiver) = mpsc::channel();
client.context.write().unwrap().add_download(Download {
username: "peer".to_string(),
filename: "f.mp3".to_string(),
token: 7,
size: 10,
download_directory: "d".to_string(),
status: DownloadStatus::Queued,
sender,
queue_position: None,
metadata: DownloadMetadata::default(),
});
Client::fail_queued_downloads(&client.context, "peer");
assert!(matches!(receiver.try_recv(), Ok(DownloadStatus::Failed(_))));
assert!(matches!(
client
.context
.read()
.unwrap()
.get_download_by_token(7)
.unwrap()
.status,
DownloadStatus::Failed(_)
));
}
#[test]
fn build_search_response_matches_shares_and_echoes_token() {
let dir = std::env::temp_dir()
.join(format!("soulseek-searchresp-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("probe_xyzzy.bin"), b"data").unwrap();
let shares = Shares::scan(&dir).unwrap();
let response = build_search_response(&shares, "me", 99, "xyzzy")
.expect("a matching share yields a response");
let mut decoded =
crate::message::Message::new_with_data(response.get_buffer());
decoded.set_pointer(8);
let result = SearchResult::new_from_message(&mut decoded).unwrap();
assert_eq!(result.username, "me");
assert_eq!(result.token, 99);
assert!(result.files.iter().any(|f| f.name.contains("probe_xyzzy")));
assert!(build_search_response(&shares, "me", 1, "nomatch").is_none());
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn test_client_removes_only_queued_downloads() {
let client = Client::new("test-user", "test-password");
let queued_download = Download {
username: "peer".to_string(),
filename: "queued.mp3".to_string(),
token: 123,
size: 100,
download_directory: "test".to_string(),
status: DownloadStatus::Queued,
sender: mpsc::channel().0,
queue_position: None,
metadata: DownloadMetadata::default(),
};
let active_download = Download {
username: "peer".to_string(),
filename: "active.mp3".to_string(),
token: 456,
size: 100,
download_directory: "test".to_string(),
status: DownloadStatus::InProgress {
bytes_downloaded: 25,
total_bytes: 100,
speed_bytes_per_sec: 10.0,
},
sender: mpsc::channel().0,
queue_position: None,
metadata: DownloadMetadata::default(),
};
{
let mut context = client.context.write().unwrap();
context.add_download(queued_download);
context.add_download(active_download);
}
assert!(client.remove_queued_download("peer", "queued.mp3"));
assert!(!client.remove_queued_download("peer", "active.mp3"));
let context = client.context.read().unwrap();
assert!(context.get_download_by_token(123).is_none());
assert!(context.get_download_by_token(456).is_some());
}
impl ClientContext {
#[must_use]
pub fn new() -> Self {
let max_threads =
thread::available_parallelism().map_or(8, std::num::NonZero::get);
let thread_pool = Arc::new(ThreadPool::new(max_threads));
let actor_system = Arc::new(ActorSystem::new(thread_pool));
Self {
peer_registry: None,
sender: None,
server_sender: None,
searches: HashMap::new(),
private_messages: Vec::new(),
pending_connect_tokens: HashMap::new(),
shares: Arc::new(Shares::empty()),
peer_addresses: HashMap::new(),
pending_peer_messages: HashMap::new(),
uploads: HashMap::new(),
pending_serves: HashMap::new(),
browse_results: HashMap::new(),
room_list: Vec::new(),
room_events: Vec::new(),
downloads: DownloadStore::new(),
actor_system,
}
}
/// Apply a chat-room event: keep the room-list snapshot current and queue
/// the event for the client/UI to drain.
pub fn apply_room_event(&mut self, event: RoomEvent) {
if let RoomEvent::List(rooms) = &event {
self.room_list.clone_from(rooms);
}
self.room_events.push(event);
}
/// The latest snapshot of the public chat-room list.
#[must_use]
pub fn room_list(&self) -> Vec<RoomInfo> {
self.room_list.clone()
}
/// Remove and return all chat-room events received since the last call.
#[must_use]
pub fn take_room_events(&mut self) -> Vec<RoomEvent> {
std::mem::take(&mut self.room_events)
}
/// Cache a peer's listen address learned from a GetPeerAddress response.
pub fn cache_peer_address(
&mut self,
username: &str,
host: String,
port: u32,
) {
self.peer_addresses
.insert(username.to_string(), (host, port));
}
/// The cached listen address for `username`, if known.
#[must_use]
pub fn peer_address(&self, username: &str) -> Option<(String, u32)> {
self.peer_addresses.get(username).cloned()
}
/// Queue a peer message to send once a control connection to `username` is up.
pub fn queue_peer_message(
&mut self,
username: &str,
message: crate::message::Message,
) {
self.pending_peer_messages
.entry(username.to_string())
.or_default()
.push(message);
}
/// Remove and return the messages queued for `username`.
pub fn take_peer_messages(
&mut self,
username: &str,
) -> Vec<crate::message::Message> {
self.pending_peer_messages
.remove(username)
.unwrap_or_default()
}
/// Store a shared-file listing received from browsing `username`.
pub fn store_browse_result(
&mut self,
username: String,
directories: Vec<SharedDirectory>,
) {
self.browse_results.insert(username, directories);
}
/// Remove and return the shared-file listing browsed from `username`.
pub fn take_browse_result(
&mut self,
username: &str,
) -> Option<Vec<SharedDirectory>> {
self.browse_results.remove(username)
}
/// Remember that a server-brokered connection to `username` is pending under
/// `token`; the peer will quote it back in a PierceFirewall.
pub fn add_pending_connect(&mut self, token: u32, username: String) {
self.pending_connect_tokens.insert(token, username);
}
/// Resolve and consume the peer expected for a brokered connection `token`.
pub fn take_pending_connect(&mut self, token: u32) -> Option<String> {
self.pending_connect_tokens.remove(&token)
}
/// Record a private message received from another user.
pub fn push_private_message(&mut self, message: UserMessage) {
self.private_messages.push(message);
}
/// Remove and return all buffered private messages.
pub fn take_private_messages(&mut self) -> Vec<UserMessage> {
std::mem::take(&mut self.private_messages)
}
}
pub struct Client {
enable_listen: bool,
listen_port: u16,
address: PeerAddress,
username: String,
password: String,
shared_directory: Option<String>,
server_handle: Option<ActorHandle<ServerMessage>>,
context: Arc<RwLock<ClientContext>>,
}
impl Client {
pub fn new(
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
Self::with_settings(ClientSettings::new(username, password))
}
#[must_use]
pub fn with_settings(settings: ClientSettings) -> Self {
logger::init();
Self {
enable_listen: settings.enable_listen,
listen_port: settings.listen_port,
address: settings.server_address,
username: settings.username,
password: settings.password,
shared_directory: settings.shared_directory,
context: Arc::new(RwLock::new(ClientContext::new())),
server_handle: None,
}
}
/// The directory whose files are shared with other peers, if configured.
#[must_use]
pub fn shared_directory(&self) -> Option<&str> {
self.shared_directory.as_deref()
}
}
mod connection;
mod downloads;
mod operations;
mod rooms;
mod search;
mod uploads;
File diff suppressed because it is too large Load Diff
+174
View File
@@ -0,0 +1,174 @@
use super::{
Client, PeerMessage, Result, RoomEvent, RoomInfo, RwLockExt, ServerMessage,
SharedDirectory, SoulseekRs, UserMessage, error,
};
impl Client {
/// Send a private message to another user via the server.
///
/// # Errors
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
pub fn send_private_message(
&self,
username: &str,
message: &str,
) -> Result<()> {
let handle = self
.server_handle
.as_ref()
.ok_or(SoulseekRs::NotConnected)?;
let msg = crate::message::server::MessageFactory::build_message_user(
username, message,
);
handle
.send(ServerMessage::SendMessage(msg))
.map_err(|_| SoulseekRs::NotConnected)?;
Ok(())
}
/// Send a raw server message via the server actor, mapping a dead channel
/// to [`SoulseekRs::NotConnected`].
fn send_server_message(
&self,
message: crate::message::Message,
) -> Result<()> {
self.server_handle
.as_ref()
.ok_or(SoulseekRs::NotConnected)?
.send(ServerMessage::SendMessage(message))
.map_err(|_| SoulseekRs::NotConnected)?;
Ok(())
}
/// Ask the server for the list of public chat rooms. The response arrives
/// asynchronously; read it with [`Client::room_list`] or by draining
/// [`Client::take_room_events`] for a [`RoomEvent::List`].
///
/// # Errors
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
pub fn request_room_list(&self) -> Result<()> {
self.send_server_message(
crate::message::server::MessageFactory::build_room_list_request(),
)
}
/// Join a public chat room. The membership list and subsequent messages
/// arrive via [`Client::take_room_events`].
///
/// # Errors
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
pub fn join_room(&self, room: &str) -> Result<()> {
self.send_server_message(
crate::message::server::MessageFactory::build_join_room(
room, false,
),
)
}
/// Leave a chat room previously joined with [`Client::join_room`].
///
/// # Errors
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
pub fn leave_room(&self, room: &str) -> Result<()> {
self.send_server_message(
crate::message::server::MessageFactory::build_leave_room(room),
)
}
/// Say `message` in chat room `room`. The server echoes it back as a
/// [`RoomEvent::Message`], so the UI should render from that echo rather
/// than optimistically.
///
/// # Errors
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
pub fn say_in_room(&self, room: &str, message: &str) -> Result<()> {
self.send_server_message(
crate::message::server::MessageFactory::build_say_chatroom(
room, message,
),
)
}
/// The latest snapshot of the public chat-room list.
#[must_use]
pub fn room_list(&self) -> Vec<RoomInfo> {
match self.context.read_safe() {
Ok(ctx) => ctx.room_list(),
Err(e) => {
error!("[client] room_list: {}", e);
Vec::new()
}
}
}
/// Remove and return all chat-room events received since the last call.
#[must_use]
pub fn take_room_events(&self) -> Vec<RoomEvent> {
match self.context.write_safe() {
Ok(mut ctx) => ctx.take_room_events(),
Err(e) => {
error!("[client] take_room_events: {}", e);
Vec::new()
}
}
}
/// Request a peer's shared-file listing. When it arrives it can be
/// retrieved with [`Client::take_browse_result`].
///
/// # Errors
/// Returns an error if the client's context lock is poisoned.
pub fn browse_user(&self, username: &str) -> Result<()> {
let request =
crate::message::server::MessageFactory::build_get_share_file_list();
let (connected, registry) = {
let ctx = self.context.read_safe()?;
(
ctx.peer_registry
.as_ref()
.is_some_and(|r| r.contains(username)),
ctx.peer_registry.clone(),
)
};
if connected {
if let Some(registry) = registry {
let _ = registry
.send_to_peer(username, PeerMessage::SendMessage(request));
}
} else {
self.context
.write_safe()?
.queue_peer_message(username, request);
if let Some(handle) = &self.server_handle {
let _ = handle
.send(ServerMessage::GetPeerAddress(username.to_string()));
}
}
Ok(())
}
/// Remove and return a peer's shared-file listing requested via
/// [`Client::browse_user`], if it has arrived.
#[must_use]
pub fn take_browse_result(
&self,
username: &str,
) -> Option<Vec<SharedDirectory>> {
self.context
.write_safe()
.ok()
.and_then(|mut ctx| ctx.take_browse_result(username))
}
/// Remove and return all private messages received since the last call.
#[must_use]
pub fn take_private_messages(&self) -> Vec<UserMessage> {
match self.context.write_safe() {
Ok(mut ctx) => ctx.take_private_messages(),
Err(e) => {
error!("[client] take_private_messages: {}", e);
Vec::new()
}
}
}
}
+105
View File
@@ -0,0 +1,105 @@
use super::{
Arc, AtomicBool, Client, Duration, HashMap, Instant, Ordering, Result,
RwLockExt, Search, SearchResult, ServerMessage, SoulseekRs, info, md5,
sleep,
};
impl Client {
pub fn search(
&self,
query: &str,
timeout: Duration,
) -> Result<Vec<SearchResult>> {
self.search_with_cancel(query, timeout, None)
}
pub fn search_with_cancel(
&self,
query: &str,
timeout: Duration,
cancel_flag: Option<Arc<AtomicBool>>,
) -> Result<Vec<SearchResult>> {
info!("Searching for {}", query);
if let Some(handle) = &self.server_handle {
let hash = md5::md5(query);
let token = u32::from_str_radix(&hash[0..5], 16)?;
self.context.write_safe()?.searches.insert(
query.to_string(),
Search {
token,
results: Vec::new(),
},
);
let _ = handle.send(ServerMessage::FileSearch {
token,
query: query.to_string(),
});
} else {
return Err(SoulseekRs::NotConnected);
}
let start = Instant::now();
loop {
sleep(Duration::from_millis(100));
// Check if cancelled
if let Some(ref flag) = cancel_flag
&& flag.load(Ordering::Relaxed)
{
info!("Search cancelled by user");
break;
}
// Check if timeout reached
if start.elapsed() >= timeout {
break;
}
}
Ok(self.get_search_results(query))
}
#[must_use]
pub fn get_search_results_count(&self, search_key: &str) -> usize {
self.context
.read_safe()
.ok()
.and_then(|ctx| {
ctx.searches.get(search_key).map(|s| s.results.len())
})
.unwrap_or(0)
}
#[must_use]
pub fn get_search_results(&self, search_key: &str) -> Vec<SearchResult> {
self.context
.read_safe()
.ok()
.and_then(|ctx| {
ctx.searches.get(search_key).map(|s| s.results.clone())
})
.unwrap_or_default()
}
/// Non-blocking variant that returns None if the lock is unavailable
#[must_use]
pub fn try_get_search_results(
&self,
search_key: &str,
) -> Option<Vec<SearchResult>> {
self.context.try_read().ok().and_then(|ctx| {
ctx.searches.get(search_key).map(|s| s.results.clone())
})
}
#[must_use]
pub fn get_all_searches(&self) -> HashMap<String, Search> {
self.context
.read_safe()
.map(|ctx| ctx.searches.clone())
.unwrap_or_default()
}
}
+70
View File
@@ -0,0 +1,70 @@
use super::{
Arc, Client, ClientContext, DownloadStatus, RwLock, RwLockExt,
collect_failed_tokens, error, thread,
};
impl Client {
/// 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;
};
drop(ctx);
let own = own_username.to_string();
let real_path = job.real_path;
thread::spawn(move || {
if let Err(e) = crate::peer::upload_peer::serve_file(
&host, port, &own, token, &real_path,
) {
error!("[client] serve {}: {}", real_path.display(), e);
}
});
}
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);
}
}
}
}
+1 -4
View File
@@ -57,10 +57,8 @@ impl Message {
let chunks = data.chunks(BYTES_PER_LINE);
for (i, chunk) in chunks.enumerate() {
// Print the offset
print!("{:04x} ", i * BYTES_PER_LINE);
// Print the hexadecimal part
for j in 0..BYTES_PER_LINE {
if j < chunk.len() {
print!("{:02x} ", chunk[j]);
@@ -76,7 +74,6 @@ impl Message {
print!(" ");
// Print the ASCII part
let mut i = 0;
for &byte in chunk {
i += 1;
@@ -101,7 +98,7 @@ impl Message {
if self.data.len() < 8 {
return 0;
}
u32::from_le_bytes(self.data[4..8].try_into().unwrap())
self.data[4..8].try_into().map_or(0, u32::from_le_bytes)
}
#[must_use]
@@ -59,9 +59,7 @@ impl MessageHandler<PeerMessage> for FileSearchResponse {
return;
};
sender
.send(PeerMessage::FileSearchResult(file_search))
.unwrap();
let _ = sender.send(PeerMessage::FileSearchResult(file_search));
}
}
@@ -23,6 +23,6 @@ impl MessageHandler<PeerMessage> for PeerInit {
username, connection_type, token
);
sender.send(PeerMessage::SetUsername(username)).unwrap();
let _ = sender.send(PeerMessage::SetUsername(username));
}
}
@@ -15,8 +15,7 @@ impl MessageHandler<PeerMessage> for PlaceInQueueResponse {
let filename = message.read_string();
let place = message.read_int32();
sender
.send(PeerMessage::PlaceInQueueResponse { filename, place })
.unwrap();
let _ =
sender.send(PeerMessage::PlaceInQueueResponse { filename, place });
}
}
@@ -13,6 +13,6 @@ impl MessageHandler<PeerMessage> for TransferRequest {
fn handle(&self, message: &mut Message, sender: Sender<PeerMessage>) {
let transfer = Transfer::new_from_message(message);
sender.send(PeerMessage::TransferRequest(transfer)).unwrap();
let _ = sender.send(PeerMessage::TransferRequest(transfer));
}
}
@@ -16,12 +16,10 @@ impl MessageHandler<PeerMessage> for TransferResponse {
let allowed = message.read_int8();
let reason = (allowed == 0).then(|| message.read_string());
sender
.send(PeerMessage::TransferResponse {
token,
allowed: allowed == 1,
reason,
})
.unwrap();
let _ = sender.send(PeerMessage::TransferResponse {
token,
allowed: allowed == 1,
reason,
});
}
}
@@ -24,14 +24,12 @@ impl MessageHandler<ServerMessage> for GetPeerAddressHandler {
let obfuscated_port = message.read_int32() as u16;
crate::debug!("GetPeerAddressHandler: {username:?}");
sender
.send(ServerMessage::GetPeerAddressResponse {
username,
host,
port,
obfuscation_type,
obfuscated_port,
})
.unwrap();
let _ = sender.send(ServerMessage::GetPeerAddressResponse {
username,
host,
port,
obfuscation_type,
obfuscated_port,
});
}
}
+3 -2
View File
@@ -16,13 +16,14 @@ impl MessageHandler<ServerMessage> for LoginHandler {
let response = message.read_int8();
if response != 1 {
return sender.send(ServerMessage::LoginStatus(false)).unwrap();
let _ = sender.send(ServerMessage::LoginStatus(false));
return;
}
info!("Login successful");
let greeting = message.read_string();
debug!("Server greeting: {:?}", greeting);
sender.send(ServerMessage::LoginStatus(true)).unwrap();
let _ = sender.send(ServerMessage::LoginStatus(true));
}
}
+4 -5
View File
@@ -380,10 +380,9 @@ impl DownloadPeer {
status: DownloadStatus,
) {
let _ = download.sender.send(status.clone());
client_context
.write()
.unwrap()
.update_download_with_status(download.token, status);
if let Ok(mut context) = client_context.write() {
context.update_download_with_status(download.token, status);
}
}
fn wait_while_paused(
@@ -491,7 +490,7 @@ impl DownloadPeer {
let _ = dl.sender.send(DownloadStatus::Queued);
client_context
.write()
.unwrap()
.map_err(|_| DownloadError::LockPoisoned)?
.update_download_with_status(dl.token, DownloadStatus::Queued);
}
+3 -1
View File
@@ -96,7 +96,9 @@ pub fn log(level: LogLevel, message: &str) {
};
let now = std::time::SystemTime::now();
let datetime = now.duration_since(std::time::UNIX_EPOCH).unwrap();
let datetime = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let secs = datetime.as_secs();
let subsec_millis = datetime.subsec_millis();
-4
View File
@@ -297,7 +297,6 @@ fn compute_md5_digest(mut v: Vec<u8>) -> String {
let word_cc = word_c;
let word_dd = word_d;
// execute round 1
let result =
round_one_operations(word_a, word_b, word_c, word_d, &table, &x);
word_a = result[0];
@@ -305,7 +304,6 @@ fn compute_md5_digest(mut v: Vec<u8>) -> String {
word_c = result[2];
word_d = result[3];
// execute round 2
let result =
round_two_operations(word_a, word_b, word_c, word_d, &table, &x);
@@ -314,7 +312,6 @@ fn compute_md5_digest(mut v: Vec<u8>) -> String {
word_c = result[2];
word_d = result[3];
// execute round 3
let result =
round_three_operations(word_a, word_b, word_c, word_d, &table, &x);
word_a = result[0];
@@ -322,7 +319,6 @@ fn compute_md5_digest(mut v: Vec<u8>) -> String {
word_c = result[2];
word_d = result[3];
// execute round 4
let result =
round_four_operations(word_a, word_b, word_c, word_d, &table, &x);
word_a = result[0];
+5 -10
View File
@@ -184,17 +184,12 @@ impl HuffmanTree {
let mut node = &mut self.root;
for i in (0..n).rev() {
let b = (codeword >> i) & 1;
if b != 0 {
if node.right.is_none() {
node.right = Some(Box::new(Node::new()));
}
node = node.right.as_mut().unwrap();
let child = if b != 0 {
&mut node.right
} else {
if node.left.is_none() {
node.left = Some(Box::new(Node::new()));
}
node = node.left.as_mut().unwrap();
}
&mut node.left
};
node = child.get_or_insert_with(|| Box::new(Node::new()));
}
node.symbol = Some(symbol);
}