git.lucas.co / cce-compositor
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 }