Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Implement wake-up pipe to make IPC command execution instant and thread-safe
src/ipc_server.rs | 13 +++--
src/main.rs | 139 ++++++++++++++++++++++++++++++++++--------------------
2 files changed, 97 insertions(+), 55 deletions(-)
diff --git a/src/ipc_server.rs b/src/ipc_server.rs
index 4a07a07..3969dc4 100644
--- a/src/ipc_server.rs
+++ b/src/ipc_server.rs
@@ -8,20 +8,20 @@ pub struct IpcReceiver {
pub rx: mpsc::Receiver<String>,
}
-pub fn spawn_ipc_server() -> IpcReceiver {
+pub fn spawn_ipc_server(pipe_write: libc::c_int) -> IpcReceiver {
let (tx, rx) = mpsc::channel::<String>();
std::thread::Builder::new()
.name("clearwm-ipc".into())
.spawn(move || {
- ipc_server_main(tx);
+ ipc_server_main(tx, pipe_write);
})
.expect("failed to spawn IPC server thread");
IpcReceiver { rx }
}
-fn ipc_server_main(tx: mpsc::Sender<String>) {
+fn ipc_server_main(tx: mpsc::Sender<String>, pipe_write: libc::c_int) {
let _ = std::fs::remove_file(IPC_SOCKET_PATH);
let listener = match UnixListener::bind(IPC_SOCKET_PATH) {
@@ -69,10 +69,17 @@ fn ipc_server_main(tx: mpsc::Sender<String>) {
}
Ok(n) => {
let s = String::from_utf8_lossy(&buf[..n]);
+ let mut sent = false;
for line in s.lines() {
let cmd = line.trim().to_string();
if !cmd.is_empty() {
let _ = tx.send(cmd);
+ sent = true;
+ }
+ }
+ if sent {
+ unsafe {
+ libc::write(pipe_write, &1u8 as *const u8 as *const libc::c_void, 1);
}
}
}
diff --git a/src/main.rs b/src/main.rs
index e655e8d..947aef0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -87,8 +87,16 @@ fn main() {
}
};
+ // Create a pipe to wake up the main loop when IPC commands arrive
+ let mut pipe_fds = [0; 2];
+ unsafe {
+ libc::pipe(pipe_fds.as_mut_ptr());
+ }
+ let pipe_read = pipe_fds[0];
+ let pipe_write = pipe_fds[1];
+
// Start the IPC server thread (for clearctl and clear-system-interface)
- let ipc_rx = ipc_server::spawn_ipc_server().rx;
+ let ipc_rx = ipc_server::spawn_ipc_server(pipe_write).rx;
// Store the status sender in the app state so RenderStart can push updates
state.status_sender = Some(status_sender);
@@ -136,7 +144,7 @@ fn main() {
// Flush any queued requests from config loading (bindings, etc.)
eprintln!("[init] flushed, entering main loop");
- // Main loop — tinyrwm pattern: just blocking_dispatch in a loop.
+ // Main loop — using poll to block on both Wayland socket and IPC wake-up pipe.
// All work (including spawning) happens inside Dispatch callbacks.
let mut loop_count: u64 = 0;
loop {
@@ -144,64 +152,91 @@ fn main() {
if loop_count % 10000 == 0 {
eprintln!("[main] loop iteration {}", loop_count);
}
- match event_queue.blocking_dispatch(&mut state) {
- Ok(n) => {
- eprintln!(
- "[main] blocking_dispatch returned Ok({}), about to flush",
- n
- );
- // Explicitly flush after every dispatch cycle.
- // blocking_dispatch only flushes when dispatched==0 (before blocking read),
- // so if events were dispatched, pending requests like manage_finish
- // and render_finish stay in the buffer until the next cycle.
- // Flushing here ensures River receives our responses promptly.
- if let Err(e) = event_queue.flush() {
- log_death(&format!("flush error after dispatch: {:?}", e));
- let _ = std::fs::copy("/tmp/clearwm.log", "/tmp/clearwm-prev.log");
- if !state.wm.exit_requested {
- restart::wm_restart();
- }
- break;
- }
- eprintln!("[main] flush ok, looping");
-
- // Process pending IPC commands from the socket
- let mut ipc_commands = false;
- loop {
- match ipc_rx.try_recv() {
- Ok(cmd) => {
- eprintln!("[main] IPC command: {}", cmd);
- ipc::handle_ipc_command(&cmd, &mut state.wm);
- ipc_commands = true;
- }
- Err(std::sync::mpsc::TryRecvError::Empty) => break,
- Err(std::sync::mpsc::TryRecvError::Disconnected) => {
- eprintln!("[main] IPC server disconnected");
- break;
- }
- }
+
+ // 1. Dispatch any already pending events in the queue
+ let _ = event_queue.dispatch_pending(&mut state);
+
+ // 2. Flush outgoing requests to the compositor
+ if let Err(e) = event_queue.flush() {
+ log_death(&format!("flush error: {:?}", e));
+ let _ = std::fs::copy("/tmp/clearwm.log", "/tmp/clearwm-prev.log");
+ if !state.wm.exit_requested {
+ restart::wm_restart();
+ }
+ break;
+ }
+
+ // 3. Prepare to read Wayland events
+ let read_guard = match event_queue.prepare_read() {
+ Some(g) => g,
+ None => {
+ // If None, events are already in the queue, dispatch them immediately
+ let _ = event_queue.dispatch_pending(&mut state);
+ continue;
+ }
+ };
+
+ // 4. Poll both the Wayland display FD and the IPC wake-up pipe
+ use std::os::fd::{AsFd, AsRawFd};
+ let wl_fd = event_queue.as_fd().as_raw_fd();
+ let mut poll_fds = [
+ nix::poll::PollFd::new(unsafe { std::os::fd::BorrowedFd::borrow_raw(wl_fd) }, nix::poll::PollFlags::POLLIN),
+ nix::poll::PollFd::new(unsafe { std::os::fd::BorrowedFd::borrow_raw(pipe_read) }, nix::poll::PollFlags::POLLIN),
+ ];
+
+ match nix::poll::poll(&mut poll_fds, nix::poll::PollTimeout::NONE) {
+ Ok(_) => {
+ // If Wayland FD is readable, read the events
+ if poll_fds[0].revents().unwrap_or(nix::poll::PollFlags::empty()).contains(nix::poll::PollFlags::POLLIN) {
+ let _ = read_guard.read();
+ } else {
+ // Otherwise drop the read guard to release the lock
+ std::mem::drop(read_guard);
}
- // Force a render sequence so the new rendering state (border
- // colors, widths, etc.) is sent to River and displayed.
- if ipc_commands {
- if let Some(ref wm) = state.window_manager {
- wm.manage_dirty();
+
+ // If IPC pipe is readable, drain it
+ if poll_fds[1].revents().unwrap_or(nix::poll::PollFlags::empty()).contains(nix::poll::PollFlags::POLLIN) {
+ let mut buf = [0u8; 128];
+ unsafe {
+ libc::read(pipe_read, buf.as_mut_ptr() as *mut libc::c_void, buf.len());
}
}
}
Err(e) => {
- log_death(&format!(
- "wayland dispatch error: {:?}\n exit_requested={} wm.exit_requested={}",
- e, state.exit_requested, state.wm.exit_requested
- ));
- // Save log before restart overwrites it
- let _ = std::fs::copy("/tmp/clearwm.log", "/tmp/clearwm-prev.log");
- if !state.wm.exit_requested {
- restart::wm_restart();
+ std::mem::drop(read_guard);
+ if e != nix::errno::Errno::EINTR {
+ log_death(&format!("poll error: {:?}", e));
+ break;
+ }
+ }
+ }
+
+ // 5. Dispatch read events
+ let _ = event_queue.dispatch_pending(&mut state);
+
+ // 6. Process pending IPC commands from the socket channel
+ let mut ipc_commands = false;
+ loop {
+ match ipc_rx.try_recv() {
+ Ok(cmd) => {
+ eprintln!("[main] IPC command: {}", cmd);
+ ipc::handle_ipc_command(&cmd, &mut state.wm);
+ ipc_commands = true;
+ }
+ Err(std::sync::mpsc::TryRecvError::Empty) => break,
+ Err(std::sync::mpsc::TryRecvError::Disconnected) => {
+ eprintln!("[main] IPC server disconnected");
+ break;
}
- break;
}
}
+ // Force a render sequence if we processed IPC commands
+ if ipc_commands {
+ if let Some(ref wm) = state.window_manager {
+ wm.manage_dirty();
+ }
+ }
+
if state.exit_requested || state.wm.exit_requested {
log_death(&format!(
"main loop exit: exit_requested={} wm.exit_requested={}",