Initial commit: phases 1-3 du portage Wayland Rust pour Redox OS
Plan directeur 14 phases / 5 ans (REDOX_COSMIC_XWAYLAND_RS_PLAN.md).
Phase 1 — Audit Redox (docs/existing-redox-gui.md, 486 lignes) :
- Orbital, graphics-ipc (API DRM compatible Linux subset KMS), inputd, vesad
- relibc support : AF_UNIX, SCM_RIGHTS, shm_open, mmap, poll
- 3 manques identifiés : memfd_create, keymap XKB, AT-SPI
Phase 2 — Validation primitives sur Redox via redoxer (5 tests + 1 POC) :
- test-unix-socket : SOCK_STREAM Wayland-shaped roundtrip
- test-fd-passing : SCM_RIGHTS mono-process (artefact kernel)
- test-fd-passing-fork : SCM_RIGHTS multi-process (validation Wayland critique)
- test-shm-open : shm_open + mmap + persistance + unlink
- test-poll-multifd : poll() multiplexing + POLLHUP
- poc-pixels : datapath shm + SCM_RIGHTS bout en bout (10000 pixels ARGB)
Phase 3 — wayland-rs sur Redox (compile + runtime) :
- wayland-{scanner,backend,server,client} compilent pour x86_64-unknown-redox
sans patch upstream (rustix supporte Redox via libc backend)
- test-handshake : server/client wl_registry handshake roundtrip
- test-shm-pipeline : pipeline complet (ListeningSocket Unix réel + fd passing
via wl_shm.create_pool + wl_shm_pool + wl_buffer + wl_surface + commit +
serveur lit pixels via fd reçu, validation pixel-perfect)
Verdict phase 3 : wayland-rs upstream est viable sur Redox out-of-the-box,
le port "Wayland sur Redox" est désormais un problème de compositor à écrire,
pas de stack à porter.
Prérequis build : redoxer (pas cargo direct, car CMSG_NXTHDR/CMSG_DATA
ne sont pas linkés autrement vers librelibc.a).
Leyoda 2026 – GPLv3
This commit is contained in:
commit
53e6626231
21 changed files with 3676 additions and 0 deletions
7
crates/redox-wl-poc-pixels/Cargo.toml
Normal file
7
crates/redox-wl-poc-pixels/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "redox-wl-poc-pixels"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
310
crates/redox-wl-poc-pixels/src/main.rs
Normal file
310
crates/redox-wl-poc-pixels/src/main.rs
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
//! POC — preuve par les pixels.
|
||||
//!
|
||||
//! Démontre concrètement que le datapath Wayland fonctionne sur Redox :
|
||||
//! - un CLIENT crée une zone shm, dessine un pattern ARGB déterministe
|
||||
//! - il envoie le fd au SERVEUR via Unix socket + SCM_RIGHTS
|
||||
//! - le SERVEUR mappe la zone, vérifie chaque pixel, écrit un PPM
|
||||
//!
|
||||
//! C'est exactement ce que fait wl_shm + wl_buffer entre un client Wayland
|
||||
//! et son compositor, sans le protocole Wayland lui-même. Si ce POC passe,
|
||||
//! la phase 3 (port wayland-rs) peut démarrer en confiance.
|
||||
//!
|
||||
//! Mono-binaire qui fork() : le child fait le client, le parent le serveur.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io::{self, Write};
|
||||
use std::mem::{self, MaybeUninit};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::process::ExitCode;
|
||||
use std::ptr;
|
||||
|
||||
const W: usize = 200;
|
||||
const H: usize = 200;
|
||||
const PIXEL_BYTES: usize = 4;
|
||||
const SHM_BYTES: usize = W * H * PIXEL_BYTES;
|
||||
const SHM_NAME: &str = "/redox-wl-poc-pixels";
|
||||
const PPM_OUTPUT: &str = "poc-shm-pixels-readback.ppm";
|
||||
|
||||
/// Pattern ARGB8888 déterministe : channel A = 0xFF, R = x, G = y, B = (x+y) mod 256.
|
||||
fn expected_pixel(x: usize, y: usize) -> u32 {
|
||||
let a: u32 = 0xFF;
|
||||
let r: u32 = (x as u32) & 0xFF;
|
||||
let g: u32 = (y as u32) & 0xFF;
|
||||
let b: u32 = ((x + y) as u32) & 0xFF;
|
||||
(a << 24) | (r << 16) | (g << 8) | b
|
||||
}
|
||||
|
||||
fn errno_str() -> String {
|
||||
let e = io::Error::last_os_error();
|
||||
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
|
||||
}
|
||||
|
||||
unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> {
|
||||
let mut fds: [RawFd; 2] = [-1, -1];
|
||||
if libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) != 0 {
|
||||
return Err(format!("socketpair: {}", errno_str()));
|
||||
}
|
||||
Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
|
||||
}
|
||||
|
||||
unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> {
|
||||
let mut iov_buf = [b'P'; 1];
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: iov_buf.as_mut_ptr() as *mut _,
|
||||
iov_len: 1,
|
||||
};
|
||||
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
|
||||
let mut cmsg_buf = vec![0u8; cmsg_space];
|
||||
let mut msg: libc::msghdr = mem::zeroed();
|
||||
msg.msg_iov = &mut iov as *mut _;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
|
||||
msg.msg_controllen = cmsg_space as _;
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
(*cmsg).cmsg_level = libc::SOL_SOCKET;
|
||||
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
|
||||
(*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
|
||||
let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd;
|
||||
ptr::write_unaligned(data_ptr, fd_to_send);
|
||||
|
||||
let n = libc::sendmsg(socket, &msg, 0);
|
||||
if n < 0 {
|
||||
return Err(format!("sendmsg: {}", errno_str()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
|
||||
let mut iov_buf = [0u8; 1];
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: iov_buf.as_mut_ptr() as *mut _,
|
||||
iov_len: 1,
|
||||
};
|
||||
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
|
||||
let mut cmsg_buf = vec![MaybeUninit::<u8>::uninit(); cmsg_space];
|
||||
let mut msg: libc::msghdr = mem::zeroed();
|
||||
msg.msg_iov = &mut iov as *mut _;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
|
||||
msg.msg_controllen = cmsg_space as _;
|
||||
|
||||
let n = libc::recvmsg(socket, &mut msg, 0);
|
||||
if n < 0 {
|
||||
return Err(format!("recvmsg: {}", errno_str()));
|
||||
}
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
if cmsg.is_null() {
|
||||
return Err("no cmsg received".into());
|
||||
}
|
||||
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
|
||||
return Err("unexpected cmsg type".into());
|
||||
}
|
||||
let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
|
||||
let received_fd = ptr::read_unaligned(data_ptr);
|
||||
Ok(OwnedFd::from_raw_fd(received_fd))
|
||||
}
|
||||
|
||||
unsafe fn shm_create_and_map(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), String> {
|
||||
let cname = CString::new(name).unwrap();
|
||||
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600);
|
||||
if fd < 0 {
|
||||
return Err(format!("shm_open: {}", errno_str()));
|
||||
}
|
||||
if libc::ftruncate(fd, size as _) != 0 {
|
||||
libc::close(fd);
|
||||
return Err(format!("ftruncate: {}", errno_str()));
|
||||
}
|
||||
let p = libc::mmap(
|
||||
ptr::null_mut(),
|
||||
size,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
if p == libc::MAP_FAILED {
|
||||
libc::close(fd);
|
||||
return Err(format!("mmap (create): {}", errno_str()));
|
||||
}
|
||||
Ok((OwnedFd::from_raw_fd(fd), p as *mut u8))
|
||||
}
|
||||
|
||||
unsafe fn mmap_existing(fd: RawFd, size: usize) -> Result<*mut u8, String> {
|
||||
let p = libc::mmap(
|
||||
ptr::null_mut(),
|
||||
size,
|
||||
libc::PROT_READ,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
if p == libc::MAP_FAILED {
|
||||
return Err(format!("mmap (recv): {}", errno_str()));
|
||||
}
|
||||
Ok(p as *mut u8)
|
||||
}
|
||||
|
||||
unsafe fn child_main(child_sock: OwnedFd) -> ! {
|
||||
eprintln!("[poc CLIENT pid={}] start", libc::getpid());
|
||||
|
||||
// Crée la zone shm
|
||||
let (shm_fd, map) = match shm_create_and_map(SHM_NAME, SHM_BYTES) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[poc CLIENT] FAIL shm_create_and_map: {e}");
|
||||
libc::_exit(2);
|
||||
}
|
||||
};
|
||||
eprintln!(
|
||||
"[poc CLIENT] shm '{}' size={} mapped at {:p} fd={}",
|
||||
SHM_NAME, SHM_BYTES, map, shm_fd.as_raw_fd()
|
||||
);
|
||||
|
||||
// Dessine le pattern ARGB déterministe
|
||||
let pixels = std::slice::from_raw_parts_mut(map as *mut u32, W * H);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
pixels[y * W + x] = expected_pixel(x, y);
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"[poc CLIENT] painted {}x{} ARGB pattern (a=0xFF, r=x, g=y, b=(x+y)&0xFF)",
|
||||
W, H
|
||||
);
|
||||
|
||||
// Envoie le fd au serveur
|
||||
if let Err(e) = send_fd_over(child_sock.as_raw_fd(), shm_fd.as_raw_fd()) {
|
||||
eprintln!("[poc CLIENT] FAIL sendmsg: {e}");
|
||||
libc::_exit(3);
|
||||
}
|
||||
eprintln!("[poc CLIENT] sent shm fd via SCM_RIGHTS");
|
||||
|
||||
// On peut munmap + close côté client : le serveur a son propre ref via SCM_RIGHTS
|
||||
libc::munmap(map as *mut _, SHM_BYTES);
|
||||
drop(shm_fd);
|
||||
|
||||
eprintln!("[poc CLIENT] done, exit 0");
|
||||
libc::_exit(0);
|
||||
}
|
||||
|
||||
unsafe fn parent_main(parent_sock: OwnedFd, child_pid: libc::pid_t) -> Result<(), String> {
|
||||
eprintln!("[poc SERVER pid={}] waiting for fd from child {}", libc::getpid(), child_pid);
|
||||
|
||||
let received = recv_fd_from(parent_sock.as_raw_fd())?;
|
||||
let recv_raw = received.as_raw_fd();
|
||||
eprintln!("[poc SERVER] received fd={recv_raw}");
|
||||
|
||||
let map = mmap_existing(recv_raw, SHM_BYTES)?;
|
||||
eprintln!("[poc SERVER] mmap'd received fd at {:p}", map);
|
||||
|
||||
// Vérifie chaque pixel
|
||||
let pixels = std::slice::from_raw_parts(map as *const u32, W * H);
|
||||
let mut mismatches: Vec<(usize, usize, u32, u32)> = Vec::new();
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let got = pixels[y * W + x];
|
||||
let want = expected_pixel(x, y);
|
||||
if got != want {
|
||||
if mismatches.len() < 5 {
|
||||
mismatches.push((x, y, got, want));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !mismatches.is_empty() {
|
||||
for (x, y, got, want) in &mismatches {
|
||||
eprintln!(
|
||||
"[poc SERVER] MISMATCH at ({x},{y}): got {got:#010x} want {want:#010x}"
|
||||
);
|
||||
}
|
||||
return Err(format!("{} pixel mismatches over {}", mismatches.len(), W * H));
|
||||
}
|
||||
eprintln!(
|
||||
"[poc SERVER] all {} pixels match expected pattern",
|
||||
W * H
|
||||
);
|
||||
|
||||
// Bonus : dump en PPM pour visualisation (P6 binaire RGB)
|
||||
match write_ppm(PPM_OUTPUT, pixels, W, H) {
|
||||
Ok(()) => eprintln!("[poc SERVER] PPM dump written to {PPM_OUTPUT}"),
|
||||
Err(e) => eprintln!("[poc SERVER] WARN: PPM dump failed: {e}"),
|
||||
}
|
||||
|
||||
libc::munmap(map as *mut _, SHM_BYTES);
|
||||
drop(received);
|
||||
|
||||
// Reap child
|
||||
let mut status: libc::c_int = 0;
|
||||
let r = libc::waitpid(child_pid, &mut status, 0);
|
||||
if r < 0 {
|
||||
return Err(format!("waitpid: {}", errno_str()));
|
||||
}
|
||||
if !libc::WIFEXITED(status) || libc::WEXITSTATUS(status) != 0 {
|
||||
return Err(format!(
|
||||
"child exited abnormally: status={status}"
|
||||
));
|
||||
}
|
||||
eprintln!("[poc SERVER] child reaped cleanly");
|
||||
|
||||
// Cleanup shm
|
||||
let cname = CString::new(SHM_NAME).unwrap();
|
||||
libc::shm_unlink(cname.as_ptr());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_ppm(path: &str, pixels: &[u32], w: usize, h: usize) -> io::Result<()> {
|
||||
let mut f = std::fs::File::create(path)?;
|
||||
write!(f, "P6\n{w} {h}\n255\n")?;
|
||||
let mut row = vec![0u8; w * 3];
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let argb = pixels[y * w + x];
|
||||
let r = ((argb >> 16) & 0xFF) as u8;
|
||||
let g = ((argb >> 8) & 0xFF) as u8;
|
||||
let b = (argb & 0xFF) as u8;
|
||||
row[x * 3] = r;
|
||||
row[x * 3 + 1] = g;
|
||||
row[x * 3 + 2] = b;
|
||||
}
|
||||
f.write_all(&row)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
println!(
|
||||
"[poc] preuve par les pixels: client {}x{} ARGB → SCM_RIGHTS → serveur",
|
||||
W, H
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let (sock_parent, sock_child) = make_socketpair()?;
|
||||
|
||||
let pid = libc::fork();
|
||||
if pid < 0 {
|
||||
return Err(format!("fork: {}", errno_str()));
|
||||
}
|
||||
if pid == 0 {
|
||||
drop(sock_parent);
|
||||
child_main(sock_child);
|
||||
}
|
||||
|
||||
drop(sock_child);
|
||||
parent_main(sock_parent, pid)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[poc] PASS: full Wayland-like datapath validated on Redox");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[poc] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/redox-wl-test-fd-passing-fork/Cargo.toml
Normal file
7
crates/redox-wl-test-fd-passing-fork/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "redox-wl-test-fd-passing-fork"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
215
crates/redox-wl-test-fd-passing-fork/src/main.rs
Normal file
215
crates/redox-wl-test-fd-passing-fork/src/main.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
//! Test 2b — fd passing via SCM_RIGHTS, multi-process via fork().
|
||||
//!
|
||||
//! Mono-process test (test-02) revealed the kernel reuses fd numbers and
|
||||
//! does not duplicate the file table entry. This test validates whether
|
||||
//! SCM_RIGHTS works correctly *across* a process boundary, which is the
|
||||
//! actual Wayland use case.
|
||||
//!
|
||||
//! Flow:
|
||||
//! 1. Parent opens a tmp file with a marker
|
||||
//! 2. socketpair(AF_UNIX, SOCK_STREAM)
|
||||
//! 3. fork()
|
||||
//! 4. Parent: closes child end, sendmsg with the tmp_fd
|
||||
//! 5. Child: closes parent end, recvmsg, reads via received fd, exits with status 0 if marker matches
|
||||
//! 6. Parent: waitpid, returns child's exit status
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::mem::{self, MaybeUninit};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::process::ExitCode;
|
||||
use std::ptr;
|
||||
|
||||
const MARKER: &[u8] = b"REDOX-FORK-FD-PASSING-OK-2026";
|
||||
|
||||
fn errno_str() -> String {
|
||||
let e = io::Error::last_os_error();
|
||||
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
|
||||
}
|
||||
|
||||
unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> {
|
||||
let mut fds: [RawFd; 2] = [-1, -1];
|
||||
if libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) != 0 {
|
||||
return Err(format!("socketpair: {}", errno_str()));
|
||||
}
|
||||
Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
|
||||
}
|
||||
|
||||
unsafe fn open_tmp_with_marker() -> Result<OwnedFd, String> {
|
||||
let path = CString::new("/tmp/test-02b-fd-passing-fork.bin").unwrap();
|
||||
let fd = libc::open(
|
||||
path.as_ptr(),
|
||||
libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC,
|
||||
0o600,
|
||||
);
|
||||
if fd < 0 {
|
||||
return Err(format!("open tmp: {}", errno_str()));
|
||||
}
|
||||
let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len());
|
||||
if n != MARKER.len() as isize {
|
||||
return Err(format!("write tmp: {}", errno_str()));
|
||||
}
|
||||
libc::lseek(fd, 0, libc::SEEK_SET);
|
||||
Ok(OwnedFd::from_raw_fd(fd))
|
||||
}
|
||||
|
||||
unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> {
|
||||
let mut iov_buf = [b'X'; 1];
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: iov_buf.as_mut_ptr() as *mut _,
|
||||
iov_len: 1,
|
||||
};
|
||||
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
|
||||
let mut cmsg_buf = vec![0u8; cmsg_space];
|
||||
let mut msg: libc::msghdr = mem::zeroed();
|
||||
msg.msg_iov = &mut iov as *mut _;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
|
||||
msg.msg_controllen = cmsg_space as _;
|
||||
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
(*cmsg).cmsg_level = libc::SOL_SOCKET;
|
||||
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
|
||||
(*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
|
||||
let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd;
|
||||
ptr::write_unaligned(data_ptr, fd_to_send);
|
||||
|
||||
let n = libc::sendmsg(socket, &msg, 0);
|
||||
if n < 0 {
|
||||
return Err(format!("sendmsg: {}", errno_str()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
|
||||
let mut iov_buf = [0u8; 1];
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: iov_buf.as_mut_ptr() as *mut _,
|
||||
iov_len: 1,
|
||||
};
|
||||
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
|
||||
let mut cmsg_buf = vec![MaybeUninit::<u8>::uninit(); cmsg_space];
|
||||
let mut msg: libc::msghdr = mem::zeroed();
|
||||
msg.msg_iov = &mut iov as *mut _;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
|
||||
msg.msg_controllen = cmsg_space as _;
|
||||
|
||||
let n = libc::recvmsg(socket, &mut msg, 0);
|
||||
if n < 0 {
|
||||
return Err(format!("recvmsg: {}", errno_str()));
|
||||
}
|
||||
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
if cmsg.is_null() {
|
||||
return Err("no cmsg".into());
|
||||
}
|
||||
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
|
||||
return Err(format!(
|
||||
"unexpected cmsg level={} type={}",
|
||||
(*cmsg).cmsg_level,
|
||||
(*cmsg).cmsg_type
|
||||
));
|
||||
}
|
||||
let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
|
||||
let received_fd = ptr::read_unaligned(data_ptr);
|
||||
Ok(OwnedFd::from_raw_fd(received_fd))
|
||||
}
|
||||
|
||||
unsafe fn child_main(child_sock: OwnedFd) -> ! {
|
||||
let raw_sock = child_sock.as_raw_fd();
|
||||
eprintln!("[test-02b CHILD pid={}] recvmsg on sock {}", libc::getpid(), raw_sock);
|
||||
let received = match recv_fd_from(raw_sock) {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => {
|
||||
eprintln!("[test-02b CHILD] recvmsg fail: {e}");
|
||||
libc::_exit(2);
|
||||
}
|
||||
};
|
||||
let recv_raw = received.as_raw_fd();
|
||||
eprintln!("[test-02b CHILD] received fd={recv_raw}");
|
||||
|
||||
libc::lseek(recv_raw, 0, libc::SEEK_SET);
|
||||
let mut readback = vec![0u8; MARKER.len()];
|
||||
let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len());
|
||||
if n != MARKER.len() as isize {
|
||||
eprintln!(
|
||||
"[test-02b CHILD] read returned {n}, errno={}",
|
||||
io::Error::last_os_error().raw_os_error().unwrap_or(0)
|
||||
);
|
||||
libc::_exit(3);
|
||||
}
|
||||
if readback != MARKER {
|
||||
eprintln!(
|
||||
"[test-02b CHILD] marker mismatch: {:?}",
|
||||
String::from_utf8_lossy(&readback)
|
||||
);
|
||||
libc::_exit(4);
|
||||
}
|
||||
eprintln!("[test-02b CHILD] marker matches, exit 0");
|
||||
libc::_exit(0);
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
println!("[test-02b] fd passing via SCM_RIGHTS across fork()");
|
||||
|
||||
unsafe {
|
||||
let (sock_parent, sock_child) = make_socketpair()?;
|
||||
let tmp_fd = open_tmp_with_marker()?;
|
||||
println!("[test-02b PARENT] tmp fd={}, sockets {}/{}",
|
||||
tmp_fd.as_raw_fd(), sock_parent.as_raw_fd(), sock_child.as_raw_fd());
|
||||
|
||||
let pid = libc::fork();
|
||||
if pid < 0 {
|
||||
return Err(format!("fork: {}", errno_str()));
|
||||
}
|
||||
if pid == 0 {
|
||||
// Child: drop tmp_fd, drop parent socket, run child_main on its own socket
|
||||
drop(tmp_fd);
|
||||
drop(sock_parent);
|
||||
child_main(sock_child);
|
||||
}
|
||||
|
||||
// Parent path
|
||||
drop(sock_child);
|
||||
println!("[test-02b PARENT] forked child pid={pid}, sending fd");
|
||||
send_fd_over(sock_parent.as_raw_fd(), tmp_fd.as_raw_fd())?;
|
||||
drop(tmp_fd);
|
||||
drop(sock_parent);
|
||||
|
||||
let mut status: libc::c_int = 0;
|
||||
let r = libc::waitpid(pid, &mut status, 0);
|
||||
if r < 0 {
|
||||
return Err(format!("waitpid: {}", errno_str()));
|
||||
}
|
||||
let exited = libc::WIFEXITED(status);
|
||||
let code = libc::WEXITSTATUS(status);
|
||||
println!("[test-02b PARENT] child reaped: exited={exited} code={code}");
|
||||
|
||||
if !exited {
|
||||
return Err("child did not exit normally".into());
|
||||
}
|
||||
if code != 0 {
|
||||
return Err(format!("child exited with status {code}"));
|
||||
}
|
||||
|
||||
// Cleanup tmp file
|
||||
let path = std::ffi::CString::new("/tmp/test-02b-fd-passing-fork.bin").unwrap();
|
||||
libc::unlink(path.as_ptr());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[test-02b] PASS: fd passed across fork() correctly");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[test-02b] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/redox-wl-test-fd-passing/Cargo.toml
Normal file
7
crates/redox-wl-test-fd-passing/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "redox-wl-test-fd-passing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
195
crates/redox-wl-test-fd-passing/src/main.rs
Normal file
195
crates/redox-wl-test-fd-passing/src/main.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
//! Test 2 — fd passing via sendmsg/recvmsg with SCM_RIGHTS.
|
||||
//!
|
||||
//! This is THE critical primitive for Wayland: clients pass shm fds
|
||||
//! (and dmabuf fds) to the compositor through this mechanism.
|
||||
//!
|
||||
//! relibc Redox (platform/redox/socket.rs:272,447,835,924) declares
|
||||
//! support — this test validates the actual behaviour.
|
||||
//!
|
||||
//! Flow:
|
||||
//! 1. socketpair(AF_UNIX, SOCK_STREAM)
|
||||
//! 2. open a tmp file, write a known marker into it
|
||||
//! 3. sendmsg the fd via SCM_RIGHTS through socket A
|
||||
//! 4. recvmsg on socket B, extract the fd
|
||||
//! 5. read from the received fd, check marker matches
|
||||
//!
|
||||
//! Single-process test (no fork needed): we send the fd to ourselves
|
||||
//! through the socketpair to validate the mechanism works end-to-end.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::mem::{self, MaybeUninit};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::process::ExitCode;
|
||||
use std::ptr;
|
||||
|
||||
const MARKER: &[u8] = b"REDOX-FD-PASSING-OK-2026";
|
||||
|
||||
fn errno_str() -> String {
|
||||
let e = io::Error::last_os_error();
|
||||
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
|
||||
}
|
||||
|
||||
unsafe fn make_socketpair() -> Result<(OwnedFd, OwnedFd), String> {
|
||||
let mut fds: [RawFd; 2] = [-1, -1];
|
||||
let r = libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr());
|
||||
if r != 0 {
|
||||
return Err(format!("socketpair: {}", errno_str()));
|
||||
}
|
||||
Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
|
||||
}
|
||||
|
||||
unsafe fn open_tmp_with_marker() -> Result<OwnedFd, String> {
|
||||
let path = CString::new("/tmp/test-02-fd-passing.bin").unwrap();
|
||||
let fd = libc::open(
|
||||
path.as_ptr(),
|
||||
libc::O_RDWR | libc::O_CREAT | libc::O_TRUNC,
|
||||
0o600,
|
||||
);
|
||||
if fd < 0 {
|
||||
return Err(format!("open tmp: {}", errno_str()));
|
||||
}
|
||||
let n = libc::write(fd, MARKER.as_ptr() as *const _, MARKER.len());
|
||||
if n != MARKER.len() as isize {
|
||||
return Err(format!("write tmp: {}", errno_str()));
|
||||
}
|
||||
libc::lseek(fd, 0, libc::SEEK_SET);
|
||||
Ok(OwnedFd::from_raw_fd(fd))
|
||||
}
|
||||
|
||||
unsafe fn send_fd_over(socket: RawFd, fd_to_send: RawFd) -> Result<(), String> {
|
||||
let mut iov_buf = [b'X'; 1];
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: iov_buf.as_mut_ptr() as *mut _,
|
||||
iov_len: 1,
|
||||
};
|
||||
|
||||
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
|
||||
let mut cmsg_buf = vec![0u8; cmsg_space];
|
||||
|
||||
let mut msg: libc::msghdr = mem::zeroed();
|
||||
msg.msg_iov = &mut iov as *mut _;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
|
||||
msg.msg_controllen = cmsg_space as _;
|
||||
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
if cmsg.is_null() {
|
||||
return Err("CMSG_FIRSTHDR null".into());
|
||||
}
|
||||
(*cmsg).cmsg_level = libc::SOL_SOCKET;
|
||||
(*cmsg).cmsg_type = libc::SCM_RIGHTS;
|
||||
(*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
|
||||
let data_ptr = libc::CMSG_DATA(cmsg) as *mut RawFd;
|
||||
ptr::write_unaligned(data_ptr, fd_to_send);
|
||||
|
||||
let n = libc::sendmsg(socket, &msg, 0);
|
||||
if n < 0 {
|
||||
return Err(format!("sendmsg: {}", errno_str()));
|
||||
}
|
||||
println!("[test-02] sendmsg sent {n} bytes + 1 fd via SCM_RIGHTS");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
unsafe fn recv_fd_from(socket: RawFd) -> Result<OwnedFd, String> {
|
||||
let mut iov_buf = [0u8; 1];
|
||||
let mut iov = libc::iovec {
|
||||
iov_base: iov_buf.as_mut_ptr() as *mut _,
|
||||
iov_len: 1,
|
||||
};
|
||||
|
||||
let cmsg_space = libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) as usize;
|
||||
let mut cmsg_buf = vec![MaybeUninit::<u8>::uninit(); cmsg_space];
|
||||
|
||||
let mut msg: libc::msghdr = mem::zeroed();
|
||||
msg.msg_iov = &mut iov as *mut _;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
|
||||
msg.msg_controllen = cmsg_space as _;
|
||||
|
||||
let n = libc::recvmsg(socket, &mut msg, 0);
|
||||
if n < 0 {
|
||||
return Err(format!("recvmsg: {}", errno_str()));
|
||||
}
|
||||
println!("[test-02] recvmsg got {n} bytes + control of {} bytes", msg.msg_controllen);
|
||||
|
||||
let cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
if cmsg.is_null() {
|
||||
return Err("no cmsg in received message".into());
|
||||
}
|
||||
if (*cmsg).cmsg_level != libc::SOL_SOCKET || (*cmsg).cmsg_type != libc::SCM_RIGHTS {
|
||||
return Err(format!(
|
||||
"unexpected cmsg level={} type={}",
|
||||
(*cmsg).cmsg_level,
|
||||
(*cmsg).cmsg_type
|
||||
));
|
||||
}
|
||||
let data_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
|
||||
let received_fd = ptr::read_unaligned(data_ptr);
|
||||
if received_fd < 0 {
|
||||
return Err(format!("received negative fd: {received_fd}"));
|
||||
}
|
||||
Ok(OwnedFd::from_raw_fd(received_fd))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
println!("[test-02] fd passing via SCM_RIGHTS");
|
||||
|
||||
unsafe {
|
||||
let (sock_a, sock_b) = make_socketpair()?;
|
||||
let tmp_fd = open_tmp_with_marker()?;
|
||||
println!("[test-02] tmp fd = {}", tmp_fd.as_raw_fd());
|
||||
|
||||
// Strategy: send a dup'd fd, then close BOTH the original and the dup.
|
||||
// If SCM_RIGHTS works per POSIX, the received fd is an independent
|
||||
// reference to the open file and must remain readable.
|
||||
let dup_fd = libc::dup(tmp_fd.as_raw_fd());
|
||||
if dup_fd < 0 {
|
||||
return Err(format!("dup: {}", errno_str()));
|
||||
}
|
||||
println!("[test-02] dup'd tmp_fd ({}) -> {dup_fd}", tmp_fd.as_raw_fd());
|
||||
|
||||
send_fd_over(sock_a.as_raw_fd(), dup_fd)?;
|
||||
let received = recv_fd_from(sock_b.as_raw_fd())?;
|
||||
let recv_raw = received.as_raw_fd();
|
||||
println!("[test-02] received fd = {recv_raw}");
|
||||
|
||||
// Close BOTH the original and the dup. If SCM_RIGHTS gave us a real
|
||||
// duplicate, the received fd must survive.
|
||||
let orig_raw = tmp_fd.as_raw_fd();
|
||||
drop(tmp_fd);
|
||||
libc::close(dup_fd);
|
||||
println!("[test-02] closed original fd ({orig_raw}) and dup ({dup_fd}); reading via received only");
|
||||
|
||||
libc::lseek(recv_raw, 0, libc::SEEK_SET);
|
||||
let mut readback = vec![0u8; MARKER.len()];
|
||||
let n = libc::read(recv_raw, readback.as_mut_ptr() as *mut _, readback.len());
|
||||
if n != MARKER.len() as isize {
|
||||
return Err(format!("read from received fd: only {n} bytes (errno {})",
|
||||
io::Error::last_os_error().raw_os_error().unwrap_or(0)));
|
||||
}
|
||||
if readback != MARKER {
|
||||
return Err(format!("marker mismatch: got {:?}", String::from_utf8_lossy(&readback)));
|
||||
}
|
||||
println!("[test-02] read {} bytes via received fd: marker matches", n);
|
||||
|
||||
// Cleanup tmp file
|
||||
let path = std::ffi::CString::new("/tmp/test-02-fd-passing.bin").unwrap();
|
||||
libc::unlink(path.as_ptr());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[test-02] PASS: fd passing works, received fd reads correct marker");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[test-02] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/redox-wl-test-handshake/Cargo.toml
Normal file
9
crates/redox-wl-test-handshake/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "redox-wl-test-handshake"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
|
||||
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
200
crates/redox-wl-test-handshake/src/main.rs
Normal file
200
crates/redox-wl-test-handshake/src/main.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
//! Test 5 — Wayland handshake server/client sur Redox.
|
||||
//!
|
||||
//! Valide que wayland-rs (server + client + backend) ne se contente pas de
|
||||
//! compiler pour Redox mais fonctionne réellement à l'exécution :
|
||||
//!
|
||||
//! 1. Serveur Wayland local + globals (wl_compositor, wl_shm)
|
||||
//! 2. socketpair AF_UNIX, on insère le côté serveur via insert_client
|
||||
//! 3. Client Wayland connecté sur l'autre côté via WAYLAND_SOCKET fd env
|
||||
//! 4. Client fait wl_display.get_registry, dispatch
|
||||
//! 5. On vérifie que les globals attendus arrivent côté client
|
||||
//!
|
||||
//! Si ça passe, le port wayland-rs sur Redox est validé runtime, pas juste
|
||||
//! compile-time.
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
|
||||
use wayland_client::{
|
||||
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue,
|
||||
backend::Backend as ClientBackend,
|
||||
protocol::wl_registry,
|
||||
};
|
||||
use wayland_server::{
|
||||
Display as ServerDisplay, GlobalDispatch, New,
|
||||
backend::ClientData,
|
||||
protocol::{wl_compositor, wl_shm},
|
||||
};
|
||||
|
||||
// ---- Server side ----
|
||||
|
||||
struct ServerState;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DumbClientData;
|
||||
impl ClientData for DumbClientData {}
|
||||
|
||||
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for ServerState {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_handle: &wayland_server::DisplayHandle,
|
||||
_client: &wayland_server::Client,
|
||||
_resource: New<wl_compositor::WlCompositor>,
|
||||
_data: &(),
|
||||
_data_init: &mut wayland_server::DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for ServerState {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &wayland_server::Client,
|
||||
_resource: &wl_compositor::WlCompositor,
|
||||
_request: wl_compositor::Request,
|
||||
_data: &(),
|
||||
_dh: &wayland_server::DisplayHandle,
|
||||
_data_init: &mut wayland_server::DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalDispatch<wl_shm::WlShm, ()> for ServerState {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_handle: &wayland_server::DisplayHandle,
|
||||
_client: &wayland_server::Client,
|
||||
_resource: New<wl_shm::WlShm>,
|
||||
_data: &(),
|
||||
_data_init: &mut wayland_server::DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_shm::WlShm, ()> for ServerState {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &wayland_server::Client,
|
||||
_resource: &wl_shm::WlShm,
|
||||
_request: wl_shm::Request,
|
||||
_data: &(),
|
||||
_dh: &wayland_server::DisplayHandle,
|
||||
_data_init: &mut wayland_server::DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Client side ----
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct ClientState {
|
||||
globals: Vec<(u32, String, u32)>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_data: &(),
|
||||
_conn: &ClientConnection,
|
||||
_qhandle: &wayland_client::QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global { name, interface, version } = event {
|
||||
println!("[test-05 CLIENT] global #{name}: {interface} v{version}");
|
||||
state.globals.push((name, interface, version));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("[test-05] wayland-rs handshake server/client on Redox");
|
||||
|
||||
// -- Server setup --
|
||||
let mut server_display: ServerDisplay<ServerState> = ServerDisplay::new()?;
|
||||
let mut dh = server_display.handle();
|
||||
dh.create_global::<ServerState, wl_compositor::WlCompositor, _>(5, ());
|
||||
dh.create_global::<ServerState, wl_shm::WlShm, _>(1, ());
|
||||
println!("[test-05 SERVER] created wl_compositor v5 + wl_shm v1 globals");
|
||||
|
||||
// -- Socket linking --
|
||||
let (s_server, s_client) = UnixStream::pair()?;
|
||||
s_server.set_nonblocking(true)?;
|
||||
let _client_handle = dh.insert_client(s_server, Arc::new(DumbClientData))?;
|
||||
println!("[test-05] linked server side via insert_client");
|
||||
|
||||
// -- Client setup using the other end of the socketpair --
|
||||
let client_backend = ClientBackend::connect(s_client)?;
|
||||
let client_conn = ClientConnection::from_backend(client_backend);
|
||||
let mut event_queue: EventQueue<ClientState> = client_conn.new_event_queue();
|
||||
let qhandle = event_queue.handle();
|
||||
let _registry = client_conn.display().get_registry(&qhandle, ());
|
||||
println!("[test-05 CLIENT] called get_registry, dispatching...");
|
||||
|
||||
let mut server_state = ServerState;
|
||||
let mut client_state = ClientState::default();
|
||||
|
||||
// -- Roundtrip: dispatch alternately until client has events --
|
||||
for round in 0..10 {
|
||||
// server side
|
||||
server_display.dispatch_clients(&mut server_state)?;
|
||||
server_display.flush_clients()?;
|
||||
// client side
|
||||
let _ = event_queue.flush();
|
||||
let processed = event_queue.dispatch_pending(&mut client_state)?;
|
||||
if processed > 0 {
|
||||
println!("[test-05] round {round}: client processed {processed} event(s)");
|
||||
}
|
||||
if !client_state.globals.is_empty() {
|
||||
client_state.done = true;
|
||||
break;
|
||||
}
|
||||
// Block briefly waiting for incoming events on client side
|
||||
match event_queue.prepare_read() {
|
||||
Some(read_guard) => {
|
||||
let _ = read_guard.read();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !client_state.done || client_state.globals.is_empty() {
|
||||
return Err(format!(
|
||||
"client did not receive globals (got {})",
|
||||
client_state.globals.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let names: Vec<&str> = client_state.globals.iter().map(|(_, n, _)| n.as_str()).collect();
|
||||
let has_compositor = names.contains(&"wl_compositor");
|
||||
let has_shm = names.contains(&"wl_shm");
|
||||
|
||||
if !has_compositor || !has_shm {
|
||||
return Err(format!("missing expected globals: got {:?}", names).into());
|
||||
}
|
||||
|
||||
println!(
|
||||
"[test-05] received {} global(s): {:?}",
|
||||
client_state.globals.len(),
|
||||
names
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[test-05] PASS: wayland-rs server/client handshake works on Redox");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[test-05] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
crates/redox-wl-test-poll-multifd/Cargo.toml
Normal file
7
crates/redox-wl-test-poll-multifd/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "redox-wl-test-poll-multifd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
120
crates/redox-wl-test-poll-multifd/src/main.rs
Normal file
120
crates/redox-wl-test-poll-multifd/src/main.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
//! Test 4 — poll() over multiple fds.
|
||||
//!
|
||||
//! A Wayland compositor multiplexes many fds: the listening socket,
|
||||
//! each connected client, the input device(s), idle timers, signals.
|
||||
//! This test validates that poll() correctly demultiplexes.
|
||||
//!
|
||||
//! Setup: 3 pipes (a, b, c). We write into b only, then poll all 3.
|
||||
//! Only fd_b should report POLLIN. We read it. Then we close a, expecting
|
||||
//! POLLHUP on a-read side. Finally we test timeout-only behaviour.
|
||||
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn errno_str() -> String {
|
||||
let e = io::Error::last_os_error();
|
||||
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
|
||||
}
|
||||
|
||||
unsafe fn make_pipe() -> Result<(RawFd, RawFd), String> {
|
||||
let mut fds: [RawFd; 2] = [-1, -1];
|
||||
if libc::pipe(fds.as_mut_ptr()) != 0 {
|
||||
return Err(format!("pipe: {}", errno_str()));
|
||||
}
|
||||
Ok((fds[0], fds[1]))
|
||||
}
|
||||
|
||||
unsafe fn poll_revents(fds: &mut [libc::pollfd], timeout_ms: i32) -> Result<i32, String> {
|
||||
let r = libc::poll(fds.as_mut_ptr(), fds.len() as _, timeout_ms);
|
||||
if r < 0 {
|
||||
return Err(format!("poll: {}", errno_str()));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
println!("[test-04] poll() multi-fd multiplexing");
|
||||
|
||||
unsafe {
|
||||
// 3 pipes; we'll only watch the read ends.
|
||||
let (ra, wa) = make_pipe()?;
|
||||
let (rb, wb) = make_pipe()?;
|
||||
let (rc, wc) = make_pipe()?;
|
||||
println!("[test-04] pipes: ra={ra} rb={rb} rc={rc}");
|
||||
|
||||
// ---- Subtest A: timeout-only, no fd ready ----
|
||||
let mut pfds = [
|
||||
libc::pollfd { fd: ra, events: libc::POLLIN, revents: 0 },
|
||||
libc::pollfd { fd: rb, events: libc::POLLIN, revents: 0 },
|
||||
libc::pollfd { fd: rc, events: libc::POLLIN, revents: 0 },
|
||||
];
|
||||
let n = poll_revents(&mut pfds, 50)?;
|
||||
if n != 0 {
|
||||
return Err(format!("expected timeout (0), got {n}"));
|
||||
}
|
||||
println!("[test-04] A: timeout 50ms with no data → poll returned 0 OK");
|
||||
|
||||
// ---- Subtest B: write into pipe B only ----
|
||||
let payload = b"X";
|
||||
let n = libc::write(wb, payload.as_ptr() as *const _, 1);
|
||||
if n != 1 {
|
||||
return Err(format!("write to wb: {}", errno_str()));
|
||||
}
|
||||
|
||||
for p in pfds.iter_mut() { p.revents = 0; }
|
||||
let n = poll_revents(&mut pfds, 1000)?;
|
||||
if n != 1 {
|
||||
return Err(format!("expected 1 ready fd, got {n}"));
|
||||
}
|
||||
let ready: Vec<_> = pfds.iter().enumerate()
|
||||
.filter(|(_, p)| p.revents & libc::POLLIN != 0)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
if ready != [1] {
|
||||
return Err(format!("expected only index 1 (rb) ready, got {ready:?}"));
|
||||
}
|
||||
println!("[test-04] B: poll detected only rb (index 1) ready, as expected");
|
||||
|
||||
let mut buf = [0u8; 1];
|
||||
let r = libc::read(rb, buf.as_mut_ptr() as *mut _, 1);
|
||||
if r != 1 || buf[0] != b'X' {
|
||||
return Err(format!("read rb: r={r} buf={:?}", buf));
|
||||
}
|
||||
println!("[test-04] B: read 1 byte 'X' from rb OK");
|
||||
|
||||
// ---- Subtest C: close write end of A, expect HUP on read end ----
|
||||
libc::close(wa);
|
||||
for p in pfds.iter_mut() { p.revents = 0; }
|
||||
let n = poll_revents(&mut pfds, 100)?;
|
||||
if n < 1 {
|
||||
return Err(format!("expected at least 1 ready (POLLHUP on ra), got {n}"));
|
||||
}
|
||||
let hup_a = pfds[0].revents & (libc::POLLHUP | libc::POLLIN) != 0;
|
||||
if !hup_a {
|
||||
return Err(format!("expected HUP/IN on ra, revents={:#x}", pfds[0].revents));
|
||||
}
|
||||
println!("[test-04] C: closing wa caused POLLHUP/POLLIN on ra (revents={:#x})",
|
||||
pfds[0].revents);
|
||||
|
||||
// Cleanup
|
||||
libc::close(ra);
|
||||
libc::close(rb); libc::close(wb);
|
||||
libc::close(rc); libc::close(wc);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[test-04] PASS: poll() multiplexes correctly across fds and detects HUP");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[test-04] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/redox-wl-test-shm-open/Cargo.toml
Normal file
7
crates/redox-wl-test-shm-open/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "redox-wl-test-shm-open"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
142
crates/redox-wl-test-shm-open/src/main.rs
Normal file
142
crates/redox-wl-test-shm-open/src/main.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//! Test 3 — shm_open + mmap MAP_SHARED.
|
||||
//!
|
||||
//! Wayland clients use shm_open + ftruncate + mmap to create wl_shm pools.
|
||||
//! The compositor then receives the fd via SCM_RIGHTS and mmaps the same region.
|
||||
//!
|
||||
//! relibc Redox maps shm_open("/foo") to open("/scheme/shm/foo").
|
||||
//! This test validates:
|
||||
//! 1. shm_open with O_RDWR | O_CREAT works
|
||||
//! 2. ftruncate sets the size
|
||||
//! 3. mmap MAP_SHARED returns a writable region
|
||||
//! 4. writes are persistent: close + reopen + remap reads back the data
|
||||
//! 5. shm_unlink removes the segment
|
||||
//!
|
||||
//! Two passes (write then read-back) simulate the producer/consumer pattern
|
||||
//! used by Wayland: client writes pixels, compositor reads them.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
use std::process::ExitCode;
|
||||
use std::ptr;
|
||||
|
||||
const SHM_NAME: &str = "/redox-wl-test-03";
|
||||
const SIZE: usize = 4096;
|
||||
|
||||
fn errno_str() -> String {
|
||||
let e = io::Error::last_os_error();
|
||||
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
|
||||
}
|
||||
|
||||
unsafe fn shm_open_rw_create(name: &str, mode: libc::mode_t) -> Result<RawFd, String> {
|
||||
let cname = CString::new(name).unwrap();
|
||||
let fd = libc::shm_open(
|
||||
cname.as_ptr(),
|
||||
libc::O_RDWR | libc::O_CREAT,
|
||||
mode as _,
|
||||
);
|
||||
if fd < 0 {
|
||||
return Err(format!("shm_open({name}): {}", errno_str()));
|
||||
}
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
unsafe fn shm_open_rw(name: &str) -> Result<RawFd, String> {
|
||||
let cname = CString::new(name).unwrap();
|
||||
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0);
|
||||
if fd < 0 {
|
||||
return Err(format!("shm_open(reopen {name}): {}", errno_str()));
|
||||
}
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
unsafe fn map_rw(fd: RawFd, size: usize) -> Result<*mut u8, String> {
|
||||
let p = libc::mmap(
|
||||
ptr::null_mut(),
|
||||
size,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
if p == libc::MAP_FAILED {
|
||||
return Err(format!("mmap MAP_SHARED: {}", errno_str()));
|
||||
}
|
||||
Ok(p as *mut u8)
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
println!("[test-03] shm_open + mmap MAP_SHARED");
|
||||
|
||||
unsafe {
|
||||
// Phase 1: create + write
|
||||
let fd1 = shm_open_rw_create(SHM_NAME, 0o600)?;
|
||||
println!("[test-03] phase1: shm_open created, fd={fd1}");
|
||||
|
||||
if libc::ftruncate(fd1, SIZE as _) != 0 {
|
||||
return Err(format!("ftruncate: {}", errno_str()));
|
||||
}
|
||||
|
||||
let map1 = map_rw(fd1, SIZE)?;
|
||||
println!("[test-03] phase1: mmap at {:p}", map1);
|
||||
|
||||
// Write a recognisable pattern (ARGB8888 simulation: 256 pixels of 0xDEADBEEF)
|
||||
let pixels = std::slice::from_raw_parts_mut(map1 as *mut u32, SIZE / 4);
|
||||
for (i, p) in pixels.iter_mut().enumerate() {
|
||||
*p = 0xDEAD_BEEF_u32.wrapping_add(i as u32);
|
||||
}
|
||||
println!("[test-03] phase1: wrote {} u32 pixels", pixels.len());
|
||||
|
||||
if libc::munmap(map1 as *mut _, SIZE) != 0 {
|
||||
return Err(format!("munmap phase1: {}", errno_str()));
|
||||
}
|
||||
libc::close(fd1);
|
||||
|
||||
// Phase 2: reopen + check pattern
|
||||
let fd2 = shm_open_rw(SHM_NAME)?;
|
||||
println!("[test-03] phase2: shm_open reopened, fd={fd2}");
|
||||
|
||||
let map2 = map_rw(fd2, SIZE)?;
|
||||
println!("[test-03] phase2: mmap at {:p}", map2);
|
||||
|
||||
let pixels2 = std::slice::from_raw_parts(map2 as *const u32, SIZE / 4);
|
||||
for (i, &p) in pixels2.iter().enumerate() {
|
||||
let expected = 0xDEAD_BEEF_u32.wrapping_add(i as u32);
|
||||
if p != expected {
|
||||
libc::munmap(map2 as *mut _, SIZE);
|
||||
libc::close(fd2);
|
||||
let cname = CString::new(SHM_NAME).unwrap();
|
||||
libc::shm_unlink(cname.as_ptr());
|
||||
return Err(format!("pixel {i} mismatch: got {p:#x}, expected {expected:#x}"));
|
||||
}
|
||||
}
|
||||
println!("[test-03] phase2: pattern verified across close/reopen");
|
||||
|
||||
if libc::munmap(map2 as *mut _, SIZE) != 0 {
|
||||
return Err(format!("munmap phase2: {}", errno_str()));
|
||||
}
|
||||
libc::close(fd2);
|
||||
|
||||
// Cleanup
|
||||
let cname = CString::new(SHM_NAME).unwrap();
|
||||
if libc::shm_unlink(cname.as_ptr()) != 0 {
|
||||
return Err(format!("shm_unlink: {}", errno_str()));
|
||||
}
|
||||
println!("[test-03] cleanup: shm_unlink OK");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[test-03] PASS: shm_open/mmap/persistence/unlink all work");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[test-03] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/redox-wl-test-shm-pipeline/Cargo.toml
Normal file
10
crates/redox-wl-test-shm-pipeline/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "redox-wl-test-shm-pipeline"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
wayland-server = { path = "../../../wayland-rs/wayland-server", default-features = false }
|
||||
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||
libc = "0.2"
|
||||
589
crates/redox-wl-test-shm-pipeline/src/main.rs
Normal file
589
crates/redox-wl-test-shm-pipeline/src/main.rs
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
//! Test 6 — Pipeline complet wayland-rs sur Redox.
|
||||
//!
|
||||
//! C'est ce que fait un compositor Wayland minimal :
|
||||
//! - serveur expose un socket Unix (ListeningSocket)
|
||||
//! - client se connecte via WAYLAND_SOCKET (vrai pattern Wayland)
|
||||
//! - client crée wl_shm_pool depuis un fd shm
|
||||
//! - client crée wl_buffer depuis le pool, peint un pattern ARGB
|
||||
//! - client crée wl_surface, attach, damage, commit
|
||||
//! - serveur reçoit le commit, mappe le buffer fd, lit les pixels, vérifie
|
||||
//!
|
||||
//! Si ce test passe, le pipeline shm Wayland complet fonctionne sur Redox.
|
||||
//! C'est la preuve technique que la phase 4-7 du plan directeur (display
|
||||
//! backend + input + surfaces shm + compositor minimal) est réalisable
|
||||
//! sans surprise majeure.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::process::ExitCode;
|
||||
use std::ptr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use wayland_client::{
|
||||
Connection as ClientConnection, Dispatch as ClientDispatch, EventQueue, Proxy as _,
|
||||
QueueHandle as ClientQueueHandle,
|
||||
backend::Backend as ClientBackend,
|
||||
protocol::{
|
||||
wl_buffer::WlBuffer as ClientBuffer, wl_compositor::WlCompositor as ClientCompositor,
|
||||
wl_registry, wl_shm::WlShm as ClientShm, wl_shm_pool::WlShmPool as ClientShmPool,
|
||||
wl_surface::WlSurface as ClientSurface,
|
||||
},
|
||||
};
|
||||
use wayland_server::{
|
||||
Client, DataInit, Display as ServerDisplay, DisplayHandle, GlobalDispatch,
|
||||
backend::ClientData,
|
||||
protocol::{
|
||||
wl_buffer, wl_callback, wl_compositor, wl_shm, wl_shm_pool, wl_surface,
|
||||
},
|
||||
};
|
||||
|
||||
const W: i32 = 100;
|
||||
const H: i32 = 100;
|
||||
const STRIDE: i32 = W * 4;
|
||||
const POOL_SIZE: i32 = STRIDE * H;
|
||||
const SOCKET_NAME: &str = "wayland-test-06";
|
||||
|
||||
fn expected_pixel(x: i32, y: i32) -> u32 {
|
||||
let a: u32 = 0xFF;
|
||||
let r: u32 = (x as u32) & 0xFF;
|
||||
let g: u32 = (y as u32) & 0xFF;
|
||||
let b: u32 = ((x + y) as u32) & 0xFF;
|
||||
(a << 24) | (r << 16) | (g << 8) | b
|
||||
}
|
||||
|
||||
fn errno_str() -> String {
|
||||
let e = io::Error::last_os_error();
|
||||
format!("{e} (errno={})", e.raw_os_error().unwrap_or(0))
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Server side
|
||||
// =====================================================================
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServerObservation {
|
||||
surface_created: bool,
|
||||
pool_created_with_fd: Option<i32>,
|
||||
buffer_created: Option<(i32, i32)>, // (w, h)
|
||||
surface_attached: bool,
|
||||
surface_committed: bool,
|
||||
/// Set by main loop after reading pixels successfully
|
||||
pixels_verified: bool,
|
||||
}
|
||||
|
||||
struct ServerState {
|
||||
obs: Arc<Mutex<ServerObservation>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DumbClientData;
|
||||
impl ClientData for DumbClientData {}
|
||||
|
||||
// ---- wl_compositor ----
|
||||
impl GlobalDispatch<wl_compositor::WlCompositor, ()> for ServerState {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: wayland_server::New<wl_compositor::WlCompositor>,
|
||||
_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_compositor::WlCompositor, ()> for ServerState {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &wl_compositor::WlCompositor,
|
||||
request: wl_compositor::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let wl_compositor::Request::CreateSurface { id } = request {
|
||||
data_init.init(id, ());
|
||||
state.obs.lock().unwrap().surface_created = true;
|
||||
eprintln!("[server] wl_compositor.create_surface");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- wl_shm ----
|
||||
impl GlobalDispatch<wl_shm::WlShm, ()> for ServerState {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_handle: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: wayland_server::New<wl_shm::WlShm>,
|
||||
_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
let shm = data_init.init(resource, ());
|
||||
// Advertise the format
|
||||
shm.format(wl_shm::Format::Argb8888);
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_shm::WlShm, ()> for ServerState {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &wl_shm::WlShm,
|
||||
request: wl_shm::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let wl_shm::Request::CreatePool { id, fd, size } = request {
|
||||
let raw_fd = fd.as_raw_fd();
|
||||
// Dup the fd so we keep it after the OwnedFd in the request is dropped
|
||||
let our_fd = unsafe { libc::dup(raw_fd) };
|
||||
state.obs.lock().unwrap().pool_created_with_fd = Some(our_fd);
|
||||
data_init.init(id, PoolData { fd: our_fd, size });
|
||||
eprintln!("[server] wl_shm.create_pool fd={raw_fd} size={size} (server dup={our_fd})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- wl_shm_pool ----
|
||||
struct PoolData {
|
||||
fd: i32,
|
||||
size: i32,
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_shm_pool::WlShmPool, PoolData> for ServerState {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &wl_shm_pool::WlShmPool,
|
||||
request: wl_shm_pool::Request,
|
||||
_data: &PoolData,
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let wl_shm_pool::Request::CreateBuffer {
|
||||
id,
|
||||
offset,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
format,
|
||||
} = request
|
||||
{
|
||||
data_init.init(id, ());
|
||||
state.obs.lock().unwrap().buffer_created = Some((width, height));
|
||||
eprintln!(
|
||||
"[server] wl_shm_pool.create_buffer offset={offset} {width}x{height} stride={stride} format={format:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- wl_buffer ----
|
||||
impl wayland_server::Dispatch<wl_buffer::WlBuffer, ()> for ServerState {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &wl_buffer::WlBuffer,
|
||||
_request: wl_buffer::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- wl_surface ----
|
||||
impl wayland_server::Dispatch<wl_surface::WlSurface, ()> for ServerState {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &wl_surface::WlSurface,
|
||||
request: wl_surface::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
wl_surface::Request::Attach { .. } => {
|
||||
state.obs.lock().unwrap().surface_attached = true;
|
||||
eprintln!("[server] wl_surface.attach");
|
||||
}
|
||||
wl_surface::Request::Damage { .. }
|
||||
| wl_surface::Request::DamageBuffer { .. } => {
|
||||
eprintln!("[server] wl_surface.damage");
|
||||
}
|
||||
wl_surface::Request::Commit => {
|
||||
state.obs.lock().unwrap().surface_committed = true;
|
||||
eprintln!("[server] wl_surface.commit");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl wayland_server::Dispatch<wl_callback::WlCallback, ()> for ServerState {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &wl_callback::WlCallback,
|
||||
_request: wl_callback::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Client side
|
||||
// =====================================================================
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
compositor: Option<ClientCompositor>,
|
||||
shm: Option<ClientShm>,
|
||||
}
|
||||
|
||||
impl ClientDispatch<wl_registry::WlRegistry, ()> for ClientState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_data: &(),
|
||||
_conn: &ClientConnection,
|
||||
qh: &ClientQueueHandle<Self>,
|
||||
) {
|
||||
if let wl_registry::Event::Global { name, interface, version } = event {
|
||||
eprintln!("[client] global #{name}: {interface} v{version}");
|
||||
match interface.as_str() {
|
||||
"wl_compositor" => {
|
||||
state.compositor = Some(registry.bind(name, version, qh, ()));
|
||||
}
|
||||
"wl_shm" => {
|
||||
state.shm = Some(registry.bind(name, version, qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! noop_dispatch {
|
||||
($iface:ty) => {
|
||||
impl ClientDispatch<$iface, ()> for ClientState {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_r: &$iface,
|
||||
_event: <$iface as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &ClientConnection,
|
||||
_qh: &ClientQueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
use wayland_client::Proxy;
|
||||
|
||||
noop_dispatch!(ClientCompositor);
|
||||
noop_dispatch!(ClientShm);
|
||||
noop_dispatch!(ClientShmPool);
|
||||
noop_dispatch!(ClientBuffer);
|
||||
noop_dispatch!(ClientSurface);
|
||||
|
||||
// =====================================================================
|
||||
// Process orchestration
|
||||
// =====================================================================
|
||||
|
||||
unsafe fn create_shm_buffer(name: &str, size: usize) -> Result<(OwnedFd, *mut u8), String> {
|
||||
let cname = CString::new(name).unwrap();
|
||||
let _ = libc::shm_unlink(cname.as_ptr());
|
||||
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600);
|
||||
if fd < 0 {
|
||||
return Err(format!("shm_open: {}", errno_str()));
|
||||
}
|
||||
if libc::ftruncate(fd, size as _) != 0 {
|
||||
libc::close(fd);
|
||||
return Err(format!("ftruncate: {}", errno_str()));
|
||||
}
|
||||
let p = libc::mmap(
|
||||
ptr::null_mut(),
|
||||
size,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_SHARED,
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
if p == libc::MAP_FAILED {
|
||||
libc::close(fd);
|
||||
return Err(format!("mmap: {}", errno_str()));
|
||||
}
|
||||
Ok((unsafe { OwnedFd::from_raw_fd(fd) }, p as *mut u8))
|
||||
}
|
||||
|
||||
use std::os::fd::FromRawFd;
|
||||
|
||||
unsafe fn child_main(server_path: &str) -> ! {
|
||||
eprintln!("[client pid={}] connecting to {server_path}", libc::getpid());
|
||||
|
||||
// Connect to the server socket
|
||||
let stream = match UnixStream::connect(server_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("[client] connect: {e}");
|
||||
libc::_exit(2);
|
||||
}
|
||||
};
|
||||
let fd = stream.into_raw_fd();
|
||||
// wayland-client connect_to_env reads WAYLAND_SOCKET
|
||||
std::env::set_var("WAYLAND_SOCKET", fd.to_string());
|
||||
|
||||
let conn = match ClientConnection::connect_to_env() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[client] connect_to_env: {e}");
|
||||
libc::_exit(3);
|
||||
}
|
||||
};
|
||||
let mut event_queue: EventQueue<ClientState> = conn.new_event_queue();
|
||||
let qh = event_queue.handle();
|
||||
let _registry = conn.display().get_registry(&qh, ());
|
||||
|
||||
let mut state = ClientState::default();
|
||||
// Roundtrip to receive globals
|
||||
if let Err(e) = event_queue.roundtrip(&mut state) {
|
||||
eprintln!("[client] roundtrip 1: {e}");
|
||||
libc::_exit(4);
|
||||
}
|
||||
if state.compositor.is_none() || state.shm.is_none() {
|
||||
eprintln!("[client] missing compositor/shm globals");
|
||||
libc::_exit(5);
|
||||
}
|
||||
eprintln!("[client] got compositor + shm globals");
|
||||
|
||||
// Create the shm buffer
|
||||
let (shm_fd, map) = match create_shm_buffer("/wl-test-06-buffer", POOL_SIZE as usize) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[client] shm: {e}");
|
||||
libc::_exit(6);
|
||||
}
|
||||
};
|
||||
|
||||
// Paint pattern
|
||||
let pixels = std::slice::from_raw_parts_mut(map as *mut u32, (W * H) as usize);
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
pixels[(y * W + x) as usize] = expected_pixel(x, y);
|
||||
}
|
||||
}
|
||||
eprintln!("[client] painted {}x{} ARGB pattern in shm", W, H);
|
||||
|
||||
// Create wl_shm_pool from the fd
|
||||
let shm = state.shm.clone().unwrap();
|
||||
let pool = shm.create_pool(shm_fd.as_fd(), POOL_SIZE, &qh, ());
|
||||
|
||||
// Create wl_buffer
|
||||
let buffer = pool.create_buffer(
|
||||
0, W, H, STRIDE,
|
||||
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||
&qh, (),
|
||||
);
|
||||
|
||||
// Create wl_surface
|
||||
let compositor = state.compositor.clone().unwrap();
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
|
||||
// attach, damage, commit
|
||||
surface.attach(Some(&buffer), 0, 0);
|
||||
surface.damage_buffer(0, 0, W, H);
|
||||
surface.commit();
|
||||
|
||||
eprintln!("[client] attached + damaged + committed");
|
||||
|
||||
// Flush all pending writes
|
||||
if let Err(e) = event_queue.flush() {
|
||||
eprintln!("[client] flush: {e}");
|
||||
libc::_exit(7);
|
||||
}
|
||||
// Roundtrip to ensure server has processed everything
|
||||
let _ = event_queue.roundtrip(&mut state);
|
||||
|
||||
eprintln!("[client] done, exit 0");
|
||||
libc::_exit(0);
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
println!("[test-06] Wayland shm pipeline on Redox");
|
||||
|
||||
// Pick a socket path. We won't use bind_auto because XDG_RUNTIME_DIR
|
||||
// is not guaranteed inside the redoxer mini-VM.
|
||||
let socket_path = format!("/tmp/{SOCKET_NAME}.sock");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
let _ = std::fs::remove_file(format!("/tmp/{SOCKET_NAME}.sock.lock"));
|
||||
|
||||
let listener = UnixListener::bind(&socket_path)
|
||||
.map_err(|e| format!("UnixListener::bind({socket_path}): {e}"))?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.map_err(|e| format!("set_nonblocking: {e}"))?;
|
||||
println!("[server] listening on {socket_path}");
|
||||
|
||||
let obs = Arc::new(Mutex::new(ServerObservation::default()));
|
||||
|
||||
let mut display: ServerDisplay<ServerState> = ServerDisplay::new()
|
||||
.map_err(|e| format!("Display::new: {e:?}"))?;
|
||||
let mut dh = display.handle();
|
||||
dh.create_global::<ServerState, wl_compositor::WlCompositor, _>(5, ());
|
||||
dh.create_global::<ServerState, wl_shm::WlShm, _>(1, ());
|
||||
|
||||
// Fork the client
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
return Err(format!("fork: {}", errno_str()));
|
||||
}
|
||||
if pid == 0 {
|
||||
unsafe { child_main(&socket_path) };
|
||||
}
|
||||
|
||||
let mut state = ServerState { obs: obs.clone() };
|
||||
let start = Instant::now();
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
let mut accepted_pid: Option<libc::pid_t> = None;
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
// Accept new client
|
||||
if accepted_pid.is_none() {
|
||||
match listener.accept() {
|
||||
Ok((stream, _addr)) => {
|
||||
stream.set_nonblocking(true).ok();
|
||||
dh.insert_client(stream, Arc::new(DumbClientData))
|
||||
.map_err(|e| format!("insert_client: {e:?}"))?;
|
||||
accepted_pid = Some(pid);
|
||||
println!("[server] accepted client connection");
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {}
|
||||
Err(e) => return Err(format!("accept: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
display
|
||||
.dispatch_clients(&mut state)
|
||||
.map_err(|e| format!("dispatch_clients: {e:?}"))?;
|
||||
display
|
||||
.flush_clients()
|
||||
.map_err(|e| format!("flush_clients: {e:?}"))?;
|
||||
|
||||
let snap = obs.lock().unwrap();
|
||||
if snap.surface_committed && snap.pool_created_with_fd.is_some() {
|
||||
// We have all we need to verify pixels
|
||||
break;
|
||||
}
|
||||
drop(snap);
|
||||
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// Capture observation state
|
||||
let surface_committed;
|
||||
let buffer_created;
|
||||
let surface_attached;
|
||||
let pool_fd;
|
||||
{
|
||||
let s = obs.lock().unwrap();
|
||||
surface_committed = s.surface_committed;
|
||||
buffer_created = s.buffer_created;
|
||||
surface_attached = s.surface_attached;
|
||||
pool_fd = s.pool_created_with_fd;
|
||||
}
|
||||
|
||||
if !surface_committed {
|
||||
return Err("server did not observe wl_surface.commit within 5s".into());
|
||||
}
|
||||
if buffer_created.is_none() {
|
||||
return Err("server did not observe wl_shm_pool.create_buffer".into());
|
||||
}
|
||||
if !surface_attached {
|
||||
return Err("server did not observe wl_surface.attach".into());
|
||||
}
|
||||
let pool_fd = pool_fd.ok_or("server did not receive shm fd")?;
|
||||
|
||||
println!("[server] all expected protocol events observed; mapping fd to verify pixels");
|
||||
|
||||
// Map the received fd and verify pixels
|
||||
let map = unsafe {
|
||||
libc::mmap(
|
||||
ptr::null_mut(),
|
||||
POOL_SIZE as usize,
|
||||
libc::PROT_READ,
|
||||
libc::MAP_SHARED,
|
||||
pool_fd,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if map == libc::MAP_FAILED {
|
||||
return Err(format!("server mmap: {}", errno_str()));
|
||||
}
|
||||
let pixels = unsafe { std::slice::from_raw_parts(map as *const u32, (W * H) as usize) };
|
||||
let mut mismatches: Vec<(i32, i32, u32, u32)> = Vec::new();
|
||||
for y in 0..H {
|
||||
for x in 0..W {
|
||||
let got = pixels[(y * W + x) as usize];
|
||||
let want = expected_pixel(x, y);
|
||||
if got != want && mismatches.len() < 5 {
|
||||
mismatches.push((x, y, got, want));
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { libc::munmap(map, POOL_SIZE as usize) };
|
||||
if !mismatches.is_empty() {
|
||||
for (x, y, got, want) in &mismatches {
|
||||
eprintln!("[server] mismatch at ({x},{y}): got {got:#010x} want {want:#010x}");
|
||||
}
|
||||
return Err(format!("{} pixel mismatches in {} total", mismatches.len(), W * H));
|
||||
}
|
||||
|
||||
println!(
|
||||
"[server] all {} pixels match expected pattern (read via fd received through Wayland protocol)",
|
||||
W * H
|
||||
);
|
||||
|
||||
// Reap child
|
||||
let mut status: libc::c_int = 0;
|
||||
unsafe { libc::waitpid(pid, &mut status, 0) };
|
||||
let exit_code = unsafe { libc::WEXITSTATUS(status) };
|
||||
if exit_code != 0 {
|
||||
return Err(format!("child exited with status {exit_code}"));
|
||||
}
|
||||
println!("[server] child reaped cleanly");
|
||||
|
||||
// Cleanup
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
let _ = std::fs::remove_file(format!("{socket_path}.lock"));
|
||||
let cname = CString::new("/wl-test-06-buffer").unwrap();
|
||||
unsafe { libc::shm_unlink(cname.as_ptr()) };
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("[test-06] PASS: full Wayland shm pipeline works on Redox");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[test-06] FAIL: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/redox-wl-test-unix-socket/Cargo.toml
Normal file
7
crates/redox-wl-test-unix-socket/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "redox-wl-test-unix-socket"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
78
crates/redox-wl-test-unix-socket/src/main.rs
Normal file
78
crates/redox-wl-test-unix-socket/src/main.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//! Test 1 — Unix socket SOCK_STREAM with Wayland-shaped messages.
|
||||
//!
|
||||
//! Wayland wire format (libwayland reference):
|
||||
//! - Header: object_id (u32) + (size << 16 | opcode) (u32) = 8 bytes
|
||||
//! - Payload: variable, 4-byte aligned
|
||||
//!
|
||||
//! This test:
|
||||
//! 1. Creates a SOCK_STREAM socketpair (AF_UNIX)
|
||||
//! 2. Side A sends a message with header + payload
|
||||
//! 3. Side B reads it back, validates byte-for-byte
|
||||
//! 4. Ensures partial reads don't corrupt the stream
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::process::ExitCode;
|
||||
|
||||
const OBJECT_ID: u32 = 0x0000_0001;
|
||||
const OPCODE: u16 = 0x000A;
|
||||
const PAYLOAD: &[u8] = b"hello-wayland-on-redox-roundtrip";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
println!("[test-01] Unix socket SOCK_STREAM Wayland-shaped roundtrip");
|
||||
|
||||
let (mut a, mut b) = match UnixStream::pair() {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
eprintln!("[test-01] FAIL: UnixStream::pair: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let payload_len = PAYLOAD.len();
|
||||
let aligned_len = (payload_len + 3) & !3;
|
||||
let total_size = 8 + aligned_len;
|
||||
|
||||
let mut msg = Vec::with_capacity(total_size);
|
||||
msg.extend_from_slice(&OBJECT_ID.to_ne_bytes());
|
||||
let size_op: u32 = ((total_size as u32) << 16) | (OPCODE as u32);
|
||||
msg.extend_from_slice(&size_op.to_ne_bytes());
|
||||
msg.extend_from_slice(PAYLOAD);
|
||||
msg.resize(total_size, 0);
|
||||
|
||||
if let Err(e) = a.write_all(&msg) {
|
||||
eprintln!("[test-01] FAIL: write_all: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
println!("[test-01] sent {} bytes (header 8 + payload {} aligned to {})",
|
||||
total_size, payload_len, aligned_len);
|
||||
|
||||
let mut header = [0u8; 8];
|
||||
if let Err(e) = b.read_exact(&mut header) {
|
||||
eprintln!("[test-01] FAIL: read header: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let recv_oid = u32::from_ne_bytes(header[0..4].try_into().unwrap());
|
||||
let recv_so = u32::from_ne_bytes(header[4..8].try_into().unwrap());
|
||||
let recv_size = (recv_so >> 16) as usize;
|
||||
let recv_op = (recv_so & 0xFFFF) as u16;
|
||||
|
||||
if recv_oid != OBJECT_ID || recv_op != OPCODE || recv_size != total_size {
|
||||
eprintln!("[test-01] FAIL: header mismatch oid={recv_oid:#x} op={recv_op:#x} size={recv_size}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let mut buf = vec![0u8; aligned_len];
|
||||
if let Err(e) = b.read_exact(&mut buf) {
|
||||
eprintln!("[test-01] FAIL: read payload: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
if &buf[..payload_len] != PAYLOAD {
|
||||
eprintln!("[test-01] FAIL: payload mismatch");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
println!("[test-01] PASS: roundtrip OK, {} bytes recv, oid={:#x} op={:#x}",
|
||||
total_size, recv_oid, recv_op);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue