arboard
arboard copied to clipboard
[X11] Unexpected behavior?
Hi, I'm a Rust beginner and I'm having different behaviors across X11 and Windows 10, so I'm wondering if I'm doing something stupid or if there's something that could be improved in this crate 🤔
Take this program:
use arboard::Clipboard;
use std::thread;
fn main() {
for n in 0..5 {
thread::spawn(move || {
let mut clipboard = Clipboard::new().unwrap();
let text = format!("thread #{}", n);
println!("Copying to clipboard: {}", text);
clipboard.set_text(text).unwrap();
})
.join()
.unwrap();
}
let mut clipboard = Clipboard::new().unwrap();
println!("Final clipboard text: {}", clipboard.get_text().unwrap());
}
It synchronously spawns 5 threads and creates a new clipboard context in each one of them (I think doing this is probably bad? but anyway...); Then, it copies the text thread #N
to the clipboard and exits the thread. And at the end it, it creates another clipboard context and displays its final content.
On my tests on a Windows 10 virtual machine, this is always the output I get when I run the program:
Copying to clipboard: thread #0
Copying to clipboard: thread #1
Copying to clipboard: thread #2
Copying to clipboard: thread #3
Copying to clipboard: thread #4
Final clipboard text: thread #4
Which is the behavior I expect... However, on my Linux machine (X11 session), I get this instead:
Copying to clipboard: thread #0
Copying to clipboard: thread #1
Copying to clipboard: thread #2
Copying to clipboard: thread #3
Copying to clipboard: thread #4
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ContentNotAvailable - "The clipboard contents were not available in the requested format or the clipboard is empty."', src/main.rs:17:63
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
I was able to work around this issue on Linux by using a mutex to share a single clipboard context across threads:
use arboard::Clipboard;
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let clipboard = Arc::new(Mutex::new(Clipboard::new().unwrap()));
for n in 0..5 {
let clipboard = Arc::clone(&clipboard);
thread::spawn(move || {
let mut clipboard = clipboard.lock().unwrap();
let text = format!("thread #{}", n);
println!("Copying to clipboard: {}", text);
(*clipboard).set_text(text).unwrap();
})
.join()
.unwrap();
}
let mut clipboard = Clipboard::new().unwrap();
println!("Final clipboard text: {}", clipboard.get_text().unwrap());
}
Which now works perfectly, but in the end I wonder if I really have to do this... 🤔
Thanks for this crate.
I think this has to do with the clipboard being flushed on Drop
. The Mutex
and Arc
aren't necessary. Simply moving the clipboard
creation before the loop fixes the problem by keeping the internal strong count non-zero.
use arboard::Clipboard;
use std::thread;
fn main() {
let mut clipboard = Clipboard::new().unwrap();
for n in 0..5 {
thread::spawn(move || {
let mut clipboard = Clipboard::new().unwrap();
let text = format!("thread #{}", n);
println!("Copying to clipboard: {}", text);
clipboard.set_text(text).unwrap();
})
.join()
.unwrap();
}
println!("Final clipboard text: {}", clipboard.get_text().unwrap());
}
@complexspaces Is this expected?
Thanks for the ping, @kjvalencik. Based on what I do know about the X11 clipboard, this does seem "expected" since its tied into arboard
's window connection to the X11 server. This means that when we destroy the connection (on Drop
), the contents go away because we declared ourselves as the owner of the contents IIUC. I would need to research this more since I don't know a whole lot more beyond that.
However, this seems unexpected and a bit of a sharp API with the Clipboard
wrapper where the platform details start to leak out without any weird code; IE: calling std::mem::forget()
on the Windows clipboard to lock it forever.
I spent a bit of time digging into this more and how the X11 event system works and I'm further confident that this is the correct and expected behavior at the system handling level.
When the Clipboard
is dropped, the fake window we create with the X11 server is destroyed. Per the spec about giving up ownership, our ownership over the clipboard object is relinquished upon the window's destruction. It's also important to note that the X11 server does not hold onto the data itself. Is appears to be the responsibility of the clipboard owner to do this.
This means that in the example when the clipboard is then opened at the end, there is no available owner that the X11 server can send our request for clipboard data too. On top of that, we've shut down the previous listener thread that had the contents stored in-memory, so they're truly gone.
From this, I see a few routes forward for arboard
:
- Document this as a caveat in
Clipboard
's documentation and note that users should keep it open themselves if they'd like to use the clipboard to communicate inside their own process. - Leak the X11 connection and listener until the process dies.
- Expose a method on the
ClipboardExtLinux
trait which allows the caller to determine if we should clean up our global clipboard when aClipboard
object is dropped:
impl ClipboardExtLinux for Clipboard {
...
fn keep_listener_running_forever(&mut keep_running: bool) {}
}
3.a. By default, this would be false
to have closer cross-platform behavior to Windows and MacOS. This detail would be documented in Clipboard
's documentation.
3.b. By default, this would be true
to keep the current resource-efficient behavior and the documentation would point to keep_listener_running_forever
.
I would be curious to hear everybody's thoughts on these ideas to get an idea which feels less like a footgun and in the spirit of the crate's ease-of-use goals.
First of all this kinda seems to me that you are using the clipboard to communicate between threads. That would be a bad idea, and you should use a channel or a Mutex for that instead.
calling
std::mem::forget()
on the Windows clipboard to lock it forever
This is incorrect. The Windows clipboard is locked at the beggining of each get/set and automatically unlocked before the get/set function returns.
This means that in the example when the clipboard is then opened at the end, there is no available owner that the X11 server can send our request for clipboard data too
In theory, this is false. On X11, there's this thing called the "clipboard manager". The clipboard manager's purpose is to take ownership of the clipboard data, whenever the previous owner is destroyed. The clipboard manager will then send this data to anyone who wants to get it. Whenever the last living instance of Clipboard
is dropped, we send the clipboard data to the clipboard manager. Note that this can happen multiple times. The clipboard manager mechanism doesn't work on some systems however. For example it doesn't seem to work on XWayland on GNOME, meaning that the data wont be available after we drop the last Clipboard instance.
Furthermore according to my experience there is a delay between passing the data to the clipboard manager and being able to read the data.
- Document this as a caveat in
Clipboard
's documentation and note that users should keep it open themselves if they'd like to use the clipboard to communicate inside their own process.
I like this.
2. Leak the X11 connection and listener until the process dies.
The issue is that we still need to hand over the clipboard data to the clipboard manager before the process finishes. Otherwise the data won't be available after the process has exited. We could maybe register a callback that gets executed when the process finishes, but as far as I know, Rust doesn't officially support anything like that.
3. Expose a method on the
ClipboardExtLinux
trait which allows the caller to determine if we should clean up our global clipboard when aClipboard
object is dropped:
Again, even if we don't clean it up, we still need to send it to the clipboard manager when our process exits, so I don't think that 3. is a good approach.
I tired inserting a sleep into the code above and it did not resolve the issue. I'm also using X11 on Gnome without Wayland (Ubuntu 20.04).
Also, in a simpler program, when the process exits, the clipboard contents is not sent--it becomes no longer available. Perhaps there's a bug here and it's not working?
Also, in a simpler program, when the process exits, the clipboard contents is not sent--it becomes no longer available.
In that case it doesn't work as intended. I opened #39 for this
I'm going to go ahead and close this one now that 3.0
is out. The new wait function can let you serve the clipboard for longer if you need to.