Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
verify/clients/src/bin/status_stub.rs (8.2K)
1 // status-stub — a fake status segment for behavioral tests.
2 //
3 // Maps an xdg toplevel with app_id "cce-status-teststub", which the compositor
4 // roles as a StatusBar (any "cce-status*" app_id) and therefore sizes from the
5 // client's own content. It maps EXPANDED_H tall — thicker than any sane
6 // bar_height, so `any_expanded_status_segment` reads it as an open in-surface
7 // menu. It subscribes to the `dismiss` topic on the compositor's status socket
8 // and reacts the way the real bar does: on the first dismiss push it shrinks
9 // to SHRUNK_H (back to a bar strip, menu closed), printing one
10 // "dismiss <payload>" line to stdout per push so a driver script can count
11 // them. Runs until killed (the cce-shadow stop sweep finds it by HOME).
12
13 use std::io::{BufRead, Write};
14 use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
15 use std::sync::mpsc;
16 use wayland_client::{
17 delegate_noop,
18 protocol::{wl_buffer, wl_compositor, wl_registry, wl_shm, wl_shm_pool, wl_surface},
19 Connection, Dispatch, QueueHandle,
20 };
21 use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
22
23 const WIDTH: i32 = 360;
24 const EXPANDED_H: i32 = 400; // > bar_height: reads as an open menu
25 const SHRUNK_H: i32 = 8; // <= bar_height (default 24): plain bar strip
26
27 #[derive(Default)]
28 struct State {
29 compositor: Option<wl_compositor::WlCompositor>,
30 shm: Option<wl_shm::WlShm>,
31 wm_base: Option<xdg_wm_base::XdgWmBase>,
32 configured: bool,
33 closed: bool,
34 }
35
36 impl Dispatch<wl_registry::WlRegistry, ()> for State {
37 fn event(
38 state: &mut Self,
39 registry: &wl_registry::WlRegistry,
40 event: wl_registry::Event,
41 _: &(),
42 _: &Connection,
43 qh: &QueueHandle<Self>,
44 ) {
45 if let wl_registry::Event::Global { name, interface, version } = event {
46 match interface.as_str() {
47 "wl_compositor" => {
48 state.compositor = Some(
49 registry.bind::<wl_compositor::WlCompositor, _, _>(name, version.min(4), qh, ()),
50 );
51 }
52 "wl_shm" => {
53 state.shm = Some(registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ()));
54 }
55 "xdg_wm_base" => {
56 state.wm_base =
57 Some(registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 1, qh, ()));
58 }
59 _ => {}
60 }
61 }
62 }
63 }
64
65 impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
66 fn event(
67 _: &mut Self,
68 wm_base: &xdg_wm_base::XdgWmBase,
69 event: xdg_wm_base::Event,
70 _: &(),
71 _: &Connection,
72 _: &QueueHandle<Self>,
73 ) {
74 if let xdg_wm_base::Event::Ping { serial } = event {
75 wm_base.pong(serial);
76 }
77 }
78 }
79
80 impl Dispatch<xdg_surface::XdgSurface, ()> for State {
81 fn event(
82 state: &mut Self,
83 xdg_surface: &xdg_surface::XdgSurface,
84 event: xdg_surface::Event,
85 _: &(),
86 _: &Connection,
87 _: &QueueHandle<Self>,
88 ) {
89 if let xdg_surface::Event::Configure { serial } = event {
90 xdg_surface.ack_configure(serial);
91 state.configured = true;
92 }
93 }
94 }
95
96 impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
97 fn event(
98 state: &mut Self,
99 _: &xdg_toplevel::XdgToplevel,
100 event: xdg_toplevel::Event,
101 _: &(),
102 _: &Connection,
103 _: &QueueHandle<Self>,
104 ) {
105 // Configure sizes are ignored: a Status window sizes itself.
106 if let xdg_toplevel::Event::Close = event {
107 state.closed = true;
108 }
109 }
110 }
111
112 delegate_noop!(State: ignore wl_compositor::WlCompositor);
113 delegate_noop!(State: ignore wl_shm::WlShm);
114 delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
115 delegate_noop!(State: ignore wl_buffer::WlBuffer);
116 delegate_noop!(State: ignore wl_surface::WlSurface);
117
118 /// One shm pool big enough for the expanded buffer; both buffers share it
119 /// (contents are a solid fill, overlap does not matter).
120 fn make_pool_fd() -> OwnedFd {
121 let size = (WIDTH * 4 * EXPANDED_H) as u64;
122 let fd = unsafe { libc::memfd_create(b"status-stub\0".as_ptr() as *const _, 0) };
123 assert!(fd >= 0, "memfd_create failed");
124 let file = unsafe { std::fs::File::from_raw_fd(fd) };
125 file.set_len(size).unwrap();
126 // Solid opaque slate; any visible pixels will do.
127 let mmapped = unsafe {
128 libc::mmap(
129 std::ptr::null_mut(),
130 size as usize,
131 libc::PROT_WRITE,
132 libc::MAP_SHARED,
133 file.as_raw_fd(),
134 0,
135 )
136 };
137 assert!(mmapped != libc::MAP_FAILED, "mmap failed");
138 unsafe {
139 let px = mmapped as *mut u32;
140 for i in 0..(WIDTH * EXPANDED_H) as usize {
141 *px.add(i) = 0xff30_3a4a;
142 }
143 libc::munmap(mmapped, size as usize);
144 }
145 OwnedFd::from(file)
146 }
147
148 fn spawn_dismiss_listener(tx: mpsc::Sender<()>) {
149 std::thread::spawn(move || {
150 let display = std::env::var("WAYLAND_DISPLAY").expect("WAYLAND_DISPLAY not set");
151 let path = format!("/tmp/cce-status-interface-{}.sock", display);
152 let mut stream = None;
153 for _ in 0..50 {
154 match std::os::unix::net::UnixStream::connect(&path) {
155 Ok(s) => {
156 stream = Some(s);
157 break;
158 }
159 Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)),
160 }
161 }
162 let mut stream = stream.expect("could not connect to status socket");
163 stream.write_all(b"dismiss\n").unwrap();
164 let reader = std::io::BufReader::new(stream);
165 for line in reader.lines() {
166 let line = match line {
167 Ok(l) => l,
168 Err(_) => break,
169 };
170 println!("dismiss {}", line);
171 std::io::stdout().flush().ok();
172 let _ = tx.send(());
173 }
174 });
175 }
176
177 fn main() {
178 let conn = Connection::connect_to_env().expect("connect to wayland display");
179 let display = conn.display();
180 let mut queue = conn.new_event_queue();
181 let qh = queue.handle();
182 let _registry = display.get_registry(&qh, ());
183
184 let mut state = State::default();
185 queue.roundtrip(&mut state).unwrap();
186
187 let compositor = state.compositor.clone().expect("no wl_compositor");
188 let shm = state.shm.clone().expect("no wl_shm");
189 let wm_base = state.wm_base.clone().expect("no xdg_wm_base");
190
191 let surface = compositor.create_surface(&qh, ());
192 let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
193 let toplevel = xdg_surface.get_toplevel(&qh, ());
194 toplevel.set_app_id("cce-status-teststub".into());
195 toplevel.set_title("escape-dismiss-test".into());
196 surface.commit();
197
198 // First configure before the first buffer, per xdg-shell.
199 while !state.configured {
200 queue.blocking_dispatch(&mut state).unwrap();
201 }
202
203 let pool_fd = make_pool_fd();
204 let pool = shm.create_pool(pool_fd.as_fd(), WIDTH * 4 * EXPANDED_H, &qh, ());
205 let tall = pool.create_buffer(
206 0, WIDTH, EXPANDED_H, WIDTH * 4, wl_shm::Format::Argb8888, &qh, (),
207 );
208 let short = pool.create_buffer(
209 0, WIDTH, SHRUNK_H, WIDTH * 4, wl_shm::Format::Argb8888, &qh, (),
210 );
211
212 surface.attach(Some(&tall), 0, 0);
213 surface.damage_buffer(0, 0, WIDTH, EXPANDED_H);
214 surface.commit();
215
216 let (tx, rx) = mpsc::channel();
217 spawn_dismiss_listener(tx);
218
219 let mut shrunk = false;
220 while !state.closed {
221 conn.flush().unwrap();
222 if let Some(guard) = conn.prepare_read() {
223 let mut pfd = libc::pollfd {
224 fd: guard.connection_fd().as_raw_fd(),
225 events: libc::POLLIN,
226 revents: 0,
227 };
228 let n = unsafe { libc::poll(&mut pfd, 1, 100) };
229 if n > 0 && (pfd.revents & libc::POLLIN) != 0 {
230 let _ = guard.read();
231 } else {
232 drop(guard);
233 }
234 }
235 queue.dispatch_pending(&mut state).unwrap();
236
237 if !shrunk && rx.try_recv().is_ok() {
238 surface.attach(Some(&short), 0, 0);
239 surface.damage_buffer(0, 0, WIDTH, SHRUNK_H);
240 surface.commit();
241 shrunk = true;
242 }
243 }
244 }