Skip to main content

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}