Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable graceful shutdown on linux #13

Merged
merged 2 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
os: [macos-latest, windows-latest, ubuntu-latest]

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4

- name: Install Rust Unix
if: runner.os != 'Windows'
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ windows-win = "3"

[target.'cfg(all(unix, not(any(target_os="macos", target_os="ios", target_os="android", target_os="emscripten"))))'.dependencies]
x11-clipboard = "0.9"
x11rb = { version = "0.13", features = ["xfixes"] }

[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2"
Expand Down
161 changes: 134 additions & 27 deletions src/master/x11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use std::io;
use std::sync::OnceLock;
use std::sync::mpsc::{self, SyncSender, Receiver, sync_channel};

use x11rb::protocol::xfixes;
use x11rb::connection::Connection;
use x11rb::protocol::xproto::ConnectionExt;

///Shutdown channel
///
///On drop requests shutdown to gracefully close clipboard listener as soon as possible.
Expand Down Expand Up @@ -65,46 +69,149 @@ impl<H: ClipboardHandler> Master<H> {
}
};

loop {
let res = clipboard.load_wait(

if let Err(error) = xfixes::query_version(&clipboard.getter.connection, 5, 0) {
return Err(io::Error::new(io::ErrorKind::Other, error));
}

let mut result = Ok(());
'main: loop {
let selection = clipboard.getter.atoms.clipboard;

let screen = match clipboard.getter.connection.setup().roots.get(clipboard.getter.screen) {
Some(screen) => screen,
None => match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, "Screen is not available")) {
CallbackResult::Next => continue,
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
result = Err(error);
break;
}
}
};

// Clear selection sources...
let cookie = xfixes::select_selection_input(
&clipboard.getter.connection,
screen.root,
clipboard.getter.atoms.primary,
xfixes::SelectionEventMask::default()
).and_then(|_| xfixes::select_selection_input(
&clipboard.getter.connection,
screen.root,
clipboard.getter.atoms.clipboard,
clipboard.getter.atoms.incr,
clipboard.getter.atoms.property,
);
match res {
Ok(_) => {
match self.handler.on_clipboard_change() {
CallbackResult::Next => (),
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
return Err(error);
xfixes::SelectionEventMask::default()
// ...and set the one requested now
)).and_then(|_| xfixes::select_selection_input(
&clipboard.getter.connection,
screen.root,
selection,
xfixes::SelectionEventMask::SET_SELECTION_OWNER | xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE | xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY
));

if let Err(error) = clipboard.getter.connection.flush() {
match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, error)) {
CallbackResult::Next => continue,
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
result = Err(error);
break;
}
}
}

let sequence_number = match cookie {
Ok(cookie) => {
let sequence_number = cookie.sequence_number();
if let Err(error) = cookie.check() {
match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, error)) {
CallbackResult::Next => continue,
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
result = Err(error);
break;
}
}
}
sequence_number
},
Err(error) => {
let error = io::Error::new(
io::ErrorKind::Other,
format!("Failed to load clipboard: {:?}", error),
);

match self.handler.on_clipboard_error(error) {
CallbackResult::Next => (),
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
return Err(error);
Err(error) => match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, error)) {
CallbackResult::Next => continue,
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
result = Err(error);
break;
}
}
};

'poll: loop {
match clipboard.getter.connection.poll_for_event_with_sequence() {
Ok(Some((_, seq))) if seq >= sequence_number => {
match self.handler.on_clipboard_change() {
CallbackResult::Next => break 'poll,
CallbackResult::Stop => break 'main,
CallbackResult::StopWithError(error) => {
result = Err(error);
break 'main;
}
}
},
Ok(_) => {
match self.recv.recv_timeout(self.handler.sleep_interval()) {
Ok(()) => break 'main,
//timeout
Err(mpsc::RecvTimeoutError::Timeout) => continue 'poll,
Err(mpsc::RecvTimeoutError::Disconnected) => break 'main,
}
}
Err(error) => {
let error = io::Error::new(
io::ErrorKind::Other,
format!("Failed to load clipboard: {:?}", error),
);

match self.handler.on_clipboard_error(error) {
CallbackResult::Next => break 'poll,
CallbackResult::Stop => break 'main,
CallbackResult::StopWithError(error) => {
result = Err(error);
break 'main;
}
}
}
}
}

match self.recv.try_recv() {
let delete = clipboard.getter.connection.delete_property(clipboard.getter.window, clipboard.getter.atoms.property)
.map_err(|error| io::Error::new(io::ErrorKind::Other, error))
.and_then(|cookie| cookie.check().map_err(|error| io::Error::new(io::ErrorKind::Other, error)));
if let Err(error) = delete {
match self.handler.on_clipboard_error(error) {
CallbackResult::Next => (),
CallbackResult::Stop => break,
CallbackResult::StopWithError(error) => {
result = Err(error);
break;
}
}
}

match self.recv.recv_timeout(self.handler.sleep_interval()) {
Ok(()) => break,
Err(mpsc::TryRecvError::Empty) => continue,
Err(mpsc::TryRecvError::Disconnected) => break,
//timeout
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}

Ok(())
match clipboard.getter.connection.delete_property(clipboard.getter.window, clipboard.getter.atoms.property) {
Ok(cookie) => match cookie.check() {
Ok(_) => result,
Err(error) => Err(io::Error::new(io::ErrorKind::Other, error)),
},
Err(error) => Err(io::Error::new(io::ErrorKind::Other, error)),
}
}

///Gets one time initialized x11 clipboard.
Expand Down
11 changes: 7 additions & 4 deletions tests/shutdown.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
use std::time;
use clipboard_master::{Master, ClipboardHandler, CallbackResult};

pub struct Handler;

impl ClipboardHandler for Handler {
fn on_clipboard_change(&mut self) -> CallbackResult {
println!("CHANGE");
CallbackResult::Next
}
}

//TODO: Make shutdown work on Linux
//This is currently difficult due to buggy x11-clipboard lib
#[cfg(not(target_arch = "linux"))]
#[test]
fn should_shutdown_successfully() {
const TIMEOUT: time::Duration = time::Duration::from_secs(5);
let mut master = Master::new(Handler).expect("To create master");
let shutdown = master.shutdown_channel();
std::thread::spawn(move || {
std::thread::sleep(core::time::Duration::from_secs(5));
std::thread::sleep(TIMEOUT);
println!("signal");
shutdown.signal();
});

println!("RUN");
let now = time::Instant::now();
master.run().expect("to finish");
assert!(now.elapsed() >= (TIMEOUT - time::Duration::from_millis(500)));
assert!(now.elapsed() <= (TIMEOUT + time::Duration::from_millis(500)));
}