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}