From a3bbe210703bb1075b4dcc0acfbf88a7e6e53eb9 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 4 Jun 2024 22:27:56 +0900 Subject: [PATCH 1/2] Enable graceful shutdown on linux --- .github/workflows/rust.yml | 2 +- src/master/x11.rs | 4 +++- tests/shutdown.rs | 10 ++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 807d7ea..6edfeac 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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' diff --git a/src/master/x11.rs b/src/master/x11.rs index 33f268b..1aad757 100644 --- a/src/master/x11.rs +++ b/src/master/x11.rs @@ -66,10 +66,11 @@ impl Master { }; loop { - let res = clipboard.load_wait( + let res = clipboard.load( clipboard.getter.atoms.clipboard, clipboard.getter.atoms.incr, clipboard.getter.atoms.property, + self.handler.sleep_interval(), ); match res { Ok(_) => { @@ -81,6 +82,7 @@ impl Master { } } }, + Err(x11_clipboard::error::Error::Timeout) => (), Err(error) => { let error = io::Error::new( io::ErrorKind::Other, diff --git a/tests/shutdown.rs b/tests/shutdown.rs index 7992377..042589b 100644 --- a/tests/shutdown.rs +++ b/tests/shutdown.rs @@ -1,3 +1,4 @@ +use std::time; use clipboard_master::{Master, ClipboardHandler, CallbackResult}; pub struct Handler; @@ -8,19 +9,20 @@ impl ClipboardHandler for Handler { } } -//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))); } From af1fef8398d7c5eebf25acdd9eabee5b62782e52 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Tue, 4 Jun 2024 23:37:49 +0900 Subject: [PATCH 2/2] Re-implement raw x11 poll for event --- Cargo.toml | 1 + src/master/x11.rs | 163 +++++++++++++++++++++++++++++++++++++--------- tests/shutdown.rs | 1 + 3 files changed, 136 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a270a73..56ec9eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/master/x11.rs b/src/master/x11.rs index 1aad757..db3d2a0 100644 --- a/src/master/x11.rs +++ b/src/master/x11.rs @@ -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. @@ -65,48 +69,149 @@ impl Master { } }; - loop { - let res = clipboard.load( + + 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, - self.handler.sleep_interval(), - ); - 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(x11_clipboard::error::Error::Timeout) => (), - 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. diff --git a/tests/shutdown.rs b/tests/shutdown.rs index 042589b..8168e87 100644 --- a/tests/shutdown.rs +++ b/tests/shutdown.rs @@ -5,6 +5,7 @@ pub struct Handler; impl ClipboardHandler for Handler { fn on_clipboard_change(&mut self) -> CallbackResult { + println!("CHANGE"); CallbackResult::Next } }