From f119300a57a01ec0639578d1413c6006f3a2cfad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Fri, 30 Oct 2026 09:31:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E4=B8=B2=E8=A1=8C=E5=A4=9A?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E2=80=94=E2=80=94=E4=BC=9A=E8=AF=9D=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E4=B8=8D=E5=86=8D=E9=80=80=E5=87=BA=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题(阶段 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 证明线程真退出 --- agent-core/src/lib.rs | 7 ++++++ agent/src/capture.rs | 8 +++++++ agent/src/main.rs | 53 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/agent-core/src/lib.rs b/agent-core/src/lib.rs index c623a91..d986129 100644 --- a/agent-core/src/lib.rs +++ b/agent-core/src/lib.rs @@ -192,6 +192,13 @@ fn capture_session( let mut seq: u64 = 0; loop { + // 会话结束守卫:发送端已 drop 接收器时立即退出。 + // 与 agent/src/capture.rs 同源——静止桌面分支不发送,只靠 send 失败退出 + // 会让线程在画面不动时永不退出(X11 无 DXGI 式独占,危害较小但同样泄漏线程)。 + if tx.receiver_count() == 0 { + eprintln!("[capture] no receiver, stop"); + return LoopEnd::ChannelClosed; + } let iter_start = Instant::now(); match cap.acquire(80) { Ok(true) => match cap.copy_frame() { diff --git a/agent/src/capture.rs b/agent/src/capture.rs index b760b50..a96d591 100644 --- a/agent/src/capture.rs +++ b/agent/src/capture.rs @@ -387,6 +387,14 @@ pub fn run(tx: Sender>>, keyreq: Arc, congestion: Arc } loop { + // 会话结束守卫:发送端已 drop 接收器时立即退出。 + // 只靠 `tx.send` 失败退出是不够的——桌面完全静止时这里根本不发送, + // 线程会永远持有 duplication,导致下一个会话的 DuplicateOutput 失败 + // (DXGI 每个输出仅允许一个活动实例)。 + if tx.receiver_count() == 0 { + eprintln!("[capture] no receiver, stop"); + return; + } let iter_start = Instant::now(); match dup.acquire(80) { Ok(true) => { diff --git a/agent/src/main.rs b/agent/src/main.rs index 56055a6..837f135 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -1,6 +1,8 @@ -//! 被控端 Agent:DXGI 桌面复制采集 → LZ4 压缩 → QUIC 下行视频;上行接收输入事件注入。 +//! 被控端 Agent:DXGI 桌面复制采集 → H.264 编码 → QUIC 下行视频;上行接收输入事件注入。 //! P0 优化:视频帧经 watch 单槽通道(latest-wins,网络慢时只发最新帧,不积压); //! 键鼠/控制走独立 QUIC 双向流,不与视频互相阻塞。 +//! 会话模型:**串行多会话**——accept 循环一次只服务一个主控,会话结束后回到 accept +//! 等下一个连接(进程不退出)。改成并发多会话需要先解决 DXGI 单实例独占,暂不做。 mod capture; mod input; @@ -10,6 +12,7 @@ use common::{read_control, write_control, ControlMsg}; use rustls::pki_types::PrivatePkcs8KeyDer; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::time::Duration; use tokio::io::AsyncWriteExt; use tokio::sync::{mpsc, watch}; @@ -59,13 +62,31 @@ async fn main() -> Result<()> { )?; println!("[agent] listening 0.0.0.0:{port}, verify code = {code}"); - let conn = endpoint - .accept() - .await - .ok_or_else(|| anyhow::anyhow!("endpoint closed"))? - .await?; - println!("[agent] peer connected: {}", conn.remote_address()); + // 串行多会话:一次只服务一个主控,结束后回到 accept。 + // 不做并发多会话——DXGI Desktop Duplication 每个输出仅允许一个活动实例, + // 第二个 DuplicateOutput 会让第一个收到 ACCESS_LOST。 + // 会话失败(含认证失败)只记日志,不能让进程退出——否则每次连接都要重启被控端。 + loop { + let Some(incoming) = endpoint.accept().await else { + println!("[agent] endpoint closed, exit"); + return Ok(()); + }; + match incoming.await { + Ok(conn) => { + println!("[agent] peer connected: {}", conn.remote_address()); + if let Err(e) = serve_session(conn, &code).await { + println!("[agent] session ended: {e:#}"); + } + println!("[agent] waiting for next connection"); + } + Err(e) => println!("[agent] handshake failed: {e}"), + } + } +} +/// 单次会话:认证 → 采集/发送/输入 → 会话结束。 +/// 返回 Err 表示会话异常结束(含认证失败),调用方继续 accept。 +async fn serve_session(conn: quinn::Connection, code: &str) -> Result<()> { // 流 1:认证 + 视频下行 let (mut send, mut recv) = conn.accept_bi().await?; @@ -93,6 +114,10 @@ async fn main() -> Result<()> { if let Some(msg) = auth_err { write_control(&mut send, &ControlMsg::AuthFail(msg.clone())).await?; send.finish()?; + // 必须等对端读到 AuthFail 再关连接:直接返回会让进程带走 QUIC 缓冲区里 + // 尚未发出的数据,客户端只会看到 "connection lost" 而不是 AuthFail。 + // (agent-core 已有同款修复,此处同源补齐——手册 §6.1 第 1 条) + let _ = tokio::time::timeout(Duration::from_secs(5), conn.closed()).await; anyhow::bail!(msg); } let (w, h) = capture::desktop_size()?; @@ -117,7 +142,14 @@ async fn main() -> Result<()> { // 拥塞反馈标志:发送侧 write 超过单帧预算(20ms) → 置位,采集线程跳帧降注入率 let congestion = Arc::new(AtomicBool::new(false)); let congestion_capture = congestion.clone(); - std::thread::spawn(move || capture::run(tx, keyreq_capture, congestion_capture)); + // 采集线程退出信号:会话结束必须等它真正退出才能 accept 下一个连接—— + // DXGI Desktop Duplication 每个输出只允许一个活动实例,旧线程还持有 duplication + // 时新线程的 DuplicateOutput 会让旧实例收到 ACCESS_LOST(表现为下一个会话黑屏)。 + let (done_tx, done_rx) = std::sync::mpsc::channel::<()>(); + std::thread::spawn(move || { + capture::run(tx, keyreq_capture, congestion_capture); + let _ = done_tx.send(()); + }); // 上行控制消息(Pong)经通道交给主循环写流 2,避免多任务争抢 let (ctl_tx, mut ctl_rx) = mpsc::unbounded_channel::(); @@ -182,6 +214,11 @@ async fn main() -> Result<()> { } } reader.abort(); + // 关掉流并丢弃接收端,让采集线程下一次 send 失败而退出 + let _ = send.finish(); + drop(frames_rx); + // 等采集线程真正退出后再返回,调用方才会 accept 下一个连接(DXGI 独占,见上) + let _ = tokio::task::spawn_blocking(move || done_rx.recv_timeout(Duration::from_secs(2))).await; println!("[agent] session closed"); Ok(()) }