From 7a8150b2db8ad8bf9fcfdb4e059a2d268b625160 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Fri, 1 May 2026 16:06:52 -0300 Subject: [PATCH] sync: print probe endpoint versions --- AGENTS.md | 1 + Cargo.lock | 6 +- client/Cargo.toml | 2 +- client/src/bin/lesavka-relayctl.rs | 55 +++++++++++++++++-- common/Cargo.toml | 2 +- scripts/manual/run_upstream_av_sync.sh | 34 +++++++++++- .../manual/run_upstream_mirrored_av_sync.sh | 34 +++++++++++- server/Cargo.toml | 2 +- .../client_manual_sync_script_contract.rs | 8 +++ 9 files changed, 131 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ca0e17e..ab5ac2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,5 +174,6 @@ Context: the mirrored browser probe finally reproduced the real failure class on - Replaying the 0.16.21 artifact after 0.16.22 analyzer hardening changes the verdict from false `catastrophic_failure` to `gross_failure`: p95 `273.8 ms`, median `-188.4 ms`, 7 paired coded pulses. The raw activity-start delta (`-3620.7 ms`) is still printed, but it is ignored for verdict/calibration because it disagrees with coded pairs by `3432.3 ms`; unpaired video/audio onsets are printed for triage. - 0.16.22 live mirrored run still failed with p95 `433.7 ms`, median `-359.4 ms`, and 5 paired coded pulses. Client telemetry showed camera uplink `latest_age_ms` repeatedly around `300-350 ms`, matching the measured skew; patch 0.16.23 to make video queues latest-only instead of draining stale-but-under-budget backlog. - 0.16.23 local validation passed for fresh-queue behavior, uplink/probe freshness contracts, sync analyzer tests, client/server binary checks, and whitespace checks. + - 0.16.23 live mirrored run improved to p95 `215.2 ms`, median `+142.2 ms`, 13 paired coded pulses, and raw activity alignment within `6.6 ms` of coded pairs. Patch 0.16.24 makes the probe print local client and remote server versions before capture so every run records what was actually tested. - [ ] Re-run the mirrored browser probe after the pre-start false-positive fix. - [ ] Run Google Meet manual validation. diff --git a/Cargo.lock b/Cargo.lock index f25a2a4..19322dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.16.23" +version = "0.16.24" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.16.23" +version = "0.16.24" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.16.23" +version = "0.16.24" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index be68e60..a163349 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.16.23" +version = "0.16.24" edition = "2024" [dependencies] diff --git a/client/src/bin/lesavka-relayctl.rs b/client/src/bin/lesavka-relayctl.rs index 032a433..f4fbcd9 100644 --- a/client/src/bin/lesavka-relayctl.rs +++ b/client/src/bin/lesavka-relayctl.rs @@ -1,10 +1,10 @@ use anyhow::{Context, Result, bail}; -#[cfg(not(coverage))] -use lesavka_common::lesavka::Empty; use lesavka_common::lesavka::{ - CapturePowerCommand, SetCapturePowerRequest, relay_client::RelayClient, + CapturePowerCommand, HandshakeSet, SetCapturePowerRequest, relay_client::RelayClient, }; #[cfg(not(coverage))] +use lesavka_common::lesavka::{Empty, handshake_client::HandshakeClient}; +#[cfg(not(coverage))] use tonic::Request; use tonic::transport::Channel; @@ -13,6 +13,7 @@ use lesavka_client::relay_transport; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum CommandKind { Status, + Version, Auto, On, Off, @@ -27,6 +28,7 @@ impl CommandKind { fn parse(value: &str) -> Option { match value { "status" | "get" => Some(Self::Status), + "version" | "versions" => Some(Self::Version), "auto" => Some(Self::Auto), "on" | "force-on" => Some(Self::On), "off" | "force-off" => Some(Self::Off), @@ -52,7 +54,7 @@ enum ParseOutcome { } fn usage() -> &'static str { - "Usage: lesavka-relayctl [--server http://HOST:50051] " + "Usage: lesavka-relayctl [--server http://HOST:50051] " } fn parse_args_outcome_from(args: I) -> Result @@ -111,6 +113,7 @@ fn parse_args() -> Result { fn capture_power_request(command: CommandKind) -> Option { let (enabled, command) = match command { CommandKind::Status + | CommandKind::Version | CommandKind::RecoverUsb | CommandKind::RecoverUac | CommandKind::RecoverUvc @@ -136,6 +139,21 @@ async fn connect(server_addr: &str) -> Result> { Ok(RelayClient::new(channel)) } +#[cfg(not(coverage))] +async fn get_server_capabilities(server_addr: &str) -> Result { + let channel = relay_transport::endpoint(server_addr)? + .tcp_nodelay(true) + .connect() + .await + .with_context(|| format!("connecting to handshake at {server_addr}"))?; + let mut client = HandshakeClient::new(channel); + Ok(client + .get_capabilities(Request::new(Empty {})) + .await + .context("querying server capabilities")? + .into_inner()) +} + #[cfg(coverage)] async fn connect(server_addr: &str) -> Result> { let channel = relay_transport::endpoint(server_addr)? @@ -154,6 +172,20 @@ fn print_state(state: lesavka_common::lesavka::CapturePowerState) { println!("detail={}", state.detail); } +fn print_versions(server_addr: &str, caps: &HandshakeSet) { + let server_version = if caps.server_version.is_empty() { + "unknown" + } else { + caps.server_version.as_str() + }; + println!("client_version={}", lesavka_client::VERSION); + println!("client_full_version={}", lesavka_client::FULL_VERSION); + println!("server_addr={server_addr}"); + println!("server_version={server_version}"); + println!("server_camera_output={}", caps.camera_output); + println!("server_camera_codec={}", caps.camera_codec); +} + #[cfg(not(coverage))] #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { @@ -164,6 +196,13 @@ async fn main() -> Result<()> { return Ok(()); } }; + + if config.command == CommandKind::Version { + let caps = get_server_capabilities(config.server.as_str()).await?; + print_versions(config.server.as_str(), &caps); + return Ok(()); + } + let mut client = connect(config.server.as_str()).await?; if let Some(request) = capture_power_request(config.command) { @@ -172,6 +211,7 @@ async fn main() -> Result<()> { CommandKind::On => "forcing capture power on", CommandKind::Off => "forcing capture power off", CommandKind::Status + | CommandKind::Version | CommandKind::RecoverUsb | CommandKind::RecoverUac | CommandKind::RecoverUvc @@ -228,7 +268,9 @@ async fn main() -> Result<()> { println!("ok={}", reply.ok); return Ok(()); } - CommandKind::Auto | CommandKind::On | CommandKind::Off => unreachable!(), + CommandKind::Version | CommandKind::Auto | CommandKind::On | CommandKind::Off => { + unreachable!() + } }; print_state(reply); @@ -253,6 +295,8 @@ mod tests { fn command_aliases_parse_to_stable_actions() { assert_eq!(CommandKind::parse("status"), Some(CommandKind::Status)); assert_eq!(CommandKind::parse("get"), Some(CommandKind::Status)); + assert_eq!(CommandKind::parse("version"), Some(CommandKind::Version)); + assert_eq!(CommandKind::parse("versions"), Some(CommandKind::Version)); assert_eq!(CommandKind::parse("force-on"), Some(CommandKind::On)); assert_eq!(CommandKind::parse("force-off"), Some(CommandKind::Off)); assert_eq!( @@ -342,6 +386,7 @@ mod tests { assert_eq!(off.command, CapturePowerCommand::ForceOff as i32); assert!(capture_power_request(CommandKind::Status).is_none()); + assert!(capture_power_request(CommandKind::Version).is_none()); assert!(capture_power_request(CommandKind::RecoverUsb).is_none()); assert!(capture_power_request(CommandKind::RecoverUac).is_none()); assert!(capture_power_request(CommandKind::RecoverUvc).is_none()); diff --git a/common/Cargo.toml b/common/Cargo.toml index 4e2fcea..a0a4098 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.16.23" +version = "0.16.24" edition = "2024" build = "build.rs" diff --git a/scripts/manual/run_upstream_av_sync.sh b/scripts/manual/run_upstream_av_sync.sh index 0414c63..27a8a2a 100755 --- a/scripts/manual/run_upstream_av_sync.sh +++ b/scripts/manual/run_upstream_av_sync.sh @@ -190,6 +190,37 @@ systemctl is-active lesavka-server lesavka-uvc lesavka-core >/dev/null REMOTE_PREFLIGHT } +print_lesavka_versions() { + echo "==> Lesavka versions under test" + if [[ ! -x "${REPO_ROOT}/target/debug/lesavka-relayctl" ]]; then + (cd "${REPO_ROOT}" && cargo build -p lesavka_client --bin lesavka-relayctl >/dev/null) + fi + local version_output + if ! version_output="$( + LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \ + "${REPO_ROOT}/target/debug/lesavka-relayctl" \ + --server "${RESOLVED_LESAVKA_SERVER_ADDR}" \ + version 2>&1 + )"; then + echo "failed to query Lesavka versions through ${RESOLVED_LESAVKA_SERVER_ADDR}" >&2 + echo "${version_output}" >&2 + return 1 + fi + if ! grep -q "^client_version=" <<<"${version_output}"; then + echo "Lesavka version query did not report client_version=; refusing to run an unattributed probe" >&2 + echo "${version_output}" >&2 + return 1 + fi + if ! grep -q "^server_version=" <<<"${version_output}"; then + echo "Lesavka version query did not report server_version=; refusing to run an unattributed probe" >&2 + echo "${version_output}" >&2 + return 1 + fi + while IFS= read -r line; do + [[ -n "${line}" ]] && echo " ↪ ${line}" + done <<<"${version_output}" +} + if [[ "${LOCAL_AUDIO_SANITY}" != "0" ]]; then echo "==> verifying local speaker-to-mic sanity before upstream sync run" "${SCRIPT_DIR}/run_local_audio_sanity.sh" @@ -199,7 +230,7 @@ if [[ "${PROBE_PREBUILD}" != "0" ]]; then echo "==> prebuilding sync probe/analyzer before opening the capture window" ( cd "${REPO_ROOT}" - cargo build -p lesavka_client --bin lesavka-sync-probe --bin lesavka-sync-analyze + cargo build -p lesavka_client --bin lesavka-sync-probe --bin lesavka-sync-analyze --bin lesavka-relayctl ) fi @@ -218,6 +249,7 @@ if [[ -n "${SERVER_TUNNEL_PID}" ]]; then echo " ↪ tunneled to ${LESAVKA_SERVER_HOST}:127.0.0.1:${SERVER_TUNNEL_REMOTE_PORT}" fi +print_lesavka_versions preflight_server_path echo "==> starting Tethys capture on ${TETHYS_HOST}" diff --git a/scripts/manual/run_upstream_mirrored_av_sync.sh b/scripts/manual/run_upstream_mirrored_av_sync.sh index b2c5085..b315983 100755 --- a/scripts/manual/run_upstream_mirrored_av_sync.sh +++ b/scripts/manual/run_upstream_mirrored_av_sync.sh @@ -154,6 +154,37 @@ start_server_tunnel_if_needed() { echo " ↪ tunneled to ${LESAVKA_SERVER_HOST}:127.0.0.1:${LESAVKA_SERVER_PORT}" } +print_lesavka_versions() { + echo "==> Lesavka versions under test" + if [[ ! -x "${REPO_ROOT}/target/debug/lesavka-relayctl" ]]; then + (cd "${REPO_ROOT}" && cargo build -p lesavka_client --bin lesavka-relayctl >/dev/null) + fi + local version_output + if ! version_output="$( + LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \ + "${REPO_ROOT}/target/debug/lesavka-relayctl" \ + --server "${RESOLVED_LESAVKA_SERVER_ADDR}" \ + version 2>&1 + )"; then + echo "failed to query Lesavka versions through ${RESOLVED_LESAVKA_SERVER_ADDR}" >&2 + echo "${version_output}" >&2 + return 1 + fi + if ! grep -q "^client_version=" <<<"${version_output}"; then + echo "Lesavka version query did not report client_version=; refusing to run an unattributed probe" >&2 + echo "${version_output}" >&2 + return 1 + fi + if ! grep -q "^server_version=" <<<"${version_output}"; then + echo "Lesavka version query did not report server_version=; refusing to run an unattributed probe" >&2 + echo "${version_output}" >&2 + return 1 + fi + while IFS= read -r line; do + [[ -n "${line}" ]] && echo " ↪ ${line}" + done <<<"${version_output}" +} + start_local_stimulus() { echo "==> starting local A/V stimulus server" python3 "${REPO_ROOT}/scripts/manual/local_av_stimulus.py" \ @@ -230,10 +261,11 @@ run_browser_capture_with_real_driver() { echo "==> prebuilding real client and analyzer" ( cd "${REPO_ROOT}" - cargo build -p lesavka_client --bin lesavka-client --bin lesavka-sync-analyze >/dev/null + cargo build -p lesavka_client --bin lesavka-client --bin lesavka-sync-analyze --bin lesavka-relayctl >/dev/null ) start_server_tunnel_if_needed +print_lesavka_versions start_local_stimulus start_real_lesavka_client run_browser_capture_with_real_driver diff --git a/server/Cargo.toml b/server/Cargo.toml index 2a1bc20..2bb5b5c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.16.23" +version = "0.16.24" edition = "2024" autobins = false diff --git a/testing/tests/client_manual_sync_script_contract.rs b/testing/tests/client_manual_sync_script_contract.rs index cbe8400..ae25f7f 100644 --- a/testing/tests/client_manual_sync_script_contract.rs +++ b/testing/tests/client_manual_sync_script_contract.rs @@ -47,6 +47,10 @@ fn upstream_sync_script_tunnels_auto_server_addr_through_ssh() { "Lesavka audio source not found in PipeWire or ALSA; capture host does not currently expose the gadget microphone.", "artifact_dir: ${LOCAL_REPORT_DIR}", "events_csv: ${LOCAL_EVENTS_CSV}", + "==> Lesavka versions under test", + "lesavka-relayctl", + "--bin lesavka-relayctl", + "server_version=", ] { assert!( SYNC_SCRIPT.contains(expected), @@ -92,6 +96,10 @@ fn mirrored_sync_script_uses_real_client_capture_path() { "run_upstream_browser_av_sync.sh", "wait_for_stimulus_page_ready 15", "Point the real webcam at the stimulus window", + "==> Lesavka versions under test", + "lesavka-relayctl", + "--bin lesavka-relayctl", + "server_version=", ] { assert!( MIRRORED_SYNC_SCRIPT.contains(expected),