wo/src/electron.rs
Stephen Hellings b82a9ca9c3 fix: clear IPC socket timeout after handshake, add vblank recovery and render diagnostics
Three bugs addressed:

1. SO_RCVTIMEO(5s) on the IPC socket was never cleared after the HELLO
   handshake. When the web page becomes static (no paint events for >5s),
   the blocking read_exact in recv_message times out, the IPC reader thread
   exits, and the socket is closed — causing ECONNRESET on the Electron
   side and permanently stalling frame delivery. Fix: clear SO_RCVTIMEO
   to zero (infinite) immediately after successful HELLO exchange.

2. If a VBlank event is lost or delayed (e.g. during DRM master transitions),
   vblank_pending stays true and all subsequent render_frame calls bail out
   permanently. Fix: if vblank_pending has been stuck for >500ms, force-clear
   it with a warning so rendering can resume.

   making it impossible to diagnose the black screen from logs. Fix: add
   INFO-level logging for first render, first page flip, first texture
   import, swapchain exhaustion on first frame, and vblank recovery events.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-09 13:08:59 -04:00

1235 lines
54 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// IPC protocol between the Wo compositor and the Electron helper process.
use std::{
os::fd::{FromRawFd, OwnedFd},
os::unix::{
io::{AsRawFd, RawFd},
net::UnixListener,
},
path::Path,
io::{IoSliceMut},
sync::{Arc, Mutex},
collections::HashMap,
};
use anyhow::{bail, Context, Result};
use nix::{
cmsg_space,
sys::{
socket::{recvmsg, send, setsockopt, sockopt::ReceiveTimeout, ControlMessage, ControlMessageOwned, MsgFlags, UnixAddr},
time::TimeVal,
},
unistd,
};
use tokio::sync::mpsc::{self, error::TrySendError};
use tracing::{debug, error, info, warn, trace};
use crate::config::WindowConfig;
pub const MAGIC_HELLO: u32 = 0x574F484C;
pub const MAGIC_FRAME: u32 = 0x574F4652;
pub const MAGIC_MOUSE_MOVE: u32 = 0x574F4D4D;
pub const MAGIC_MOUSE_BUTTON: u32 = 0x574F4D42;
pub const MAGIC_KEYBOARD: u32 = 0x574F4B42;
pub const MAGIC_SCROLL: u32 = 0x574F5343;
pub const MAGIC_ACTION: u32 = 0x574F4341;
pub const MAGIC_FOCUS_CHANGE: u32 = 0x574F4643; // "WOFC"
pub const MAGIC_WINDOW_META: u32 = 0x574F574D; // "WOWM"
pub const MAGIC_WINDOW_POS: u32 = 0x574F5750; // "WOWP"
pub const MAGIC_SYSCALL: u32 = 0x574F5359; // "WOSY" - fixed conflict!
pub const MAGIC_FRAME_ACK: u32 = 0x574F4641; // "WOFA"
pub const MAGIC_SURFACE_BUFFER: u32 = 0x574F5342; // "WOSB" — Wayland surface pixels sent to Electron
pub const MAGIC_SHM_BUFFER: u32 = 0x574F534D; // "WOSM" — Wayland SHM surface sent to Electron via process FD
pub const MAGIC_DMABUF_FRAME: u32 = 0x574F4446; // "WODF" — Wayland surface DMABUF fds sent to Electron
pub const MAGIC_FORWARD_POINTER: u32 = 0x574F5045; // "WOPE"
pub const MAGIC_FORWARD_KEYBOARD: u32 = 0x574F4B45; // "WOKE"
pub const MAGIC_FORWARD_RELATIVE_POINTER: u32 = 0x574F5245; // "WORE"
pub const MAGIC_FORWARD_POINTER_BUTTON: u32 = 0x574F5042; // "WOPB"
pub const MAGIC_FORWARD_POINTER_SCROLL: u32 = 0x574F5053; // "WOPS"
pub const MAGIC_POINTER_LOCK_REQUEST: u32 = 0x574F504C; // "WOPL" — server-to-client pointer lock request
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
pub struct PlaneInfoWire {
pub offset: u32,
pub stride: u32,
pub mod_hi: u32,
pub mod_lo: u32,
}
/// Parsed plane description with the received file descriptor.
#[derive(Debug)]
pub struct PlaneInfo {
pub fd: OwnedFd,
pub offset: u32,
pub stride: u32,
pub modifier: u64,
}
/// A fully parsed frame from an Electron window.
#[derive(Debug)]
pub struct ElectronFrame {
/// Which window this frame belongs to (matches `WindowConfig::name`).
pub name: String,
pub seq: u64,
pub width: u32,
pub height: u32,
pub format: u32, // DRM fourcc
pub planes: Vec<PlaneInfo>,
}
/// Window position update from client
#[derive(Debug, Clone)]
pub struct WindowPositionUpdate {
pub window_name: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
/// Syscall request from client
#[derive(Debug, Clone)]
pub struct SyscallRequest {
pub window_name: String,
pub syscall_type: String, // "exec", "read", "write", etc.
pub payload: String, // JSON payload with request parameters
}
/// Messages that can be received from Electron clients
#[derive(Debug)]
pub enum ElectronMessage {
Frame(ElectronFrame),
WindowPosition(WindowPositionUpdate),
Syscall(SyscallRequest),
Action(CompositorActionMessage),
/// Forwarded pointer motion from web UI canvas (window-local coordinates)
ForwardedPointer { window_name: String, x: f64, y: f64 },
/// Forwarded keyboard event from web UI canvas (evdev keycode)
ForwardedKeyboard { window_name: String, key: u32, pressed: bool, time: u32 },
/// Forwarded relative motion from web UI canvas
ForwardedRelativePointer { window_name: String, dx: f64, dy: f64 },
/// Forwarded pointer button from web UI
ForwardedPointerButton { window_name: String, x: f64, y: f64, button: u32, pressed: bool, time: u32 },
/// Forwarded pointer scroll from web UI
ForwardedPointerScroll { window_name: String, dx: f64, dy: f64 },
}
/// Actions that Electron clients can request from the compositor.
#[derive(Debug, Clone)]
pub enum CompositorAction {
/// Request the compositor to quit
Quit { code: i32 },
/// Custom extension action with JSON payload
Custom { action: String, payload: Option<String> },
}
/// A message sent from an Electron client to the compositor.
#[derive(Debug, Clone)]
pub struct CompositorActionMessage {
pub window_name: String,
pub action: CompositorAction,
}
/// Input event types that can be sent to Electron clients.
#[derive(Debug, Clone)]
pub enum ElectronInputEvent {
MouseMove { x: f64, y: f64 },
MouseButton { button: u32, pressed: bool, time: u32 },
Keyboard { key: u32, pressed: bool, time: u32 },
Scroll { vertical: i32, horizontal: i32, time: u32 },
FocusChange { window_name: String, focused: bool },
WindowMetadata { metadata: String }, // JSON payload
PointerLockRequest { window_name: String, lock: bool },
}
/// A bidirectional connection to an Electron client.
pub struct ElectronClientConnection {
pub name: String,
fd: OwnedFd,
/// Serializes all writes to this socket. The IPC reader thread and the
/// compositor main thread both write ACKs / input events to the same
/// socket; without this lock interleaved `write(2)` calls corrupt the
/// byte stream Electron receives, breaking `FRAME_ACK` parsing and
/// permanently sticking `inFlightFrameSeqs` at the backpressure limit.
write_lock: Mutex<()>,
}
impl ElectronClientConnection {
/// Serialize a raw byte buffer to the socket, holding `write_lock`.
fn write_bytes(&self, buf: &[u8]) -> Result<()> {
let _guard = self.write_lock.lock().unwrap();
write_all(self.fd.as_raw_fd(), buf)
}
/// Non-blocking variant: drops the message silently on EAGAIN.
/// Uses try_lock so the caller never blocks waiting for write_lock.
/// If a concurrent write is in progress the message is dropped,
/// which is acceptable for input events and metadata broadcasts.
fn write_bytes_nonblocking(&self, buf: &[u8]) -> Result<()> {
let _guard = match self.write_lock.try_lock() {
Ok(g) => g,
Err(_) => return Ok(()),
};
write_nonblocking(self.fd.as_raw_fd(), buf)
}
/// Send a FRAME_ACK for `seq` to unblock Electron's inFlight counter.
/// Uses a blocking write: the message is 12 bytes on a local Unix socket so
/// it completes in nanoseconds, and dropping ACKs would permanently stall
/// Electron's backpressure mechanism (inFlightFrameSeqs never decrements).
pub fn write_frame_ack(&self, seq: u64) -> Result<()> {
let mut buf = [0u8; 12];
buf[0..4].copy_from_slice(&MAGIC_FRAME_ACK.to_le_bytes());
buf[4..12].copy_from_slice(&seq.to_le_bytes());
self.write_bytes(&buf)
}
/// Send a Wayland surface's pixel content to the Electron client.
/// Wire format: magic(4) + name_len(4) + name(N) + width(4) + height(4) + stride(4) + data_len(4) + pixels(data_len)
/// Uses non-blocking write — if the socket buffer is full the frame is dropped.
pub fn send_surface_buffer(
&self,
window_name: &str,
width: u32,
height: u32,
stride: u32,
pixels: &[u8],
) -> Result<()> {
let name_bytes = window_name.as_bytes();
let header_len = 4 + 4 + name_bytes.len() + 4 + 4 + 4 + 4;
let mut buf = vec![0u8; header_len + pixels.len()];
let mut off = 0;
buf[off..off + 4].copy_from_slice(&MAGIC_SURFACE_BUFFER.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
off += 4;
buf[off..off + name_bytes.len()].copy_from_slice(name_bytes);
off += name_bytes.len();
buf[off..off + 4].copy_from_slice(&width.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&height.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&stride.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&(pixels.len() as u32).to_le_bytes());
off += 4;
buf[off..].copy_from_slice(pixels);
self.write_bytes_nonblocking(&buf)
}
/// Send a Wayland surface's SHM buffer metadata to the Electron client.
/// Wire format: magic(4) + name_len(4) + name(N) + width(4) + height(4) + stride(4) + pid(4) + fd(4)
/// The compositor PID and fd number let Electron open /proc/<pid>/fd/<fd>
/// to mmap pixels without copying.
pub fn send_shm_buffer(
&self,
window_name: &str,
width: u32,
height: u32,
stride: u32,
pid: u32,
memfd_fd: i32,
) -> Result<()> {
let name_bytes = window_name.as_bytes();
let header_len = 4 + 4 + name_bytes.len() + 4 + 4 + 4 + 4 + 4;
let mut buf = vec![0u8; header_len];
let mut off = 0;
buf[off..off + 4].copy_from_slice(&MAGIC_SHM_BUFFER.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
off += 4;
buf[off..off + name_bytes.len()].copy_from_slice(name_bytes);
off += name_bytes.len();
buf[off..off + 4].copy_from_slice(&width.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&height.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&stride.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&pid.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&(memfd_fd as u32).to_le_bytes());
self.write_bytes_nonblocking(&buf)
}
/// Send a DMABUF-backed Wayland surface frame to the Electron client.
/// Sends the window name, dimensions, format, and plane information as metadata,
/// with the actual file descriptors sent via Unix socket ancillary data (zero-copy).
/// Wire format: magic(4) + name_len(4) + name(N) + width(4) + height(4) + format(4) + num_planes(4) + planes*24
pub fn send_dmabuf_frame(
&self,
window_name: &str,
dmabuf: &smithay::backend::allocator::dmabuf::Dmabuf,
) -> Result<()> {
use smithay::backend::allocator::Buffer as _;
let name_bytes = window_name.as_bytes();
// Get buffer properties
let num_planes = dmabuf.num_planes();
let (w, h) = {
let size = dmabuf.size();
(size.w as u32, size.h as u32)
};
let format = dmabuf.format();
// Build metadata message: magic + name_len + name + width + height + format + num_planes + plane_info
let header_len = 4 + 4 + name_bytes.len() + 4 + 4 + 4 + 4 + (num_planes * 24);
let mut buf = vec![0u8; header_len];
let mut off = 0;
buf[off..off + 4].copy_from_slice(&MAGIC_DMABUF_FRAME.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
off += 4;
buf[off..off + name_bytes.len()].copy_from_slice(name_bytes);
off += name_bytes.len();
// Width and height
buf[off..off + 4].copy_from_slice(&w.to_le_bytes());
off += 4;
buf[off..off + 4].copy_from_slice(&h.to_le_bytes());
off += 4;
let fourcc = format.code as u32;
buf[off..off + 4].copy_from_slice(&fourcc.to_le_bytes());
off += 4;
// Number of planes
buf[off..off + 4].copy_from_slice(&(num_planes as u32).to_le_bytes());
off += 4;
// For each plane: fd(4) + offset(8) + stride(4) + modifier(8)
// Note: We'll collect actual plane data from dmabuf handles iterator
let mut plane_idx = 0;
for _handle in dmabuf.handles() {
// fd: will be sent via ancillary data, so we put 0 as placeholder
buf[off..off + 4].copy_from_slice(&0u32.to_le_bytes());
off += 4;
// offset: 0 for now (can be extended later)
buf[off..off + 8].copy_from_slice(&0u64.to_le_bytes());
off += 8;
// stride: will depend on format (approximate)
let stride = (w * 4) as u32; // ARGB8888 = 4 bytes/pixel, adjust as needed
buf[off..off + 4].copy_from_slice(&stride.to_le_bytes());
off += 4;
// modifier (8 bytes) - DrmModifier converts to u64
let modifier_val: u64 = format.modifier.into();
buf[off..off + 8].copy_from_slice(&modifier_val.to_le_bytes());
off += 8;
plane_idx += 1;
}
// Send metadata with file descriptors as ancillary data
self.sendmsg_with_fds(&buf, dmabuf)
}
/// Send a message with file descriptors via Unix socket ancillary data.
fn sendmsg_with_fds(
&self,
metadata: &[u8],
dmabuf: &smithay::backend::allocator::dmabuf::Dmabuf,
) -> Result<()> {
use std::os::unix::io::AsRawFd;
let _guard = match self.write_lock.try_lock() {
Ok(g) => g,
Err(_) => return Ok(()), // Drop if lock contested
};
// Build ancillary data for file descriptors
let fds: Vec<RawFd> = dmabuf
.handles()
.map(|h| h.as_raw_fd())
.collect();
if fds.is_empty() {
// No FDs to send, just send metadata
return write_nonblocking(self.fd.as_raw_fd(), metadata);
}
// Use nix sendmsg with file descriptor passing via control message
// Convert metadata slice to IoSlice for sendmsg
let iov = [std::io::IoSlice::new(metadata)];
let mut cmsgs = vec![];
cmsgs.push(ControlMessage::ScmRights(&fds[..]));
match nix::sys::socket::sendmsg::<UnixAddr>(
self.fd.as_raw_fd(),
&iov,
&cmsgs,
MsgFlags::MSG_NOSIGNAL,
None,
) {
Ok(_) => Ok(()),
Err(e) => {
debug!("sendmsg failed: {}", e);
Ok(()) // Non-blocking: drop on error
}
}
}
/// Send an input event to the Electron client.
pub fn send_input_event(&self, event: &ElectronInputEvent) -> Result<()> {
match event {
ElectronInputEvent::MouseMove { x, y } => {
let mut buf = [0u8; 20];
buf[0..4].copy_from_slice(&MAGIC_MOUSE_MOVE.to_le_bytes());
buf[4..12].copy_from_slice(&x.to_le_bytes());
buf[12..20].copy_from_slice(&y.to_le_bytes());
self.write_bytes(&buf)?;
},
ElectronInputEvent::MouseButton { button, pressed, time } => {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&MAGIC_MOUSE_BUTTON.to_le_bytes());
buf[4..8].copy_from_slice(&button.to_le_bytes());
buf[8..12].copy_from_slice(&(*pressed as u32).to_le_bytes());
buf[12..16].copy_from_slice(&time.to_le_bytes());
self.write_bytes(&buf)?;
},
ElectronInputEvent::Keyboard { key, pressed, time } => {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&MAGIC_KEYBOARD.to_le_bytes());
buf[4..8].copy_from_slice(&key.to_le_bytes());
buf[8..12].copy_from_slice(&(*pressed as u32).to_le_bytes());
buf[12..16].copy_from_slice(&time.to_le_bytes());
self.write_bytes(&buf)?;
},
ElectronInputEvent::Scroll { vertical, horizontal, time } => {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&MAGIC_SCROLL.to_le_bytes());
buf[4..8].copy_from_slice(&vertical.to_le_bytes());
buf[8..12].copy_from_slice(&horizontal.to_le_bytes());
buf[12..16].copy_from_slice(&time.to_le_bytes());
self.write_bytes(&buf)?;
},
ElectronInputEvent::FocusChange { window_name, focused } => {
let name_bytes = window_name.as_bytes();
let mut buf = vec![0u8; 12 + name_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_FOCUS_CHANGE.to_le_bytes());
buf[4..8].copy_from_slice(&(*focused as u32).to_le_bytes());
buf[8..12].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
buf[12..].copy_from_slice(name_bytes);
self.write_bytes(&buf)?;
},
ElectronInputEvent::WindowMetadata { metadata } => {
let meta_bytes = metadata.as_bytes();
let mut buf = vec![0u8; 8 + meta_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_WINDOW_META.to_le_bytes());
buf[4..8].copy_from_slice(&(meta_bytes.len() as u32).to_le_bytes());
buf[8..].copy_from_slice(meta_bytes);
self.write_bytes(&buf)?;
},
ElectronInputEvent::PointerLockRequest { window_name, lock } => {
let name_bytes = window_name.as_bytes();
let mut buf = vec![0u8; 12 + name_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_POINTER_LOCK_REQUEST.to_le_bytes());
buf[4..8].copy_from_slice(&(*lock as u32).to_le_bytes());
buf[8..12].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
buf[12..].copy_from_slice(name_bytes);
self.write_bytes(&buf)?;
},
}
Ok(())
}
/// Send an input event to the Electron client without blocking.
/// If the socket send buffer is full (EAGAIN), the event is silently
/// dropped — this prevents the compositor main thread from stalling
/// when a client is slow to drain its receive buffer.
pub fn try_send_input_event(&self, event: &ElectronInputEvent) -> Result<()> {
match event {
ElectronInputEvent::MouseMove { x, y } => {
let mut buf = [0u8; 20];
buf[0..4].copy_from_slice(&MAGIC_MOUSE_MOVE.to_le_bytes());
buf[4..12].copy_from_slice(&x.to_le_bytes());
buf[12..20].copy_from_slice(&y.to_le_bytes());
self.write_bytes_nonblocking(&buf)
},
ElectronInputEvent::MouseButton { button, pressed, time } => {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&MAGIC_MOUSE_BUTTON.to_le_bytes());
buf[4..8].copy_from_slice(&button.to_le_bytes());
buf[8..12].copy_from_slice(&(*pressed as u32).to_le_bytes());
buf[12..16].copy_from_slice(&time.to_le_bytes());
self.write_bytes_nonblocking(&buf)
},
ElectronInputEvent::Keyboard { key, pressed, time } => {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&MAGIC_KEYBOARD.to_le_bytes());
buf[4..8].copy_from_slice(&key.to_le_bytes());
buf[8..12].copy_from_slice(&(*pressed as u32).to_le_bytes());
buf[12..16].copy_from_slice(&time.to_le_bytes());
self.write_bytes_nonblocking(&buf)
},
ElectronInputEvent::Scroll { vertical, horizontal, time } => {
let mut buf = [0u8; 16];
buf[0..4].copy_from_slice(&MAGIC_SCROLL.to_le_bytes());
buf[4..8].copy_from_slice(&vertical.to_le_bytes());
buf[8..12].copy_from_slice(&horizontal.to_le_bytes());
buf[12..16].copy_from_slice(&time.to_le_bytes());
self.write_bytes_nonblocking(&buf)
},
ElectronInputEvent::FocusChange { window_name, focused } => {
let name_bytes = window_name.as_bytes();
let mut buf = vec![0u8; 12 + name_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_FOCUS_CHANGE.to_le_bytes());
buf[4..8].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
buf[8..12].copy_from_slice(&(*focused as u32).to_le_bytes());
buf[12..].copy_from_slice(name_bytes);
self.write_bytes_nonblocking(&buf)
},
ElectronInputEvent::WindowMetadata { metadata } => {
let meta_bytes = metadata.as_bytes();
let mut buf = vec![0u8; 8 + meta_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_WINDOW_META.to_le_bytes());
buf[4..8].copy_from_slice(&(meta_bytes.len() as u32).to_le_bytes());
buf[8..].copy_from_slice(meta_bytes);
self.write_bytes_nonblocking(&buf)
},
ElectronInputEvent::PointerLockRequest { window_name, lock } => {
let name_bytes = window_name.as_bytes();
let mut buf = vec![0u8; 12 + name_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_POINTER_LOCK_REQUEST.to_le_bytes());
buf[4..8].copy_from_slice(&(name_bytes.len() as u32).to_le_bytes());
buf[8..12].copy_from_slice(&(*lock as u32).to_le_bytes());
buf[12..].copy_from_slice(name_bytes);
self.write_bytes_nonblocking(&buf)
}
}
}
}
pub struct ElectronProcess {
pub name: String,
pub config: WindowConfig,
pub child: std::process::Child,
}
impl ElectronProcess {
/// Spawn `electron <app-dir>` for the given window config.
/// The app dir is resolved to `<config-dir>/wo-electron`.
///
/// `render_node` is the DRI render node (e.g. `/dev/dri/renderD128`) that
/// matches the GPU the compositor opened. The native `wo_dmabuf.node`
/// module inside Electron uses it to allocate GBM buffers that are
/// importable by the compositor's EGL context.
pub fn spawn(
config: &WindowConfig,
electron_bin: &str,
app_dir: &Path,
ipc_socket: &str,
render_node: &str,
) -> Result<Self> {
use std::process::{Command, Stdio};
tracing::info!("Spawning Electron for window '{}' with config: {:?}", config.name, config);
let serialized = serde_json::to_string(config)
.context("serializing window config")?;
tracing::info!("Serialized config: {}", serialized);
let main_js = app_dir.join("dist/main.js");
let child = Command::new(electron_bin)
.arg(&main_js)
.env("WO_IPC_SOCKET", ipc_socket)
.env("WO_WINDOW_CONFIG", &serialized)
.env("WO_DRM_RENDER_NODE", render_node)
// Headless Ozone backend: Electron is used purely for offscreen
// rendering — it must not try to connect to a Wayland or X11
// display server (there is none for it to connect to; *we* are
// the compositor). The headless platform lets Chromium's OSR
// paint pipeline work while the native wo_dmabuf module handles
// GPU buffer allocation via its own GBM/EGL context on the
// render node.
.arg("--ozone-platform=headless")
.arg("--no-sandbox")
.arg("--disable-gpu-sandbox")
// Use the *software* GL implementation inside Chromium for its
// own compositing (we do not need hardware-accelerated web
// content — the paint callback gives us BGRA bitmaps that the
// native module imports into a GBM BO). This avoids Chromium
// fighting with the compositor over the DRM render node.
.arg("--use-gl=swiftshader")
// Make sure OSR (offscreen) rendering actually produces
// paint callbacks at the requested frame rate.
.arg("--enable-features=Vulkan")
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("spawning electron for window '{}'", config.name))?;
info!(window = %config.name, pid = child.id(), render_node, "spawned Electron process");
Ok(Self {
name: config.name.clone(),
config: config.clone(),
child,
})
}
pub fn kill(&mut self) {
let _ = self.child.kill();
}
}
impl Drop for ElectronProcess {
fn drop(&mut self) {
self.kill();
}
}
pub struct ElectronIpc {
pub tx: mpsc::Sender<ElectronMessage>,
/// Map of window name -> client connection for sending input events.
pub clients: Arc<Mutex<HashMap<String, Arc<ElectronClientConnection>>>>,
}
impl ElectronIpc {
/// Bind the socket and start accepting connections.
pub fn listen(socket_path: &str) -> Result<(Self, mpsc::Receiver<ElectronMessage>)> {
// Remove stale socket file.
let _ = std::fs::remove_file(socket_path);
let listener = UnixListener::bind(socket_path)
.with_context(|| format!("binding IPC socket {socket_path}"))?;
info!(socket = socket_path, "IPC socket listening");
let (tx, rx) = mpsc::channel(8);
let tx2 = tx.clone();
let clients = Arc::new(Mutex::new(HashMap::new()));
let clients2 = clients.clone();
std::thread::spawn(move || {
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let tx = tx2.clone();
let clients = clients2.clone();
std::thread::spawn(move || {
if let Err(e) = handle_connection(stream.as_raw_fd(), tx, clients) {
error!("electron IPC error: {e:#}");
}
});
}
Err(e) => error!("IPC accept error: {e}"),
}
}
});
Ok((Self { tx, clients }, rx))
}
/// Send input event to a specific window (non-blocking).
/// If the socket buffer is full or the write lock is contended, the event
/// is silently dropped. This prevents the compositor main thread from
/// stalling when an Electron client is slow to drain its receive buffer.
pub fn send_to_window(&self, window_name: &str, event: &ElectronInputEvent) -> Result<()> {
let client = {
let clients = self.clients.lock().unwrap();
clients.get(window_name).cloned()
};
// clients Mutex released — safe to do I/O without blocking other IPC ops.
if let Some(client) = client {
client.try_send_input_event(event)?;
}
Ok(())
}
/// Broadcast window metadata to all windows
pub fn broadcast_metadata(&self, metadata: &str) -> Result<()> {
let snapshot: Vec<_> = {
let clients = self.clients.lock().unwrap();
clients.values().cloned().collect()
};
// clients Mutex released — safe to do I/O.
let event = ElectronInputEvent::WindowMetadata {
metadata: metadata.to_string(),
};
for client in &snapshot {
if let Err(e) = client.try_send_input_event(&event) {
warn!("metadata broadcast to '{}' failed (stream may be corrupted): {e:#}", client.name);
}
}
Ok(())
}
/// Send syscall response to a window (non-blocking).
/// If the socket buffer is full, the response is dropped and a warning is
/// logged. This is preferable to blocking the compositor event loop — the
/// Electron client will time out its IPC promise rather than deadlocking
/// the entire compositor.
pub fn send_syscall_response(&self, window_name: &str, response: &str) -> Result<()> {
let client = {
let clients = self.clients.lock().unwrap();
clients.get(window_name).cloned()
};
// clients Mutex released — safe to do I/O.
if let Some(client) = client {
let response_bytes = response.as_bytes();
let mut buf = vec![0u8; 8 + response_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_SYSCALL.to_le_bytes());
buf[4..8].copy_from_slice(&(response_bytes.len() as u32).to_le_bytes());
buf[8..].copy_from_slice(response_bytes);
if let Err(e) = client.write_bytes_nonblocking(&buf) {
warn!("syscall response to '{}' dropped (socket full): {e:#}", client.name);
}
}
Ok(())
}
/// Acknowledge that a frame was consumed/imported so the sender can apply backpressure.
pub fn send_frame_ack(&self, window_name: &str, seq: u64) -> Result<()> {
let client = {
let clients = self.clients.lock().unwrap();
clients.get(window_name).cloned()
};
// clients Mutex released — safe to do I/O.
// ACKs are only 12 bytes; a blocking write is acceptable here since
// it completes in nanoseconds on a local socket and dropping ACKs
// would permanently stall Electron's backpressure.
if let Some(client) = client {
client.write_frame_ack(seq)?;
}
Ok(())
}
/// Broadcast a Wayland surface's pixel content to all Electron clients.
/// Non-blocking: drops the buffer for clients that can't keep up.
pub fn broadcast_surface_buffer(
&self,
window_name: &str,
width: u32,
height: u32,
stride: u32,
pixels: &[u8],
) -> Result<()> {
let snapshot: Vec<_> = {
let clients = self.clients.lock().unwrap();
clients.values().cloned().collect()
};
// clients Mutex released — safe to do I/O.
for client in &snapshot {
if let Err(e) = client.send_surface_buffer(window_name, width, height, stride, pixels) {
debug!("surface buffer to '{}' dropped: {e:#}", client.name);
}
}
Ok(())
}
/// Broadcast a Wayland surface's SHM buffer metadata to all Electron clients.
/// Non-blocking: drops the buffer for clients that can't keep up.
pub fn broadcast_shm_buffer(
&self,
window_name: &str,
width: u32,
height: u32,
stride: u32,
pid: u32,
memfd_fd: i32,
) -> Result<()> {
let snapshot: Vec<_> = {
let clients = self.clients.lock().unwrap();
clients.values().cloned().collect()
};
// clients Mutex released — safe to do I/O.
for client in &snapshot {
if let Err(e) = client.send_shm_buffer(window_name, width, height, stride, pid, memfd_fd) {
debug!("shm buffer to '{}' dropped: {e:#}", client.name);
}
}
Ok(())
}
/// Broadcast a Wayland surface's DMABUF frame to all Electron clients.
/// Sends file descriptors via Unix socket ancillary data (zero-copy).
/// Non-blocking: drops the frame for clients that can't keep up.
pub fn broadcast_dmabuf_frame(
&self,
window_name: &str,
dmabuf: &smithay::backend::allocator::dmabuf::Dmabuf,
) -> Result<()> {
let snapshot: Vec<_> = {
let clients = self.clients.lock().unwrap();
clients.values().cloned().collect()
};
// clients Mutex released — safe to do I/O.
for client in &snapshot {
if let Err(e) = client.send_dmabuf_frame(window_name, dmabuf) {
debug!("dmabuf frame to '{}' dropped: {e:#}", client.name);
}
}
Ok(())
}
}
fn handle_connection(
fd: RawFd,
tx: mpsc::Sender<ElectronMessage>,
clients: Arc<Mutex<HashMap<String, Arc<ElectronClientConnection>>>>,
) -> Result<()> {
// Set a 5-second receive timeout before any blocking read so the thread
// cannot hang indefinitely if Electron connects but stalls mid-handshake.
{
let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
let timeout = TimeVal::new(5, 0);
if let Err(e) = setsockopt(&borrowed, ReceiveTimeout, &timeout) {
warn!("Failed to set SO_RCVTIMEO on IPC socket: {e}");
}
}
// 1. Read HelloMessage
let mut buf = [0u8; 4096];
let name = recv_hello(fd, &mut buf)?;
info!(window = %name, "Electron window connected");
let fd_for_write = {
let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
unistd::dup(borrowed).context("duplicating IPC socket fd")?
};
// Register this client connection for input event dispatch
let client = Arc::new(ElectronClientConnection {
name: name.clone(),
fd: fd_for_write,
write_lock: Mutex::new(()),
});
clients.lock().unwrap().insert(name.clone(), client.clone());
// Clear the handshake-only SO_RCVTIMEO so the reader thread blocks
// indefinitely between messages. A static web page may produce no paint
// events for many seconds; a 5-second timeout would incorrectly kill the
// connection during these normal idle periods.
{
let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
let no_timeout = TimeVal::new(0, 0);
if let Err(e) = setsockopt(&borrowed, ReceiveTimeout, &no_timeout) {
warn!("Failed to clear SO_RCVTIMEO on IPC socket: {e}");
}
}
// 2. Stream Messages (frames, syscalls, positions, etc.)
let mut msg_count = 0u32;
loop {
match recv_message(fd, &name, &mut buf) {
Ok(msg) => {
msg_count += 1;
trace!(window = %name, msg_num = msg_count, "received IPC message");
match msg {
ElectronMessage::Frame(frame) => {
let seq = frame.seq;
debug!(window = %name, seq, "sending FRAME_ACK");
if let Err(e) = client.write_frame_ack(seq) {
error!(window = %name, seq, "failed to send FRAME_ACK: {e:#}");
}
match tx.try_send(ElectronMessage::Frame(frame)) {
Ok(()) => {
trace!(window = %name, "frame queued for compositor");
}
Err(TrySendError::Full(_)) => {
debug!(window = %name, seq, "dropping frame (channel full, already ACKed)");
}
Err(TrySendError::Closed(_)) => {
warn!(window = %name, "compositor channel closed, exiting IPC reader");
break;
}
}
}
_ => {
// Non-frame messages (WindowPosition, Syscall, Action): use try_send
// to avoid blocking the IPC reader thread. If the channel is full,
// the message is dropped — non-critical messages will arrive again
// on the next event (positions, actions), or are processed
// asynchronously anyway.
match tx.try_send(msg) {
Ok(()) => {
trace!(window = %name, "non-frame message queued");
}
Err(TrySendError::Full(_)) => {
debug!(window = %name, "dropping non-frame IPC message (channel full)");
}
Err(TrySendError::Closed(_)) => {
warn!(window = %name, "compositor channel closed, exiting IPC reader");
break;
}
}
}
}
}
Err(e) => {
// Connection closed = normal Electron exit, but warn so it's visible.
warn!(window = %name, msg_count, "IPC connection closed: {e}");
break;
}
}
}
// Clean up client registration on disconnect
clients.lock().unwrap().remove(&name);
info!(window = %name, msg_count, "IPC reader thread exiting");
Ok(())
}
fn recv_message(fd: RawFd, name: &str, buf: &mut [u8]) -> Result<ElectronMessage> {
// Read magic to determine message type
read_exact(fd, &mut buf[..4]).context("reading message magic")?;
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
trace!("recv_message: got magic 0x{:08X}", magic);
match magic {
MAGIC_FRAME => {
trace!("recv_message: parsing FRAME message");
// Read header: magic(4) + name_len(4)
read_exact(fd, &mut buf[4..8]).context("reading FRAME name_len")?;
let name_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if name_len > 256 { bail!("window name too long in FRAME: {name_len}"); }
// Read the window name
read_exact(fd, &mut buf[8..8+name_len]).context("reading FRAME window name")?;
let frame_name = String::from_utf8_lossy(&buf[8..8+name_len]).into_owned();
// Rest of frame header: seq(8) + width(4) + height(4) + format(4) + num_planes(4)
const REST_HEADER: usize = 8 + 4 + 4 + 4 + 4;
read_exact(fd, &mut buf[..REST_HEADER]).context("reading FRAME header fields")?;
let frame = parse_frame_message(fd, &frame_name, buf)
.context("parsing FRAME message data")?;
trace!("recv_message: FRAME parsed successfully: {}x{}", frame.width, frame.height);
Ok(ElectronMessage::Frame(frame))
}
MAGIC_WINDOW_POS => {
trace!("recv_message: parsing WINDOW_POS message");
// Window position: magic(4) + name_len(4) + name(N) + x(4) + y(4) + width(4) + height(4)
read_exact(fd, &mut buf[4..8]).context("reading WINDOW_POS name_len")?;
let name_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if name_len > 256 { bail!("window name too long in WINDOW_POS: {name_len}"); }
let mut pos_buf = vec![0u8; name_len + 16];
read_exact(fd, &mut pos_buf).context("reading WINDOW_POS data")?;
let x = i32::from_le_bytes(pos_buf[name_len..name_len+4].try_into().unwrap());
let y = i32::from_le_bytes(pos_buf[name_len+4..name_len+8].try_into().unwrap());
let width = u32::from_le_bytes(pos_buf[name_len+8..name_len+12].try_into().unwrap());
let height = u32::from_le_bytes(pos_buf[name_len+12..name_len+16].try_into().unwrap());
Ok(ElectronMessage::WindowPosition(WindowPositionUpdate {
window_name: name.to_string(),
x, y, width, height,
}))
}
MAGIC_SYSCALL => {
trace!("recv_message: parsing SYSCALL message");
// Syscall: magic(4) + type_len(4) + payload_len(4) + type_str + payload_str
read_exact(fd, &mut buf[4..12]).context("reading SYSCALL header")?;
let type_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
let payload_len = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize;
if type_len > 256 || payload_len > 1024 * 1024 {
bail!("syscall message too large: type_len={} payload_len={}", type_len, payload_len);
}
read_exact(fd, &mut buf[..type_len]).context("reading SYSCALL type")?;
let syscall_type = String::from_utf8_lossy(&buf[..type_len]).into_owned();
let mut payload_buf = vec![0u8; payload_len];
read_exact(fd, &mut payload_buf).context("reading SYSCALL payload")?;
let payload = String::from_utf8_lossy(&payload_buf).into_owned();
Ok(ElectronMessage::Syscall(SyscallRequest {
window_name: name.to_string(),
syscall_type,
payload,
}))
}
MAGIC_ACTION => {
trace!("recv_message: parsing ACTION message");
// Action: magic(4) + action_len(4) + action_str(action_len) + payload_len(4) + payload_str
read_exact(fd, &mut buf[4..8]).context("reading ACTION action_len")?;
let action_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if action_len > 256 {
bail!("action string too large: action_len={}", action_len);
}
read_exact(fd, &mut buf[..action_len]).context("reading ACTION string")?;
let action = String::from_utf8_lossy(&buf[..action_len]).into_owned();
// Now read payload_len
read_exact(fd, &mut buf[..4]).context("reading ACTION payload_len")?;
let payload_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
if payload_len > 1024 * 1024 {
bail!("action payload too large: payload_len={}", payload_len);
}
let payload = if payload_len > 0 {
let mut payload_buf = vec![0u8; payload_len];
read_exact(fd, &mut payload_buf).context("reading ACTION payload")?;
Some(String::from_utf8_lossy(&payload_buf).into_owned())
} else {
None
};
let compositor_action = if action == "quit" {
let code = payload.as_ref()
.and_then(|p| {
// Payload may be JSON like {"code":0} or a plain integer
if let Ok(v) = serde_json::from_str::<serde_json::Value>(p) {
v.get("code").and_then(|c| c.as_i64()).map(|c| c as i32)
} else {
p.parse::<i32>().ok()
}
})
.unwrap_or(0);
CompositorAction::Quit { code }
} else {
CompositorAction::Custom { action, payload }
};
Ok(ElectronMessage::Action(CompositorActionMessage {
window_name: name.to_string(),
action: compositor_action,
}))
}
MAGIC_FORWARD_POINTER => {
trace!("recv_message: parsing FORWARD_POINTER message");
read_exact(fd, &mut buf[4..8]).context("reading FORWARD_POINTER window_len")?;
let window_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if window_len > 256 { bail!("window name too long in FORWARD_POINTER: {window_len}"); }
let mut data_buf = vec![0u8; window_len + 16];
read_exact(fd, &mut data_buf).context("reading FORWARD_POINTER data")?;
let window_name = String::from_utf8_lossy(&data_buf[..window_len]).into_owned();
let x = f64::from_le_bytes(data_buf[window_len..window_len+8].try_into().unwrap());
let y = f64::from_le_bytes(data_buf[window_len+8..window_len+16].try_into().unwrap());
Ok(ElectronMessage::ForwardedPointer { window_name, x, y })
}
MAGIC_FORWARD_KEYBOARD => {
trace!("recv_message: parsing FORWARD_KEYBOARD message");
read_exact(fd, &mut buf[4..8]).context("reading FORWARD_KEYBOARD window_len")?;
let window_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if window_len > 256 { bail!("window name too long in FORWARD_KEYBOARD: {window_len}"); }
let mut data_buf = vec![0u8; window_len + 12];
read_exact(fd, &mut data_buf).context("reading FORWARD_KEYBOARD data")?;
let window_name = String::from_utf8_lossy(&data_buf[..window_len]).into_owned();
let key = u32::from_le_bytes(data_buf[window_len..window_len+4].try_into().unwrap());
let pressed = u32::from_le_bytes(data_buf[window_len+4..window_len+8].try_into().unwrap()) != 0;
let time = u32::from_le_bytes(data_buf[window_len+8..window_len+12].try_into().unwrap());
Ok(ElectronMessage::ForwardedKeyboard { window_name, key, pressed, time })
}
MAGIC_FORWARD_RELATIVE_POINTER => {
trace!("recv_message: parsing FORWARD_RELATIVE_POINTER message");
read_exact(fd, &mut buf[4..8]).context("reading FORWARD_RELATIVE_POINTER window_len")?;
let window_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if window_len > 256 { bail!("window name too long in FORWARD_RELATIVE_POINTER: {window_len}"); }
let mut data_buf = vec![0u8; window_len + 16];
read_exact(fd, &mut data_buf).context("reading FORWARD_RELATIVE_POINTER data")?;
let window_name = String::from_utf8_lossy(&data_buf[..window_len]).into_owned();
let dx = f64::from_le_bytes(data_buf[window_len..window_len+8].try_into().unwrap());
let dy = f64::from_le_bytes(data_buf[window_len+8..window_len+16].try_into().unwrap());
Ok(ElectronMessage::ForwardedRelativePointer { window_name, dx, dy })
}
MAGIC_FORWARD_POINTER_BUTTON => {
trace!("recv_message: parsing FORWARD_POINTER_BUTTON message");
read_exact(fd, &mut buf[4..8]).context("reading FORWARD_POINTER_BUTTON window_len")?;
let window_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if window_len > 256 { bail!("window name too long in FORWARD_POINTER_BUTTON: {window_len}"); }
let mut data_buf = vec![0u8; window_len + 28];
read_exact(fd, &mut data_buf).context("reading FORWARD_POINTER_BUTTON data")?;
let window_name = String::from_utf8_lossy(&data_buf[..window_len]).into_owned();
let x = f64::from_le_bytes(data_buf[window_len..window_len+8].try_into().unwrap());
let y = f64::from_le_bytes(data_buf[window_len+8..window_len+16].try_into().unwrap());
let button = u32::from_le_bytes(data_buf[window_len+16..window_len+20].try_into().unwrap());
let pressed = u32::from_le_bytes(data_buf[window_len+20..window_len+24].try_into().unwrap()) != 0;
let time = u32::from_le_bytes(data_buf[window_len+24..window_len+28].try_into().unwrap());
Ok(ElectronMessage::ForwardedPointerButton { window_name, x, y, button, pressed, time })
}
MAGIC_FORWARD_POINTER_SCROLL => {
trace!("recv_message: parsing FORWARD_POINTER_SCROLL message");
read_exact(fd, &mut buf[4..8]).context("reading FORWARD_POINTER_SCROLL window_len")?;
let window_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if window_len > 256 { bail!("window name too long in FORWARD_POINTER_SCROLL: {window_len}"); }
let mut data_buf = vec![0u8; window_len + 16];
read_exact(fd, &mut data_buf).context("reading FORWARD_POINTER_SCROLL data")?;
let window_name = String::from_utf8_lossy(&data_buf[..window_len]).into_owned();
let dx = f64::from_le_bytes(data_buf[window_len..window_len+8].try_into().unwrap());
let dy = f64::from_le_bytes(data_buf[window_len+8..window_len+16].try_into().unwrap());
Ok(ElectronMessage::ForwardedPointerScroll { window_name, dx, dy })
}
_ => {
error!("recv_message: unknown magic 0x{:08X}", magic);
bail!("unknown message magic: 0x{magic:08X}");
}
}
}
fn parse_frame_message(fd: RawFd, name: &str, buf: &mut [u8]) -> Result<ElectronFrame> {
// Buffer contains: seq(8) + width(4) + height(4) + format(4) + num_planes(4)
let seq = u64::from_le_bytes(buf[0..8].try_into().unwrap());
let width = u32::from_le_bytes(buf[8..12].try_into().unwrap());
let height = u32::from_le_bytes(buf[12..16].try_into().unwrap());
let format = u32::from_le_bytes(buf[16..20].try_into().unwrap());
let num_planes = u32::from_le_bytes(buf[20..24].try_into().unwrap()) as usize;
if num_planes == 0 || num_planes > 4 {
bail!("invalid num_planes: {num_planes}");
}
// Read PlaneInfoWire structs (16 bytes each) + SCM_RIGHTS fds.
const PLANE_SIZE: usize = 16;
let plane_bytes = num_planes * PLANE_SIZE;
read_exact(fd, &mut buf[..plane_bytes])?;
// Receive file descriptors via recvmsg SCM_RIGHTS.
let fds = recv_fds(fd, num_planes)?;
if fds.len() != num_planes {
bail!("expected {num_planes} fds, got {}", fds.len());
}
let mut planes = Vec::with_capacity(num_planes);
for (i, fd) in fds.into_iter().enumerate() {
let base = i * PLANE_SIZE;
let offset = u32::from_le_bytes(buf[base..base+4].try_into().unwrap());
let stride = u32::from_le_bytes(buf[base+4..base+8].try_into().unwrap());
let mod_hi = u32::from_le_bytes(buf[base+8..base+12].try_into().unwrap());
let mod_lo = u32::from_le_bytes(buf[base+12..base+16].try_into().unwrap());
let modifier = ((mod_hi as u64) << 32) | mod_lo as u64;
planes.push(PlaneInfo { fd, offset, stride, modifier });
}
Ok(ElectronFrame {
name: name.to_string(),
seq, width, height, format, planes,
})
}
fn read_exact(fd: RawFd, buf: &mut [u8]) -> Result<()> {
use std::os::unix::io::FromRawFd;
use std::io::Read;
// SAFETY: fd is valid for the lifetime of this call.
let mut f = unsafe { std::fs::File::from_raw_fd(fd) };
let r = f.read_exact(buf);
std::mem::forget(f); // don't close fd
r.context("IPC read")
}
fn write_all(fd: RawFd, buf: &[u8]) -> Result<()> {
use std::os::unix::io::FromRawFd;
use std::io::Write;
// SAFETY: fd is valid for the lifetime of this call.
let mut f = unsafe { std::fs::File::from_raw_fd(fd) };
let r = f.write_all(buf);
std::mem::forget(f); // don't close fd
r.context("IPC write")
}
/// Non-blocking socket write: sends `buf` only if the entire message fits in the
/// kernel send buffer without blocking. If there is not enough space, the message
/// is silently dropped. This prevents both:
/// 1. Blocking the compositor main thread on a slow IPC client.
/// 2. Partial writes that would corrupt the framed byte stream — on SOCK_STREAM a
/// partial `send(MSG_DONTWAIT)` writes some bytes and returns n < buf.len(),
/// leaving the receiver with a truncated message. All subsequent messages
/// (including FRAME_ACKs) then misalign, causing Electron's inFlightFrameSeqs
/// counter to stall at its limit and stopping all frame delivery.
///
/// Strategy: query the number of bytes already queued in the kernel send buffer
/// via TIOCOUTQ. Compare against SO_SNDBUF. Only call send() when the full
/// message provably fits, ensuring atomic all-or-nothing delivery.
fn write_nonblocking(fd: RawFd, buf: &[u8]) -> Result<()> {
// Query bytes currently waiting in the kernel send buffer (unread by receiver).
let mut queued: libc::c_int = 0;
let ioctl_ret = unsafe { libc::ioctl(fd, libc::TIOCOUTQ, &mut queued as *mut libc::c_int) };
if ioctl_ret == -1 {
// If TIOCOUTQ is unavailable (shouldn't happen on Linux), fall back to a
// plain MSG_DONTWAIT send and treat partial writes as drops.
return match send(fd, buf, MsgFlags::MSG_DONTWAIT) {
Ok(n) if n == buf.len() => Ok(()),
Ok(_) => Ok(()), // partial write — drop silently, stream may be corrupt
Err(nix::errno::Errno::EAGAIN) | Err(nix::errno::Errno::EWOULDBLOCK) => Ok(()),
Err(e) => Err(anyhow::anyhow!("IPC non-blocking write: {e}")),
};
}
// SO_SNDBUF: Linux reports 2× the requested value; use it directly as a
// conservative upper bound for available send-buffer capacity.
let sndbuf = nix::sys::socket::getsockopt(
&unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) },
nix::sys::socket::sockopt::SndBuf,
)
.unwrap_or(212992) as libc::c_int; // 212992 is the Linux default SO_SNDBUF
let available = sndbuf.saturating_sub(queued) as usize;
if available < buf.len() {
// Not enough room — drop this non-critical message to avoid a partial write.
return Ok(());
}
match send(fd, buf, MsgFlags::MSG_DONTWAIT) {
Ok(n) if n == buf.len() => Ok(()),
// With the space check above a partial write should not happen, but handle
// it defensively: drop and signal the caller so it can log a warning.
Ok(n) => Err(anyhow::anyhow!(
"IPC partial non-blocking write: {n}/{} bytes sent despite space check",
buf.len()
)),
Err(nix::errno::Errno::EAGAIN) | Err(nix::errno::Errno::EWOULDBLOCK) => Ok(()),
Err(e) => Err(anyhow::anyhow!("IPC non-blocking write: {e}")),
}
}
fn recv_hello(fd: RawFd, buf: &mut [u8]) -> Result<String> {
// magic(4) + name_len(4)
read_exact(fd, &mut buf[..8])?;
let magic = u32::from_le_bytes(buf[0..4].try_into().unwrap());
if magic != MAGIC_HELLO {
bail!("bad hello magic: 0x{magic:08X}");
}
let name_len = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
if name_len > 256 {
bail!("name too long: {name_len}");
}
read_exact(fd, &mut buf[..name_len])?;
// Save name before overwriting the buffer with the width/height fields below.
let name = String::from_utf8_lossy(&buf[..name_len]).into_owned();
// width(4) + height(4) – consume but ignore (comes from config)
read_exact(fd, &mut buf[..8])?;
Ok(name)
}
fn recv_fds(sock_fd: RawFd, count: usize) -> Result<Vec<OwnedFd>> {
let mut cmsg_buf = cmsg_space!([RawFd; 4]);
let mut iov_buf = [0u8; 1];
let mut iov = [IoSliceMut::new(&mut iov_buf[..])];
let msg = recvmsg::<UnixAddr>(sock_fd, &mut iov, Some(&mut cmsg_buf), MsgFlags::empty())
.context("recvmsg for fds")?;
let mut fds: Vec<OwnedFd> = Vec::new();
for cmsg in msg.cmsgs().context("iterating control messages")? {
if let ControlMessageOwned::ScmRights(received) = cmsg {
for fd in received {
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
fds.push(owned);
}
}
}
if fds.len() < count {
bail!("received {} fds, need {}", fds.len(), count);
}
fds.truncate(count);
Ok(fds)
}