186 lines
6.6 KiB
Rust
186 lines
6.6 KiB
Rust
|
|
//! 3b 桌面验收:相对鼠标(协议 v4)的像素级闭环。
|
|||
|
|
//!
|
|||
|
|
//! 为什么必须单独写一个 proof:V-B1 的判据是「位移**恰好**等于请求值」,
|
|||
|
|
//! 而 Android 侧的真机脚本只能看到 UI 文本,看不到远端指针实际走了多少像素。
|
|||
|
|
//! 这里直接走完整链路:viewer-core 发 `MouseMoveRel` → QUIC → agent 注入 →
|
|||
|
|
//! `GetCursorPos` 读回,逐项比对。
|
|||
|
|
//!
|
|||
|
|
//! 覆盖判据:
|
|||
|
|
//! - V-B1a 方向与量值:dx=+50 → +50px,dx=-50 → -50px(误差 ≤1px)
|
|||
|
|
//! - V-B1b 慢速不丢位移:连发 20 次 dx=+1 → 累计 +20px(误差 ≤2px)
|
|||
|
|
//! - V-B1c 撞边不积债:右边界 +200 后 -50 → 立即 -50
|
|||
|
|
//!
|
|||
|
|
//! 用法(agent 需已启动):
|
|||
|
|
//! cargo run --release -p viewer-core --example rel_mouse_proof -- --host 127.0.0.1 --port 47800 --code t
|
|||
|
|
//!
|
|||
|
|
//! 安全:运行前记录光标原位,结束时还原(不污染用户桌面状态)。
|
|||
|
|
|
|||
|
|
use std::time::Duration;
|
|||
|
|
use viewer_core::{ConnectConfig, Session};
|
|||
|
|
|
|||
|
|
fn arg(name: &str) -> Option<String> {
|
|||
|
|
let a: Vec<String> = std::env::args().collect();
|
|||
|
|
a.iter()
|
|||
|
|
.position(|x| x == name)
|
|||
|
|
.and_then(|i| a.get(i + 1))
|
|||
|
|
.cloned()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 读当前光标(物理像素)。用 windows crate 而不是 shell,避免进程 DPI 差异。
|
|||
|
|
fn cursor() -> (i32, i32) {
|
|||
|
|
use windows::Win32::Foundation::POINT;
|
|||
|
|
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
|
|||
|
|
let mut p = POINT::default();
|
|||
|
|
unsafe {
|
|||
|
|
let _ = GetCursorPos(&mut p);
|
|||
|
|
}
|
|||
|
|
(p.x, p.y)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn set_cursor(x: i32, y: i32) {
|
|||
|
|
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
|
|||
|
|
unsafe {
|
|||
|
|
let _ = SetCursorPos(x, y);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn screen_size() -> (i32, i32) {
|
|||
|
|
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
|
|||
|
|
unsafe { (GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)) }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn enable_dpi_awareness() {
|
|||
|
|
use windows::Win32::UI::HiDpi::{
|
|||
|
|
SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
|
|||
|
|
};
|
|||
|
|
unsafe {
|
|||
|
|
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 发一串相对增量,每次之间留出注入完成的时间,然后读回光标。
|
|||
|
|
fn apply_rel(session: &Session, steps: &[(i16, i16)], settle_ms: u64) {
|
|||
|
|
for &(dx, dy) in steps {
|
|||
|
|
session.send_mouse_rel(dx, dy);
|
|||
|
|
// 让 QUIC 上行 + agent 注入 + 光标更新都完成;太小会读到中间态
|
|||
|
|
std::thread::sleep(Duration::from_millis(settle_ms));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::main]
|
|||
|
|
async fn main() -> anyhow::Result<()> {
|
|||
|
|
enable_dpi_awareness();
|
|||
|
|
let port: u16 = arg("--port").map(|s| s.parse()).transpose()?.unwrap_or(47800);
|
|||
|
|
let code = arg("--code").unwrap_or_else(|| "t".into());
|
|||
|
|
let host = arg("--host").unwrap_or_else(|| "127.0.0.1".into());
|
|||
|
|
|
|||
|
|
let (sw, sh) = screen_size();
|
|||
|
|
println!("[proof] screen {sw}x{sh} (physical, DPI-aware)");
|
|||
|
|
|
|||
|
|
let session = Session::connect(ConnectConfig::direct(&host, port, code)).await?;
|
|||
|
|
let (dw, dh) = session.desktop_size();
|
|||
|
|
println!("[proof] agent desktop {dw}x{dh}");
|
|||
|
|
|
|||
|
|
// 记录原位,结束时还原(不污染桌面状态)
|
|||
|
|
let (ox, oy) = cursor();
|
|||
|
|
println!("[proof] cursor origin ({ox},{oy})");
|
|||
|
|
|
|||
|
|
let mut failures = 0usize;
|
|||
|
|
|
|||
|
|
// ---- V-B1a 方向与量值 ----
|
|||
|
|
println!("\n=== V-B1a direction & magnitude ===");
|
|||
|
|
for &delta in &[50i16, -50i16, 120i16, -120i16] {
|
|||
|
|
// 每次都从屏幕中心出发,避免被边界 clamp 干扰量值判定
|
|||
|
|
set_cursor(sw / 2, sh / 2);
|
|||
|
|
std::thread::sleep(Duration::from_millis(60));
|
|||
|
|
let (bx, _) = cursor();
|
|||
|
|
apply_rel(&session, &[(delta, 0)], 80);
|
|||
|
|
let (ax, _) = cursor();
|
|||
|
|
let applied = ax - bx;
|
|||
|
|
let ok = (applied - delta as i32).abs() <= 1;
|
|||
|
|
println!(
|
|||
|
|
" dx={delta:+5} from x={bx:4} to x={ax:4} applied={applied:+5} {}",
|
|||
|
|
if ok { "PASS" } else { "FAIL" }
|
|||
|
|
);
|
|||
|
|
if !ok {
|
|||
|
|
failures += 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 纵向同理(同一套代码路径,但轴不同,clamp 上界不同)
|
|||
|
|
for &delta in &[40i16, -40i16] {
|
|||
|
|
set_cursor(sw / 2, sh / 2);
|
|||
|
|
std::thread::sleep(Duration::from_millis(60));
|
|||
|
|
let (_, by) = cursor();
|
|||
|
|
apply_rel(&session, &[(0, delta)], 80);
|
|||
|
|
let (_, ay) = cursor();
|
|||
|
|
let applied = ay - by;
|
|||
|
|
let ok = (applied - delta as i32).abs() <= 1;
|
|||
|
|
println!(
|
|||
|
|
" dy={delta:+5} from y={by:4} to y={ay:4} applied={applied:+5} {}",
|
|||
|
|
if ok { "PASS" } else { "FAIL" }
|
|||
|
|
);
|
|||
|
|
if !ok {
|
|||
|
|
failures += 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- V-B1b 慢速小幅不丢位移 ----
|
|||
|
|
println!("\n=== V-B1b slow small steps must not be lost ===");
|
|||
|
|
{
|
|||
|
|
set_cursor(sw / 2, sh / 2);
|
|||
|
|
std::thread::sleep(Duration::from_millis(60));
|
|||
|
|
let (bx, _) = cursor();
|
|||
|
|
// 20 次 dx=+1:若注入层按 mickey 透传,dx=1 会被加速吞成 0 像素(实测如此)
|
|||
|
|
let steps: Vec<(i16, i16)> = (0..20).map(|_| (1i16, 0i16)).collect();
|
|||
|
|
apply_rel(&session, &steps, 45);
|
|||
|
|
std::thread::sleep(Duration::from_millis(120));
|
|||
|
|
let (ax, _) = cursor();
|
|||
|
|
let applied = ax - bx;
|
|||
|
|
let ok = (applied - 20).abs() <= 2;
|
|||
|
|
println!(" 20 x dx=+1 from x={bx} to x={ax} applied={applied:+} (expect +20) {}",
|
|||
|
|
if ok { "PASS" } else { "FAIL" });
|
|||
|
|
if !ok {
|
|||
|
|
failures += 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- V-B1c 撞边不积债 ----
|
|||
|
|
println!("\n=== V-B1c edge clamp leaves no step debt ===");
|
|||
|
|
{
|
|||
|
|
set_cursor(sw - 1, sh / 2);
|
|||
|
|
std::thread::sleep(Duration::from_millis(60));
|
|||
|
|
let (bx, _) = cursor();
|
|||
|
|
apply_rel(&session, &[(200, 0)], 80);
|
|||
|
|
let (mx, _) = cursor();
|
|||
|
|
let clamped = mx - bx;
|
|||
|
|
// 回滑必须立即生效(若累积器"记住"了被 clamp 丢掉的 200px,这里会先空转)
|
|||
|
|
apply_rel(&session, &[(-50, 0)], 80);
|
|||
|
|
let (ax, _) = cursor();
|
|||
|
|
let back = ax - mx;
|
|||
|
|
let ok = clamped == 0 && (back + 50).abs() <= 1;
|
|||
|
|
println!(
|
|||
|
|
" at right edge x={bx}: +200 -> applied {clamped:+} (clamped); -50 -> applied {back:+} {}",
|
|||
|
|
if ok { "PASS" } else { "FAIL" }
|
|||
|
|
);
|
|||
|
|
if !ok {
|
|||
|
|
failures += 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 还原光标
|
|||
|
|
set_cursor(ox, oy);
|
|||
|
|
|
|||
|
|
println!("\n========== RESULT ==========");
|
|||
|
|
if failures == 0 {
|
|||
|
|
println!("REL MOUSE PROOF PASS (all V-B1a/b/c checks)");
|
|||
|
|
} else {
|
|||
|
|
println!("REL MOUSE PROOF FAIL ({failures} checks failed)");
|
|||
|
|
}
|
|||
|
|
session.close().await;
|
|||
|
|
if failures > 0 {
|
|||
|
|
std::process::exit(1);
|
|||
|
|
}
|
|||
|
|
Ok(())
|
|||
|
|
}
|