use super::OleContext;
use super::data_object::DataObject;
use super::error::from_windows_error;
use crate::cancellation::CancellationToken;
use crate::{PlatformErrorKind, Result, TransferRequest};
use std::ffi::c_void;
use windows::Win32::Foundation::S_OK;
use windows::Win32::System::Com::IDataObject;
use windows::Win32::System::DataExchange::{CloseClipboard, EmptyClipboard, OpenClipboard};
use windows::Win32::System::Ole::OleSetClipboard;
use windows::core::{HRESULT, Interface};
#[link(name = "ole32")]
unsafe extern "system" {
#[link_name = "OleIsCurrentClipboard"]
fn ole_is_current_clipboard(data_object: *mut c_void) -> HRESULT;
}
pub(crate) struct ClipboardSession {
data_object: IDataObject,
cancellation: CancellationToken,
_ole: OleContext,
}
impl ClipboardSession {
pub fn cancel(&self) {
self.cancellation.cancel();
}
}
impl Drop for ClipboardSession {
fn drop(&mut self) {
if unsafe { OpenClipboard(None) }.is_err() {
return;
}
let is_current = unsafe { ole_is_current_clipboard(self.data_object.as_raw()) == S_OK };
if is_current {
let _ = unsafe { EmptyClipboard() };
}
let _ = unsafe { CloseClipboard() };
}
}
pub(crate) fn copy_to_clipboard(request: TransferRequest) -> Result<ClipboardSession> {
let ole = OleContext::new()
.map_err(|error| from_windows_error(PlatformErrorKind::InitializationFailed, error))?;
let TransferRequest {
files,
cancellation: token,
} = request;
let data_object: IDataObject = DataObject::new(files, token.clone(), None)?.into();
unsafe { OleSetClipboard(&data_object) }
.map_err(|error| from_windows_error(PlatformErrorKind::TransferFailed, error))?;
Ok(ClipboardSession {
data_object,
cancellation: token,
_ole: ole,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FileSource, SourceReader, VirtualFile};
use std::io::{self, Cursor};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
struct TrackedSource {
drops: Arc<AtomicUsize>,
}
impl FileSource for TrackedSource {
fn open(&self) -> io::Result<SourceReader> {
Ok(SourceReader::seekable(Cursor::new(b"hello".to_vec())))
}
}
impl Drop for TrackedSource {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
}
}
fn make_request(drops: Arc<AtomicUsize>) -> TransferRequest {
let source = TrackedSource { drops };
let file = VirtualFile::new("hello.txt", Box::new(source)).unwrap();
TransferRequest::new(vec![file])
}
fn clear_clipboard() {
unsafe {
OleSetClipboard(None::<&IDataObject>).unwrap();
}
}
#[test]
#[ignore = "Uses the system clipboard"]
fn replacing_clipboard_releases_previous_data_object() {
let first_drops = Arc::new(AtomicUsize::new(0));
let second_drops = Arc::new(AtomicUsize::new(0));
let first = copy_to_clipboard(make_request(Arc::clone(&first_drops))).unwrap();
assert_eq!(first_drops.load(Ordering::SeqCst), 0);
let second = copy_to_clipboard(make_request(Arc::clone(&second_drops))).unwrap();
assert_eq!(first_drops.load(Ordering::SeqCst), 0);
drop(first);
assert_eq!(first_drops.load(Ordering::SeqCst), 1);
assert_eq!(second_drops.load(Ordering::SeqCst), 0);
clear_clipboard();
assert_eq!(second_drops.load(Ordering::SeqCst), 0);
drop(second);
assert_eq!(second_drops.load(Ordering::SeqCst), 1);
}
#[test]
#[ignore = "Uses the system clipboard"]
fn dropping_session_releases_current_clipboard_data_object() {
let drops = Arc::new(AtomicUsize::new(0));
let session = copy_to_clipboard(make_request(Arc::clone(&drops))).unwrap();
assert_eq!(drops.load(Ordering::SeqCst), 0);
drop(session);
assert_eq!(drops.load(Ordering::SeqCst), 1);
}
#[test]
#[ignore = "Uses the system clipboard"]
fn dropping_old_session_preserves_new_clipboard_data() {
let first_drops = Arc::new(AtomicUsize::new(0));
let second_drops = Arc::new(AtomicUsize::new(0));
let first = copy_to_clipboard(make_request(Arc::clone(&first_drops))).unwrap();
let second = copy_to_clipboard(make_request(Arc::clone(&second_drops))).unwrap();
drop(first);
assert_eq!(second_drops.load(Ordering::SeqCst), 0);
let hr = unsafe { ole_is_current_clipboard(second.data_object.as_raw()) };
assert_eq!(hr, S_OK);
drop(second);
assert_eq!(first_drops.load(Ordering::SeqCst), 1);
assert_eq!(second_drops.load(Ordering::SeqCst), 1);
}
#[test]
#[ignore = "Clipboard lock helper"]
fn hold_clipboard_in_subprocess() {
if std::env::var_os("VFT_CLIPBOARD_HOLDER").is_none() {
return;
}
use std::io::{Read, Write};
use windows::Win32::System::DataExchange::{CloseClipboard, OpenClipboard};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED,
};
use windows::core::w;
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE(0),
w!("STATIC"),
w!("Clipboard Test Holder"),
WS_OVERLAPPED,
0,
0,
1,
1,
None,
None,
None,
None,
)
.unwrap()
};
unsafe { OpenClipboard(Some(hwnd)).unwrap() };
println!("VFT_LOCKED");
std::io::stdout().flush().unwrap();
let mut signal = [0u8; 1];
let read_result = std::io::stdin().read_exact(&mut signal);
let close_result = unsafe { CloseClipboard() };
unsafe { DestroyWindow(hwnd).unwrap() };
read_result.unwrap();
close_result.unwrap();
}
#[test]
#[ignore = "Uses the system clipboard"]
fn dropping_session_while_clipboard_is_locked_does_not_block() {
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
let drops = Arc::new(AtomicUsize::new(0));
let session = copy_to_clipboard(make_request(Arc::clone(&drops))).unwrap();
let mut child = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"windows::clipboard::tests::hold_clipboard_in_subprocess",
"--ignored",
"--nocapture",
])
.env("VFT_CLIPBOARD_HOLDER", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = BufReader::new(child.stdout.take().unwrap());
loop {
let mut line = String::new();
assert_ne!(stdout.read_line(&mut line).unwrap(), 0);
if line.contains("VFT_LOCKED") {
break;
}
}
let start = Instant::now();
drop(session);
let elapsed = start.elapsed();
child.stdin.take().unwrap().write_all(b"x").unwrap();
assert!(child.wait().unwrap().success());
assert!(
elapsed < Duration::from_secs(3),
"ClipboardSession::drop() blocked for {elapsed:?}"
);
let _ole = OleContext::new().unwrap();
clear_clipboard();
}
}