fix!: make concurrent invocations work instead of silently seeing nothing (#22)
Running several one-shot commands at once returned "no results" for queries that had plenty. Four defects, each of which made a dead session look like an empty network: - The peer listener bound its port inside a detached thread and panicked on EADDRINUSE, unwinding that thread alone. The process carried on with no listener, so no peer could deliver a search response. `Listen::bind` now takes the socket before login and falls back to an ephemeral port when the configured one is held. - SetWaitPort advertised the configured port whether or not it was bound, so peers dialled a sibling process that discarded responses to tickets it never issued. The port the listener really holds is what gets advertised, mapped through UPnP, and reported by `whoami` and `serve`. - A zero-byte read is EOF on a TCP socket, but MessageReader reported it as success, so a server that closed cleanly left the actor polling a dead socket. It is now an error, and Relogged (code 41) has a handler. The peer actor classifies that error: a control connection that was established and then closed is a clean disconnect, because peers drop idle sockets while the bytes move over their own connection, and reporting an error there fails every download queued with that peer including the one streaming. A connection that never established keeps the error path, which is what the server-brokered fallback reads to know a peer is unreachable. - A session the server cut off - usually because another process logged in under the same name - exited 4, documented as "it worked and found nothing". Callers concluded the file was unavailable. Sessions now record why they ended and those verdicts become exit 7. The server allows one session per account, so concurrency still needs a distinct --username per run; SKILL.md documents that. The listener port needs no such care any more. Six e2e tests against soulfind cover it, each of which fails without the fix: a taken listen port, a displaced session, three clients searching at once under one configured port, the two CLI equivalents, and a peer that hangs up its control connection while a transfer is still to come. BREAKING CHANGE: `Listen::start` is replaced by `Listen::bind` and `Listen::serve`, so the caller owns the socket and can read the port it actually bound. `Client::listen_port` and `Client::session_loss` are new.
This commit is contained in:
@@ -39,8 +39,14 @@ impl MessageReader {
|
||||
) -> io::Result<()> {
|
||||
let mut temp_buffer = [0; 1024]; // Temporary buffer for reading from the socket
|
||||
let bytes_read = stream.read(&mut temp_buffer)?;
|
||||
// On a TCP socket a zero-byte read is the other end hanging up. Passing
|
||||
// that back as success left the caller polling a dead socket forever,
|
||||
// which is how a server that closes cleanly went unnoticed.
|
||||
if bytes_read == 0 {
|
||||
return Ok(());
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"the remote end closed the connection",
|
||||
));
|
||||
}
|
||||
|
||||
// Add the read bytes to the internal buffer
|
||||
@@ -121,4 +127,32 @@ mod tests {
|
||||
assert!(buffered_reader.buffer.is_empty());
|
||||
assert_eq!(vec![1, 2, 3], rest);
|
||||
}
|
||||
|
||||
/// A hangup has to reach the caller as an error. Reported as success it
|
||||
/// leaves the actors polling a socket that will never speak again, which is
|
||||
/// how a server that closed cleanly went unnoticed.
|
||||
#[test]
|
||||
fn a_closed_connection_reads_as_an_error_not_as_success() {
|
||||
use std::io::Write;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let mut ours = TcpStream::connect(addr).unwrap();
|
||||
let (mut theirs, _) = listener.accept().unwrap();
|
||||
|
||||
theirs.write_all(&[1, 2, 3]).unwrap();
|
||||
theirs.flush().unwrap();
|
||||
drop(theirs);
|
||||
|
||||
let mut reader = MessageReader::new();
|
||||
// Whatever was sent before the close still arrives.
|
||||
while reader.buffer_len() < 3 {
|
||||
reader.read_from_socket(&mut ours).unwrap();
|
||||
}
|
||||
let error = reader
|
||||
.read_from_socket(&mut ours)
|
||||
.expect_err("a closed connection is not a successful read");
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ mod message_user;
|
||||
mod parent_min_speed;
|
||||
mod parent_speed_ratio;
|
||||
mod privileged_users;
|
||||
mod relogged;
|
||||
mod room_list;
|
||||
mod say_chatroom;
|
||||
mod user_info;
|
||||
@@ -29,6 +30,7 @@ pub use message_user::MessageUser;
|
||||
pub use parent_min_speed::ParentMinSpeedHandler;
|
||||
pub use parent_speed_ratio::ParentSpeedRatioHandler;
|
||||
pub use privileged_users::{CheckPrivilegesHandler, PrivilegedUsersHandler};
|
||||
pub use relogged::ReloggedHandler;
|
||||
pub use room_list::RoomListHandler;
|
||||
pub use say_chatroom::SayChatroomHandler;
|
||||
pub use user_info::{GetUserStatsHandler, GetUserStatusHandler};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
use crate::actor::server_actor::ServerMessage;
|
||||
use crate::message::{Message, MessageHandler};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
/// `Relogged` (code 41): the server is about to cut this connection because
|
||||
/// the same username logged in somewhere else.
|
||||
pub struct ReloggedHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for ReloggedHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
41
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let _ = message;
|
||||
let _ = sender.send(ServerMessage::Relogged);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user