text_check 走的是产品同一条路径(Session::send_text → v3 线格式 → agent SendInput KEYEVENTF_UNICODE),手机掉线也能先把 Windows 半段验实。 参数是十进制 code unit 列表而不是自然语言字符串:命令行到 Rust 的编码往返 会引入与本轮无关的变量,由 PowerShell 从 .NET 字符串取 char[] 更忠实。 proof 脚本刻意用自有 WinForms 靶窗而非记事本:Win11 记事本所有标签页同进程, 本机当时开着用户文档,往里注入等于改用户文件。靶窗自报文本(写文件), 判据不依赖截图和 OCR。注入前后各断言一次前台窗口——SendInput 打的是前台, 靶窗不在前台时的注入会进别的应用,宁可失败也不误注入。
88 lines
3.3 KiB
Rust
88 lines
3.3 KiB
Rust
//! 协议 v3 文本注入链路的桌面真边界验证工具(loop-2b,不依赖 Android)。
|
||
//!
|
||
//! 证明的事:`Session::send_text` 经线格式到被控端后,`agent` 的
|
||
//! `KEYEVENTF_UNICODE` 注入把**同一段 UTF-16 code unit** 送进了前台应用。
|
||
//! 判据由外部读取目标窗口的实际文本内容(调用方负责),本工具只负责发送。
|
||
//!
|
||
//! 用法:
|
||
//! 终端1: target\release\agent.exe --port 47800 --code t
|
||
//! 终端2: 把目标应用(记事本)切到前台,然后
|
||
//! cargo run -p viewer-core --example text_check -- \
|
||
//! --port 47800 --code t --units 20320,22909,55357,56832
|
||
//!
|
||
//! `--units` 是十进制的 UTF-16 code unit 列表(逗号分隔):
|
||
//! 「你好😀」= 20320,22909,55357,56832(😀 是代理对)。
|
||
//! 刻意不收自然语言字符串参数:命令行到 Rust 的编码往返会引入与本轮无关的变量,
|
||
//! 由调用方(PowerShell)从 .NET 字符串取 char[] 更忠实。
|
||
//!
|
||
//! `--delay`:连接后等待毫秒再注入。给调用方把目标窗口切到前台的时间——
|
||
//! SendInput 注入的是**前台**应用,窗口没在前台等于测了个寂寞。
|
||
//! `--hold`:注入后保持会话的毫秒数,等消息在远端消化完再关连接。
|
||
|
||
use anyhow::{bail, Result};
|
||
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()
|
||
}
|
||
|
||
fn parse_units(spec: &str) -> Result<Vec<u16>> {
|
||
if spec.trim().is_empty() {
|
||
bail!("--units 为空:本轮要验证的是「有文本送出并被注入」,空输入没有验证价值");
|
||
}
|
||
spec.split(',')
|
||
.map(|s| s.trim())
|
||
.filter(|s| !s.is_empty())
|
||
.map(|s| {
|
||
s.parse::<u32>()
|
||
.ok()
|
||
.and_then(|v| u16::try_from(v).ok())
|
||
.ok_or_else(|| anyhow::anyhow!("--units 必须是 0..=65535 的十进制数,got {s:?}"))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[tokio::main]
|
||
async fn main() -> Result<()> {
|
||
let port: u16 = arg("--port")
|
||
.map(|s| s.parse())
|
||
.transpose()?
|
||
.unwrap_or(47800);
|
||
let code = arg("--code").unwrap_or_else(|| "123456".into());
|
||
let host = arg("--host").unwrap_or_else(|| "127.0.0.1".into());
|
||
let delay = arg("--delay")
|
||
.map(|s| s.parse())
|
||
.transpose()?
|
||
.unwrap_or(1500u64);
|
||
let hold = arg("--hold")
|
||
.map(|s| s.parse())
|
||
.transpose()?
|
||
.unwrap_or(800u64);
|
||
let units = parse_units(&arg("--units").unwrap_or_default())?;
|
||
|
||
let session = Session::connect(ConnectConfig::direct(&host, port, &code)).await?;
|
||
println!(
|
||
"[text_check] connected peer={} units={}",
|
||
session.peer_addr(),
|
||
units.len()
|
||
);
|
||
|
||
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||
session.send_text(&units);
|
||
println!("[text_check] sent (只报长度,不打印内容)");
|
||
|
||
tokio::time::sleep(Duration::from_millis(hold)).await;
|
||
let st = session.stats();
|
||
println!(
|
||
"[text_check] frames={} rtt={}ms — 会话活着,注入已发给前台应用",
|
||
st.frames_in, st.rtt_ms
|
||
);
|
||
session.close().await;
|
||
Ok(())
|
||
}
|