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