sync: print probe endpoint versions

This commit is contained in:
Brad Stein 2026-05-01 16:06:52 -03:00
parent b948994811
commit 7a8150b2db
9 changed files with 131 additions and 13 deletions

View File

@ -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. - 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.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 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. - [ ] Re-run the mirrored browser probe after the pre-start false-positive fix.
- [ ] Run Google Meet manual validation. - [ ] Run Google Meet manual validation.

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.16.23" version = "0.16.24"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.16.23" version = "0.16.24"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.16.23" version = "0.16.24"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.16.23" version = "0.16.24"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -1,10 +1,10 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
#[cfg(not(coverage))]
use lesavka_common::lesavka::Empty;
use lesavka_common::lesavka::{ use lesavka_common::lesavka::{
CapturePowerCommand, SetCapturePowerRequest, relay_client::RelayClient, CapturePowerCommand, HandshakeSet, SetCapturePowerRequest, relay_client::RelayClient,
}; };
#[cfg(not(coverage))] #[cfg(not(coverage))]
use lesavka_common::lesavka::{Empty, handshake_client::HandshakeClient};
#[cfg(not(coverage))]
use tonic::Request; use tonic::Request;
use tonic::transport::Channel; use tonic::transport::Channel;
@ -13,6 +13,7 @@ use lesavka_client::relay_transport;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CommandKind { enum CommandKind {
Status, Status,
Version,
Auto, Auto,
On, On,
Off, Off,
@ -27,6 +28,7 @@ impl CommandKind {
fn parse(value: &str) -> Option<Self> { fn parse(value: &str) -> Option<Self> {
match value { match value {
"status" | "get" => Some(Self::Status), "status" | "get" => Some(Self::Status),
"version" | "versions" => Some(Self::Version),
"auto" => Some(Self::Auto), "auto" => Some(Self::Auto),
"on" | "force-on" => Some(Self::On), "on" | "force-on" => Some(Self::On),
"off" | "force-off" => Some(Self::Off), "off" | "force-off" => Some(Self::Off),
@ -52,7 +54,7 @@ enum ParseOutcome {
} }
fn usage() -> &'static str { fn usage() -> &'static str {
"Usage: lesavka-relayctl [--server http://HOST:50051] <status|auto|on|off|recover-usb|recover-uac|recover-uvc|reset-usb>" "Usage: lesavka-relayctl [--server http://HOST:50051] <status|version|auto|on|off|recover-usb|recover-uac|recover-uvc|reset-usb>"
} }
fn parse_args_outcome_from<I, S>(args: I) -> Result<ParseOutcome> fn parse_args_outcome_from<I, S>(args: I) -> Result<ParseOutcome>
@ -111,6 +113,7 @@ fn parse_args() -> Result<ParseOutcome> {
fn capture_power_request(command: CommandKind) -> Option<SetCapturePowerRequest> { fn capture_power_request(command: CommandKind) -> Option<SetCapturePowerRequest> {
let (enabled, command) = match command { let (enabled, command) = match command {
CommandKind::Status CommandKind::Status
| CommandKind::Version
| CommandKind::RecoverUsb | CommandKind::RecoverUsb
| CommandKind::RecoverUac | CommandKind::RecoverUac
| CommandKind::RecoverUvc | CommandKind::RecoverUvc
@ -136,6 +139,21 @@ async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
Ok(RelayClient::new(channel)) Ok(RelayClient::new(channel))
} }
#[cfg(not(coverage))]
async fn get_server_capabilities(server_addr: &str) -> Result<HandshakeSet> {
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)] #[cfg(coverage)]
async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> { async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
let channel = relay_transport::endpoint(server_addr)? let channel = relay_transport::endpoint(server_addr)?
@ -154,6 +172,20 @@ fn print_state(state: lesavka_common::lesavka::CapturePowerState) {
println!("detail={}", state.detail); 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))] #[cfg(not(coverage))]
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@ -164,6 +196,13 @@ async fn main() -> Result<()> {
return Ok(()); 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?; let mut client = connect(config.server.as_str()).await?;
if let Some(request) = capture_power_request(config.command) { if let Some(request) = capture_power_request(config.command) {
@ -172,6 +211,7 @@ async fn main() -> Result<()> {
CommandKind::On => "forcing capture power on", CommandKind::On => "forcing capture power on",
CommandKind::Off => "forcing capture power off", CommandKind::Off => "forcing capture power off",
CommandKind::Status CommandKind::Status
| CommandKind::Version
| CommandKind::RecoverUsb | CommandKind::RecoverUsb
| CommandKind::RecoverUac | CommandKind::RecoverUac
| CommandKind::RecoverUvc | CommandKind::RecoverUvc
@ -228,7 +268,9 @@ async fn main() -> Result<()> {
println!("ok={}", reply.ok); println!("ok={}", reply.ok);
return Ok(()); return Ok(());
} }
CommandKind::Auto | CommandKind::On | CommandKind::Off => unreachable!(), CommandKind::Version | CommandKind::Auto | CommandKind::On | CommandKind::Off => {
unreachable!()
}
}; };
print_state(reply); print_state(reply);
@ -253,6 +295,8 @@ mod tests {
fn command_aliases_parse_to_stable_actions() { fn command_aliases_parse_to_stable_actions() {
assert_eq!(CommandKind::parse("status"), Some(CommandKind::Status)); assert_eq!(CommandKind::parse("status"), Some(CommandKind::Status));
assert_eq!(CommandKind::parse("get"), 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-on"), Some(CommandKind::On));
assert_eq!(CommandKind::parse("force-off"), Some(CommandKind::Off)); assert_eq!(CommandKind::parse("force-off"), Some(CommandKind::Off));
assert_eq!( assert_eq!(
@ -342,6 +386,7 @@ mod tests {
assert_eq!(off.command, CapturePowerCommand::ForceOff as i32); assert_eq!(off.command, CapturePowerCommand::ForceOff as i32);
assert!(capture_power_request(CommandKind::Status).is_none()); 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::RecoverUsb).is_none());
assert!(capture_power_request(CommandKind::RecoverUac).is_none()); assert!(capture_power_request(CommandKind::RecoverUac).is_none());
assert!(capture_power_request(CommandKind::RecoverUvc).is_none()); assert!(capture_power_request(CommandKind::RecoverUvc).is_none());

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.16.23" version = "0.16.24"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -190,6 +190,37 @@ systemctl is-active lesavka-server lesavka-uvc lesavka-core >/dev/null
REMOTE_PREFLIGHT 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 if [[ "${LOCAL_AUDIO_SANITY}" != "0" ]]; then
echo "==> verifying local speaker-to-mic sanity before upstream sync run" echo "==> verifying local speaker-to-mic sanity before upstream sync run"
"${SCRIPT_DIR}/run_local_audio_sanity.sh" "${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" echo "==> prebuilding sync probe/analyzer before opening the capture window"
( (
cd "${REPO_ROOT}" 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 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}" echo " ↪ tunneled to ${LESAVKA_SERVER_HOST}:127.0.0.1:${SERVER_TUNNEL_REMOTE_PORT}"
fi fi
print_lesavka_versions
preflight_server_path preflight_server_path
echo "==> starting Tethys capture on ${TETHYS_HOST}" echo "==> starting Tethys capture on ${TETHYS_HOST}"

View File

@ -154,6 +154,37 @@ start_server_tunnel_if_needed() {
echo " ↪ tunneled to ${LESAVKA_SERVER_HOST}:127.0.0.1:${LESAVKA_SERVER_PORT}" 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() { start_local_stimulus() {
echo "==> starting local A/V stimulus server" echo "==> starting local A/V stimulus server"
python3 "${REPO_ROOT}/scripts/manual/local_av_stimulus.py" \ 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" echo "==> prebuilding real client and analyzer"
( (
cd "${REPO_ROOT}" 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 start_server_tunnel_if_needed
print_lesavka_versions
start_local_stimulus start_local_stimulus
start_real_lesavka_client start_real_lesavka_client
run_browser_capture_with_real_driver run_browser_capture_with_real_driver

View File

@ -10,7 +10,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.16.23" version = "0.16.24"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -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.", "Lesavka audio source not found in PipeWire or ALSA; capture host does not currently expose the gadget microphone.",
"artifact_dir: ${LOCAL_REPORT_DIR}", "artifact_dir: ${LOCAL_REPORT_DIR}",
"events_csv: ${LOCAL_EVENTS_CSV}", "events_csv: ${LOCAL_EVENTS_CSV}",
"==> Lesavka versions under test",
"lesavka-relayctl",
"--bin lesavka-relayctl",
"server_version=",
] { ] {
assert!( assert!(
SYNC_SCRIPT.contains(expected), SYNC_SCRIPT.contains(expected),
@ -92,6 +96,10 @@ fn mirrored_sync_script_uses_real_client_capture_path() {
"run_upstream_browser_av_sync.sh", "run_upstream_browser_av_sync.sh",
"wait_for_stimulus_page_ready 15", "wait_for_stimulus_page_ready 15",
"Point the real webcam at the stimulus window", "Point the real webcam at the stimulus window",
"==> Lesavka versions under test",
"lesavka-relayctl",
"--bin lesavka-relayctl",
"server_version=",
] { ] {
assert!( assert!(
MIRRORED_SYNC_SCRIPT.contains(expected), MIRRORED_SYNC_SCRIPT.contains(expected),