lesavka/client/src/handshake.rs

156 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// client/src/handshake.rs
#![forbid(unsafe_code)]
use lesavka_common::lesavka::{self as pb, handshake_client::HandshakeClient};
use std::time::Duration;
use tokio::time::timeout;
use tonic::{Code, transport::Endpoint};
use tracing::{info, warn};
#[derive(Default, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PeerCaps {
pub camera: bool,
pub microphone: bool,
pub camera_output: Option<String>,
pub camera_codec: Option<String>,
pub camera_width: Option<u32>,
pub camera_height: Option<u32>,
pub camera_fps: Option<u32>,
}
fn likely_port_typo_hint(uri: &str) -> Option<&'static str> {
if uri.contains(":5005") && !uri.contains(":50051") {
Some("possible typo: lesavka server listens on port 50051")
} else {
None
}
}
/// Negotiate the server capabilities the client should honor locally.
///
/// Inputs: the server URI to dial for the gRPC handshake.
/// Outputs: the negotiated peer capability set, or defaults when the server
/// is unreachable or does not implement the handshake service yet.
/// Why: the rest of client startup depends on these capabilities, but a
/// missing or misconfigured server should fall back to safe defaults instead
/// of aborting the whole client session.
pub async fn negotiate(uri: &str) -> PeerCaps {
info!(%uri, "🤝 dial handshake");
let Some(hint) = likely_port_typo_hint(uri) else {
let ep = match Endpoint::from_shared(uri.to_owned()) {
Ok(ep) => ep
.tcp_nodelay(true)
.http2_keep_alive_interval(Duration::from_secs(15))
.connect_timeout(Duration::from_secs(5)),
Err(e) => {
warn!("🤝 invalid handshake endpoint '{uri}': {e} assuming defaults");
return PeerCaps::default();
}
};
let channel = match timeout(Duration::from_secs(8), ep.connect()).await {
Ok(Ok(channel)) => channel,
Ok(Err(e)) => {
if let Some(hint) = likely_port_typo_hint(uri) {
warn!("🤝 handshake connect failed: {e} ({hint}) assuming defaults");
} else {
warn!("🤝 handshake connect failed: {e} assuming defaults");
}
return PeerCaps::default();
}
Err(_) => {
if let Some(hint) = likely_port_typo_hint(uri) {
warn!("🤝 handshake connect timed out ({hint}) assuming defaults");
} else {
warn!("🤝 handshake connect timed out assuming defaults");
}
return PeerCaps::default();
}
};
info!("🤝 handshake channel connected");
let mut cli = HandshakeClient::new(channel);
info!("🤝 fetching capabilities…");
return match timeout(Duration::from_secs(5), cli.get_capabilities(pb::Empty {})).await {
Ok(Ok(rsp)) => {
let rsp = rsp.get_ref();
let caps = PeerCaps {
camera: rsp.camera,
microphone: rsp.microphone,
camera_output: if rsp.camera_output.is_empty() {
None
} else {
Some(rsp.camera_output.clone())
},
camera_codec: if rsp.camera_codec.is_empty() {
None
} else {
Some(rsp.camera_codec.clone())
},
camera_width: if rsp.camera_width == 0 {
None
} else {
Some(rsp.camera_width)
},
camera_height: if rsp.camera_height == 0 {
None
} else {
Some(rsp.camera_height)
},
camera_fps: if rsp.camera_fps == 0 {
None
} else {
Some(rsp.camera_fps)
},
};
info!(?caps, "🤝 handshake ok");
caps
}
Ok(Err(e)) if e.code() == Code::Unimplemented => {
warn!("🤝 handshake not implemented on server assuming defaults");
PeerCaps::default()
}
Ok(Err(e)) => {
warn!("🤝 handshake failed: {e} assuming defaults");
PeerCaps::default()
}
Err(_) => {
warn!("🤝 handshake timed out assuming defaults");
PeerCaps::default()
}
};
};
warn!("🤝 handshake endpoint '{uri}' looks wrong ({hint}) assuming defaults");
PeerCaps::default()
}
#[cfg(test)]
mod tests {
use super::{PeerCaps, likely_port_typo_hint, negotiate};
#[test]
fn likely_port_typo_hint_flags_common_port_mistype() {
assert_eq!(
likely_port_typo_hint("http://127.0.0.1:5005"),
Some("possible typo: lesavka server listens on port 50051")
);
assert_eq!(likely_port_typo_hint("http://127.0.0.1:50051"), None);
}
#[tokio::test]
async fn negotiate_returns_defaults_for_invalid_endpoint() {
let caps = negotiate("not a uri").await;
assert_eq!(caps, PeerCaps::default());
}
#[tokio::test]
async fn negotiate_returns_defaults_for_port_typo_hint() {
let caps = negotiate("http://127.0.0.1:5005").await;
assert_eq!(caps, PeerCaps::default());
}
}