Skip to content

Commit 079b484

Browse files
fix(dvc-pipe-proxy): handle pre-connected Windows clients (#1447)
1 parent 76ad145 commit 079b484

7 files changed

Lines changed: 177 additions & 64 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-dvc-pipe-proxy/src/platform/windows.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
use async_trait::async_trait;
22
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
33
use tokio::net::windows::named_pipe;
4+
use tracing::debug;
45

56
use crate::error::DvcPipeProxyError;
67
use crate::os_pipe::OsPipe;
78

89
const PIPE_BUFFER_SIZE: u32 = 64 * 1024;
10+
// ConnectNamedPipe reports this when the client wins the create/accept race.
11+
const ERROR_PIPE_CONNECTED: i32 = 535;
912

10-
/// Unix-specific implementation of the OS pipe trait.
13+
/// Windows-specific implementation of the OS pipe trait.
1114
pub(crate) struct WindowsPipe {
1215
pipe_server: named_pipe::NamedPipeServer,
1316
}
1417

1518
#[async_trait]
1619
impl OsPipe for WindowsPipe {
1720
async fn connect(pipe_name: &str) -> Result<Self, DvcPipeProxyError> {
18-
let pipe_name = format!("\\\\.\\pipe\\{pipe_name}");
21+
let pipe_path = format!("\\\\.\\pipe\\{pipe_name}");
22+
debug!(%pipe_name, %pipe_path, "Creating DVC proxy Windows named pipe");
1923

2024
let pipe_server = named_pipe::ServerOptions::new()
2125
.first_pipe_instance(true)
@@ -25,10 +29,28 @@ impl OsPipe for WindowsPipe {
2529
.in_buffer_size(PIPE_BUFFER_SIZE)
2630
.out_buffer_size(PIPE_BUFFER_SIZE)
2731
.pipe_mode(named_pipe::PipeMode::Byte)
28-
.create(pipe_name)
29-
.map_err(DvcPipeProxyError::Io)?;
32+
.create(&pipe_path)
33+
.map_err(|error| {
34+
debug!(%pipe_name, %pipe_path, %error, "Failed to create DVC proxy Windows named pipe");
35+
DvcPipeProxyError::Io(error)
36+
})?;
3037

31-
pipe_server.connect().await.map_err(DvcPipeProxyError::Io)?;
38+
debug!(%pipe_name, %pipe_path, "Waiting for DVC proxy Windows named-pipe client");
39+
match pipe_server.connect().await {
40+
Ok(()) => {}
41+
Err(error) if error.raw_os_error() == Some(ERROR_PIPE_CONNECTED) => {
42+
debug!(
43+
%pipe_name,
44+
%pipe_path,
45+
"DVC proxy Windows named-pipe client connected before accept"
46+
);
47+
}
48+
Err(error) => {
49+
debug!(%pipe_name, %pipe_path, %error, "Failed to accept DVC proxy Windows named-pipe client");
50+
return Err(DvcPipeProxyError::Io(error));
51+
}
52+
}
53+
debug!(%pipe_name, %pipe_path, "Connected DVC proxy Windows named-pipe client");
3254

3355
Ok(Self { pipe_server })
3456
}

crates/ironrdp-dvc-pipe-proxy/src/proxy.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use ironrdp_core::impl_as_any;
44
use ironrdp_dvc::{DvcClientProcessor, DvcMessage, DvcProcessor};
55
use ironrdp_pdu::{PduResult, pdu_other_err};
66
use ironrdp_svc::SvcMessage;
7-
use tracing::debug;
7+
use tracing::{debug, error};
88

99
use crate::worker::{OnWriteDvcMessage, WorkerCtx, run_worker};
1010

@@ -69,17 +69,27 @@ impl DvcProcessor for DvcNamedPipeProxy {
6969
channel_id,
7070
};
7171

72+
#[cfg(not(target_os = "windows"))]
73+
let worker = run_worker::<crate::platform::unix::UnixPipe>(ctx);
74+
75+
#[cfg(target_os = "windows")]
76+
let worker = run_worker::<crate::platform::windows::WindowsPipe>(ctx);
77+
78+
if let Err(worker_error) = worker {
79+
error!(
80+
channel_name = %self.channel_name,
81+
pipe_name = %self.named_pipe_name,
82+
%worker_error,
83+
"Failed to start DVC pipe proxy worker thread"
84+
);
85+
return Err(pdu_other_err!("start DVC pipe proxy worker: {worker_error}"));
86+
}
87+
7288
self.worker = Some(WorkerControlCtx {
7389
to_pipe_tx,
7490
abort_event,
7591
});
7692

77-
#[cfg(not(target_os = "windows"))]
78-
run_worker::<crate::platform::unix::UnixPipe>(ctx);
79-
80-
#[cfg(target_os = "windows")]
81-
run_worker::<crate::platform::windows::WindowsPipe>(ctx);
82-
8393
Ok(vec![])
8494
}
8595

crates/ironrdp-dvc-pipe-proxy/src/worker.rs

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::time::Duration;
12
use std::sync::{Arc, mpsc};
23

34
use ironrdp_dvc::encode_dvc_messages;
@@ -11,6 +12,8 @@ use crate::message::RawDataDvcMessage;
1112
use crate::os_pipe::OsPipe;
1213

1314
const IO_BUFFER_SIZE: usize = 1024 * 64; // 64K
15+
const INITIAL_RECONNECT_DELAY: Duration = Duration::from_millis(100);
16+
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(5);
1417

1518
pub(crate) type OnWriteDvcMessage = Box<dyn Fn(u32, Vec<SvcMessage>) -> PduResult<()> + Send>;
1619

@@ -23,38 +26,85 @@ pub(crate) struct WorkerCtx {
2326
pub(crate) channel_id: u32,
2427
}
2528

26-
pub(crate) fn run_worker<P: OsPipe>(ctx: WorkerCtx) {
27-
let _ = std::thread::spawn(move || {
29+
pub(crate) fn run_worker<P: OsPipe>(ctx: WorkerCtx) -> std::io::Result<()> {
30+
let thread_name = format!("ironrdp-dvc-pipe-{}", ctx.channel_id);
31+
let (startup_tx, startup_rx) = mpsc::sync_channel(1);
32+
33+
std::thread::Builder::new().name(thread_name).spawn(move || {
2834
let channel_name = ctx.channel_name.clone();
2935
let pipe_name = ctx.pipe_name.clone();
36+
debug!(%channel_name, %pipe_name, "Starting DVC pipe proxy worker thread");
3037

31-
let runtime = tokio::runtime::Builder::new_current_thread()
32-
.enable_all()
33-
.build()
34-
.map_err(DvcPipeProxyError::Io);
35-
36-
let runtime = match runtime {
38+
let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
3739
Ok(runtime) => runtime,
3840
Err(error) => {
3941
error!(
4042
%channel_name,
4143
%pipe_name,
42-
?error,
43-
"DVC pipe proxy worker thread initialization failed"
44+
%error,
45+
"Failed to initialize DVC pipe proxy worker thread"
4446
);
47+
let _ = startup_tx.send(Err(error));
4548
return;
4649
}
4750
};
4851

49-
if let Err(error) = runtime.block_on(worker::<P>(ctx)) {
52+
let (async_tx, async_rx) = tokio::sync::mpsc::unbounded_channel();
53+
let WorkerCtx {
54+
on_write_dvc,
55+
to_pipe_rx: std_rx,
56+
abort_event,
57+
pipe_name,
58+
channel_name,
59+
channel_id,
60+
} = ctx;
61+
62+
let bridge_thread_name = format!("ironrdp-dvc-pipe-{channel_id}-bridge");
63+
if let Err(error) = std::thread::Builder::new().name(bridge_thread_name).spawn(move || {
64+
while let Ok(data) = std_rx.recv() {
65+
if async_tx.send(data).is_err() {
66+
break; // Receiver dropped
67+
}
68+
}
69+
}) {
5070
error!(
5171
%channel_name,
5272
%pipe_name,
53-
?error,
54-
"DVC pipe proxy worker thread has failed"
73+
%error,
74+
"Failed to start DVC pipe proxy bridge thread"
5575
);
76+
let _ = startup_tx.send(Err(error));
77+
return;
78+
}
79+
80+
let ctx = BridgedWorkerCtx {
81+
on_write_dvc,
82+
to_pipe_rx: async_rx,
83+
abort_event,
84+
pipe_name,
85+
channel_name,
86+
channel_id,
87+
};
88+
89+
if startup_tx.send(Ok(())).is_err() {
90+
return;
91+
}
92+
93+
debug!(
94+
channel_name = %ctx.channel_name,
95+
pipe_name = %ctx.pipe_name,
96+
"Started DVC pipe proxy worker thread"
97+
);
98+
if let Err(error) = runtime.block_on(worker::<P>(ctx)) {
99+
error!(?error, "DVC pipe proxy worker thread has failed");
56100
}
57-
});
101+
})?;
102+
103+
startup_rx.recv().unwrap_or_else(|_| {
104+
Err(std::io::Error::other(
105+
"dvc pipe proxy worker stopped before startup completed",
106+
))
107+
})
58108
}
59109

60110
enum NextWorkerState {
@@ -134,57 +184,39 @@ async fn process_client<P: OsPipe>(ctx: &mut BridgedWorkerCtx) -> Result<NextWor
134184
if let Err(error) = pipe.write_all(&data).await
135185
{
136186
error!(%channel_name, %pipe_name, ?error, "Failed to write to DVC pipe");
137-
continue;
187+
return Ok(NextWorkerState::Reconnect);
138188
}
139189
}
140190
};
141191
}
142192
}
143193

144-
async fn worker<P: OsPipe>(ctx: WorkerCtx) -> Result<(), DvcPipeProxyError> {
145-
// Create a bridge between std::sync::mpsc and tokio for async compatibility.
146-
// It is fine to use unbounded channel here because we are using it only to
147-
// forward data from a bounded channel (with size IO_MPSC_CHANNEL_SIZE),
148-
// so we will never have unbounded memory growth.
149-
let (async_tx, async_rx) = tokio::sync::mpsc::unbounded_channel();
150-
151-
let WorkerCtx {
152-
on_write_dvc,
153-
to_pipe_rx: std_rx,
154-
abort_event,
155-
pipe_name,
156-
channel_name,
157-
channel_id,
158-
} = ctx;
159-
160-
// Spawn a thread to bridge std::sync::mpsc to tokio::sync::mpsc.
161-
std::thread::spawn(move || {
162-
while let Ok(data) = std_rx.recv() {
163-
if async_tx.send(data).is_err() {
164-
break; // Receiver dropped
165-
}
166-
}
167-
});
168-
169-
let mut bridged_ctx = BridgedWorkerCtx {
170-
on_write_dvc,
171-
to_pipe_rx: async_rx,
172-
abort_event,
173-
pipe_name,
174-
channel_name,
175-
channel_id,
176-
};
194+
async fn worker<P: OsPipe>(mut bridged_ctx: BridgedWorkerCtx) -> Result<(), DvcPipeProxyError> {
195+
let mut reconnect_delay = INITIAL_RECONNECT_DELAY;
196+
177197
loop {
178-
match process_client::<P>(&mut bridged_ctx).await? {
179-
NextWorkerState::Abort => {
198+
match process_client::<P>(&mut bridged_ctx).await {
199+
Err(error) => {
200+
error!(
201+
channel_name = %bridged_ctx.channel_name,
202+
pipe_name = %bridged_ctx.pipe_name,
203+
?error,
204+
retry_delay_ms = reconnect_delay.as_millis(),
205+
"DVC pipe proxy connection failed; retrying"
206+
);
207+
std::thread::sleep(reconnect_delay);
208+
reconnect_delay = reconnect_delay.saturating_mul(2).min(MAX_RECONNECT_DELAY);
209+
}
210+
Ok(NextWorkerState::Abort) => {
180211
debug!(
181212
channel_name = %bridged_ctx.channel_name,
182213
pipe_name = %bridged_ctx.pipe_name,
183214
"Abort DVC proxy worker thread"
184215
);
185216
break;
186217
}
187-
NextWorkerState::Reconnect => {
218+
Ok(NextWorkerState::Reconnect) => {
219+
reconnect_delay = INITIAL_RECONNECT_DELAY;
188220
debug!(
189221
channel_name = %bridged_ctx.channel_name,
190222
pipe_name = %bridged_ctx.pipe_name,

crates/ironrdp-testsuite-extra/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ ironrdp-async.path = "../ironrdp-async"
2929
ironrdp-agent = { path = "../ironrdp-agent", features = ["internal"] }
3030
ironrdp-client.path = "../ironrdp-client"
3131
ironrdp-core.path = "../ironrdp-core"
32+
ironrdp-dvc.path = "../ironrdp-dvc"
33+
ironrdp-dvc-pipe-proxy.path = "../ironrdp-dvc-pipe-proxy"
3234
ironrdp-input.path = "../ironrdp-input"
3335
ironrdp-propertyset.path = "../ironrdp-propertyset"
3436
ironrdp-viewer.path = "../ironrdp-viewer"
@@ -37,7 +39,7 @@ ironrdp-tls = { path = "../ironrdp-tls", features = ["rustls"] }
3739
semver = "1.0"
3840
tracing = { version = "0.1", features = ["log"] }
3941
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
40-
tokio = { version = "1", features = ["sync", "time"] }
42+
tokio = { version = "1", features = ["sync", "time", "net", "rt", "macros", "io-util"] }
4143
uuid = { version = "1", features = ["v4"] }
4244

4345
[lints]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#[cfg(windows)]
2+
use core::time::Duration;
3+
#[cfg(windows)]
4+
use std::sync::mpsc;
5+
6+
#[cfg(windows)]
7+
use ironrdp_dvc::DvcProcessor as _;
8+
#[cfg(windows)]
9+
use ironrdp_dvc_pipe_proxy::DvcNamedPipeProxy;
10+
#[cfg(windows)]
11+
use tokio::io::AsyncWriteExt as _;
12+
#[cfg(windows)]
13+
use tokio::net::windows::named_pipe::ClientOptions;
14+
15+
#[cfg(windows)]
16+
#[tokio::test]
17+
async fn connects_and_forwards_windows_pipe_data() {
18+
let name = format!("ironrdp-dvc-pipe-proxy-test-{}", std::process::id());
19+
let (callback_tx, callback_rx) = mpsc::channel();
20+
let mut proxy = DvcNamedPipeProxy::new("test", &name, move |_, messages| {
21+
callback_tx
22+
.send(messages)
23+
.expect("test callback receiver must remain alive");
24+
Ok(())
25+
});
26+
proxy.start(1).expect("start DVC pipe proxy");
27+
28+
let pipe_path = format!(r"\\.\pipe\{name}");
29+
let mut client = (0..200)
30+
.find_map(|_| match ClientOptions::new().open(&pipe_path) {
31+
Ok(client) => Some(client),
32+
Err(_) => {
33+
std::thread::sleep(Duration::from_millis(10));
34+
None
35+
}
36+
})
37+
.expect("DVC pipe proxy must create the pipe within two seconds");
38+
39+
client.write_all(b"test data").await.expect("write to DVC pipe");
40+
let messages = callback_rx
41+
.recv_timeout(Duration::from_secs(1))
42+
.expect("DVC pipe proxy must forward pipe data to its callback");
43+
assert!(!messages.is_empty(), "DVC pipe data must produce an SVC message");
44+
}

crates/ironrdp-testsuite-extra/tests/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44
mod agent;
55
mod client_config;
6+
mod dvc_pipe_proxy;
67
mod e2e;

0 commit comments

Comments
 (0)