Skip to content

Commit

Permalink
Enable graceful shutdown on linux (#13)
Browse files Browse the repository at this point in the history
* Enable graceful shutdown on linux

* Re-implement raw x11 poll for event
  • Loading branch information
DoumanAsh authored Jun 4, 2024
1 parent 5f79713 commit 1cf6367
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 32 deletions.
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)));
}

0 comments on commit 1cf6367

Please sign in to comment.