remote/agent-core/src/lib.rs

694 lines
28 KiB
Rust
Raw Normal View History

feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
//! 平台无关的被控端会话层:QUIC/TLS 握手、验证码认证、H.264 编码下行、输入分发。
//!
//! 从 `agent/`(Windows)抽出来,供 Windows 与 Linux 被控端共用,避免两端各写一份
//! QUIC 握手/认证/丢帧逻辑造成协议漂移。平台差异只通过两个 trait 注入:
//!
//! - [`ScreenCapturer`]:屏幕采集(Windows = DXGI Desktop Duplication,Linux = X11)
//! - [`InputInjector`]:输入注入(Windows = SendInput,Linux = XTest)
//!
//! 线格式完全复用 `common`,**不改协议**:键盘沿用 Set-1 扫描码 + extended 标志,
//! 鼠标沿用虚拟桌面归一化坐标 0..=65535。
use anyhow::Result;
use common::{
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
encode_video_frame, h264_contains_idr, read_control, write_control, ControlMsg, InputEvent,
MouseButton, VideoFrameHeader,
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
};
use openh264::encoder::{
BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType,
};
use openh264::formats::{BgraSliceU8, YUVBuffer};
use openh264::OpenH264API;
use rustls::pki_types::PrivatePkcs8KeyDer;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, watch};
/// 屏幕采集后端。
///
/// 语义刻意对齐 DXGI Desktop Duplication,使 Linux 侧只需实现同一套模型:
/// `acquire()` 阻塞至多 `timeout_ms`,返回是否拿到**新**帧;`copy_frame()` 再取出像素。
pub trait ScreenCapturer: Send {
/// 桌面尺寸(物理像素)
fn size(&self) -> (u32, u32);
/// 阻塞至多 `timeout_ms`。
/// `Ok(true)` = 有新帧(已拷进内部缓冲,可由 `copy_frame` 取出);
/// `Ok(false)` = 超时且画面未变(静止桌面)。
fn acquire(&mut self, timeout_ms: u32) -> Result<bool>;
/// 取出最近一帧,BGRA 紧凑排列(已去掉 row pitch),供 openh264 直接编码。
fn copy_frame(&mut self) -> Result<Vec<u8>>;
}
/// 输入注入后端。实现需可在异步运行时中被多任务共享,故要求 `Send + Sync`。
pub trait InputInjector: Send + Sync + 'static {
fn mouse_move(&self, x: u16, y: u16);
fn mouse_button(&self, b: MouseButton, down: bool);
fn wheel(&self, dy: i32);
fn key(&self, scancode: u16, extended: bool, down: bool);
/// 协议 v3 的文本注入(UTF-16 code unit)。
///
/// **默认实现刻意不注入任何东西,但必须留下可见痕迹**:Linux 后端本轮未实现
/// 文本注入(X11 需要 keysym/XKB 映射,与 Windows 的 `KEYEVENTF_UNICODE`
/// 不同构,另立循环)。全局规则要求异常路径有可见后果——用户对着 Linux 被控端
/// 打字时「什么都没发生」不能是静默的,日志必须说明是没实现。
///
/// 日志纪律:只打印长度,**不打印用户输入内容**。
fn text(&self, units: &[u16]) {
eprintln!(
"[agent-core] 文本注入未实现(忽略 {} 个 UTF-16 code unit)",
units.len()
);
}
/// 协议 v4 的相对鼠标移动(单位:被控端桌面物理像素)。
///
/// 与 [`InputInjector::text`] 同一条纪律:**默认实现不注入,但必须留下可见痕迹**。
/// Linux/X11 后端本轮未实现相对移动(XTest 的相对语义要自己维护指针状态,
/// 与 Windows 的 `GetCursorPos`+绝对注入不同构,另立循环)。用户用相对模式
/// 操作 Linux 被控端时「指针纹丝不动」不能是静默的,日志必须说明是没实现。
fn mouse_move_rel(&self, dx: i16, dy: i16) {
eprintln!("[agent-core] 相对鼠标注入未实现(忽略 dx={dx} dy={dy})");
}
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
}
/// 把一条协议输入事件分发到注入后端。
pub fn dispatch_input(inj: &dyn InputInjector, ev: InputEvent) {
match ev {
InputEvent::MouseMove { x, y } => inj.mouse_move(x, y),
InputEvent::MouseDown(b) => inj.mouse_button(b, true),
InputEvent::MouseUp(b) => inj.mouse_button(b, false),
InputEvent::Wheel { dy } => inj.wheel(dy),
InputEvent::KeyDown { scancode, extended } => inj.key(scancode, extended, true),
InputEvent::KeyUp { scancode, extended } => inj.key(scancode, extended, false),
InputEvent::Text { units } => {
// 上限在前端截断,截断必须可见(否则用户以为长文本已送达)。
let (units, truncated) = common::clamp_text_units(units);
if truncated {
eprintln!(
"[agent-core] 文本超过上限,已截断到 {} 个 code unit",
common::MAX_TEXT_UNITS
);
}
inj.text(&units);
}
// v4:相对移动原样下发(含负值)。clamp 与像素换算由注入后端负责——
// 只有它知道被控端桌面的真实像素尺寸(`agent-core` 是平台无关层)。
InputEvent::MouseMoveRel { dx, dy } => inj.mouse_move_rel(dx, dy),
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
}
}
/// 会话参数
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub port: u16,
pub code: String,
}
/// 编码档位。默认值即 Windows 现有档位,改动需两端同步评估。
#[derive(Debug, Clone, Copy)]
pub struct EncoderSettings {
pub bitrate_bps: u32,
pub max_fps: f32,
pub intra_period: u32,
}
impl Default for EncoderSettings {
fn default() -> Self {
// 关键帧周期:以**时间**为准而非固定帧数,避免帧率变化后恢复窗口漂移。
// 1 秒 @30fps = 30 帧。主恢复路径是 KeyFrameReq(协议 v2 后已确认生效),
// 自动 IDR 只是兜底;此前 300 帧 @30fps 意味着兜底窗口长达 10 秒。
let max_fps = 30.0f32;
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
Self {
bitrate_bps: 5_000_000,
max_fps,
intra_period: (max_fps as u32).max(1),
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
}
}
}
/// 解析 `--port` / `--code`(两端共用,保持命令行一致)
pub fn parse_args() -> Result<(u16, String)> {
let mut port = 47800u16;
let mut code = "123456".to_string();
let a: Vec<String> = std::env::args().collect();
let mut i = 1;
while i + 1 < a.len() {
match a[i].as_str() {
"--port" => port = a[i + 1].parse()?,
"--code" => code = a[i + 1].clone(),
_ => {}
}
i += 2;
}
Ok((port, code))
}
fn make_encoder(enc: &EncoderSettings) -> Result<Encoder> {
Ok(Encoder::with_api_config(
OpenH264API::from_source(),
EncoderConfig::new()
.bitrate(BitRate::from_bps(enc.bitrate_bps))
.max_frame_rate(FrameRate::from_hz(enc.max_fps))
.rate_control_mode(RateControlMode::Bitrate)
.usage_type(UsageType::ScreenContentRealTime)
.intra_frame_period(IntraFramePeriod::from_num_frames(enc.intra_period)),
)?)
}
enum LoopEnd {
/// 下行通道关闭 —— 会话结束,采集线程退出
ChannelClosed,
/// 采集后端失效(Windows 的 ACCESS_LOST / X11 连接断开)—— 重建后继续
Rebuild,
}
/// 编码一帧并推入下行通道。返回 false 表示通道已关闭(会话结束)。
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
///
/// `seq` 由调用方持有并自增:帧序号必须覆盖**每一次实际发送**(含静止桌面重发),
/// 不能由编码器计数(编码器只知道编码了多少帧,不知道发了多少帧)。
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
fn encode_send(
encoder: &mut Encoder,
raw: &[u8],
(w, h): (u32, u32),
ts_ms: u64,
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
seq: u64,
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
tx: &watch::Sender<Option<Vec<u8>>>,
last_frame: &mut Option<Vec<u8>>,
) -> bool {
let yuv = YUVBuffer::from_bgra8_source(BgraSliceU8::new(raw, (w as usize, h as usize)));
match encoder.encode(&yuv) {
Ok(ebs) => {
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
let payload = ebs.to_vec();
let header = VideoFrameHeader {
width: w,
height: h,
ts_ms,
seq,
keyframe: h264_contains_idr(&payload),
};
let frame = encode_video_frame(&header, &payload);
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
if tx.send(Some(frame.clone())).is_err() {
return false;
}
*last_frame = Some(frame);
true
}
Err(e) => {
eprintln!("[capture] encode failed: {e}");
true
}
}
}
/// 单次采集会话:抓帧 → 编码 → 推入 watch 单槽。
fn capture_session(
cap: &mut dyn ScreenCapturer,
tx: &watch::Sender<Option<Vec<u8>>>,
keyreq: &AtomicBool,
enc: &EncoderSettings,
) -> LoopEnd {
let mut encoder = match make_encoder(enc) {
Ok(e) => e,
Err(e) => {
eprintln!("[capture] encoder init failed: {e}");
return LoopEnd::ChannelClosed;
}
};
let (w, h) = cap.size();
eprintln!("[capture] started {w}x{h}");
let start = Instant::now();
let min_frame = Duration::from_secs_f32(1.0 / enc.max_fps);
let mut last_frame: Option<Vec<u8>> = None;
// 保留最近一帧裸像素:静止桌面收到关键帧请求时,仍能重编一个真关键帧
let mut last_raw: Option<Vec<u8>> = None;
let mut last_sent = Instant::now() - Duration::from_secs(10);
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
// 帧序号:覆盖每一次实际发送(含静止桌面重发),供主控端做丢帧检测
let mut seq: u64 = 0;
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
loop {
feat(agent): 串行多会话——会话结束不再退出进程 问题(阶段 2 的硬阻塞) accept() 只接一次,会话结束进程退出。阶段 1 一次验收看不出来, 但阶段 2 要反复测试输入,每测一次都要手动重启被控端。 (即公共阻碍清单里的 P5,此前判断为「只影响调试效率」, 现在它阻塞迭代,提前做掉。) 改动 1. main:会话体抽成 serve_session(),外层 accept 循环。 会话失败(含认证失败)只记日志不退出进程。 明确不做并发多会话:DXGI Desktop Duplication 每个输出仅允许一个 活动实例,第二个 DuplicateOutput 会让第一个收到 ACCESS_LOST。 2. 认证失败路径补 conn.closed() 等待(agent-core 已有同款修复, 此处同源补齐 = 手册 §6.1 第 1 条):否则客户端只看到 connection lost 而不是 AuthFail。 3. capture 循环加 tx.receiver_count()==0 守卫。 只靠「send 失败退出」不够——桌面完全静止时那个分支根本不发送, 线程会永远持有 duplication,下一个会话的 DuplicateOutput 直接失败 (表现为新会话黑屏)。agent-core 同源修复。 4. 会话收尾:finish 流 + drop 接收端 + 等采集线程退出(2s 超时)后才 返回,调用方才 accept 下一个连接。 验证(连续两个会话,中间不重启 agent) 会话1:收到帧 40/40,结论 ✅ agent 存活:✅ 会话2:收到帧 40/40,结论 ✅ 日志:session closed -> waiting for next connection -> peer connected -> auth ok;stderr 有 [capture] no receiver, stop 证明线程真退出
2026-10-29 21:31:44 -04:00
// 会话结束守卫:发送端已 drop 接收器时立即退出。
// 与 agent/src/capture.rs 同源——静止桌面分支不发送,只靠 send 失败退出
// 会让线程在画面不动时永不退出(X11 无 DXGI 式独占,危害较小但同样泄漏线程)。
if tx.receiver_count() == 0 {
eprintln!("[capture] no receiver, stop");
return LoopEnd::ChannelClosed;
}
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
let iter_start = Instant::now();
match cap.acquire(80) {
Ok(true) => match cap.copy_frame() {
Ok(raw) => {
// 主控端请求关键帧(解码异常恢复)
if keyreq.swap(false, Ordering::Relaxed) {
encoder.force_intra_frame();
}
let ts = start.elapsed().as_millis() as u64;
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
if !encode_send(&mut encoder, &raw, (w, h), ts, seq, tx, &mut last_frame) {
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
return LoopEnd::ChannelClosed; // 会话结束
}
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
seq = seq.wrapping_add(1);
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
last_raw = Some(raw);
last_sent = Instant::now();
// 帧间隔节流到 max_fps
let el = iter_start.elapsed();
if el < min_frame {
std::thread::sleep(min_frame - el);
}
}
Err(e) => {
eprintln!("[capture] frame copy failed: {e}");
return LoopEnd::Rebuild;
}
},
Ok(false) => {
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
// 静止桌面:先看关键帧请求,再决定是否走 500ms 保活节流。
//
// ⚠️ 这里必须保证「消费了标志就一定发送」:先前写法是
// `if force && last_raw.is_some() { ... }`,当 force 为真但
// last_raw 还是 None(刚建会话、尚未抓到任何一帧)时,标志被
// swap 消费掉却没有发送,解码端的这次请求就永久丢失。
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
let force = keyreq.swap(false, Ordering::Relaxed);
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
let can_key = last_raw.is_some();
if force && !can_key {
// 还没有裸帧可重编:把请求还回去,让下一轮(拿到首帧后)继续生效
keyreq.store(true, Ordering::Relaxed);
eprintln!("[capture] KeyFrameReq 暂存:尚无裸帧可重编");
std::thread::sleep(Duration::from_millis(8));
} else if force {
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
// 静止桌面下的关键帧恢复:画面没变不代表可以不给关键帧——
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
// 解码端一旦出错,若只会收到旧 P 帧就永远恢复不了。
// 关键帧请求绕过 500ms 节流:它是错误恢复路径,延迟一次 IDR
// 的代价是持续黑屏,远高于多发一帧的带宽成本。
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
encoder.force_intra_frame();
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
let raw = last_raw.as_ref().expect("can_key 已保证");
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
let ts = start.elapsed().as_millis() as u64;
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
if !encode_send(&mut encoder, raw, (w, h), ts, seq, tx, &mut last_frame) {
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
return LoopEnd::ChannelClosed;
}
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
seq = seq.wrapping_add(1);
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
last_sent = Instant::now();
} else if last_sent.elapsed() > Duration::from_millis(500) && last_frame.is_some() {
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
// 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变)。
// 帧头必须重建:直接 clone 旧帧会让 seq/ts_ms 停在上次发送时刻,
// 破坏 seq 单调递增契约。
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
let f = last_frame.as_ref().expect("上面已判断 is_some");
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
match common::split_video_frame(f) {
Ok((mut hdr, payload)) => {
hdr.seq = seq;
hdr.ts_ms = start.elapsed().as_millis() as u64;
let frame = encode_video_frame(&hdr, payload);
if tx.send(Some(frame)).is_err() {
return LoopEnd::ChannelClosed;
}
seq = seq.wrapping_add(1);
last_sent = Instant::now();
}
Err(e) => {
// 自己产出的帧解析失败 = 编码/封装路径有 bug。
// 丢弃缓存而不是每轮重复报错:静止桌面下这里会以 ~125Hz 触发。
eprintln!("[capture] 静止重发时帧格式异常,丢弃缓存: {e}");
last_frame = None;
std::thread::sleep(Duration::from_millis(8));
}
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
}
} else {
std::thread::sleep(Duration::from_millis(8));
}
}
Err(e) => {
eprintln!("[capture] acquire failed: {e}");
return LoopEnd::Rebuild;
}
}
}
}
/// 采集主循环:独立线程运行;后端失效时经 `factory` 重建,通道关闭时退出。
/// 通道为 watch 单槽(latest-wins):发送即覆盖,网络慢时不产生积压。
pub fn run_capture_loop<F>(
tx: watch::Sender<Option<Vec<u8>>>,
keyreq: Arc<AtomicBool>,
mut cap: Box<dyn ScreenCapturer>,
mut factory: F,
enc: EncoderSettings,
) where
F: FnMut() -> Result<Box<dyn ScreenCapturer>>,
{
loop {
match capture_session(cap.as_mut(), &tx, keyreq.as_ref(), &enc) {
LoopEnd::ChannelClosed => return,
LoopEnd::Rebuild => {}
}
std::thread::sleep(Duration::from_millis(300));
loop {
match factory() {
Ok(c) => {
cap = c;
break;
}
Err(e) => {
eprintln!("[capture] init failed: {e}, retry in 2s");
std::thread::sleep(Duration::from_secs(2));
}
}
}
}
}
/// 跑一次完整的被控端会话:监听 → 认证 → 视频下行 + 输入上行。
///
/// `capture_factory` 可被多次调用(采集后端失效时重建),故需为 `FnMut`。
pub async fn serve<F>(
cfg: SessionConfig,
enc: EncoderSettings,
mut capture_factory: F,
injector: Arc<dyn InputInjector>,
) -> Result<()>
where
F: FnMut() -> Result<Box<dyn ScreenCapturer>> + Send + 'static,
{
rustls::crypto::ring::default_provider()
.install_default()
.ok();
// 自签名证书(MVP:客户端跳过校验;正式版改为固定公钥/账号体系)
let ck = rcgen::generate_simple_self_signed(vec!["localhost".into()])?;
let cert = ck.cert.der().clone();
let key = PrivatePkcs8KeyDer::from(ck.key_pair.serialize_der());
// 传输参数调优:
// - 收发窗口放大,避免高 RTT 下吞吐被 窗口/RTT 死锁(默认 512KB 流窗口)
// - 发送缓冲上限 512KB:QUIC 内部排队 ≈ 3 帧 @5Mbps,E2E 延迟天花板 ~150ms;
// 配合上游 latest-wins 丢帧,超量部分在进 QUIC 前就被丢弃
// (与 Windows 端 agent/src/main.rs 保持一致,沿用同一次调优结论)
let mut transport = quinn::TransportConfig::default();
transport.receive_window(quinn::VarInt::from_u32(16 * 1024 * 1024));
transport.stream_receive_window(quinn::VarInt::from_u32(8 * 1024 * 1024));
transport.send_window(512 * 1024);
let mut server_config = quinn::ServerConfig::with_single_cert(vec![cert], key.into())?;
server_config.transport_config(Arc::new(transport));
let endpoint = quinn::Endpoint::server(server_config, format!("0.0.0.0:{}", cfg.port).parse()?)?;
println!(
"[agent] listening 0.0.0.0:{}, verify code = {}",
cfg.port, cfg.code
);
let conn = endpoint
.accept()
.await
.ok_or_else(|| anyhow::anyhow!("endpoint closed"))?
.await?;
println!("[agent] peer connected: {}", conn.remote_address());
// 流 1:认证 + 视频下行
let (mut send, mut recv) = conn.accept_bi().await?;
let first = read_control(&mut recv).await?;
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
// 先校验版本,再校验验证码:版本不一致时字段布局可能不同,验证码的解析结果不可信
let auth_err: Option<String> = match &first {
ControlMsg::Auth {
code: c,
proto_version,
} => {
if *proto_version != common::PROTO_VERSION {
Some(format!(
"protocol version mismatch: client={proto_version}, server={}",
common::PROTO_VERSION
))
} else if *c != cfg.code {
Some("bad code".into())
} else {
None
}
}
other => Some(format!("expected Auth, got {other:?}")),
};
if let Some(msg) = auth_err {
write_control(&mut send, &ControlMsg::AuthFail(msg.clone())).await?;
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
send.finish()?;
// 必须等对端读到 AuthFail 并挂断再退出:直接 return 会让进程带走 QUIC
// 缓冲区里尚未发出的数据,客户端只会看到 "connection lost" 而不是 AuthFail
let _ = tokio::time::timeout(Duration::from_secs(5), conn.closed()).await;
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
anyhow::bail!(msg);
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
}
// 采集后端先建一次:既拿桌面尺寸回给主控端,也避免白建一个 duplication 只为读尺寸
let first_cap = capture_factory()?;
let (w, h) = first_cap.size();
feat(common): 协议 v2——版本号 + 帧头 seq/keyframe + 长度下界校验 为接入安卓主控做准备,先把三端共享契约改到能承载多客户端。改动集中在 common/src/lib.rs,三端适配在同一提交内完成以保证可编译。 协议版本(PROTO_VERSION=2) - ControlMsg::Auth/AuthOk 增加 proto_version 字段;认证顺序改为「先版本后验证码」: 版本不一致时字段布局可能不同,验证码的解析结果不可信 - 任一端版本不符即 AuthFail 并明确报错,把「连上了但行为诡异」变成「连不上且提示升级」 视频帧头(VideoFrameHeader) - 新增 seq: u64(每次实际发送递增,含静止桌面重发)与 keyframe: bool - keyframe 由新增的 common::h264_contains_idr() 从实际 Annex-B 码流判定, 不依赖编码器 API 推断——NVENC 与 openh264 规则不同,实际输出才是唯一可信来源 - 该字段是 MediaCodec 判断解码器重置时机的必要条件,避免客户端自己写 NAL 扫描 不可信输入防护 - read_video_frame 补齐长度下界:total 必须 >= 4,hlen 必须落在 [1, total-4] - 旧实现只校验上界,构造 total<4 的帧会让接收端在 buf[0..4] 处 panic(进程级DoS) - 测试从 0 增至 11 个,覆盖序列化往返与 6 类越界/损坏输入 顺带修复(与上述契约直接相关) - 新增 common::split_video_frame():静止桌面重发时复用载荷但重建帧头, 否则 seq/ts_ms 会停在上次发送时刻,破坏 seq 单调递增契约 - agent-core 与 agent 两端的静止重发路径均改用该函数 验证:cargo test -p common → 20 passed(9 signaling + 11 新增)
2026-10-28 04:01:09 -04:00
write_control(
&mut send,
&ControlMsg::AuthOk {
width: w,
height: h,
proto_version: common::PROTO_VERSION,
},
)
.await?;
feat(agent): 新增 agent-core 与 Linux 被控端(X11 + XTest,纯新增不动 agent/) 本期只在新增文件里做,**未改动 Windows agent/ 一个字节**(与 NVENC 线并行,避免争抢)。 - 新增 agent-core:平台无关会话层 —— QUIC/TLS 握手、传输调优(沿用 512KB 发送窗口 结论)、验证码认证、会话/select 循环、latest-wins 单槽、输入分发,以及软件编码 视频管线(openh264)。同步跟进协议新增的 Pong{lost,sent} 下行丢包统计字段 - 新增 linux-agent:X11 GetImage 采集 + 整帧变更检测;XTest 注入 + 扫描码→X11 keycode 映射(主键区 = scancode+8,E0 扩展键必须查表,含单元测试) - 协议零改动:复用 Set-1 扫描码与虚拟桌面归一化坐标,viewer 无需任何修改 - 新增端到端冒烟测试 tests/smoke.rs 与 scripts/linux-smoke.sh(自建 Xvfb,无需 图形桌面):采集→编码→QUIC 解帧、认证(含错误码拒绝)、Ping/Pong(含丢包字段)、 静止桌面关键帧恢复、输入注入实证(注入后从另一条 X11 连接读回指针真实坐标) - 该关键帧用例做过变异测试:把修复退回会失败、恢复后通过,确认断言有效 - .gitattributes:shell 脚本强制 LF(Windows checkout 成 CRLF 会导致容器内无法执行) - 手册:修正已失效的工具链路径;补充 cargo 镜像;新增第 8 节(Linux)与 6.1 节 (三条待修问题,含两条 Windows 侧仍存在的关键帧缺陷) 验证: - Linux(容器, USTC 源 + Xvfb): cargo test -p linux-agent 通过,2 passed - Windows: 本期未改 agent/,default-members 未变,`cargo build --release` 行为不变
2026-10-23 01:31:07 -04:00
println!("[agent] auth ok, desktop {w}x{h}");
// 流 2:键鼠输入 + 控制上行(与视频分离,避免被视频积压阻塞)
let (mut isend, mut irecv) = conn.accept_bi().await?;
// 采集线程 → 视频帧(watch 单槽:新帧覆盖旧帧,latest-wins)
let (tx, mut frames_rx) = watch::channel::<Option<Vec<u8>>>(None);
let keyreq = Arc::new(AtomicBool::new(false));
let keyreq_capture = keyreq.clone();
let enc_settings = enc;
std::thread::spawn(move || {
run_capture_loop(tx, keyreq_capture, first_cap, capture_factory, enc_settings)
});
// 上行控制消息(Pong)经通道交给主循环写流 2,避免多任务争抢
let (ctl_tx, mut ctl_rx) = mpsc::unbounded_channel::<ControlMsg>();
let t0 = Instant::now();
// 读循环(流 2):输入事件 + Ping;Pong 附带**视频下行方向**的 QUIC 丢包统计
// (conn.stats().path 统计的是本端发送方向,被控端的发送方向正是视频下行)
let keyreq_reader = keyreq.clone();
let conn_stats = conn.clone();
let reader = tokio::spawn(async move {
loop {
match read_control(&mut irecv).await {
Ok(ControlMsg::Input(ev)) => dispatch_input(injector.as_ref(), ev),
Ok(ControlMsg::KeyFrameReq) => {
keyreq_reader.store(true, Ordering::Relaxed);
}
Ok(ControlMsg::Ping(t)) => {
let agent_ts = t0.elapsed().as_millis() as u64;
let p = conn_stats.stats().path;
let _ = ctl_tx.send(ControlMsg::Pong {
t,
agent_ts,
lost: p.lost_packets,
sent: p.sent_packets,
});
}
Ok(_) => {}
Err(_) => break,
}
}
});
loop {
tokio::select! {
changed = frames_rx.changed() => {
if changed.is_err() {
break; // 采集线程退出
}
if let Some(data) = frames_rx.borrow().clone() {
if send.write_all(&data).await.is_err() {
break;
}
}
}
ctl = ctl_rx.recv() => {
match ctl {
Some(m) => {
if write_control(&mut isend, &m).await.is_err() {
break;
}
}
None => break,
}
}
}
}
reader.abort();
println!("[agent] session closed");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
/// 记录调用轨迹的注入后端:验证分发与截断,不碰任何真实输入。
#[derive(Default)]
struct MockInjector {
calls: Mutex<Vec<String>>,
}
impl MockInjector {
fn take(&self) -> Vec<String> {
let mut g = self.calls.lock().unwrap();
std::mem::take(&mut g)
}
}
impl InputInjector for MockInjector {
fn mouse_move(&self, x: u16, y: u16) {
self.calls.lock().unwrap().push(format!("move {x},{y}"));
}
fn mouse_button(&self, b: MouseButton, down: bool) {
self.calls.lock().unwrap().push(format!("btn {b:?} {down}"));
}
fn wheel(&self, dy: i32) {
self.calls.lock().unwrap().push(format!("wheel {dy}"));
}
fn key(&self, scancode: u16, extended: bool, down: bool) {
self.calls
.lock()
.unwrap()
.push(format!("key {scancode:#x} ext={extended} {down}"));
}
// 刻意**不**实现 text():让默认实现进入测试面,验证「后端没做文本注入
// 时也必须被 dispatch_input 正常调用、且不 panic」——Linux 端正是这个形态。
}
/// 实现了 text() 的后端,用来验证截断后的长度真的传给了注入侧。
struct TextCapableInjector {
received: Mutex<Vec<usize>>,
}
impl InputInjector for TextCapableInjector {
fn mouse_move(&self, _x: u16, _y: u16) {}
fn mouse_button(&self, _b: MouseButton, _down: bool) {}
fn wheel(&self, _dy: i32) {}
fn key(&self, _scancode: u16, _extended: bool, _down: bool) {}
fn text(&self, units: &[u16]) {
self.received.lock().unwrap().push(units.len());
}
}
#[test]
fn dispatch_routes_each_event_kind() {
let inj = MockInjector::default();
dispatch_input(&inj, InputEvent::MouseMove { x: 1, y: 2 });
dispatch_input(&inj, InputEvent::MouseDown(MouseButton::Right));
dispatch_input(&inj, InputEvent::Wheel { dy: -120 });
dispatch_input(
&inj,
InputEvent::KeyDown {
scancode: 0x48,
extended: true,
},
);
let calls = inj.take();
assert_eq!(
calls,
vec![
"move 1,2",
"btn Right true",
"wheel -120",
"key 0x48 ext=true true",
]
);
}
/// v4:相对移动必须被分发,**负值原样到达后端**(负值是相对语义的核心,
/// 绝对坐标是无符号的,这条路径是新引入的有符号通道)。
#[test]
fn rel_move_reaches_backend_with_signed_deltas() {
struct RelInjector {
got: Mutex<Vec<(i16, i16)>>,
}
impl InputInjector for RelInjector {
fn mouse_move(&self, _x: u16, _y: u16) {}
fn mouse_button(&self, _b: MouseButton, _down: bool) {}
fn wheel(&self, _dy: i32) {}
fn key(&self, _s: u16, _e: bool, _d: bool) {}
fn mouse_move_rel(&self, dx: i16, dy: i16) {
self.got.lock().unwrap().push((dx, dy));
}
}
let inj = RelInjector {
got: Mutex::new(Vec::new()),
};
dispatch_input(&inj, InputEvent::MouseMoveRel { dx: 12, dy: -34 });
dispatch_input(&inj, InputEvent::MouseMoveRel { dx: -1, dy: 0 });
assert_eq!(
*inj.got.lock().unwrap(),
vec![(12, -34), (-1, 0)],
"相对增量(含负值)必须原样到达注入后端"
);
}
/// 后端**没有**实现 mouse_move_rel 时不得 panic,且相对事件不得被误路由成
/// 绝对移动(Linux/X11 后端的真实形态:默认实现 + 日志)。
#[test]
fn rel_move_default_backend_does_not_panic() {
let inj = MockInjector::default();
dispatch_input(&inj, InputEvent::MouseMoveRel { dx: 5, dy: 5 });
// 默认实现只打日志,不应产生任何注入调用(尤其不能变成 move/绝对移动)
assert!(
inj.take().is_empty(),
"默认实现不得把相对移动降级成绝对移动"
);
}
/// v3 关键回归:`Text` 必须被分发,且**后端没有实现 text() 时不能 panic**。
/// 这是 Linux/X11 后端的真实形态——用户对着 Linux 被控端打字会走到这里。
#[test]
fn text_event_reaches_default_backend_without_panic() {
let inj = MockInjector::default();
dispatch_input(
&inj,
InputEvent::Text {
units: vec![0x4F60, 0x597D],
},
);
// 默认实现只记日志,不产生调用记录;此处断言"没炸"即为通过。
assert!(inj.take().is_empty());
}
/// 截断契约:超过 `MAX_TEXT_UNITS` 的输入必须以截断后的长度到达注入侧。
/// 不截断会让长粘贴把远端注入队列刷爆;截断不告知则是静默丢数据。
#[test]
fn text_is_clamped_to_protocol_cap() {
let inj = TextCapableInjector {
received: Mutex::new(vec![]),
};
dispatch_input(
&inj,
InputEvent::Text {
units: vec![0x61; common::MAX_TEXT_UNITS + 500],
},
);
dispatch_input(
&inj,
InputEvent::Text {
units: vec![0x62; 10],
},
);
let got = inj.received.lock().unwrap().clone();
assert_eq!(got, vec![common::MAX_TEXT_UNITS, 10], "超限截断、未超限原样");
}
/// 空 Text 仍会调用后端(是否忽略由后端决定):主控侧 `send_text` 已拦掉空输入,
/// 但被控端不能假设对端守规矩——线格式输入一律按不可信处理。
#[test]
fn empty_text_still_dispatches() {
let inj = TextCapableInjector {
received: Mutex::new(vec![]),
};
dispatch_input(&inj, InputEvent::Text { units: vec![] });
assert_eq!(inj.received.lock().unwrap().clone(), vec![0]);
}
}