Files
soulseek-rs-modded/soulseek-rs-lib/src/client/connection.rs
T
Michel de Graaf 727e2bc8db chore: satisfy clippy 1.99
CI's Lint job runs current stable, which gained two lints that failed
develop: `redundant_else` in `Client::login`, now written with early
returns, and `assert_is_empty`, which asks every `assert!(x.is_empty())`
in the tests to become `assert_eq!(x, [] as [T; 0])`. The second trades a
clear check for a type-cast chore, so it joins the curated allow-list.
2026-10-26 17:16:18 +01:00

428 lines
15 KiB
Rust

use super::{
Arc, AtomicBool, Client, ClientContext, ClientOperation, ConnectionType,
DownloadPeer, Listen, Peer, PeerRegistry, Receiver, Result, RwLock,
RwLockExt, Sender, ServerActor, ServerMessage, Shares, SoulseekRs,
TcpStream, error, info, mpsc, thread, trace, warn,
};
use std::sync::atomic::Ordering;
use std::sync::mpsc::RecvTimeoutError;
use std::time::{Duration, Instant};
const LISTENER_RELEASE_TIMEOUT: Duration = Duration::from_secs(2);
const LOGIN_POLL_INTERVAL: Duration = Duration::from_millis(100);
/// Ceiling on the wait for a login verdict. Generous enough for a slow server
/// and a retrying connect, short enough that an unattended caller ends.
const LOGIN_RESPONSE_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(45);
/// Soft open-file limit below which a session should expect "too many open
/// files": every peer holds a socket, and one busy search talks to hundreds
/// of them.
const COMFORTABLE_OPEN_FILES: u64 = 1024;
/// Say up front when the process cannot hold a busy session's sockets, so an
/// operator reads a warning with a number in it instead of an EMFILE storm an
/// hour later. The embedding process owns its rlimits; the library only looks.
#[cfg(unix)]
fn warn_if_open_file_limit_is_low() {
// SAFETY: getrlimit only fills the struct it is handed.
let limit = unsafe {
let mut limit = std::mem::zeroed::<libc::rlimit>();
if libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut limit) != 0 {
return;
}
limit.rlim_cur
};
if limit < COMFORTABLE_OPEN_FILES {
warn!(
"only {limit} open files allowed; a busy session wants \
{COMFORTABLE_OPEN_FILES} or more — expect \"too many open \
files\" under load"
);
}
}
#[cfg(not(unix))]
fn warn_if_open_file_limit_is_low() {}
impl Client {
pub fn connect(&mut self) -> Result<()> {
warn_if_open_file_limit_is_low();
let (sender, message_reader): (
Sender<ClientOperation>,
Receiver<ClientOperation>,
) = mpsc::channel();
let mut ctx = self.context.write_safe()?;
let peer_registry = PeerRegistry::with_capacity(
ctx.actor_system.clone(),
sender.clone(),
self.username.clone(),
ctx.max_peers.clone(),
);
ctx.peer_registry = Some(peer_registry);
ctx.operations = Some(sender.clone());
let listen_sender = sender.clone();
// Scan the shared directories once into the read-only index, and
// report the real folder/file counts to the server on login.
let roots: Vec<std::path::PathBuf> = self
.shared_directories
.iter()
.filter(|dir| !dir.trim().is_empty())
.map(std::path::PathBuf::from)
.collect();
let shares = if roots.is_empty() {
Arc::new(Shares::empty())
} else {
let scanned = Shares::scan_many(&roots);
info!(
"Sharing {} files in {} folders from {} directories",
scanned.file_count(),
scanned.folder_count(),
roots.len()
);
Arc::new(scanned)
};
let shared_folder_count = shares.folder_count();
let shared_file_count = shares.file_count();
ctx.shares = shares;
ctx.shared_directories.clone_from(&self.shared_directories);
if self.cancel.is_cancelled() {
return Err(SoulseekRs::NotConnected);
}
// Bind before logging in: the port we advertise has to be the port we
// hold, and a bind that fails outright is the caller's to see rather
// than a panic on a thread nobody is watching.
let listener = if self.enable_listen {
let listener = Listen::bind(self.listen_port)?;
self.bound_port = Some(listener.local_addr()?.port());
Some(listener)
} else {
None
};
self.listener_stopped = Arc::new(AtomicBool::new(false));
let mut server_actor = ServerActor::new(
self.address.clone(),
sender,
self.bound_port.unwrap_or(self.listen_port),
self.enable_listen,
shared_folder_count,
shared_file_count,
);
server_actor.set_session_watch(self.session.clone());
server_actor.set_cancel(self.cancel.clone());
self.server_handle = Some(ctx.actor_system.spawn_with_handle(
server_actor,
|actor, handle| {
actor.set_self_handle(handle);
},
));
if let Some(listener) = listener {
let client_sender = listen_sender;
let context = self.context.clone();
let own_username = self.username.clone();
let stopped = self.listener_stopped.clone();
let released = Arc::new(AtomicBool::new(false));
self.listener_released = released.clone();
thread::spawn(move || {
Listen::serve(
&listener,
client_sender,
context,
own_username,
stopped,
);
drop(listener);
released.store(true, Ordering::Relaxed);
});
}
Self::listen_to_client_operations(
message_reader,
self.context.clone(),
self.username.clone(),
);
Ok(())
}
/// Close the server connection and every peer connection.
///
/// The server sees the socket close and marks us offline, which is what
/// releases the username: a client that simply goes out of scope without
/// this leaves a session the server still believes in, so mail addressed
/// to us is delivered to a connection nobody is reading and later logins
/// collide with our own ghost.
///
/// Called automatically when the client is dropped. The peer listener's
/// bound port, if one was opened, is free again when this returns, unless
/// the listener cannot be woken within two seconds.
pub fn disconnect(&mut self) {
if let Some(handle) = self.server_handle.take() {
let _ = handle.stop();
}
let registry = match self.context.write_safe() {
Ok(mut ctx) => {
// Dropping our end of the operations channel is what lets its
// thread finish once the peers and the listener have gone.
ctx.operations = None;
ctx.peer_registry.clone()
}
Err(_) => None,
};
if let Some(registry) = registry {
registry.stop_all();
}
// The listener thread is parked in accept(), so it only notices the
// flag when a connection arrives: dial it once ourselves. Without
// this the port stays bound for the life of the process, and the next
// client falls back to an ephemeral port nobody was told about.
self.listener_stopped.store(true, Ordering::Relaxed);
if let Some(port) = self.bound_port.take()
&& TcpStream::connect(("127.0.0.1", port)).is_ok()
{
let deadline = Instant::now() + LISTENER_RELEASE_TIMEOUT;
while !self.listener_released.load(Ordering::Relaxed)
&& Instant::now() < deadline
{
thread::sleep(Duration::from_millis(5));
}
}
}
/// Log in and wait for the server's verdict.
///
/// The wait is bounded: the actor only answers once it has a working
/// connection, so a server that never completes the TCP handshake would
/// otherwise block the caller forever. A caller that hits
/// [`SoulseekRs::Timeout`] can retry or give up, but it always regains
/// control.
pub fn login(&self) -> Result<bool> {
info!("Logging in as {}", self.username);
let Some(handle) = &self.server_handle else {
return Err(SoulseekRs::NotConnected);
};
if self.cancel.is_cancelled() {
return Err(SoulseekRs::NotConnected);
}
let (tx, rx) = std::sync::mpsc::channel();
let _ = handle.send(ServerMessage::Login {
username: self.username.clone(),
password: self.password.clone(),
version: self.version,
response: tx,
});
let deadline = Instant::now() + LOGIN_RESPONSE_TIMEOUT;
loop {
match rx.recv_timeout(LOGIN_POLL_INTERVAL) {
Ok(result) => return result,
Err(RecvTimeoutError::Timeout)
if self.cancel.is_cancelled() =>
{
return Err(SoulseekRs::NotConnected);
}
Err(RecvTimeoutError::Timeout) if Instant::now() < deadline => {
}
Err(_) => return Err(SoulseekRs::Timeout),
}
}
}
/// 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(())
}
pub fn set_max_peers(&self, max_peers: usize) {
if let Ok(ctx) = self.context.read_safe() {
ctx.max_peers
.store(max_peers.max(1), std::sync::atomic::Ordering::Relaxed);
}
}
#[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 register_peer_or_fail(
client_context: &Arc<RwLock<ClientContext>>,
peer: Peer,
stream: Option<TcpStream>,
reader: Option<crate::message::MessageReader>,
) -> bool {
let username = peer.username.clone();
let registered = {
let context = match client_context.read_safe() {
Ok(c) => c,
Err(e) => {
error!("[client] register peer read: {}", e);
return false;
}
};
if let Some(ref registry) = context.peer_registry {
let protected = context.protected_peers();
match registry
.register_peer_protected(peer, stream, reader, &protected)
{
Ok(_) => true,
Err(e) => {
warn!(
"[client] refusing peer connection for {:?}: {:?}",
username, e
);
false
}
}
} else {
trace!("PeerRegistry not initialized");
false
}
};
if !registered {
Self::fail_queued_downloads(client_context, &username);
}
registered
}
/// Tell the server we could not reach the peer it asked us to connect to,
/// quoting the token that peer is waiting on.
pub(crate) fn report_cant_connect(
client_context: &Arc<RwLock<ClientContext>>,
token: u32,
username: &str,
) {
let sender = match client_context.read_safe() {
Ok(ctx) => ctx.server_sender.clone(),
Err(_) => return,
};
let Some(sender) = sender else { return };
let message =
crate::message::server::MessageFactory::build_cant_connect_to_peer(
token, username,
);
let _ = sender.send(ServerMessage::SendMessage(message));
}
pub(crate) fn connect_to_peer(
peer: Peer,
client_context: Arc<RwLock<ClientContext>>,
own_username: String,
stream: Option<TcpStream>,
) {
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 => {
Self::register_peer_or_fail(
&client_context,
peer_clone,
stream,
None,
);
}
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,
);
let brokered = peer_clone.brokered;
let username = peer_clone.username;
match download_peer.download_file(
client_context.clone(),
None,
None,
) {
Ok((download, filename)) => {
trace!(
"[client] downloaded {} bytes {:?} ",
filename, download.size
);
}
Err(e) => {
trace!("[client] failed to download: {}", e);
// A file connection the server asked us to make and
// we could not complete owes that peer a
// CantConnectToPeer, exactly as a control connection
// does: it is waiting on this token.
if brokered {
Self::report_cant_connect(
&client_context,
token,
&username,
);
}
}
}
}
ConnectionType::D => {
// ponytail: we never dial out to adopt a child — children
// reach us through the listener. Tell the peer rather than
// leave it waiting; wire the outbound half if firewalled
// children turn out to matter.
if let (true, Some(token)) = (peer.brokered, peer.token) {
Self::report_cant_connect(
&client_context,
token,
&peer.username,
);
}
}
}
}
}