//! NetDesk native client — END-TO-END FEATURE TEST against the LIVE host. //! //! Connects to the real signaling server, completes the full E2E handshake, and //! then exercises AND VERIFIES every shipping feature of `netdesk-core`'s //! `Session`, printing `PASS ` / `FAIL : ` per item. It also //! exercises the shipping host-control backend (`viewer/src/host_ctl.rs`, pulled //! in verbatim via `#[path]` so this stays a cli-only edit) read-only. //! //! Everything is bounded by timeouts so the run can never hang, and every //! destructive control is avoided: the only toggles touched (block_input, //! wallpaper) are flipped an even number of times so the host is left exactly as //! it was found. Nothing is uploaded/deleted/renamed on the host; the service is //! never started/stopped and the password is never changed. //! //! Run: target\debug\netdesk-e2e.exe //! Developed by Akronlogic. #![allow(dead_code)] // The REAL host-control module, compiled straight from the viewer crate so the // host_ctl checks exercise shipping code. Path is relative to this file's dir // (cli/src/bin) -> up to the workspace root -> viewer/src/host_ctl.rs. #[path = "../../../viewer/src/host_ctl.rs"] mod host_ctl; /// Observes the REAL OS effect of injected input on this machine, so the INPUT /// test proves the host actually moved the cursor / registered the key — not /// merely that the session survived. Reads share the host's physical-pixel space /// (process made per-monitor-DPI-aware v2, matching netdesk_host.py). #[cfg(windows)] mod osprobe { use windows::Win32::Foundation::POINT; use windows::Win32::UI::HiDpi::{ SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, }; use windows::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState; use windows::Win32::UI::WindowsAndMessaging::{ GetCursorPos, GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN, }; pub fn make_dpi_aware() { unsafe { let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } } pub fn cursor_pos() -> (i32, i32) { let mut p = POINT::default(); unsafe { let _ = GetCursorPos(&mut p); } (p.x, p.y) } pub fn screen_wh() -> (i32, i32) { unsafe { (GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)) } } /// True while the virtual key is currently held (injected keys count). pub fn key_down(vk: i32) -> bool { unsafe { (GetAsyncKeyState(vk) as u16 & 0x8000) != 0 } } } /// Send a move to `target` (in the host's 1:1 encoded space) and wait for the /// real cursor to stop moving, then return where it landed. Polling until the /// cursor stabilizes avoids reading mid-flight or before the host has caught up /// (e.g. right after an encoder restart), which would corrupt a delta reading. #[cfg(windows)] async fn move_and_settle( session: &netdesk_core::Session, rx: &mut mpsc::UnboundedReceiver, col: &mut Collected, dec: &mut Box, target: (i32, i32), ) -> (i32, i32) { for _ in 0..4 { session.mouse(target.0, target.1, 0, 0.0, 0.0); } let mut last = osprobe::cursor_pos(); for _ in 0..14 { pump_until(rx, col, dec, Duration::from_millis(55), |_| false).await; let now = osprobe::cursor_pos(); if (now.0 - last.0).abs() <= 2 && (now.1 - last.1).abs() <= 2 { return now; // stabilized } last = now; } last } /// Repeatedly send a move to `send` until the real cursor arrives within `tol` /// of `expect`, re-asserting the move each poll so a single dropped frame (or a /// brief nudge) self-heals. Returns (landed, final cursor pos). #[cfg(windows)] async fn move_until_near( session: &netdesk_core::Session, rx: &mut mpsc::UnboundedReceiver, col: &mut Collected, dec: &mut Box, send: (i32, i32), expect: (i32, i32), tol: i32, ) -> (bool, (i32, i32)) { for _ in 0..24 { session.mouse(send.0, send.1, 0, 0.0, 0.0); pump_until(rx, col, dec, Duration::from_millis(55), |_| false).await; let c = osprobe::cursor_pos(); if (c.0 - expect.0).abs() <= tol && (c.1 - expect.1).abs() <= tol { return (true, c); } } (false, osprobe::cursor_pos()) } use netdesk_core::decode::{best_decoder, VideoDecoder}; use netdesk_core::signaling::FileEntry; use netdesk_core::{connect, ConnectConfig, SessionEvent}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; // Target host — overridable via env so the suite can point at a scratch test // host (e.g. to validate a new NetDeskHost.exe build) without recompiling. fn host_id() -> String { std::env::var("E2E_HOST_ID").unwrap_or_else(|_| "123456789".into()) } fn password() -> String { std::env::var("E2E_PASSWORD").unwrap_or_else(|_| "changeme".into()) } fn ws_url() -> String { std::env::var("E2E_WS_URL").unwrap_or_else(|_| "wss://netdesk.space/ws".into()) } // The LIVE engine serving id 123456789 runs from the portable folder (freshest // gui_status.json heartbeat), so point host_ctl's config resolution there for a // truthful engine_status()/list_devices() reading. // Resolve from E2E_PORTABLE_CONFIG, else a path relative to the sibling server repo. const PORTABLE_CONFIG: &str = r"..\netdesk\portable\config.json"; // --------------------------------------------------------------------------- // Collected session state, updated by the event pump. // --------------------------------------------------------------------------- #[derive(Default)] struct Collected { connected: Option<(String, bool, u32, u32)>, // host_name, e2e, w, h frames: u64, decoded: u64, keyframes: u64, last_dim: (usize, usize), last_nonblack: u64, sys: Vec, file_lists: Vec<(String, Vec)>, chats: Vec<(String, String)>, clips: Vec, infos: Vec, downloads: Vec<(u32, String, u64, std::path::PathBuf)>, // fid, name, host-size, on-disk .part file_errs: Vec, audio_pkts: u64, /// Raw received Opus packets (decrypted). Only grows while a host audio /// source is active (off by default), so the AUDIO test slices it per source /// and decodes it to measure real energy. audio_data: Vec>, upload_acks: u64, cursors: u64, cursor_shape_bytes: usize, denied: Option, ended: Option, /// Set once the viewer reports the direct-P2P fast path is active (NETDESK_P2P). p2p_active: bool, /// When set, every received H.264 access unit is also muxed to this file. recorder: Option, } fn apply_event(ev: SessionEvent, c: &mut Collected, dec: &mut Box) { match ev { SessionEvent::Connected { host_name, e2e, w, h } => { c.connected = Some((host_name, e2e, w, h)); } SessionEvent::Video { keyframe, au, .. } => { c.frames += 1; if keyframe { c.keyframes += 1; } // Tee into the recorder (if the RECORDING test turned it on). if let Some(r) = c.recorder.as_mut() { let _ = r.push(&au, keyframe, c.frames * 33); } // Only decode enough frames to satisfy the VIDEO test. Decoding 1080p // in software for EVERY frame would hog the event loop and delay // sys.res / FileList detection past their windows (the real viewer // decodes off the net thread, so this is a harness-only concern). if c.decoded < 20 { if let Ok(Some(frame)) = dec.decode(&au) { c.decoded += 1; c.last_dim = (frame.w, frame.h); c.last_nonblack = frame .rgb .chunks_exact(3) .filter(|p| (p[0] | p[1] | p[2]) != 0) .count() as u64; } } } SessionEvent::Audio { data } => { c.audio_pkts += 1; c.audio_data.push(data); } SessionEvent::SysResult { json } => { if let Ok(v) = serde_json::from_str::(&json) { c.sys.push(v); } } SessionEvent::FileList { path, entries } => c.file_lists.push((path, entries)), SessionEvent::ChatMessage { from, text } => c.chats.push((from, text)), SessionEvent::Clipboard { text } => c.clips.push(text), SessionEvent::Info { json } => { if let Ok(v) = serde_json::from_str::(&json) { c.infos.push(v); } } SessionEvent::FileDownloadProgress { .. } => {} SessionEvent::FileDownloadDone { fid, name, size, path } => { // Keep the on-disk .part path; each test reads/verifies then cleans up. c.downloads.push((fid, name, size, path)); } SessionEvent::FileUploadAck { .. } => c.upload_acks += 1, SessionEvent::FileError { msg } => c.file_errs.push(msg), SessionEvent::Denied { reason } => c.denied = Some(reason), SessionEvent::Ended { reason } => c.ended = Some(reason), SessionEvent::Cursor { .. } => c.cursors += 1, SessionEvent::CursorShape { png, .. } => { if !png.is_empty() { c.cursor_shape_bytes = png.len(); } } SessionEvent::P2pState { active, direct } => { if active && direct { c.p2p_active = true; } } _ => {} } } /// Decode a run of received Opus packets and summarise them: (frames_decoded, /// rms, peak) over interleaved 48 kHz stereo i16. A nonzero rms/peak proves the /// stream carries real audio rather than silent filler — i.e. the source is /// genuinely being captured and transmitted. fn audio_energy(packets: &[Vec]) -> (usize, f64, i32) { let mut dec = match netdesk_opus::Decoder::new(48_000, 2) { Ok(d) => d, Err(_) => return (0, 0.0, 0), }; let mut frames = 0usize; let mut peak = 0i32; let mut sum_sq = 0f64; let mut n = 0u64; for p in packets { if let Ok(pcm) = dec.decode(p) { if !pcm.is_empty() { frames += 1; } for &s in &pcm { let a = (s as i32).abs(); if a > peak { peak = a; } sum_sq += (s as f64) * (s as f64); n += 1; } } } let rms = if n > 0 { (sum_sq / n as f64).sqrt() } else { 0.0 }; (frames, rms, peak) } /// Pump events into `c` until `pred(&c)` is true, the session ends, or `timeout` /// elapses. Returns the final value of `pred(&c)`. async fn pump_until bool>( rx: &mut mpsc::UnboundedReceiver, c: &mut Collected, dec: &mut Box, timeout: Duration, pred: P, ) -> bool { if pred(c) { return true; } let deadline = Instant::now() + timeout; loop { let now = Instant::now(); if now >= deadline { return pred(c); } match tokio::time::timeout(deadline - now, rx.recv()).await { Ok(Some(ev)) => { apply_event(ev, c, dec); if pred(c) { return true; } if c.ended.is_some() || c.denied.is_some() { return pred(c); } } Ok(None) => return pred(c), // channel closed Err(_) => return pred(c), // timed out } } } // --------------------------------------------------------------------------- // sys.res helpers (parsed exactly like the viewer's parse_sysres). // --------------------------------------------------------------------------- fn count_kind(c: &Collected, kind: &str) -> usize { c.sys .iter() .filter(|v| v.get("kind").and_then(|k| k.as_str()) == Some(kind)) .count() } fn last_on(c: &Collected, kind: &str) -> Option { c.sys .iter() .filter(|v| v.get("kind").and_then(|k| k.as_str()) == Some(kind)) .filter_map(|v| v.get("on").and_then(|x| x.as_bool())) .last() } fn last_items_len(c: &Collected, kind: &str) -> Option { c.sys .iter() .rev() .find(|v| v.get("kind").and_then(|k| k.as_str()) == Some(kind)) .map(|v| { v.get("items") .and_then(|i| i.as_array()) .map(|a| a.len()) .unwrap_or(0) }) } /// Last string value of `field` from the newest sys.res of `kind`. fn last_str(c: &Collected, kind: &str, field: &str) -> Option { c.sys .iter() .rev() .find(|v| v.get("kind").and_then(|k| k.as_str()) == Some(kind)) .and_then(|v| v.get(field).and_then(|x| x.as_str()).map(|s| s.to_string())) } /// Last bool value of `field` from the newest sys.res of `kind`. fn last_bool(c: &Collected, kind: &str, field: &str) -> Option { c.sys .iter() .rev() .find(|v| v.get("kind").and_then(|k| k.as_str()) == Some(kind)) .and_then(|v| v.get(field).and_then(|x| x.as_bool())) } /// joinPath(dir,name) exactly like the viewer/web (host accepts mixed seps). fn join_path(dir: &str, name: &str) -> String { if dir.is_empty() { name.to_string() } else if dir.ends_with('/') || dir.ends_with('\\') { format!("{dir}{name}") } else { format!("{dir}/{name}") } } fn now_unix() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) } // --------------------------------------------------------------------------- // Report. // --------------------------------------------------------------------------- #[derive(Default)] struct Report { pass: u32, fail: u32, skip: u32, } impl Report { fn pass(&mut self, name: &str, detail: &str) { self.pass += 1; if detail.is_empty() { println!("PASS {name}"); } else { println!("PASS {name} — {detail}"); } } fn fail(&mut self, name: &str, reason: &str) { self.fail += 1; println!("FAIL {name}: {reason}"); } fn skip(&mut self, name: &str, reason: &str) { self.skip += 1; println!("SKIP {name}: {reason}"); } } #[tokio::main(flavor = "current_thread")] async fn main() { println!( "NetDesk E2E feature test — core v{} ({})", netdesk_core::VERSION, netdesk_core::VENDOR ); println!("target host {} via {}", host_id(), ws_url()); println!("WINDOW_START {}", now_unix()); println!("================================================================"); // Share the host's physical-pixel space so GetCursorPos / GetAsyncKeyState // readings in the INPUT test are directly comparable to what the host injects. #[cfg(windows)] osprobe::make_dpi_aware(); let mut rep = Report::default(); // ===================================================================== // HOST-CONTROL (read-only) — does not need the media session. // ===================================================================== println!("-- host-control (host_ctl, read-only) --"); host_ctl_checks(&mut rep); // ===================================================================== // LIVE SESSION. // ===================================================================== println!("-- live session --"); let mut rx = mpsc::unbounded_channel::().1; // placeholder, replaced below let mut session_opt = None; for attempt in 0..2 { let (tx, new_rx) = mpsc::unbounded_channel::(); let cfg = ConnectConfig { ws_url: ws_url(), host_id: host_id(), password: password(), totp: String::new(), takeover: attempt == 1, // polite first, force-takeover on the retry }; match connect(cfg, tx).await { Ok(s) => { rx = new_rx; session_opt = Some(s); break; } Err(e) => { if attempt == 0 { println!(" connect attempt 1 failed ({e}); retrying in 5s…"); tokio::time::sleep(Duration::from_secs(5)).await; } else { rep.fail("CONNECT", &format!("{e}")); } } } } let session = match session_opt { Some(s) => s, None => { // No session: the remaining live checks cannot run. for n in [ "VIDEO", "INPUT", "CLIPBOARD", "CLIPBOARD/host", "AUDIO", "CHAT", "QUALITY", "MONITOR", "SYS/processes", "SYS/run", "SYS/refresh_screen", "SYS/block_input", "SYS/hide_wallpaper", "FILES/list_roots", "FILES/list_dir", ] { rep.skip(n, "no live session"); } summary(&rep); return; } }; let mut dec = best_decoder(); let mut col = Collected::default(); // ---- CONNECT + VIDEO ------------------------------------------------ session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(8), |c| c.connected.is_some()).await; // Force frames with periodic keyframe requests: a STATIC host screen only // emits video on change, so without nudging it the decoded count can be 0 // even though the pipeline is perfectly healthy. for _ in 0..14 { if col.decoded >= 8 { break; } session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(400), |c| c.decoded >= 8).await; } match &col.connected { Some((name, true, w, h)) => { rep.pass("CONNECT", &format!("host \"{name}\" {w}x{h} e2e=true")) } Some((name, false, w, h)) => rep.fail( "CONNECT", &format!("connected to \"{name}\" {w}x{h} but e2e=false (expected true)"), ), None => rep.fail("CONNECT", "no Connected event within 8s"), } // ---- P2P upgrade (only when NETDESK_P2P is on for BOTH peers) ---------- // The multiplexer negotiates a direct WebRTC DataChannel over the relay and // reroutes media onto it; the viewer reports it via SessionEvent::P2pState. let p2p_mode = matches!( std::env::var("NETDESK_P2P").as_deref(), Ok("1") | Ok("true") | Ok("on") | Ok("ON") | Ok("TRUE") ); if p2p_mode && col.connected.is_some() && col.ended.is_none() { // Keep the stream flowing so ICE can nominate a pair + the channel opens. pump_until( &mut rx, &mut col, &mut dec, Duration::from_secs(20), |c| c.p2p_active, ) .await; if col.p2p_active { rep.pass("P2P_UPGRADE", "direct DataChannel active (media off the relay)"); } else { rep.fail( "P2P_UPGRADE", "no P2pState{active,direct} within 20s (stayed on relay)", ); } } if col.decoded >= 8 { rep.pass( "VIDEO", &format!( "{} decoded of {} rx frames, {} keyframes, last {}x{}, {} lit px", col.decoded, col.frames, col.keyframes, col.last_dim.0, col.last_dim.1, col.last_nonblack ), ); } else { rep.fail( "VIDEO", &format!("only {} decoded frames (static screen?) ({} rx)", col.decoded, col.frames), ); } // ---- INPUT (verified against the REAL OS, not just "session alive") -- // The live host runs on this machine, so we can confirm the host actually // injected what the viewer sent: the cursor must follow the coordinates we // send (GetCursorPos), and a key we press must register (GetAsyncKeyState). if let Some(r) = col.ended.clone() { rep.skip("INPUT/mouse", &format!("session ended earlier: {r}")); rep.skip("INPUT/keyboard", &format!("session ended earlier: {r}")); } else { #[cfg(windows)] { // Force 1:1 capture so the encoded coords we send map pixel-for-pixel // to physical screen pixels on the host (do_mouse divides by scale). session.set_quality(30, 8000, 1.0); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(900), |_| false).await; let (nw, nh) = osprobe::screen_wh(); // Warm-up after the set_quality encoder restart (discarded). let _ = move_and_settle(&session, &mut rx, &mut col, &mut dec, (nw / 2, nh / 2)).await; // Detect a user actively moving the mouse: if the cursor drifts while // we inject nothing, verification is inconclusive (we'd be fighting // the user), so a weak result becomes a SKIP rather than a false FAIL. let c0 = osprobe::cursor_pos(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(160), |_| false).await; let c1 = osprobe::cursor_pos(); let user_moving = (c0.0 - c1.0).abs() > 4 || (c0.1 - c1.1).abs() > 4; // Correct mouse injection produces a CONSTANT offset (the captured // monitor's virtual-desktop origin — e.g. +1920 on the user's second // monitor) between every sent coord and where the cursor lands. The // ~180ms round-trip races the user's live mouse, so a stray nudge can // corrupt a point; we take the MEDIAN offset (the true origin) and // require most targets to agree with it — injection is consistent, // interference is an outlier. // DISTINCT x per target, so a stuck/non-injecting cursor (fixed at // one point) yields all-different x-offsets and can NEVER fake an // x-cluster. Working injection lands every point at (target + the // captured monitor's constant origin), so the x-offset is identical // every time — the dominant x-offset cluster IS proof of injection, // and it survives the user moving the mouse vertically mid-test. let targets = [ (nw * 20 / 100, nh * 30 / 100), (nw * 35 / 100, nh * 55 / 100), (nw * 50 / 100, nh * 30 / 100), (nw * 65 / 100, nh * 60 / 100), (nw * 80 / 100, nh * 40 / 100), (nw * 45 / 100, nh * 70 / 100), ]; let mut offs: Vec<(i32, i32)> = Vec::new(); for t in targets.iter() { let c = move_and_settle(&session, &mut rx, &mut col, &mut dec, *t).await; offs.push((c.0 - t.0, c.1 - t.1)); } let tol = (nw / 30).max(16); let (mut best, mut best_x) = (0usize, 0i32); for cand in offs.iter() { let n = offs.iter().filter(|o| (o.0 - cand.0).abs() <= tol).count(); if n > best { best = n; best_x = cand.0; } } // median y among the agreeing (origin) points, for reporting let mut ys: Vec = offs.iter().filter(|o| (o.0 - best_x).abs() <= tol).map(|o| o.1).collect(); ys.sort_unstable(); let oy = ys.get(ys.len() / 2).copied().unwrap_or(0); let detail: Vec = offs.iter().map(|o| format!("{:?}", o)).collect(); // best>=2 is proof: the 6 targets have DISTINCT x, so a stuck/ // non-injecting cursor yields all-different x-offsets (best==1). Two // landings sharing an offset can only come from real injection (and // needs just 2 clean windows out of 6, surviving an active mouse). if best >= 2 { rep.pass("INPUT/mouse", &format!( "cursor tracked injected moves to the monitor's constant origin (~{best_x},{oy}); {best}/{} targets agree (tol {tol}px) — host injected mouse", targets.len())); } else if user_moving { rep.skip("INPUT/mouse", &format!( "inconclusive — the mouse was in active use during the test ({best}/{} agree); rerun when idle (injection itself is proven)", targets.len())); } else { rep.fail("INPUT/mouse", &format!( "only {best}/{} x-offsets agree at ~{best_x} (tol {tol}px); offsets {}", targets.len(), detail.join(" "))); } // Keyboard: inert F13 so nothing lands in whatever window has focus. // SendInput-injected keys are visible to GetAsyncKeyState. const VK_F13: i32 = 0x7C; session.key("F13", true); let mut saw_down = false; for _ in 0..12 { pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(50), |_| false).await; if osprobe::key_down(VK_F13) { saw_down = true; break; } } session.key("F13", false); let mut saw_up = false; for _ in 0..12 { pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(50), |_| false).await; if !osprobe::key_down(VK_F13) { saw_up = true; break; } } if saw_down && saw_up { rep.pass("INPUT/keyboard", "F13 observed down then up via GetAsyncKeyState — host injected the key (wire c/d OK)"); } else { rep.fail("INPUT/keyboard", &format!( "F13 not registered by the OS (down={saw_down}, up={saw_up}) — key not injected")); } } #[cfg(not(windows))] { let base = col.frames; for _ in 0..5 { session.mouse(5, 5, 0, 0.0, 0.0); } session.key("ShiftLeft", true); session.key("ShiftLeft", false); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(1500), |_| false).await; if let Some(r) = col.ended.clone() { rep.fail("INPUT", &format!("session ended after input: {r}")); } else { rep.pass("INPUT", &format!("input sent; session alive, +{} frames (OS-effect check is Windows-only)", col.frames - base)); } } } // ---- CLIPBOARD ------------------------------------------------------ if let Some(r) = col.ended.clone() { rep.skip("CLIPBOARD", &format!("session ended earlier: {r}")); } else { // Real check: the live host runs on this machine, so after we push a // clip the actual Windows clipboard here must contain it. Save & restore // the user's clipboard around the test so nothing is lost. let saved = arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()); let payload = format!("netdesk-e2e-clip-{}", now_unix()); session.clipboard(&payload); let mut matched = false; for _ in 0..15 { pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(100), |_| false).await; if let Ok(mut c) = arboard::Clipboard::new() { if c.get_text().map(|t| t == payload).unwrap_or(false) { matched = true; break; } } } // restore if let Ok(mut c) = arboard::Clipboard::new() { let _ = c.set_text(saved.clone().unwrap_or_default()); } if let Some(r) = col.ended.clone() { rep.fail("CLIPBOARD", &format!("session ended after clip.set: {r}")); } else if matched { rep.pass("CLIPBOARD", "viewer->host: the host's actual clipboard now holds the sent text (restored after)"); } else { rep.fail("CLIPBOARD", "viewer->host: host clipboard did not update to the sent text"); } } // ---- CLIPBOARD/host->viewer (host watches its clipboard, pushes to us) -- if let Some(r) = col.ended.clone() { rep.skip("CLIPBOARD/host", &format!("session ended earlier: {r}")); } else { // Simulate "copied on the host": set this machine's OS clipboard, then // the host's watcher must push it to us as a clip.set (origin host). let saved = arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()); let marker = format!("netdesk-host-clip-{}", now_unix()); let n0 = col.clips.len(); if let Ok(mut c) = arboard::Clipboard::new() { let _ = c.set_text(marker.clone()); } pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| { c.clips.iter().skip(n0).any(|t| t == &marker) }) .await; let got = col.clips.iter().any(|t| t == &marker); if let Ok(mut c) = arboard::Clipboard::new() { let _ = c.set_text(saved.clone().unwrap_or_default()); } if got { rep.pass("CLIPBOARD/host", "host->viewer: text copied on the host arrived at the viewer"); } else { rep.fail("CLIPBOARD/host", "host->viewer: the viewer never received the host's clipboard"); } } // ---- AUDIO (on-demand source: system, then microphone) ------------- if let Some(r) = col.ended.clone() { rep.skip("AUDIO", &format!("session ended earlier: {r}")); } else { // System loopback only captures while something renders — loop a WAV for // the whole audio phase. On a laptop the built-in mic also picks this up // acoustically, so mic energy should rise too when speakers are the output. #[cfg(windows)] let _snd = std::process::Command::new("powershell") .args([ "-NoProfile", "-WindowStyle", "Hidden", "-Command", "$p=New-Object Media.SoundPlayer 'C:\\Windows\\Media\\Ring01.wav'; $p.PlayLooping(); Start-Sleep 16", ]) .spawn(); // Let playback actually start before switching the source. pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(600), |_| false).await; // --- SYSTEM ("what you hear" — WASAPI render loopback) --- let i_sys = col.audio_data.len(); let n0 = col.audio_pkts; session.audio_set(1); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(5), |c| c.audio_pkts >= n0 + 20).await; let sys_pkts = col.audio_pkts - n0; let sys_packets: Vec> = col.audio_data[i_sys..].to_vec(); let (sys_frames, sys_rms, sys_peak) = audio_energy(&sys_packets); // --- MICROPHONE (default capture device) --- let i_mic = col.audio_data.len(); let n1 = col.audio_pkts; session.audio_set(2); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(5), |c| c.audio_pkts >= n1 + 20).await; let mic_pkts = col.audio_pkts - n1; let mic_packets: Vec> = col.audio_data[i_mic..].to_vec(); let (mic_frames, mic_rms, mic_peak) = audio_energy(&mic_packets); // --- OFF (stream must stop) --- session.audio_set(0); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(400), |_| false).await; let n_off = col.audio_pkts; pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(1200), |_| false).await; let after_off = col.audio_pkts - n_off; let detail = format!( "SYSTEM={sys_pkts}pkts/{sys_frames}dec rms={sys_rms:.0} peak={sys_peak} | \ MIC={mic_pkts}pkts/{mic_frames}dec rms={mic_rms:.0} peak={mic_peak} | off->{after_off} more" ); // Transmission is proven when packets arrive AND decode to valid PCM. // SYSTEM additionally must carry real signal (the WAV is loud; silent // filler would be ~0 RMS). MIC transmission = decodable packets flowing // (ambient level varies by room/output device — reported, not gated). let sys_ok = sys_pkts > 0 && sys_frames > 0 && sys_rms > 150.0; let mic_ok = mic_pkts > 0 && mic_frames > 0; let off_ok = after_off <= 2; if sys_ok && mic_ok && off_ok { rep.pass("AUDIO", &detail); } else { let mut why = Vec::new(); if !sys_ok { why.push("SYSTEM not transmitting real audio"); } if !mic_ok { why.push("MIC not transmitting"); } if !off_ok { why.push("did not stop on 'off'"); } rep.fail("AUDIO", &format!("{} [{detail}]", why.join("; "))); } } // ---- CHAT ----------------------------------------------------------- if let Some(r) = col.ended.clone() { rep.skip("CHAT", &format!("session ended earlier: {r}")); } else { let before = col.chats.len(); session.chat("e2e ping"); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(2), |c| { c.chats.len() > before }) .await; if let Some(r) = col.ended.clone() { rep.fail("CHAT", &format!("session ended after chat: {r}")); } else if col.chats.len() > before { rep.pass( "CHAT", &format!("echo received: {:?}", col.chats.last().map(|(_, t)| t.clone())), ); } else { rep.pass("CHAT", "sent 'e2e ping'; no echo to sender (expected), session alive"); } } // ---- QUALITY -------------------------------------------------------- if let Some(r) = col.ended.clone() { rep.skip("QUALITY", &format!("session ended earlier: {r}")); } else { // Nudge with keyframe requests so a static host screen still yields // frames (the host only emits video on change). let b1 = col.frames; session.set_quality(30, 12000, 1.0); // "High" preset for _ in 0..5 { session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(400), |_| false).await; } let d_high = col.frames - b1; let b2 = col.frames; session.set_quality(30, 6000, 1.0); // "Balanced" preset for _ in 0..5 { session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(400), |_| false).await; } let d_bal = col.frames - b2; if let Some(r) = col.ended.clone() { rep.fail("QUALITY", &format!("session ended during set_quality: {r}")); } else if d_high > 0 && d_bal > 0 { rep.pass( "QUALITY", &format!("high:+{d_high} frames, balanced:+{d_bal} frames (stream kept flowing)"), ); } else { rep.fail( "QUALITY", &format!("frames stalled (high:+{d_high}, balanced:+{d_bal})"), ); } } // ---- QUALITY/scale — the resolution-downscale lever the sliders expose -- // set_quality(scale=0.5) must HALVE the host's encoded frame. We read the // dims the HOST reports via session.info (authoritative; an MF decoder does // not re-report dims on a mid-stream resolution change, which the real // viewer handles by trusting session.info). if let Some(r) = col.ended.clone() { rep.skip("QUALITY/scale", &format!("session ended earlier: {r}")); } else { let latest_wh = |c: &Collected| -> (u64, u64) { c.infos .iter() .rev() .find_map(|v| { let w = v.get("w").and_then(|x| x.as_u64())?; let h = v.get("h").and_then(|x| x.as_u64())?; if w > 0 && h > 0 { Some((w, h)) } else { None } }) .unwrap_or((0, 0)) }; session.set_quality(30, 8000, 1.0); session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(2), |_| false).await; let (mut w1, mut h1) = latest_wh(&col); if w1 == 0 { if let Some((_, _, w, h)) = &col.connected { w1 = *w as u64; h1 = *h as u64; } } let target = (w1 as f32 * 0.8) as u64; session.set_quality(30, 8000, 0.5); session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(6), |c| { let (w, _) = latest_wh(c); w > 0 && w < target }) .await; let (w2, h2) = latest_wh(&col); // restore full resolution for the live host + the rest of the run session.set_quality(30, 12000, 1.0); session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(2), |_| false).await; if w1 > 0 && w2 > 0 && w2 < target { rep.pass("QUALITY/scale", &format!( "host re-encoded at scale 0.5: {w1}x{h1} -> {w2}x{h2} (restored to full) — downscale lever works")); } else { rep.fail("QUALITY/scale", &format!( "host did not downscale at scale 0.5: {w1}x{h1} -> {w2}x{h2}")); } } // ---- MONITOR -------------------------------------------------------- if let Some(r) = col.ended.clone() { rep.skip("MONITOR", &format!("session ended earlier: {r}")); } else { let mons = session.monitors().to_vec(); let kb = col.keyframes; session.monitor_switch(0); session.request_keyframe(); let got_kf = pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(3), |c| c.keyframes > kb) .await; let fb = col.frames; // Nudge with keyframe requests so a static host screen still yields a // frame after the switch. for _ in 0..6 { if col.frames > fb { break; } session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(350), |c| c.frames > fb).await; } let resumed = col.frames > fb; if let Some(r) = col.ended.clone() { rep.fail("MONITOR", &format!("session ended during monitor switch: {r}")); } else if resumed { rep.pass( "MONITOR", &format!( "{} monitor(s) advertised; switched to 0, keyframe={}, frames resumed (+{})", mons.len(), got_kf, col.frames - fb ), ); } else { rep.fail("MONITOR", "no frames after monitor_switch(0)+keyframe"); } } // ---- INPUT per MONITOR (the regression that broke input on monitor 2) -- // Switch the host to EACH advertised monitor and confirm injected mouse // lands on THAT monitor: the cursor offset (cursor - sent) must be a // CONSTANT (that monitor's virtual-desktop origin) across targets. Distinct // target x's mean a stuck/mis-mapped cursor can't fake a cluster. #[cfg(windows)] if let Some(r) = col.ended.clone() { rep.skip("INPUT/per-monitor", &format!("session ended earlier: {r}")); } else { let mons = session.monitors().to_vec(); if mons.len() < 2 { rep.skip("INPUT/per-monitor", &format!("only {} monitor(s)", mons.len())); } else { session.set_quality(30, 8000, 1.0); // 1:1 -> sent coords are physical pixels pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(700), |_| false).await; let mut all_ok = true; let mut details: Vec = Vec::new(); for m in &mons { session.monitor_switch(m.i); session.request_keyframe(); let fb = col.frames; pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(2), |c| c.frames > fb).await; let (fw, fh) = (m.w as i32, m.h as i32); let targets = [ (fw * 30 / 100, fh * 40 / 100), (fw * 55 / 100, fh * 35 / 100), (fw * 45 / 100, fh * 65 / 100), (fw * 70 / 100, fh * 55 / 100), ]; let mut offs: Vec<(i32, i32)> = Vec::new(); for t in targets.iter() { let c = move_and_settle(&session, &mut rx, &mut col, &mut dec, *t).await; offs.push((c.0 - t.0, c.1 - t.1)); } let tol = (fw / 30).max(16); let (mut best, mut best_x, mut best_y) = (0usize, 0i32, 0i32); for cand in offs.iter() { let n = offs .iter() .filter(|o| (o.0 - cand.0).abs() <= tol && (o.1 - cand.1).abs() <= tol) .count(); if n > best { best = n; best_x = cand.0; best_y = cand.1; } } let ok = best >= 2; // >=2 distinct-x targets at one offset = proof all_ok &= ok; details.push(format!( "mon#{} origin~({},{}) {}/{}{}", m.i, best_x, best_y, best, targets.len(), if ok { "" } else { " FAIL" } )); } session.monitor_switch(0); // restore if all_ok { rep.pass("INPUT/per-monitor", &format!("injected mouse lands on EVERY monitor: {}", details.join("; "))); } else { rep.fail("INPUT/per-monitor", &format!("input mis-maps on some monitor: {}", details.join("; "))); } } } // ---- SYS/processes -------------------------------------------------- if let Some(r) = col.ended.clone() { rep.skip("SYS/processes", &format!("session ended earlier: {r}")); } else { let n0 = count_kind(&col, "processes"); session.list_processes(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| { count_kind(c, "processes") > n0 && last_items_len(c, "processes").unwrap_or(0) > 0 }) .await; match last_items_len(&col, "processes") { Some(n) if n > 0 => rep.pass("SYS/processes", &format!("sys.res kind=processes, {n} items")), Some(_) => rep.fail("SYS/processes", "processes reply had 0 items"), None => rep.fail("SYS/processes", "no sys.res kind=processes within 4s"), } } // ---- SYS/run (execute CMD/PowerShell on the host — admin feature) --- if let Some(r) = col.ended.clone() { rep.skip("SYS/run", &format!("session ended earlier: {r}")); } else { let n0 = count_kind(&col, "run"); session.run_command("echo NETDESK-EXEC-OK && whoami"); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(6), |c| { count_kind(c, "run") > n0 }) .await; let msg = last_str(&col, "run", "msg").unwrap_or_default(); let elevated = last_bool(&col, "run", "elevated"); if msg.contains("NETDESK-EXEC-OK") { rep.pass( "SYS/run", &format!( "host executed the command and returned output ({} bytes); elevated={:?}", msg.len(), elevated ), ); } else if count_kind(&col, "run") > n0 { rep.fail( "SYS/run", &format!("run reply arrived but marker missing; msg='{}'", msg.chars().take(80).collect::()), ); } else { rep.fail("SYS/run", "no sys.res kind=run within 6s"); } } // ---- SYS/refresh_screen (forces a keyframe) ------------------------- if let Some(r) = col.ended.clone() { rep.skip("SYS/refresh_screen", &format!("session ended earlier: {r}")); } else { let kb = col.keyframes; session.refresh_screen(); let got = pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| c.keyframes > kb) .await; if got { rep.pass("SYS/refresh_screen", &format!("keyframe received (+{})", col.keyframes - kb)); } else { rep.fail("SYS/refresh_screen", "no keyframe within 4s"); } } // ---- SYS/block_input (toggle on then back off) ---------------------- if let Some(r) = col.ended.clone() { rep.skip("SYS/block_input", &format!("session ended earlier: {r}")); } else { let n0 = count_kind(&col, "block_input"); session.block_input(); let got1 = pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| { count_kind(c, "block_input") > n0 }) .await; let on1 = last_on(&col, "block_input"); let n1 = count_kind(&col, "block_input"); session.block_input(); let got2 = pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| { count_kind(c, "block_input") > n1 }) .await; let on2 = last_on(&col, "block_input"); // SAFETY: never leave the host's input blocked. if last_on(&col, "block_input") == Some(true) { let n2 = count_kind(&col, "block_input"); session.block_input(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(3), |c| { count_kind(c, "block_input") > n2 }) .await; } if got1 && got2 && on1 == Some(true) && on2 == Some(false) { rep.pass("SYS/block_input", "on=true then on=false (input restored)"); } else { rep.fail( "SYS/block_input", &format!("expected replies on=true then on=false, saw got1={got1} on1={on1:?} got2={got2} on2={on2:?}"), ); } } // ---- SYS/hide_wallpaper (toggle then restore) ----------------------- if let Some(r) = col.ended.clone() { rep.skip("SYS/hide_wallpaper", &format!("session ended earlier: {r}")); } else { let n0 = count_kind(&col, "wallpaper"); session.hide_wallpaper(); let got1 = pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| { count_kind(c, "wallpaper") > n0 }) .await; let on1 = last_on(&col, "wallpaper"); let n1 = count_kind(&col, "wallpaper"); session.hide_wallpaper(); let got2 = pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(4), |c| { count_kind(c, "wallpaper") > n1 }) .await; let on2 = last_on(&col, "wallpaper"); // SAFETY: never leave the wallpaper hidden. if last_on(&col, "wallpaper") == Some(true) { let n2 = count_kind(&col, "wallpaper"); session.hide_wallpaper(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(3), |c| { count_kind(c, "wallpaper") > n2 }) .await; } if got1 && got2 && on2 == Some(false) { rep.pass( "SYS/hide_wallpaper", &format!("sys.res kind=wallpaper (on1={on1:?} then on2=false, restored)"), ); } else { rep.fail( "SYS/hide_wallpaper", &format!("expected two wallpaper replies ending on=false, saw got1={got1} on1={on1:?} got2={got2} on2={on2:?}"), ); } } // ---- FILES/list_roots ---------------------------------------------- let mut first_root: Option = None; if let Some(r) = col.ended.clone() { rep.skip("FILES/list_roots", &format!("session ended earlier: {r}")); } else { let before = col.file_lists.len(); session.list_roots(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(5), |c| { c.file_lists.len() > before }) .await; match col.file_lists.get(before).cloned() { Some((_path, entries)) if !entries.is_empty() => { first_root = entries.first().map(|e| e.name.clone()); let names: Vec = entries.iter().map(|e| e.name.clone()).collect(); rep.pass( "FILES/list_roots", &format!("{} root(s): {}", entries.len(), names.join(", ")), ); } Some(_) => rep.fail("FILES/list_roots", "roots listing was empty"), None => rep.fail("FILES/list_roots", "no FileList within 5s"), } } // ---- FILES/list_dir (+ optional download) --------------------------- if let Some(r) = col.ended.clone() { rep.skip("FILES/list_dir", &format!("session ended earlier: {r}")); rep.skip("FILES/download", "depends on list_dir"); } else if let Some(root) = first_root.clone() { let before = col.file_lists.len(); session.list_dir(&root); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(5), |c| { c.file_lists.len() > before }) .await; match col.file_lists.get(before).cloned() { Some((_path, entries)) if !entries.is_empty() => { rep.pass( "FILES/list_dir", &format!("{} entries under {}", entries.len(), root), ); // optional: download the smallest regular file <= 256 KiB. let small = entries .iter() .filter(|e| !e.is_dir && e.size > 0 && e.size <= 256 * 1024) .min_by_key(|e| e.size) .cloned(); if let Some(f) = small { let full = join_path(&root, &f.name); let ndl = col.downloads.len(); let nerr = col.file_errs.len(); session.download(&full); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(8), |c| { c.downloads.len() > ndl || c.file_errs.len() > nerr }) .await; if let Some((_fid, name, size, path)) = col.downloads.get(ndl).cloned() { let len = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0); let _ = std::fs::remove_file(&path); if len == size && size == f.size { rep.pass( "FILES/download", &format!("{name}: {len} bytes on disk == host size {size} == listing size"), ); } else { rep.fail( "FILES/download", &format!( "{name}: got {len} bytes, host size {size}, listing size {}", f.size ), ); } } else if let Some(e) = col.file_errs.get(nerr) { rep.fail("FILES/download", &format!("file.err: {e}")); } else { rep.fail("FILES/download", "no FileDownloadDone within 8s"); } } else { rep.skip( "FILES/download", &format!("no regular file <=256KiB in {root} to safely test"), ); } } Some(_) => { rep.fail("FILES/list_dir", &format!("{root} listed but 0 entries")); rep.skip("FILES/download", "list_dir returned no entries"); } None => { rep.fail("FILES/list_dir", "no FileList within 5s"); rep.skip("FILES/download", "list_dir did not respond"); } } } else { rep.skip("FILES/list_dir", "no root available from list_roots"); rep.skip("FILES/download", "no root available from list_roots"); } // ---- FILES/upload (verified on disk — the live host writes here) ----- // This is the same path the viewer's "Upload…" button and drag & drop use. if let Some(r) = col.ended.clone() { rep.skip("FILES/upload", &format!("session ended earlier: {r}")); } else { let dir = std::env::temp_dir(); let fname = format!("netdesk-e2e-up-{}.txt", now_unix()); let content = format!("netdesk upload e2e {}", now_unix()).into_bytes(); let dest = dir.join(&fname); let _ = std::fs::remove_file(&dest); let dir_s = dir.to_string_lossy().to_string(); session.upload(&dir_s, &fname, &content); let mut ok = false; for _ in 0..25 { pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(120), |_| false).await; if std::fs::read(&dest).map(|g| g == content).unwrap_or(false) { ok = true; break; } } let _ = std::fs::remove_file(&dest); if ok { rep.pass("FILES/upload", &format!( "uploaded {} bytes as {fname}; file on the host matches byte-for-byte (removed after)", content.len())); } else { rep.fail("FILES/upload", "uploaded file never appeared on the host with matching content"); } } // ---- FILES/download-stream — multi-chunk, disk-streamed, byte-verified -- // Round-trip a 1 MiB file (4 x 256 KiB chunks) to exercise the sequential // seek/write disk-streaming path (no RAM buffering) and prove integrity. if let Some(r) = col.ended.clone() { rep.skip("FILES/download-stream", &format!("session ended earlier: {r}")); } else { let dir = std::env::temp_dir(); let fname = format!("netdesk-e2e-stream-{}.bin", now_unix()); let hostfile = dir.join(&fname); // host writes here (host == this machine) let _ = std::fs::remove_file(&hostfile); let data: Vec = (0..1024u32 * 1024) .map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8) .collect(); session.upload(&dir.to_string_lossy(), &fname, &data); let mut uploaded = false; for _ in 0..60 { pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(120), |_| false).await; if std::fs::metadata(&hostfile).map(|m| m.len() == data.len() as u64).unwrap_or(false) { uploaded = true; break; } } // Settle so the host flushes/closes its write handle before we read the // file back — otherwise the download can race the still-open writer and // return same-length-but-different bytes. pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(800), |_| false).await; if !uploaded { rep.fail("FILES/download-stream", "upload of the 1 MiB test file never completed"); } else { let ndl = col.downloads.len(); let nerr = col.file_errs.len(); session.download(&hostfile.to_string_lossy()); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(20), |c| { c.downloads.len() > ndl || c.file_errs.len() > nerr }) .await; if let Some((_fid, _name, size, path)) = col.downloads.get(ndl).cloned() { let got = std::fs::read(&path).unwrap_or_default(); let _ = std::fs::remove_file(&path); if got.len() == data.len() && got == data { rep.pass("FILES/download-stream", &format!( "1 MiB round-trip ({} chunks) streamed to disk, byte-for-byte match (host size {size})", data.len() / (256 * 1024))); } else { rep.fail("FILES/download-stream", &format!( "mismatch: {} bytes vs {} (host size {size})", got.len(), data.len())); } } else if let Some(e) = col.file_errs.get(nerr) { rep.fail("FILES/download-stream", &format!("file.err: {e}")); } else { rep.fail("FILES/download-stream", "no FileDownloadDone within 20s"); } } let _ = std::fs::remove_file(&hostfile); } // ---- remote cursor overlay (position + bitmap stream) ---------------- if col.cursors > 0 { rep.pass( "CURSOR", &format!( "{} remote-cursor positions, shape bitmap {} bytes", col.cursors, col.cursor_shape_bytes ), ); } else { rep.fail("CURSOR", "no remote-cursor events received"); } // ---- RECORDING — mux the live H.264 into a real .mp4, read it back ----- // The viewer's Rec button tees the same access units into this recorder. if let Some(r) = col.ended.clone() { rep.skip("RECORDING", &format!("session ended earlier: {r}")); } else { let (w, h) = col .connected .as_ref() .map(|c| (c.2 as u16, c.3 as u16)) .unwrap_or((1920, 1080)); let path = std::env::temp_dir().join(format!("netdesk-e2e-rec-{}.mp4", now_unix())); col.recorder = Some(netdesk_core::recorder::Mp4Recorder::new(&path, w, h)); session.set_quality(30, 8000, 1.0); // Force several frames even if the host screen is static. for _ in 0..8 { session.request_keyframe(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_millis(400), |_| false).await; } let rec = col.recorder.take().unwrap(); let fed = rec.samples(); match rec.finish() { Ok((p, _)) => { let sz = std::fs::metadata(&p).map(|m| m.len()).unwrap_or(0); match netdesk_core::recorder::probe_mp4(&p) { Ok((vt, samples, pw, ph)) if vt >= 1 && samples > 0 => rep.pass( "RECORDING", &format!( "valid .mp4 ({sz} bytes): {vt} video track, {samples} samples, {pw}x{ph} — H.264 remux, no re-encode" ), ), Ok((vt, samples, _, _)) => rep.fail( "RECORDING", &format!("mp4 parsed but empty: {vt} tracks / {samples} samples (fed {fed})"), ), Err(e) => rep.fail("RECORDING", &format!("mp4 unreadable: {e} (fed {fed})")), } let _ = std::fs::remove_file(&p); } Err(e) => rep.fail("RECORDING", &format!("finalize failed: {e}")), } } // ---- close cleanly -------------------------------------------------- session.close(); pump_until(&mut rx, &mut col, &mut dec, Duration::from_secs(2), |c| c.ended.is_some()).await; println!( " session closed (ended reason: {})", col.ended.as_deref().unwrap_or("") ); println!( " tallies: frames_rx={} decoded={} keyframes={} audio_pkts={} sys_replies={} file_lists={}", col.frames, col.decoded, col.keyframes, col.audio_pkts, col.sys.len(), col.file_lists.len() ); summary(&rep); } fn host_ctl_checks(rep: &mut Report) { // Point config resolution at the live engine's portable config (fresh // heartbeat) unless the operator already set NETDESK_CONFIG. if std::env::var_os("NETDESK_CONFIG").is_none() { std::env::set_var("NETDESK_CONFIG", PORTABLE_CONFIG); } match host_ctl::read_config() { Ok(cfg) => { if cfg.id == host_id() { rep.pass( "HOSTCTL/read_config", &format!("id={} name={} server={}", cfg.id, cfg.name, cfg.server), ); } else { rep.fail( "HOSTCTL/read_config", &format!("id={} (expected {})", cfg.id, host_id()), ); } let es = host_ctl::engine_status(&cfg); if es.online { rep.pass( "HOSTCTL/engine_status", &format!("online=true state={} in_session={}", es.state, es.in_session), ); } else { rep.fail( "HOSTCTL/engine_status", &format!("online=false state={}", es.state), ); } match host_ctl::ping_relay(&cfg) { Ok(ms) => rep.pass("HOSTCTL/ping_relay", &format!("{ms} ms to {}/health", cfg.api_base())), Err(e) => rep.fail("HOSTCTL/ping_relay", &format!("{e}")), } match host_ctl::list_devices(&cfg) { Ok(devs) => { let names: Vec = devs .iter() .map(|d| format!("{}({},{})", d.name, d.id, if d.online { "online" } else { "offline" })) .collect(); let has_devpc = devs.iter().any(|d| d.name.to_uppercase().contains("DEVPC")); if !devs.is_empty() && has_devpc { rep.pass( "HOSTCTL/list_devices", &format!("{} device(s): {}", devs.len(), names.join(", ")), ); } else if !devs.is_empty() { rep.fail( "HOSTCTL/list_devices", &format!("{} device(s) but none named DEVPC: {}", devs.len(), names.join(", ")), ); } else { rep.fail("HOSTCTL/list_devices", "0 devices returned"); } } Err(e) => rep.fail("HOSTCTL/list_devices", &format!("{e}")), } } Err(e) => { rep.fail("HOSTCTL/read_config", &format!("{e}")); rep.fail("HOSTCTL/engine_status", "no config"); rep.fail("HOSTCTL/ping_relay", "no config"); rep.fail("HOSTCTL/list_devices", "no config"); } } } fn summary(rep: &Report) { println!("================================================================"); println!("WINDOW_END {}", now_unix()); println!( "SUMMARY: {} / {} passed ({} skipped)", rep.pass, rep.pass + rep.fail, rep.skip ); }