Skip to main content

kwin_mouse_loc/
lib.rs

1//! # kwin-mouse-loc
2//!
3//! A very simple mouse controller that uses `libc::process_vm_readv` to read mouse location. Need to access kwin_wayland's memory, may often need root permissions.
4//!
5//! # Usage
6//!
7//! Since mouse and keyboard operations is very dangerous, it might be easily be poisoned.
8//! And since there is no guarateen that crate owner is not evil, I wrote this simple crate.
9//!
10//! The main aim of this crate is that, make user ensure they use a *SAFE* crate that cannot be poisoned.
11//! The *BEST* practice of using this crate should be just copy the `build.rs` and `lib.rs` into your project.
12//!
13//! If you like this crate, you could make it as an optional dependencies, BUT please keep one thing in mind:
14//!
15//! > DO NOT ENABLE THE DEPENDENCIES OF THIS CRATE
16//!
17//! # Example
18//!
19//! (all the tests require root permissions, without root permissions, the program might failed to execute)
20//!
21//! ```no_run
22//! use kwin_mouse_loc::pointer::Workspace;
23//! fn main(){
24//!     let mouse = unsafe{Workspace::new(true).get_mouse()};
25//!     let (x,y) = mouse.loc();
26//!     println!("mouse is located at ({x}, {y})");
27//!     // do some other things.
28//!     std::thread::sleep(std::time::Duration::from_millis(300));
29//!
30//!     // obtain mouse location again, with display.
31//!     println!("mouse is located at {mouse}");
32//! }
33//! ```
34//! # Features
35//! `docgen-detect` : for publish (and doc generation without process header file) thus enabled by default. For normal use, just disable it.
36//!
37//! `keyboard`      : requires `uinput`, allow using keyboard commands
38//!
39//! `uinput`        : process uinput constants from header files.
40//!
41//! `test`          : enable tests, since most of the tests needs root permission, be aware.
42//!
43//! `update-offset` : update the offset of workspace related to libkwin. Especially useful after the libkwin.so updated.
44//!
45//! `update-pos`    : requires `update-offset`, also update the field offset of mouse position
46#![warn(unsafe_op_in_unsafe_fn)]
47#![cfg_attr(doc, feature(doc_cfg))]
48#[cfg(feature = "uinput")]
49pub mod device;
50/// Some constants, which could be updated if feature `update-offset` is set.
51/// It is worth mention that, the `update-offset` feature highly relies on `readelf` executable, and use the following sections:
52/// ```text
53/// .kwin.mouse.loc.pos
54/// .kwin.mouse.loc.kwin
55/// .kwin.mouse.loc.offset
56/// ```
57/// Adding other variable into such section may damage the executable.
58pub mod consts {
59    include!(concat!(env!("OUT_DIR"), "/consts.rs"));
60    #[cfg_attr(doc, doc(cfg(feature = "update-offset")))]
61    #[cfg(any(doc, feature = "update-offset"))]
62    include!("update_offset.rs");
63}
64
65/// pointer of kwin workspace and its cursor's position
66pub mod pointer {
67    use crate::consts::*;
68    use libc::{iovec, process_vm_readv};
69    use std::{ffi::c_void, fmt::Display, fs::File, io::Read, process::Command, ptr};
70    /// PID of kwin_wayland.
71    /// SAFETY: users should ensure this is the pid of kwin_wayland, and this PID is valid before this program exited.
72    #[derive(Clone, Copy, Eq, PartialEq)]
73    pub struct KWinPid(i32);
74    impl KWinPid {
75        /// SAFETY: users should ensure this is the pid of kwin_wayland, and this PID is valid before this program exited.
76        pub unsafe fn from(i: i32) -> Self {
77            unsafe {
78                if libc::getuid() != 0 {
79                    // if is not root
80                    if libc::setuid(0) != 0 {
81                        // if cannot be root
82                        panic!("cannot set uid to 0, further code could not be executed.")
83                    }
84                }
85            }
86            Self(i)
87        }
88        /// SAFETY: users should ensure this is the pid of kwin_wayland, and this PID is valid before this program exited.
89        pub unsafe fn search(all_user: bool) -> Self {
90            unsafe {
91                Self::from(
92                    String::from_utf8_lossy(
93                        &Command::new("ps")
94                            .arg(if all_user { "ax" } else { "x" }) // "a" is needed since there might not be a wayland window running by root.
95                            .output()
96                            .expect("cannot enumerate programs")
97                            .stdout,
98                    )
99                    .lines()
100                    .filter(|x| x.contains("/kwin_wayland "))
101                    .next()
102                    .expect("failed to find kwin_wayland session")
103                    .trim()
104                    .split_once(' ')
105                    .expect("cannot parse `ps`'s output")
106                    .0
107                    .parse()
108                    .expect("cannot parse the pid"),
109                )
110            }
111        }
112    }
113    #[derive(Eq, PartialEq)]
114    /// pointer of workspace
115    pub struct Workspace(KWinPid, *mut c_void);
116    impl Workspace {
117        /// Automatically create a workspace pointer with default offset (might be wrong!) and automatically detected kwin_wayland (may also wrong!).
118        /// Use for test and demo only.
119        ///
120        /// SAFETY: Ensure the WORKSPACE_OFFSET is correct.
121        ///
122        /// require root permissions to calculate the workspace's offset. If the root permission is provided by
123        pub unsafe fn new(search_all_user: bool) -> Self {
124            unsafe { Self::get(KWinPid::search(search_all_user), WORKSPACE_OFFSET) }
125        }
126        /// get workspace from kwin_wayland, the pid should met kwin_wayland's pid, otherwise I cannot tell what happens.
127        /// since it relys on reading "/proc/{pid}/maps", root access might be needed.
128        ///
129        /// It will use an offset that calculated in compile-time, if runtime detect is needed, using `get_with_readelf` instead (executable`readelf` should in enviroment `$PATH`)
130        ///
131        /// require root permissions to calculate the workspace's offset.
132        pub fn get(pid: KWinPid, workspace_offset: usize) -> Self {
133            let mut buffer = String::new();
134
135            // require root permissions
136            File::open(&format!("/proc/{}/maps", pid.0))
137                .unwrap_or_else(|e| panic!("cannot open file (require permissions?)\n{:?}", e))
138                .read_to_string(&mut buffer)
139                .expect("read maps failed");
140            let buffer0 = buffer
141                .split_once("libkwin.so")
142                .expect("program does not load libkwin.so (is it really kwin_wayland?)")
143                .0;
144            let buffer1 = buffer0.rsplit_once('\n').unwrap_or(("", buffer0)).1.trim();
145            // 70642a400000-70642a54a000 r--p 00000000 103:02 3323906                   /usr/lib/libkwin.so.6.1.4
146            let Some((offset, start)) = buffer1.split_once(" r--p ") else {
147                panic!("get offset failed, the buffer line is `{buffer1}`")
148            };
149            assert!(start.trim().starts_with("00000000"));
150            let offset1 = offset.split_once('-').expect("maps format error").0;
151            let base =
152                usize::from_str_radix(offset1, 16).expect("cannot parse to base 16") as *mut c_void;
153            let ret = unsafe { base.byte_add(workspace_offset) };
154            println!("base offset: {base:?}, {ret:?}");
155            Self(pid, ret)
156        }
157        /// using `readelf` to detect the true offset in `path_to_libkwin.so`.
158        ///
159        /// By default, param `readelf` could be str `"readelf"` since the executable `readelf` often in $PATH.
160        /// And set `path_to_libkwin` to `"/usr/lib/libkwin.so"` suits most of the cases.
161        pub fn get_offset_with_readelf(readelf: &str, path_to_libkwin: &str) -> usize {
162            usize::from_str_radix(
163                &String::from_utf8(
164                    Command::new(readelf)
165                        .args(["-WCs", path_to_libkwin])
166                        .output()
167                        .expect("readelf execute failed")
168                        .stdout,
169                )
170                .expect("failed to parse readelf")
171                .split_once(r#"KWin::Workspace::_self"#)
172                .expect("cannot find KWin::Workspace::_self")
173                .0
174                .rsplit_once('\n')
175                .expect("cannot read offset of KWin::Workspace::_self")
176                .1
177                .split_once(':')
178                .expect("parse `:` failed.")
179                .1
180                .trim()
181                .split_once(' ')
182                .expect("cannot parse space")
183                .0,
184                16,
185            )
186            .expect("failed to process readelf.")
187        }
188
189        /// get mouse_pos offset from pointer of workspace.
190        ///
191        /// Due to unsafety of KWinPid, this function is actually unsafe. Caller should ensure the pid is still valid.
192        pub fn get_mouse(&self) -> Mouse {
193            let mut addr: *mut c_void = ptr::null_mut();
194            let local = iovec {
195                iov_base: &mut addr as *mut _ as *mut c_void,
196                iov_len: 8,
197            };
198            let remote = iovec {
199                iov_base: self.1,
200                iov_len: 8,
201            };
202            // SAFETY: As KWinPid suggests, the safety of KWinPid ensure that the pid is valid,
203            //         Since offset is ensured to be valid, the result is safe.
204            match unsafe { process_vm_readv(self.0.0, &local, 1, &remote, 1, 0) } {
205                8 => assert!(!addr.is_null()),
206                -1 => {
207                    eprintln!("failed, check errno for more details.")
208                }
209                x => eprintln!("unknown bytes readed: {x}"),
210            }
211            // SAFETY: the offset is readed by bindgen.
212            Mouse(self.0, unsafe { addr.byte_add(POS_OFFSET) })
213        }
214    }
215    /// pointer of focusMousePos
216    #[derive(Eq, PartialEq)]
217    pub struct Mouse(KWinPid, *mut c_void);
218    impl Mouse {
219        /// read mouse location from kwin workspace (it is read-only object, cannot write back.)
220        pub fn loc(&self) -> (f64, f64) {
221            let mut xy = [0f64; 2];
222            let local = iovec {
223                iov_base: xy.as_mut_ptr() as *mut c_void,
224                iov_len: 16,
225            };
226            let remote = iovec {
227                iov_base: self.1,
228                iov_len: 16,
229            };
230            // SAFETY: If you could read the code, it is safe.
231            //         Otherwise it is very unsafe.
232            match unsafe { process_vm_readv(self.0.0, &local, 1, &remote, 1, 0) } {
233                16 => return (xy[0], xy[1]),
234                -1 => {
235                    eprintln!("failed, check errno for more details.")
236                }
237                x => eprintln!("unknown bytes readed: {x}"),
238            }
239            panic!("reading failed.");
240        }
241    }
242    /// allow print mouse location directly.
243    impl Display for Mouse {
244        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245            std::fmt::Debug::fmt(&self.loc(), f)
246        }
247    }
248}
249
250#[cfg(test)]
251#[cfg(feature = "test")]
252mod test {
253    use crate::*;
254    use consts::WORKSPACE_OFFSET;
255    use pointer::{KWinPid, Workspace};
256    #[test]
257    fn equality() {
258        let w1 = unsafe { Workspace::new(true) }; // most simple way. Note: if use suid and running the program as the wayland user, use Workspace::new(false) could be better.
259
260        let pid = unsafe { KWinPid::search(true) }; // calc pid
261        let offset = Workspace::get_offset_with_readelf("readelf", "/usr/lib/libkwin.so"); // calc offset
262        let w2 = Workspace::get(pid, offset); // get workspace from pid and offset
263        assert!(w1 == w2);
264        assert!(WORKSPACE_OFFSET == offset);
265    }
266    #[test]
267    fn get_loc() {
268        let workspace = unsafe { Workspace::new(true) };
269        let mouse = workspace.get_mouse();
270        println!("{:?} {}", mouse.loc(), mouse);
271    }
272}