Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ In the relay's window:

| Symptom | Cause | What to do |
|---|---|---|
| `Connect TCP` appears to do nothing and the status stays `false` | The connection failed. No error is displayed, and pressing `Connect TCP` again fails silently because the previous thread still exists. | Press `Disconnect TCP` first, then `Connect TCP`. This is the most common first-time snag. |
| `Connect TCP` fails and the status stays `false` | Nothing is listening at that address, or the address is wrong | Read the reason in the error banner and the event log, fix the address, then press `Connect TCP` again. `Disconnect TCP` first is no longer needed |
| `:( No Baton Connection` | The plugin is not loaded, or its window is closed | Check `baton_debug.log` in your temp folder (`%TEMP%` on Windows). Every connection attempt is logged there |
| Data stops part-way through a flight | The "Positional Flight Data" window was closed or hidden (Windows/Linux only) | Reopen it |
| The relay runs but never picks up the plugin | The socket is already in use. A second relay instance, or a stale `baton.sock`. The relay logs this once, then gives up silently. | `Disconnect IPC`, then `Connect IPC`. Or restart the relay |
Expand Down
26 changes: 24 additions & 2 deletions relay/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,14 @@ impl State {
stream
}
Err(e) => {
human_log("TCP", &format!("Connection failed: {}", e));
bail!("Failed to connect to TCP");
let reason = format!("TCP connection failed: {}", e);
human_log("TCP", &reason);
let _ = child_bichannel.set_is_conn_to_endpoint(false);
// Report it so the GUI can show the failure and drop this
// thread, instead of the error only reaching stderr.
let _ = child_bichannel
.send_to_parent(FromTcpThreadMessage::Disconnected(reason.clone()));
bail!(reason);
}
};

Expand Down Expand Up @@ -653,6 +659,9 @@ impl State {
});

self.tcp_thread_handle = Some(tcp_thread_handle);
// Clear any banner left by an earlier failure, so a successful retry
// does not keep showing the old error.
self.error_message = None;
Ok(())
}

Expand All @@ -679,6 +688,19 @@ impl State {
Ok(res?)
}

// Drops a TCP thread that has already exited, so tcp_connect is allowed
// to start a new one. The join runs detached because the thread returned
// an error and its Result is of no use here.
pub fn reap_tcp_thread(&mut self) {
if let Some(handle) = self.tcp_thread_handle.take() {
spawn(move || {
let _ = handle.join();
});
}
self.tcp_bichannel = None;
self.tcp_connected = false;
}

pub fn is_tcp_connected(&self) -> bool {
if let Some(status) = self
.tcp_bichannel
Expand Down
96 changes: 79 additions & 17 deletions relay/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,30 @@ pub(crate) fn update(state: &mut State, message: Message) -> Task<Message> {
}

// process TCP messages
if let Some(tcp_bichannel) = &state.tcp_bichannel {
for msg in tcp_bichannel.received_messages() {
match msg {
FromTcpThreadMessage::Connected => {
state.log_event("TCP connected to iMotions".into());
state.tcp_connected = true;
}
FromTcpThreadMessage::Disconnected(reason) => {
state.log_event(format!("TCP disconnected: {}", reason));
state.tcp_connected = false;
}
FromTcpThreadMessage::Sent(bytes) => {
state.on_tcp_packet_sent(bytes);
}
FromTcpThreadMessage::SendError(err) => {
state.log_event(format!("TCP send error: {}", err));
}
let tcp_messages = state
.tcp_bichannel
.as_ref()
.map(|bichannel| bichannel.received_messages())
.unwrap_or_default();
for msg in tcp_messages {
match msg {
FromTcpThreadMessage::Connected => {
state.log_event("TCP connected to iMotions".into());
state.tcp_connected = true;
}
FromTcpThreadMessage::Disconnected(reason) => {
state.log_event(format!("TCP disconnected: {}", reason));
state.error_message = Some(reason);
state.tcp_connected = false;
// The thread has exited. Clear it so Connect TCP works
// again without pressing Disconnect TCP first.
state.reap_tcp_thread();
}
FromTcpThreadMessage::Sent(bytes) => {
state.on_tcp_packet_sent(bytes);
}
FromTcpThreadMessage::SendError(err) => {
state.log_event(format!("TCP send error: {}", err));
}
}
}
Expand Down Expand Up @@ -346,6 +353,61 @@ mod tests {
let _ = state.tcp_disconnect();
}

// Pumps the Update message the GUI subscription would send, so queued
// thread messages are processed.
fn pump_update(state: &mut State, ticks: usize) {
for _ in 0..ticks {
let _ = update(state, Message::Update);
sleep(StdDuration::from_millis(10));
}
}

// A port nothing listens on, obtained by binding and then dropping.
fn closed_port_addr() -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind failed");
let addr = listener.local_addr().expect("local_addr failed").to_string();
drop(listener);
addr
}

#[test]
fn failed_connect_surfaces_an_error_and_allows_a_retry() {
let mut state = State::default();
state
.tcp_connect(closed_port_addr())
.expect("tcp_connect should spawn a thread");

pump_update(&mut state, 200);

let error = state.error_message.clone().expect("no error surfaced");
assert!(
error.starts_with("TCP connection failed:"),
"unexpected error: {error}"
);
assert!(!state.tcp_connected);
assert!(state.tcp_thread_handle.is_none(), "dead thread was kept");
assert!(
state.event_log.iter().any(|e| e.contains("TCP disconnected")),
"failure missing from the event log: {:?}",
state.event_log
);

// The retry is the point: this used to fail with "TCP thread already
// exists" until Disconnect TCP was pressed.
let listener = TcpListener::bind("127.0.0.1:0").expect("bind failed");
let addr = listener.local_addr().expect("local_addr failed").to_string();
state.tcp_connect(addr).expect("retry was rejected");
let _ = listener.accept().expect("accept failed");
assert!(wait_until(|| state.is_tcp_connected()), "retry never connected");
assert!(
state.error_message.is_none(),
"stale failure banner after a successful retry: {:?}",
state.error_message
);

let _ = state.tcp_disconnect();
}

#[test]
fn send_packet_without_tcp_reports_an_error() {
let mut state = State::default();
Expand Down
Loading