Open History to see the last commit
1use super::*;
2use std::{
3 io::{BufRead, BufReader},
4 process::{Child, Command, Stdio},
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicBool, AtomicUsize, Ordering},
8 },
9};
10
11#[derive(Debug, zbus::DBusError)]
12#[zbus(prefix = "org.kde.KWin.ScreenShot2.Error")]
13enum CaptureError {
14 NoAuthorized(String),
15 InvalidWindow(String),
16}
17
18#[derive(Default)]
19struct State {
20 denied: AtomicBool,
21 change_identity: AtomicBool,
22 spin_title: AtomicBool,
23 captures: AtomicUsize,
24 unloaded: AtomicUsize,
25 script: Mutex<String>,
26 captured_ids: Mutex<Vec<String>>,
27}
28struct Screenshots(Arc<State>);
29#[zbus::interface(name = "org.kde.KWin.ScreenShot2")]
30impl Screenshots {
31 #[zbus(property)]
32 fn version(&self) -> u32 {
33 5
34 }
35
36 fn capture_window(
37 &self,
38 handle: &str,
39 _options: HashMap<String, OwnedValue>,
40 pipe: Fd<'_>,
41 ) -> std::result::Result<HashMap<String, OwnedValue>, CaptureError> {
42 if self.0.denied.load(Ordering::SeqCst) {
43 return Err(CaptureError::NoAuthorized("Denied".into()));
44 }
45 if handle != "{window-1}" {
46 return Err(CaptureError::InvalidWindow("Not a window".into()));
47 }
48 self.0.captured_ids.lock().unwrap().push(handle.into());
49 self.write(pipe)
50 }
51 fn capture_active_window(
52 &self,
53 _options: HashMap<String, OwnedValue>,
54 pipe: Fd<'_>,
55 ) -> std::result::Result<HashMap<String, OwnedValue>, CaptureError> {
56 if self.0.denied.load(Ordering::SeqCst) {
57 return Err(CaptureError::NoAuthorized("Denied".into()));
58 }
59 self.write(pipe)
60 }
61}
62impl Screenshots {
63 fn write(
64 &self,
65 pipe: Fd<'_>,
66 ) -> std::result::Result<HashMap<String, OwnedValue>, CaptureError> {
67 self.0.captures.fetch_add(1, Ordering::SeqCst);
68 let fd = pipe.as_fd().try_clone_to_owned().unwrap();
69 std::thread::spawn(move || {
70 let mut file = std::fs::File::from(fd);
71 file.write_all(&[0xff112233_u32.to_ne_bytes(), 0xff445566_u32.to_ne_bytes()].concat())
72 .unwrap();
73 });
74 Ok(HashMap::from([
75 ("type".into(), Value::from("raw").try_to_owned().unwrap()),
76 ("width".into(), 2_u32.into()),
77 ("height".into(), 1_u32.into()),
78 ("stride".into(), 8_u32.into()),
79 ("format".into(), 5_u32.into()),
80 ]))
81 }
82}
83
84struct Scripting(Arc<State>);
85#[zbus::interface(name = "org.kde.kwin.Scripting")]
86impl Scripting {
87 #[zbus(name = "loadScript")]
88 fn load_script(&self, path: &str, _name: &str) -> i32 {
89 *self.0.script.lock().unwrap() = std::fs::read_to_string(path).unwrap();
90 1
91 }
92 #[zbus(name = "unloadScript")]
93 fn unload_script(&self, _name: &str) -> bool {
94 self.0.unloaded.fetch_add(1, Ordering::SeqCst);
95 true
96 }
97}
98struct LoadedScript(Arc<State>);
99#[zbus::interface(name = "org.kde.kwin.Script")]
100impl LoadedScript {
101 #[zbus(name = "run")]
102 async fn run(&self, #[zbus(connection)] connection: &zbus::Connection) {
103 let source = self.0.script.lock().unwrap().clone();
104 let destination = source
105 .split("callDBus(")
106 .nth(1)
107 .unwrap()
108 .split(',')
109 .next()
110 .unwrap();
111 let destination: String = serde_json::from_str(destination).unwrap();
112 let changed = self.0.change_identity.load(Ordering::SeqCst)
113 && self.0.captures.load(Ordering::SeqCst) > 0;
114 let title = if changed {
115 "New document"
116 } else if self.0.spin_title.load(Ordering::SeqCst) {
117 if self.0.captures.load(Ordering::SeqCst) > 0 {
118 "⠙ Document"
119 } else {
120 "⠋ Document"
121 }
122 } else {
123 "Document"
124 };
125 let value = serde_json::json!({"id": "{window-1}", "title": title, "appName": "Kate", "appIdentifier": "org.kde.kate", "processId": 123, "bounds": {"x": 0, "y": 0, "width": 800, "height": 600}, "clientBounds": {"x": 0, "y": 29, "width": 800, "height": 571}}).to_string();
126 connection
127 .call_method(
128 Some(destination.as_str()),
129 "/com/t3tools/KdeCapture",
130 Some("com.t3tools.KdeCapture"),
131 "Reply",
132 &(value,),
133 )
134 .await
135 .unwrap();
136 }
137}
138
139pub(super) struct Fixture {
140 daemon: Child,
141 directory: std::path::PathBuf,
142 pub(super) _server: Connection,
143 pub(super) client: Connection,
144 pub(super) address: String,
145 state: Arc<State>,
146}
147impl Fixture {
148 pub(super) fn new() -> Self {
149 // mktemp provides a private directory; no predictable paths or desktop bus env changes.
150 let output = Command::new("mktemp")
151 .args(["-d", "/tmp/t3-kde-bus-XXXXXX"])
152 .output()
153 .unwrap();
154 assert!(output.status.success());
155 let directory = std::path::PathBuf::from(String::from_utf8(output.stdout).unwrap().trim());
156 let mut daemon = Command::new("dbus-daemon")
157 .args([
158 "--session",
159 "--nofork",
160 "--nopidfile",
161 "--print-address",
162 &format!("--address=unix:path={}/bus", directory.display()),
163 ])
164 .stdout(Stdio::piped())
165 .spawn()
166 .unwrap();
167 let mut address = String::new();
168 BufReader::new(daemon.stdout.take().unwrap())
169 .read_line(&mut address)
170 .unwrap();
171 let state = Arc::new(State::default());
172 let server = Builder::address(address.trim())
173 .unwrap()
174 .name(SCREENSHOT)
175 .unwrap()
176 .name("org.kde.KWin")
177 .unwrap()
178 .serve_at("/org/kde/KWin/ScreenShot2", Screenshots(state.clone()))
179 .unwrap()
180 .serve_at("/Scripting", Scripting(state.clone()))
181 .unwrap()
182 .serve_at("/Scripting/Script1", LoadedScript(state.clone()))
183 .unwrap()
184 .build()
185 .unwrap();
186 let client = Builder::address(address.trim())
187 .unwrap()
188 .method_timeout(DEADLINE)
189 .build()
190 .unwrap();
191 Self {
192 daemon,
193 directory,
194 _server: server,
195 client,
196 address: address.trim().to_owned(),
197 state,
198 }
199 }
200}
201impl Drop for Fixture {
202 fn drop(&mut self) {
203 let _ = self.daemon.kill();
204 let _ = self.daemon.wait();
205 let _ = std::fs::remove_dir_all(&self.directory);
206 }
207}
208
209#[test]
210fn permission_probe_never_captures_and_rejects_denial() {
211 let f = Fixture::new();
212 check(&f.client).unwrap();
213 assert_eq!(f.state.captures.load(Ordering::SeqCst), 0);
214 f.state.denied.store(true, Ordering::SeqCst);
215 assert!(
216 check(&f.client)
217 .unwrap_err()
218 .to_string()
219 .contains("NoAuthorized")
220 );
221 assert!(capture(&f.client, &f.directory).is_err());
222 assert_eq!(f.state.captures.load(Ordering::SeqCst), 0);
223}
224
225#[test]
226fn captures_pinned_window_through_real_fd_and_cleans_scripts() {
227 let f = Fixture::new();
228 let identity = capture(&f.client, &f.directory).unwrap().unwrap();
229 assert_eq!(identity.process_id, 123);
230 assert_eq!(
231 identity.client_bounds,
232 Bounds {
233 x: 0,
234 y: 29,
235 width: 800,
236 height: 571
237 }
238 );
239 assert_eq!(*f.state.captured_ids.lock().unwrap(), ["{window-1}"]);
240 assert_eq!(f.state.unloaded.load(Ordering::SeqCst), 2);
241 assert!(!f.directory.join("window.js").exists());
242 let mut decoder = png::Decoder::new(BufReader::new(
243 std::fs::File::open(f.directory.join("capture.png")).unwrap(),
244 ))
245 .read_info()
246 .unwrap();
247 let mut pixels = vec![0; decoder.output_buffer_size().unwrap()];
248 decoder.next_frame(&mut pixels).unwrap();
249 assert_eq!(pixels, [0x11, 0x22, 0x33, 255, 0x44, 0x55, 0x66, 255]);
250 let png = std::fs::read(f.directory.join("capture.png")).unwrap();
251 let idat = png.windows(4).position(|tag| tag == b"IDAT").unwrap();
252 // zlib FLEVEL 1 selects levels 2-5; the slow default level 6 reports 2.
253 assert_eq!(png[idat + 5] >> 6, 1);
254}
255
256#[test]
257fn omits_identity_when_window_changes_during_capture() {
258 let f = Fixture::new();
259 f.state.change_identity.store(true, Ordering::SeqCst);
260 assert!(capture(&f.client, &f.directory).unwrap().is_none());
261 assert!(f.directory.join("capture.png").exists());
262}
263
264#[test]
265fn keeps_captured_identity_when_only_the_title_spinner_changes() {
266 let f = Fixture::new();
267 f.state.spin_title.store(true, Ordering::SeqCst);
268 let identity = capture(&f.client, &f.directory).unwrap().unwrap();
269 assert_eq!(identity.title, "⠋ Document");
270 assert_eq!(identity.process_id, 123);
271 assert_eq!(*f.state.captured_ids.lock().unwrap(), ["{window-1}"]);
272 assert_eq!(f.state.unloaded.load(Ordering::SeqCst), 2);
273}