Say when there is nobody to contact
Two freshly wiped devices joined the same network, each got an address, and nothing happened. The report said "none known yet; nobody else has joined", which reads as patience — as if somebody were expected and had not arrived. What had actually happened is that neither agent had anywhere to look: this project publishes nothing about who is in which network, so a first meeting needs one of them to be told the other's endpoint id, and the hints an agent remembers from earlier sessions had gone with the wipe. Candidates are now in the report, and having none of them with nobody connected is said plainly, with both ways out of it and this device's own id ready to hand to the other side. Having some and no sessions yet is the ordinary state of a network coming up, and stays ungraded. The local control protocol is 12. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2529,15 +2529,40 @@ fn network_section(
|
||||
let online = rows.iter().filter(|row| row.online()).count();
|
||||
if rows.is_empty() {
|
||||
// Why there is nobody, rather than just that there is nobody: the
|
||||
// two reasons want different actions.
|
||||
section.push(Row::new(
|
||||
Health::Info,
|
||||
"members",
|
||||
match (&network.range, &network.range_conflict) {
|
||||
(None, Some(_)) => "none: this network has no range to allocate from",
|
||||
_ => "none known yet; nobody else has joined",
|
||||
},
|
||||
));
|
||||
// three reasons want different actions, and the third one used to
|
||||
// read as the first.
|
||||
match (&network.range, &network.range_conflict, network.candidates) {
|
||||
(None, Some(_), _) => section.push(Row::new(
|
||||
Health::Info,
|
||||
"members",
|
||||
"none: this network has no range to allocate from",
|
||||
)),
|
||||
// Nobody to contact and nowhere to look. An agent finds a peer
|
||||
// by being told about one, or from what it remembers of an
|
||||
// earlier session — with neither it waits for ever, and the
|
||||
// report should say so rather than imply patience.
|
||||
(_, _, 0) => section.push(
|
||||
Row::new(
|
||||
Health::Degraded,
|
||||
"members",
|
||||
"none, and nobody to contact: no candidates in this network",
|
||||
)
|
||||
.with_note(format!(
|
||||
"somebody has to make the introduction. Start this agent with \
|
||||
`--peer <their-endpoint-id>`, or have them start theirs with \
|
||||
`--peer {}`. Once they have met, each remembers the other.",
|
||||
short(own_id, 12)
|
||||
)),
|
||||
),
|
||||
_ => section.push(Row::new(
|
||||
Health::Info,
|
||||
"members",
|
||||
format!(
|
||||
"none yet · {} candidate(s) being tried",
|
||||
network.candidates
|
||||
),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
section.push(Row::new(
|
||||
Health::Info,
|
||||
@@ -3504,6 +3529,7 @@ async fn build_report(
|
||||
name: network.name.to_string(),
|
||||
network_id: network.network_id.to_string(),
|
||||
active: matches!(network.state, tsunagi::agent::NetworkState::Active),
|
||||
candidates: network.candidates.len() as u32,
|
||||
peers: network
|
||||
.peers
|
||||
.iter()
|
||||
@@ -3798,6 +3824,7 @@ mod status_tests {
|
||||
name: "LAB".into(),
|
||||
network_id: "xa7gyz".into(),
|
||||
active: true,
|
||||
candidates: 1,
|
||||
peers: vec![PeerReport {
|
||||
endpoint_id: ONLINE.into(),
|
||||
hostname: Some("music".into()),
|
||||
@@ -3847,6 +3874,43 @@ mod status_tests {
|
||||
assert!(text.contains("mistyped secret"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_with_nobody_to_contact_says_so_instead_of_waiting_quietly() {
|
||||
// The wiped-and-restarted case: both devices join the same network
|
||||
// and sit there. An agent finds a peer by being told about one or
|
||||
// by remembering an earlier session, and with neither it will wait
|
||||
// for ever — which "nobody else has joined" reads as patience.
|
||||
let mut network = network_after_a_peer_returned();
|
||||
network.peers.clear();
|
||||
network.members.clear();
|
||||
network.overlay = None;
|
||||
network.candidates = 0;
|
||||
|
||||
let mut out = report::Report::new();
|
||||
out.push(network_section(&network, OWN, false));
|
||||
let text = out.render(false);
|
||||
assert!(text.contains("nobody to contact"), "{text}");
|
||||
assert!(text.contains("--peer"), "the fix is named: {text}");
|
||||
assert!(text.contains(OWN), "with this device's own id: {text}");
|
||||
assert_eq!(out.worst(), Health::Degraded, "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidates_with_nobody_connected_yet_is_patience_rather_than_a_fault() {
|
||||
// Something to try is the ordinary state of a network coming up.
|
||||
let mut network = network_after_a_peer_returned();
|
||||
network.peers.clear();
|
||||
network.members.clear();
|
||||
network.overlay = None;
|
||||
network.candidates = 2;
|
||||
|
||||
let mut out = report::Report::new();
|
||||
out.push(network_section(&network, OWN, false));
|
||||
let text = out.render(false);
|
||||
assert!(text.contains("2 candidate(s) being tried"), "{text}");
|
||||
assert_ne!(out.worst(), Health::Degraded, "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_network_with_no_range_says_that_is_why_it_is_empty() {
|
||||
// Rather than "nobody else has joined", which points at the wrong
|
||||
|
||||
@@ -267,6 +267,13 @@ pub struct NetworkReport {
|
||||
/// left to report is a dial-failure counter — which describes the symptom
|
||||
/// and not the cause.
|
||||
pub members: Vec<MemberReport>,
|
||||
/// Unverified candidates this network currently knows of.
|
||||
///
|
||||
/// Not peers: somewhere to try. Zero of them, with nobody connected,
|
||||
/// is the difference between "nobody has joined yet" and "this agent
|
||||
/// has no way to reach anybody" — which look identical in a report
|
||||
/// that counts only members.
|
||||
pub candidates: u32,
|
||||
/// Outbound dials that failed.
|
||||
pub dial_failures: u64,
|
||||
/// Handshakes rejected in either direction.
|
||||
|
||||
@@ -367,7 +367,7 @@ pub async fn set_dns(
|
||||
///
|
||||
/// Bump it whenever [`Request`], [`Response`] or anything they contain
|
||||
/// changes shape.
|
||||
pub const CONTROL_PROTOCOL: u32 = u32::from_be_bytes([b'T', b'S', b'N', 11]);
|
||||
pub const CONTROL_PROTOCOL: u32 = u32::from_be_bytes([b'T', b'S', b'N', 12]);
|
||||
|
||||
async fn write_message<T: serde::Serialize>(stream: &mut UnixStream, value: &T) -> Result<()> {
|
||||
let encoded = postcard::to_stdvec(value)
|
||||
|
||||
Reference in New Issue
Block a user