win_text_inject/delayed.rs
1//! Delayed clipboard rendering: know exactly when the target read the clipboard.
2//!
3//! The "it pasted my previous clipboard" bug exists because the injector restores on a timer while
4//! the target reads the clipboard whenever its message pump gets round to it. Any fixed delay is a
5//! guess, and under load the guess is wrong. Tuning the delay upward — which is the shipped
6//! mitigation in every tool surveyed — only moves the threshold.
7//!
8//! Delayed rendering removes the guess. Instead of publishing the text, publish a promise:
9//! `SetClipboardData(CF_UNICODETEXT, NULL)` with this process as clipboard owner. Windows then
10//! sends `WM_RENDERFORMAT` to the owner at the instant a consumer actually asks for the data, and
11//! the owner supplies it then. That message *is* the "the target has read it" signal, so the
12//! restore can be sequenced strictly after the read instead of racing it.
13//!
14//! Requires a window with a running message pump, so this owns a hidden window on its own thread.
15
16use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
17use std::sync::{Condvar, Mutex, OnceLock};
18use std::time::Duration;
19
20use windows::core::w;
21use windows::Win32::Foundation::{HANDLE, HWND, LPARAM, LRESULT, WPARAM};
22use windows::Win32::System::DataExchange::{
23 CloseClipboard, EmptyClipboard, GetClipboardOwner, OpenClipboard, SetClipboardData,
24};
25use windows::Win32::System::LibraryLoader::GetModuleHandleW;
26use windows::Win32::UI::WindowsAndMessaging::{
27 CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, RegisterClassW,
28 TranslateMessage, CW_USEDEFAULT, MSG, WM_DESTROYCLIPBOARD, WM_RENDERALLFORMATS,
29 WM_RENDERFORMAT, WNDCLASSW, WS_OVERLAPPED,
30};
31
32use crate::clipboard::{alloc_global_public, utf16_bytes_public, CF_UNICODETEXT_PUBLIC};
33use crate::Error;
34
35/// Text formats Windows may ask us to render. `CF_UNICODETEXT` is what we advertise; the others are
36/// synthesized from it, and a consumer asking for one of those still routes back to us.
37const CF_TEXT: u32 = 1;
38const CF_OEMTEXT: u32 = 7;
39
40struct State {
41 /// Text to hand over when a consumer asks. Cleared once ownership is lost.
42 pending: Option<String>,
43 /// Set when a consumer actually requested the data.
44 rendered: bool,
45 /// How many times the data has been requested since publishing.
46 ///
47 /// Consumers are not guaranteed to read exactly once. Chromium in particular touches the
48 /// clipboard more than once per paste, so the first render is not proof the paste completed.
49 render_count: u32,
50 /// Tick of the most recent render, for debouncing the restore.
51 last_render: Option<std::time::Instant>,
52 /// Render count observed when the paste was triggered. Reads at or below this were caused by
53 /// something other than the paste (a clipboard manager, history service) and prove nothing.
54 baseline: u32,
55}
56
57fn state() -> &'static (Mutex<State>, Condvar) {
58 static S: OnceLock<(Mutex<State>, Condvar)> = OnceLock::new();
59 S.get_or_init(|| {
60 (
61 Mutex::new(State {
62 pending: None,
63 rendered: false,
64 render_count: 0,
65 last_render: None,
66 baseline: 0,
67 }),
68 Condvar::new(),
69 )
70 })
71}
72
73static OWNER_HWND: AtomicIsize = AtomicIsize::new(0);
74static THREAD_STARTED: AtomicBool = AtomicBool::new(false);
75
76unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
77 match msg {
78 // A consumer asked for one specific format. The clipboard is already open by the requester,
79 // so this must call SetClipboardData without opening it.
80 WM_RENDERFORMAT => {
81 render(wp.0 as u32);
82 LRESULT(0)
83 }
84 // We are losing ownership or shutting down; supply everything we promised.
85 WM_RENDERALLFORMATS => {
86 if unsafe { OpenClipboard(Some(hwnd)) }.is_ok() {
87 render(CF_UNICODETEXT_PUBLIC);
88 let _ = unsafe { CloseClipboard() };
89 }
90 LRESULT(0)
91 }
92 // Another process took the clipboard. Our promise is void.
93 WM_DESTROYCLIPBOARD => {
94 let (lock, _) = state();
95 if let Ok(mut s) = lock.lock() {
96 s.pending = None;
97 }
98 LRESULT(0)
99 }
100 _ => unsafe { DefWindowProcW(hwnd, msg, wp, lp) },
101 }
102}
103
104/// Supply the promised text for `format`, and record that a real read occurred.
105fn render(format: u32) {
106 if !matches!(format, CF_UNICODETEXT_PUBLIC | CF_TEXT | CF_OEMTEXT) {
107 return;
108 }
109 let (lock, cvar) = state();
110 let Ok(mut s) = lock.lock() else { return };
111 let Some(text) = s.pending.clone() else {
112 return;
113 };
114
115 if let Ok(handle) = alloc_global_public(&utf16_bytes_public(&text)) {
116 // Deliberately always CF_UNICODETEXT: Windows synthesizes the narrow formats from it, so
117 // one render satisfies a consumer that asked for any of them.
118 let _ = unsafe { SetClipboardData(CF_UNICODETEXT_PUBLIC, Some(HANDLE(handle.0))) };
119 }
120
121 s.rendered = true;
122 s.render_count += 1;
123 s.last_render = Some(std::time::Instant::now());
124 cvar.notify_all();
125}
126
127/// Start the owner window and its message pump. Idempotent.
128fn ensure_owner() -> Result<HWND, Error> {
129 if let 0 = OWNER_HWND.load(Ordering::SeqCst) {
130 } else {
131 return Ok(HWND(OWNER_HWND.load(Ordering::SeqCst) as *mut _));
132 }
133
134 if THREAD_STARTED.swap(true, Ordering::SeqCst) {
135 // Another caller is mid-startup; wait for the handle to appear.
136 for _ in 0..200 {
137 let h = OWNER_HWND.load(Ordering::SeqCst);
138 if h != 0 {
139 return Ok(HWND(h as *mut _));
140 }
141 std::thread::sleep(Duration::from_millis(5));
142 }
143 return Err(Error::OwnerWindowFailed);
144 }
145
146 std::thread::Builder::new()
147 .name("win-text-inject-clipboard-owner".into())
148 .spawn(|| unsafe {
149 let instance = match GetModuleHandleW(None) {
150 Ok(i) => i,
151 Err(_) => return,
152 };
153 let class = w!("WinTextInjectClipboardOwner");
154 let wc = WNDCLASSW {
155 lpfnWndProc: Some(wndproc),
156 hInstance: instance.into(),
157 lpszClassName: class,
158 ..Default::default()
159 };
160 RegisterClassW(&wc);
161
162 // Never shown. Not HWND_MESSAGE: message-only windows are not reliable clipboard
163 // owners, and clipboard owner messages are sent directly to the window anyway.
164 let hwnd = match CreateWindowExW(
165 Default::default(),
166 class,
167 w!("win-text-inject clipboard owner"),
168 WS_OVERLAPPED,
169 CW_USEDEFAULT,
170 CW_USEDEFAULT,
171 0,
172 0,
173 None,
174 None,
175 Some(instance.into()),
176 None,
177 ) {
178 Ok(h) => h,
179 Err(_) => return,
180 };
181
182 OWNER_HWND.store(hwnd.0 as isize, Ordering::SeqCst);
183
184 let mut msg = MSG::default();
185 while GetMessageW(&mut msg, None, 0, 0).as_bool() {
186 let _ = TranslateMessage(&msg);
187 DispatchMessageW(&msg);
188 }
189 })
190 .map_err(|_| Error::OwnerWindowFailed)?;
191
192 for _ in 0..200 {
193 let h = OWNER_HWND.load(Ordering::SeqCst);
194 if h != 0 {
195 return Ok(HWND(h as *mut _));
196 }
197 std::thread::sleep(Duration::from_millis(5));
198 }
199 Err(Error::OwnerWindowFailed)
200}
201
202/// A promise of text placed on the clipboard, not yet materialized.
203pub struct Offer;
204
205impl Offer {
206 /// Advertise `text` on the clipboard without publishing it.
207 ///
208 /// Nothing is copied until a consumer asks, at which point [`Offer::wait_for_read`] returns.
209 pub fn publish(text: &str) -> Result<Self, Error> {
210 let hwnd = ensure_owner()?;
211
212 {
213 let (lock, _) = state();
214 let mut s = lock.lock().map_err(|_| Error::OwnerWindowFailed)?;
215 s.pending = Some(text.to_owned());
216 s.rendered = false;
217 s.render_count = 0;
218 s.last_render = None;
219 s.baseline = 0;
220 }
221
222 {
223 // Retrying open is essential here: a contended clipboard otherwise fails outright with
224 // ERROR_ACCESS_DENIED, which in a dictation app means a dropped transcript.
225 let _guard = crate::clipboard::ClipboardGuard::open_owned_by(hwnd)?;
226 let result = unsafe {
227 (|| {
228 EmptyClipboard().map_err(Error::Clipboard)?;
229 // NULL data is the promise; Windows comes back with WM_RENDERFORMAT.
230 //
231 // For delayed rendering SetClipboardData returns NULL on *success*, which the
232 // bindings surface as Err with a zeroed last-error. Only a non-zero code is a real
233 // failure here.
234 if let Err(e) = SetClipboardData(CF_UNICODETEXT_PUBLIC, None) {
235 if e.code().0 != 0 {
236 return Err(Error::Clipboard(e));
237 }
238 }
239 crate::clipboard::attach_privacy_formats();
240 Ok::<(), Error>(())
241 })()
242 };
243 result?;
244
245 // Ownership is the real confirmation the promise was accepted.
246 if unsafe { GetClipboardOwner() }.unwrap_or_default() != hwnd {
247 return Err(Error::OwnerWindowFailed);
248 }
249 }
250
251 Ok(Self)
252 }
253
254 /// Block until a consumer actually read the clipboard, or `timeout` elapses.
255 ///
256 /// Returns `true` if the data was read. A `false` return means the paste never reached the
257 /// target, which is itself useful: the caller can report that instead of silently assuming
258 /// success.
259 pub fn wait_for_read(&self, timeout: Duration) -> bool {
260 let (lock, cvar) = state();
261 let Ok(guard) = lock.lock() else { return false };
262 let Ok((guard, _)) = cvar.wait_timeout_while(guard, timeout, |s| !s.rendered) else {
263 return false;
264 };
265 guard.rendered
266 }
267
268 /// Wait for the first read, then until reads have been quiet for `quiet`.
269 ///
270 /// A single `WM_RENDERFORMAT` is *not* proof the paste completed. Chromium touches the
271 /// clipboard more than once per paste — an early probe, then the real read — so restoring after
272 /// the first render puts the old text back before the read that matters, which is precisely the
273 /// bug this was meant to fix. Waiting for renders to go quiet covers multi-read consumers.
274 ///
275 /// Returns the number of reads observed, or `None` if none arrived within `timeout`.
276 pub fn wait_for_reads_to_settle(&self, timeout: Duration, quiet: Duration) -> Option<u32> {
277 if !self.wait_for_read(timeout) {
278 return None;
279 }
280 loop {
281 let last = {
282 let (lock, _) = state();
283 let s = lock.lock().ok()?;
284 s.last_render?
285 };
286 let elapsed = last.elapsed();
287 if elapsed >= quiet {
288 break;
289 }
290 std::thread::sleep(quiet - elapsed);
291 }
292 let (lock, _) = state();
293 let s = lock.lock().ok()?;
294 Some(s.render_count)
295 }
296
297 /// Number of times a consumer has asked for the data since publishing.
298 pub fn read_count(&self) -> u32 {
299 state().0.lock().map(|s| s.render_count).unwrap_or(0)
300 }
301
302 /// Record that the paste has now been triggered.
303 ///
304 /// Anything that reads the clipboard — a clipboard manager, a history service — satisfies the
305 /// render, so a read observed *before* the paste says nothing about the target. Marking here
306 /// lets [`Offer::wait_for_target_read`] ignore those and wait for a read caused by the paste.
307 pub fn mark_paste_sent(&self) {
308 if let Ok(mut s) = state().0.lock() {
309 s.baseline = s.render_count;
310 }
311 }
312
313 /// Whether the promise was already materialized before the paste was sent.
314 ///
315 /// Once *any* consumer forces the render, the clipboard holds real data and Windows sends no
316 /// further `WM_RENDERFORMAT`. The target's read is then unobservable — not delayed, gone. A
317 /// clipboard manager that archives every change causes exactly this, so callers must have a
318 /// fallback rather than waiting for a signal that can never arrive.
319 pub fn consumed_before_paste(&self) -> bool {
320 state().0.lock().map(|s| s.baseline > 0).unwrap_or(false)
321 }
322
323 /// Wait for a read that happened *after* [`Offer::mark_paste_sent`], then for reads to settle.
324 ///
325 /// Cannot be satisfied by a clipboard manager that read the promise the moment it was
326 /// published. Returns `None` if no such read arrives, which includes the case where the promise
327 /// was already consumed — check [`Offer::consumed_before_paste`] to tell those apart.
328 pub fn wait_for_target_read(&self, timeout: Duration, quiet: Duration) -> Option<u32> {
329 let (lock, cvar) = state();
330 {
331 let guard = lock.lock().ok()?;
332 let (guard, timed_out) = cvar
333 .wait_timeout_while(guard, timeout, |s| s.render_count <= s.baseline)
334 .ok()?;
335 if timed_out.timed_out() && guard.render_count <= guard.baseline {
336 return None;
337 }
338 }
339 loop {
340 let last = { lock.lock().ok()?.last_render? };
341 let elapsed = last.elapsed();
342 if elapsed >= quiet {
343 break;
344 }
345 std::thread::sleep(quiet - elapsed);
346 }
347 let s = lock.lock().ok()?;
348 Some(s.render_count - s.baseline)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn owner_window_starts_and_is_reused() {
358 let a = ensure_owner().expect("owner window");
359 let b = ensure_owner().expect("owner window again");
360 assert_eq!(a.0, b.0);
361 assert!(!a.0.is_null());
362 }
363
364 #[test]
365 fn non_text_formats_are_not_rendered() {
366 // CF_BITMAP (2) is not something we promise; asking for it must not mark a read.
367 let (lock, _) = state();
368 {
369 let mut s = lock.lock().unwrap();
370 s.pending = Some("x".into());
371 s.rendered = false;
372 }
373 render(2);
374 assert!(!lock.lock().unwrap().rendered);
375 }
376}