From f6fbbd1b12b2dd5b82d615724356c5a8e73b6653 Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Fri, 23 Jan 2026 20:17:10 -0500 Subject: [PATCH 1/2] feat(socket): implement Unix domain sockets with socketpair() syscall Add AF_UNIX socket support for local IPC via socketpair(): Kernel implementation: - UnixStreamPair and UnixStreamSocket in kernel/src/socket/unix.rs - 64KB buffer per socket with VecDeque-based ring buffer - Bidirectional communication (full duplex) - SOCK_NONBLOCK support (EAGAIN on empty read/full write) - SOCK_CLOEXEC support (FD_CLOEXEC propagation) - EOF detection when peer closes - EPIPE on write to closed peer - Error handling: EAFNOSUPPORT, EINVAL, EFAULT Userspace support: - socketpair() wrapper in libbreenix - EAFNOSUPPORT errno added (97) Test coverage (12 phases): - Socket pair creation and fd validation - Bidirectional data transfer - EOF on peer close - SOCK_NONBLOCK with EAGAIN - EPIPE on broken pipe - Error handling (bad domain/type/protocol/pointer) - Buffer-full EAGAIN scenario - SOCK_CLOEXEC flag verification Co-Authored-By: Claude Opus 4.5 --- kernel/src/ipc/fd.rs | 11 + kernel/src/ipc/poll.rs | 19 ++ kernel/src/main.rs | 4 + kernel/src/process/process.rs | 5 + kernel/src/socket/mod.rs | 1 + kernel/src/socket/types.rs | 13 + kernel/src/socket/unix.rs | 259 ++++++++++++++ kernel/src/syscall/dispatcher.rs | 2 + kernel/src/syscall/handlers.rs | 132 +++++++ kernel/src/syscall/mod.rs | 4 + kernel/src/syscall/pipe.rs | 5 + kernel/src/syscall/socket.rs | 131 +++++++ kernel/src/test_exec.rs | 35 ++ libs/libbreenix/src/errno.rs | 3 + libs/libbreenix/src/socket.rs | 37 ++ libs/libbreenix/src/syscall.rs | 2 + userspace/tests/Cargo.toml | 4 + userspace/tests/build.sh | 3 + userspace/tests/unix_socket_test.rs | 511 ++++++++++++++++++++++++++++ 19 files changed, 1181 insertions(+) create mode 100644 kernel/src/socket/unix.rs create mode 100644 userspace/tests/unix_socket_test.rs diff --git a/kernel/src/ipc/fd.rs b/kernel/src/ipc/fd.rs index 02dfd2dd..8277731e 100644 --- a/kernel/src/ipc/fd.rs +++ b/kernel/src/ipc/fd.rs @@ -114,6 +114,8 @@ pub enum FdKind { /// Allow unused - constructed when opening /dev/pts/N in Phase 2 #[allow(dead_code)] PtySlave(u32), + /// Unix stream socket (AF_UNIX, SOCK_STREAM) - for socketpair IPC + UnixStream(alloc::sync::Arc>), } impl core::fmt::Debug for FdKind { @@ -133,6 +135,10 @@ impl core::fmt::Debug for FdKind { FdKind::DevptsDirectory { position } => write!(f, "DevptsDirectory(pos={})", position), FdKind::PtyMaster(n) => write!(f, "PtyMaster({})", n), FdKind::PtySlave(n) => write!(f, "PtySlave({})", n), + FdKind::UnixStream(s) => { + let sock = s.lock(); + write!(f, "UnixStream({:?})", sock.endpoint) + } } } } @@ -497,6 +503,11 @@ impl Drop for FdTable { // PTY slave doesn't own the pair, just decrement reference log::debug!("FdTable::drop() - released PTY slave fd {}", i); } + FdKind::UnixStream(socket) => { + // Close the Unix socket endpoint + socket.lock().close(); + log::debug!("FdTable::drop() - closed Unix stream socket fd {}", i); + } } } } diff --git a/kernel/src/ipc/poll.rs b/kernel/src/ipc/poll.rs index cf3ff391..757a0781 100644 --- a/kernel/src/ipc/poll.rs +++ b/kernel/src/ipc/poll.rs @@ -232,6 +232,25 @@ pub fn poll_fd(fd_entry: &FileDescriptor, events: i16) -> i16 { revents |= events::POLLERR; } } + FdKind::UnixStream(socket_ref) => { + let socket = socket_ref.lock(); + // Check for readable data + if (events & events::POLLIN) != 0 { + if socket.has_data() { + revents |= events::POLLIN; + } + } + // Check for writable + if (events & events::POLLOUT) != 0 { + if !socket.peer_closed() { + revents |= events::POLLOUT; + } + } + // Check for peer closed + if socket.peer_closed() && !socket.has_data() { + revents |= events::POLLHUP; + } + } } revents diff --git a/kernel/src/main.rs b/kernel/src/main.rs index b836e3b1..e3953234 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -1048,6 +1048,10 @@ fn kernel_main_continue() -> ! { log::info!("=== IPC TEST: Pipe syscall functionality ==="); test_exec::test_pipe(); + // Test Unix domain socket (AF_UNIX) socketpair syscall + log::info!("=== IPC TEST: Unix domain socket (socketpair) functionality ==="); + test_exec::test_unix_socket(); + // NOTE: Pipe + fork and concurrent pipe tests removed to reduce test load. // The core pipe functionality is validated by test_pipe() above. // These complex multi-process tests cause timing-related timeouts. diff --git a/kernel/src/process/process.rs b/kernel/src/process/process.rs index 44c5d097..1ff84d5e 100644 --- a/kernel/src/process/process.rs +++ b/kernel/src/process/process.rs @@ -290,6 +290,11 @@ impl Process { // PTY slave doesn't own the pair, just decrement reference log::debug!("Process::close_all_fds() - released PTY slave fd {}", fd); } + FdKind::UnixStream(socket) => { + // Close Unix socket endpoint + socket.lock().close(); + log::debug!("Process::close_all_fds() - closed Unix stream socket fd {}", fd); + } } } } diff --git a/kernel/src/socket/mod.rs b/kernel/src/socket/mod.rs index f97bb907..1a397b94 100644 --- a/kernel/src/socket/mod.rs +++ b/kernel/src/socket/mod.rs @@ -4,6 +4,7 @@ pub mod types; pub mod udp; +pub mod unix; use spin::Mutex; diff --git a/kernel/src/socket/types.rs b/kernel/src/socket/types.rs index 6ca534b1..97114ba8 100644 --- a/kernel/src/socket/types.rs +++ b/kernel/src/socket/types.rs @@ -2,6 +2,13 @@ //! //! POSIX-compatible socket address structures. +/// Address family: Unix (local) +pub const AF_UNIX: u16 = 1; + +/// Address family: Unix (alias for AF_UNIX) +#[allow(dead_code)] +pub const AF_LOCAL: u16 = 1; + /// Address family: IPv4 pub const AF_INET: u16 = 2; @@ -11,6 +18,12 @@ pub const SOCK_STREAM: u16 = 1; /// Socket type: Datagram (UDP) pub const SOCK_DGRAM: u16 = 2; +/// Socket flag: Non-blocking mode +pub const SOCK_NONBLOCK: u32 = 0x800; + +/// Socket flag: Close-on-exec +pub const SOCK_CLOEXEC: u32 = 0x80000; + /// IPv4 socket address structure (matches Linux sockaddr_in) #[repr(C)] #[derive(Debug, Clone, Copy)] diff --git a/kernel/src/socket/unix.rs b/kernel/src/socket/unix.rs new file mode 100644 index 00000000..a9b1ea58 --- /dev/null +++ b/kernel/src/socket/unix.rs @@ -0,0 +1,259 @@ +//! Unix domain socket implementation +//! +//! Provides AF_UNIX socket support for local inter-process communication. +//! Currently supports SOCK_STREAM via socketpair(). + +use alloc::collections::VecDeque; +use alloc::sync::Arc; +use alloc::vec::Vec; +use spin::Mutex; + +/// Default buffer size for Unix stream sockets (64 KB) +const UNIX_SOCKET_BUFFER_SIZE: usize = 65536; + +/// Shared state for a Unix stream socket pair +/// +/// This structure is shared between both endpoints of a socketpair. +/// Each endpoint writes to one buffer and reads from the other. +pub struct UnixStreamPair { + /// Buffer A→B (endpoint A writes here, endpoint B reads from here) + buffer_a_to_b: Mutex>, + /// Buffer B→A (endpoint B writes here, endpoint A reads from here) + buffer_b_to_a: Mutex>, + /// Threads waiting to read on endpoint A (waiting for data in buffer_b_to_a) + waiters_a: Mutex>, + /// Threads waiting to read on endpoint B (waiting for data in buffer_a_to_b) + waiters_b: Mutex>, + /// Endpoint A closed + closed_a: Mutex, + /// Endpoint B closed + closed_b: Mutex, +} + +impl UnixStreamPair { + /// Create a new Unix stream socket pair + pub fn new() -> Self { + UnixStreamPair { + buffer_a_to_b: Mutex::new(VecDeque::with_capacity(UNIX_SOCKET_BUFFER_SIZE)), + buffer_b_to_a: Mutex::new(VecDeque::with_capacity(UNIX_SOCKET_BUFFER_SIZE)), + waiters_a: Mutex::new(Vec::new()), + waiters_b: Mutex::new(Vec::new()), + closed_a: Mutex::new(false), + closed_b: Mutex::new(false), + } + } +} + +impl Default for UnixStreamPair { + fn default() -> Self { + Self::new() + } +} + +/// Which endpoint of the socket pair this is +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UnixEndpoint { + A, + B, +} + +/// A Unix stream socket endpoint +/// +/// This is a file descriptor wrapper around one end of a UnixStreamPair. +pub struct UnixStreamSocket { + /// The shared socket pair state + pub pair: Arc, + /// Which endpoint this socket represents + pub endpoint: UnixEndpoint, + /// Non-blocking mode + pub nonblocking: bool, +} + +impl UnixStreamSocket { + /// Create a new pair of connected Unix stream sockets + pub fn new_pair(nonblocking: bool) -> (Arc>, Arc>) { + let pair = Arc::new(UnixStreamPair::new()); + + let socket_a = Arc::new(Mutex::new(UnixStreamSocket { + pair: pair.clone(), + endpoint: UnixEndpoint::A, + nonblocking, + })); + + let socket_b = Arc::new(Mutex::new(UnixStreamSocket { + pair, + endpoint: UnixEndpoint::B, + nonblocking, + })); + + (socket_a, socket_b) + } + + /// Write data to the socket (sends to peer) + /// + /// Returns the number of bytes written, or an error code. + pub fn write(&self, data: &[u8]) -> Result { + // Check if peer is closed + let peer_closed = match self.endpoint { + UnixEndpoint::A => *self.pair.closed_b.lock(), + UnixEndpoint::B => *self.pair.closed_a.lock(), + }; + + if peer_closed { + return Err(crate::syscall::errno::EPIPE); + } + + // Get the buffer to write to (peer's read buffer) + let buffer = match self.endpoint { + UnixEndpoint::A => &self.pair.buffer_a_to_b, + UnixEndpoint::B => &self.pair.buffer_b_to_a, + }; + + // Write data to buffer + let mut buf = buffer.lock(); + + // Check available space + let available = UNIX_SOCKET_BUFFER_SIZE.saturating_sub(buf.len()); + if available == 0 { + if self.nonblocking { + return Err(crate::syscall::errno::EAGAIN); + } + // For blocking mode, we'd need to block here + // For now, just write nothing and return EAGAIN + return Err(crate::syscall::errno::EAGAIN); + } + + let to_write = data.len().min(available); + for &byte in &data[..to_write] { + buf.push_back(byte); + } + + drop(buf); + + // Wake waiting readers on the peer endpoint + let waiters = match self.endpoint { + UnixEndpoint::A => &self.pair.waiters_b, + UnixEndpoint::B => &self.pair.waiters_a, + }; + + let waiter_ids: Vec = waiters.lock().clone(); + for thread_id in waiter_ids { + crate::task::scheduler::with_scheduler(|sched| { + sched.unblock(thread_id); + }); + } + + Ok(to_write) + } + + /// Read data from the socket (receives from peer) + /// + /// Returns the number of bytes read, or an error code. + pub fn read(&self, buf: &mut [u8]) -> Result { + // Get the buffer to read from + let buffer = match self.endpoint { + UnixEndpoint::A => &self.pair.buffer_b_to_a, + UnixEndpoint::B => &self.pair.buffer_a_to_b, + }; + + let mut rx_buf = buffer.lock(); + + if rx_buf.is_empty() { + // Check if peer is closed + let peer_closed = match self.endpoint { + UnixEndpoint::A => *self.pair.closed_b.lock(), + UnixEndpoint::B => *self.pair.closed_a.lock(), + }; + + if peer_closed { + // EOF - peer closed and no more data + return Ok(0); + } + + if self.nonblocking { + return Err(crate::syscall::errno::EAGAIN); + } + + // For blocking mode, indicate no data available + // The caller (sys_read) handles the blocking logic + return Err(crate::syscall::errno::EAGAIN); + } + + // Read available data + let to_read = buf.len().min(rx_buf.len()); + for i in 0..to_read { + buf[i] = rx_buf.pop_front().unwrap(); + } + + Ok(to_read) + } + + /// Check if data is available for reading + pub fn has_data(&self) -> bool { + let buffer = match self.endpoint { + UnixEndpoint::A => &self.pair.buffer_b_to_a, + UnixEndpoint::B => &self.pair.buffer_a_to_b, + }; + !buffer.lock().is_empty() + } + + /// Check if peer has closed + pub fn peer_closed(&self) -> bool { + match self.endpoint { + UnixEndpoint::A => *self.pair.closed_b.lock(), + UnixEndpoint::B => *self.pair.closed_a.lock(), + } + } + + /// Register a thread as waiting for data + pub fn register_waiter(&self, thread_id: u64) { + let waiters = match self.endpoint { + UnixEndpoint::A => &self.pair.waiters_a, + UnixEndpoint::B => &self.pair.waiters_b, + }; + let mut w = waiters.lock(); + if !w.contains(&thread_id) { + w.push(thread_id); + } + } + + /// Unregister a thread from waiting + pub fn unregister_waiter(&self, thread_id: u64) { + let waiters = match self.endpoint { + UnixEndpoint::A => &self.pair.waiters_a, + UnixEndpoint::B => &self.pair.waiters_b, + }; + waiters.lock().retain(|&id| id != thread_id); + } + + /// Mark this endpoint as closed and wake any waiters on peer + pub fn close(&self) { + // Mark ourselves as closed + match self.endpoint { + UnixEndpoint::A => *self.pair.closed_a.lock() = true, + UnixEndpoint::B => *self.pair.closed_b.lock() = true, + } + + // Wake peer's waiters (they'll see EOF) + let waiters = match self.endpoint { + UnixEndpoint::A => &self.pair.waiters_b, + UnixEndpoint::B => &self.pair.waiters_a, + }; + + let waiter_ids: Vec = waiters.lock().clone(); + for thread_id in waiter_ids { + crate::task::scheduler::with_scheduler(|sched| { + sched.unblock(thread_id); + }); + } + } +} + +impl core::fmt::Debug for UnixStreamSocket { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("UnixStreamSocket") + .field("endpoint", &self.endpoint) + .field("nonblocking", &self.nonblocking) + .finish() + } +} diff --git a/kernel/src/syscall/dispatcher.rs b/kernel/src/syscall/dispatcher.rs index c89941f3..920dc800 100644 --- a/kernel/src/syscall/dispatcher.rs +++ b/kernel/src/syscall/dispatcher.rs @@ -58,6 +58,7 @@ pub fn dispatch_syscall( SyscallNumber::Accept => super::socket::sys_accept(arg1, arg2, arg3), SyscallNumber::Listen => super::socket::sys_listen(arg1, arg2), SyscallNumber::Shutdown => super::socket::sys_shutdown(arg1, arg2), + SyscallNumber::Socketpair => super::socket::sys_socketpair(arg1, arg2, arg3, arg4), SyscallNumber::Poll => handlers::sys_poll(arg1, arg2, arg3 as i32), SyscallNumber::Select => handlers::sys_select(arg1 as i32, arg2, arg3, arg4, arg5), SyscallNumber::Pipe => super::pipe::sys_pipe(arg1), @@ -94,6 +95,7 @@ pub fn dispatch_syscall( SyscallNumber::Ptsname => super::pty::sys_ptsname(arg1, arg2, arg3), // Graphics syscalls (Breenix-specific) SyscallNumber::FbInfo => super::graphics::sys_fbinfo(arg1), + SyscallNumber::FbDraw => super::graphics::sys_fbdraw(arg1), // Testing/diagnostic syscalls (Breenix-specific) SyscallNumber::CowStats => super::handlers::sys_cow_stats(arg1), SyscallNumber::SimulateOom => super::handlers::sys_simulate_oom(arg1), diff --git a/kernel/src/syscall/handlers.rs b/kernel/src/syscall/handlers.rs index 5e687c75..77c20b0a 100644 --- a/kernel/src/syscall/handlers.rs +++ b/kernel/src/syscall/handlers.rs @@ -442,6 +442,20 @@ pub fn sys_write(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { } } } + FdKind::UnixStream(socket_ref) => { + // Write to Unix stream socket + let socket = socket_ref.lock(); + match socket.write(&buffer) { + Ok(n) => { + log::debug!("sys_write: Wrote {} bytes to Unix socket", n); + SyscallResult::Ok(n as u64) + } + Err(e) => { + log::debug!("sys_write: Unix socket write error: {}", e); + SyscallResult::Err(e as u64) + } + } + } } } @@ -931,6 +945,124 @@ pub fn sys_read(fd: u64, buf_ptr: u64, count: u64) -> SyscallResult { } } } + FdKind::UnixStream(socket_ref) => { + // Read from Unix stream socket + let is_nonblocking = (fd_entry.status_flags & crate::ipc::fd::status_flags::O_NONBLOCK) != 0; + let socket_clone = socket_ref.clone(); + + // Drop manager guard before potentially blocking + drop(manager_guard); + + let mut user_buf = alloc::vec![0u8; count as usize]; + + loop { + // Register as waiter FIRST to avoid race condition + let socket = socket_clone.lock(); + socket.register_waiter(thread_id); + drop(socket); + + // Try to read + let socket = socket_clone.lock(); + match socket.read(&mut user_buf) { + Ok(n) => { + socket.unregister_waiter(thread_id); + drop(socket); + + if n > 0 { + // Copy to userspace + if copy_to_user(buf_ptr, user_buf.as_ptr() as u64, n).is_err() { + return SyscallResult::Err(14); // EFAULT + } + } + log::debug!("sys_read: Read {} bytes from Unix socket", n); + return SyscallResult::Ok(n as u64); + } + Err(11) => { + // EAGAIN - no data available + if is_nonblocking { + socket.unregister_waiter(thread_id); + drop(socket); + return SyscallResult::Err(11); // EAGAIN + } + + // Check if peer closed (EOF case) + if socket.peer_closed() { + socket.unregister_waiter(thread_id); + drop(socket); + return SyscallResult::Ok(0); // EOF + } + + drop(socket); + + // Block the thread + crate::task::scheduler::with_scheduler(|sched| { + sched.block_current(); + if let Some(thread) = sched.current_thread_mut() { + thread.blocked_in_syscall = true; + } + }); + + // Double-check for data after setting Blocked state + let socket = socket_clone.lock(); + if socket.has_data() || socket.peer_closed() { + socket.unregister_waiter(thread_id); + drop(socket); + crate::task::scheduler::with_scheduler(|sched| { + if let Some(thread) = sched.current_thread_mut() { + thread.blocked_in_syscall = false; + thread.set_ready(); + } + }); + continue; + } + drop(socket); + + // Re-enable preemption before HLT loop + crate::per_cpu::preempt_enable(); + + // HLT loop + loop { + crate::task::scheduler::yield_current(); + x86_64::instructions::interrupts::enable_and_hlt(); + + let still_blocked = crate::task::scheduler::with_scheduler(|sched| { + if let Some(thread) = sched.current_thread_mut() { + thread.state == crate::task::thread::ThreadState::Blocked + } else { + false + } + }).unwrap_or(false); + + if !still_blocked { + crate::per_cpu::preempt_disable(); + break; + } + } + + // Clear blocked_in_syscall + crate::task::scheduler::with_scheduler(|sched| { + if let Some(thread) = sched.current_thread_mut() { + thread.blocked_in_syscall = false; + } + }); + crate::interrupts::timer::reset_quantum(); + crate::task::scheduler::check_and_clear_need_resched(); + + // Unregister and retry + let socket = socket_clone.lock(); + socket.unregister_waiter(thread_id); + drop(socket); + continue; + } + Err(e) => { + socket.unregister_waiter(thread_id); + drop(socket); + log::debug!("sys_read: Unix socket read error: {}", e); + return SyscallResult::Err(e as u64); + } + } + } + } } } diff --git a/kernel/src/syscall/mod.rs b/kernel/src/syscall/mod.rs index 79442523..a7f58b1d 100644 --- a/kernel/src/syscall/mod.rs +++ b/kernel/src/syscall/mod.rs @@ -58,6 +58,7 @@ pub enum SyscallNumber { Shutdown = 48, // Linux syscall number for shutdown Bind = 49, // Linux syscall number for bind Listen = 50, // Linux syscall number for listen + Socketpair = 53, // Linux syscall number for socketpair Exec = 59, // Linux syscall number for execve Wait4 = 61, // Linux syscall number for wait4/waitpid Kill = 62, // Linux syscall number for kill @@ -90,6 +91,7 @@ pub enum SyscallNumber { Ptsname = 403, // Breenix: get PTY slave path // Graphics syscalls (Breenix-specific) FbInfo = 410, // Breenix: get framebuffer info + FbDraw = 411, // Breenix: draw to framebuffer (left pane) CowStats = 500, // Breenix: get Copy-on-Write statistics (for testing) SimulateOom = 501, // Breenix: enable/disable OOM simulation (for testing) } @@ -130,6 +132,7 @@ impl SyscallNumber { 48 => Some(Self::Shutdown), 49 => Some(Self::Bind), 50 => Some(Self::Listen), + 53 => Some(Self::Socketpair), 59 => Some(Self::Exec), 61 => Some(Self::Wait4), 62 => Some(Self::Kill), @@ -162,6 +165,7 @@ impl SyscallNumber { 403 => Some(Self::Ptsname), // Graphics syscalls 410 => Some(Self::FbInfo), + 411 => Some(Self::FbDraw), 500 => Some(Self::CowStats), 501 => Some(Self::SimulateOom), _ => None, diff --git a/kernel/src/syscall/pipe.rs b/kernel/src/syscall/pipe.rs index 244fa5d7..e7849d8a 100644 --- a/kernel/src/syscall/pipe.rs +++ b/kernel/src/syscall/pipe.rs @@ -210,6 +210,11 @@ pub fn sys_close(fd: i32) -> SyscallResult { // PTY slave doesn't own the pair, just log closure log::debug!("sys_close: Closed PTY slave fd={} (pty {})", fd, pty_num); } + FdKind::UnixStream(socket) => { + // Close Unix socket endpoint + socket.lock().close(); + log::debug!("sys_close: Closed Unix stream socket fd={}", fd); + } } log::debug!("sys_close: returning to userspace fd={}", fd); SyscallResult::Ok(0) diff --git a/kernel/src/syscall/socket.rs b/kernel/src/syscall/socket.rs index ea98064d..488bc00f 100644 --- a/kernel/src/syscall/socket.rs +++ b/kernel/src/syscall/socket.rs @@ -1203,6 +1203,137 @@ pub fn sys_shutdown(fd: u64, how: u64) -> SyscallResult { } } +/// sys_socketpair - Create a pair of connected Unix domain sockets +/// +/// Arguments: +/// domain: Address family (must be AF_UNIX = 1) +/// sock_type: Socket type (SOCK_STREAM = 1, optionally OR'd with SOCK_NONBLOCK/SOCK_CLOEXEC) +/// protocol: Protocol (must be 0) +/// sv_ptr: Pointer to int[2] to receive the file descriptors +/// +/// Returns: 0 on success, negative errno on error +pub fn sys_socketpair(domain: u64, sock_type: u64, protocol: u64, sv_ptr: u64) -> SyscallResult { + use crate::socket::types::{AF_UNIX, SOCK_CLOEXEC, SOCK_NONBLOCK}; + use crate::socket::unix::UnixStreamSocket; + use crate::ipc::fd::{FileDescriptor, flags, status_flags}; + + log::debug!( + "sys_socketpair: domain={}, type={:#x}, protocol={}, sv_ptr={:#x}", + domain, sock_type, protocol, sv_ptr + ); + + // Validate domain - must be AF_UNIX + if domain as u16 != AF_UNIX { + log::debug!("sys_socketpair: unsupported domain {}", domain); + return SyscallResult::Err(EAFNOSUPPORT as u64); + } + + // Validate protocol - must be 0 + if protocol != 0 { + log::debug!("sys_socketpair: unsupported protocol {}", protocol); + return SyscallResult::Err(EINVAL as u64); + } + + // Extract flags from sock_type + let nonblocking = (sock_type as u32 & SOCK_NONBLOCK) != 0; + let cloexec = (sock_type as u32 & SOCK_CLOEXEC) != 0; + let base_type = sock_type as u32 & !(SOCK_NONBLOCK | SOCK_CLOEXEC); + + // Validate socket type - only SOCK_STREAM supported for now + if base_type != crate::socket::types::SOCK_STREAM as u32 { + log::debug!("sys_socketpair: unsupported socket type {}", base_type); + return SyscallResult::Err(EINVAL as u64); + } + + // Validate output pointer + if sv_ptr == 0 { + return SyscallResult::Err(EFAULT as u64); + } + + // Get current thread and process + let current_thread_id = match crate::per_cpu::current_thread() { + Some(thread) => thread.id, + None => { + log::error!("sys_socketpair: No current thread in per-CPU data!"); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let mut manager_guard = crate::process::manager(); + let manager = match *manager_guard { + Some(ref mut m) => m, + None => { + log::error!("sys_socketpair: No process manager!"); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + let (_pid, process) = match manager.find_process_by_thread_mut(current_thread_id) { + Some(p) => p, + None => { + log::error!("sys_socketpair: No process found for thread_id={}", current_thread_id); + return SyscallResult::Err(ErrorCode::NoSuchProcess as u64); + } + }; + + // Create the socket pair + let (socket_a, socket_b) = UnixStreamSocket::new_pair(nonblocking); + + // Create file descriptor entries with appropriate flags + let fd_flags = if cloexec { flags::FD_CLOEXEC } else { 0 }; + let fd_status_flags = if nonblocking { status_flags::O_NONBLOCK } else { 0 }; + + let fd_entry_a = FileDescriptor::with_flags( + FdKind::UnixStream(socket_a), + fd_flags, + fd_status_flags, + ); + let fd_entry_b = FileDescriptor::with_flags( + FdKind::UnixStream(socket_b), + fd_flags, + fd_status_flags, + ); + + // Allocate file descriptors + let fd_a = match process.fd_table.alloc_with_entry(fd_entry_a) { + Ok(fd) => fd, + Err(e) => { + log::error!("sys_socketpair: Failed to allocate fd_a: {}", e); + return SyscallResult::Err(e as u64); + } + }; + + let fd_b = match process.fd_table.alloc_with_entry(fd_entry_b) { + Ok(fd) => fd, + Err(e) => { + // Clean up fd_a on failure + let _ = process.fd_table.close(fd_a); + log::error!("sys_socketpair: Failed to allocate fd_b: {}", e); + return SyscallResult::Err(e as u64); + } + }; + + // Write the file descriptors to userspace sv[0], sv[1] + let sv: [i32; 2] = [fd_a, fd_b]; + unsafe { + let sv_user = sv_ptr as *mut [i32; 2]; + // Validate pointer is in userspace range + if sv_ptr < 0x1000 || sv_ptr > 0x7FFFFFFFFFFF { + let _ = process.fd_table.close(fd_a); + let _ = process.fd_table.close(fd_b); + return SyscallResult::Err(EFAULT as u64); + } + core::ptr::write_volatile(sv_user, sv); + } + + log::info!( + "sys_socketpair: Created Unix socket pair sv=[{}, {}] nonblocking={} cloexec={}", + fd_a, fd_b, nonblocking, cloexec + ); + + SyscallResult::Ok(0) +} + #[cfg(test)] mod tests { use super::*; diff --git a/kernel/src/test_exec.rs b/kernel/src/test_exec.rs index ec321a32..77783d6f 100644 --- a/kernel/src/test_exec.rs +++ b/kernel/src/test_exec.rs @@ -1024,6 +1024,41 @@ pub fn test_signal_regs() { } } +/// Test Unix domain socket (AF_UNIX) socketpair syscall +/// +/// TWO-STAGE VALIDATION PATTERN: +/// - Stage 1 (Checkpoint): Process creation +/// - Marker: "Unix socket test: process scheduled for execution" +/// - This is a CHECKPOINT confirming process creation succeeded +/// - Stage 2 (Boot stage): Validates socketpair read/write/close functionality +/// - Marker: "UNIX_SOCKET_TEST_PASSED" +/// - This PROVES Unix socket creation, bidirectional IPC, and EOF on close work +pub fn test_unix_socket() { + log::info!("Testing Unix domain socket (socketpair) functionality"); + + #[cfg(feature = "testing")] + let unix_socket_test_elf_buf = crate::userspace_test::get_test_binary("unix_socket_test"); + #[cfg(feature = "testing")] + let unix_socket_test_elf: &[u8] = &unix_socket_test_elf_buf; + #[cfg(not(feature = "testing"))] + let unix_socket_test_elf = &create_hello_world_elf(); + + match crate::process::creation::create_user_process( + String::from("unix_socket_test"), + unix_socket_test_elf, + ) { + Ok(pid) => { + log::info!("Created unix_socket_test process with PID {:?}", pid); + log::info!("Unix socket test: process scheduled for execution."); + log::info!(" -> Emits pass marker on success (UNIX_SOCKET_TEST_...)"); + } + Err(e) => { + log::error!("Failed to create unix_socket_test process: {}", e); + log::error!("Unix socket test cannot run without valid userspace process"); + } + } +} + /// Test pipe syscall functionality /// /// TWO-STAGE VALIDATION PATTERN: diff --git a/libs/libbreenix/src/errno.rs b/libs/libbreenix/src/errno.rs index ec706561..598f29db 100644 --- a/libs/libbreenix/src/errno.rs +++ b/libs/libbreenix/src/errno.rs @@ -74,6 +74,8 @@ pub enum Errno { ENOSYS = 38, /// Directory not empty ENOTEMPTY = 39, + /// Address family not supported + EAFNOSUPPORT = 97, } impl Errno { @@ -125,6 +127,7 @@ impl Errno { 32 => Errno::EPIPE, 38 => Errno::ENOSYS, 39 => Errno::ENOTEMPTY, + 97 => Errno::EAFNOSUPPORT, _ => Errno::EINVAL, // Unknown error } } diff --git a/libs/libbreenix/src/socket.rs b/libs/libbreenix/src/socket.rs index 90633f37..4e3ea653 100644 --- a/libs/libbreenix/src/socket.rs +++ b/libs/libbreenix/src/socket.rs @@ -21,6 +21,12 @@ use crate::syscall::{nr, raw}; +/// Address family: Unix (local) +pub const AF_UNIX: i32 = 1; + +/// Address family: Unix (alias) +pub const AF_LOCAL: i32 = 1; + /// Address family: IPv4 pub const AF_INET: i32 = 2; @@ -33,6 +39,9 @@ pub const SOCK_DGRAM: i32 = 2; /// Socket flag: Non-blocking pub const SOCK_NONBLOCK: i32 = 0x800; +/// Socket flag: Close-on-exec +pub const SOCK_CLOEXEC: i32 = 0x80000; + /// Shutdown how: Stop receiving pub const SHUT_RD: i32 = 0; @@ -327,3 +336,31 @@ pub fn shutdown(fd: i32, how: i32) -> Result<(), i32> { Ok(()) } } + +/// Create a pair of connected Unix domain sockets +/// +/// # Arguments +/// * `domain` - Address family (must be AF_UNIX) +/// * `sock_type` - Socket type (SOCK_STREAM, optionally OR'd with SOCK_NONBLOCK, SOCK_CLOEXEC) +/// * `protocol` - Protocol (must be 0) +/// +/// # Returns +/// Tuple of two file descriptors (sv[0], sv[1]) on success, or negative errno on error +pub fn socketpair(domain: i32, sock_type: i32, protocol: i32) -> Result<(i32, i32), i32> { + let mut sv: [i32; 2] = [0, 0]; + let ret = unsafe { + raw::syscall4( + nr::SOCKETPAIR, + domain as u64, + sock_type as u64, + protocol as u64, + sv.as_mut_ptr() as u64, + ) + }; + + if (ret as i64) < 0 { + Err(-(ret as i64) as i32) + } else { + Ok((sv[0], sv[1])) + } +} diff --git a/libs/libbreenix/src/syscall.rs b/libs/libbreenix/src/syscall.rs index c0c26d45..daa1b027 100644 --- a/libs/libbreenix/src/syscall.rs +++ b/libs/libbreenix/src/syscall.rs @@ -45,6 +45,7 @@ pub mod nr { pub const SHUTDOWN: u64 = 48; pub const BIND: u64 = 49; pub const LISTEN: u64 = 50; + pub const SOCKETPAIR: u64 = 53; // Linux x86_64 socketpair pub const EXEC: u64 = 59; // Linux x86_64 execve pub const WAIT4: u64 = 61; // Linux x86_64 wait4/waitpid pub const KILL: u64 = 62; // Linux x86_64 kill @@ -67,6 +68,7 @@ pub mod nr { pub const GETDENTS64: u64 = 260; // Breenix: directory listing syscall // Graphics syscalls (Breenix-specific) pub const FBINFO: u64 = 410; // Breenix: get framebuffer info + pub const FBDRAW: u64 = 411; // Breenix: draw to framebuffer // Testing syscalls (Breenix-specific) pub const COW_STATS: u64 = 500; // Breenix: get CoW statistics (for testing) pub const SIMULATE_OOM: u64 = 501; // Breenix: enable/disable OOM simulation (for testing) diff --git a/userspace/tests/Cargo.toml b/userspace/tests/Cargo.toml index bb3ba262..afe32ef2 100644 --- a/userspace/tests/Cargo.toml +++ b/userspace/tests/Cargo.toml @@ -460,6 +460,10 @@ path = "echo_argv_test.rs" name = "rm_argv_test" path = "rm_argv_test.rs" +[[bin]] +name = "unix_socket_test" +path = "unix_socket_test.rs" + [profile.release] panic = "abort" lto = true diff --git a/userspace/tests/build.sh b/userspace/tests/build.sh index 0f8d0a67..2b506dd9 100755 --- a/userspace/tests/build.sh +++ b/userspace/tests/build.sh @@ -53,6 +53,7 @@ BINARIES=( "tcp_socket_test" "tcp_client_test" "tcp_blocking_test" + "unix_socket_test" "dns_test" "http_test" "pipe_test" @@ -139,6 +140,8 @@ BINARIES=( # Graphics syscall tests "fbinfo_test" "resolution" + "demo" + "bounce" # Coreutils argv integration tests "mkdir_argv_test" "cp_mv_argv_test" diff --git a/userspace/tests/unix_socket_test.rs b/userspace/tests/unix_socket_test.rs new file mode 100644 index 00000000..6c7bf4f4 --- /dev/null +++ b/userspace/tests/unix_socket_test.rs @@ -0,0 +1,511 @@ +//! Unix domain socket test program +//! +//! Tests the socketpair() syscall for AF_UNIX sockets using libbreenix. + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use libbreenix::errno::Errno; +use libbreenix::io::{self, close, fcntl_getfd, fd_flags}; +use libbreenix::process; +use libbreenix::socket::{socketpair, AF_UNIX, AF_INET, SOCK_STREAM, SOCK_DGRAM, SOCK_NONBLOCK, SOCK_CLOEXEC}; +use libbreenix::syscall::{nr, raw}; + +// Buffer size (must match kernel's UNIX_SOCKET_BUFFER_SIZE) +const UNIX_SOCKET_BUFFER_SIZE: usize = 65536; + +/// Helper to write a file descriptor using raw syscall (to test sockets directly) +fn write_fd(fd: i32, data: &[u8]) -> Result { + let ret = unsafe { + raw::syscall3(nr::WRITE, fd as u64, data.as_ptr() as u64, data.len() as u64) + } as i64; + + if ret < 0 { + Err(Errno::from_raw(-ret)) + } else { + Ok(ret as usize) + } +} + +/// Helper to read from a file descriptor using raw syscall +fn read_fd(fd: i32, buf: &mut [u8]) -> Result { + let ret = unsafe { + raw::syscall3(nr::READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64) + } as i64; + + if ret < 0 { + Err(Errno::from_raw(-ret)) + } else { + Ok(ret as usize) + } +} + +/// Helper to call socketpair with a raw pointer (for testing EFAULT) +fn socketpair_raw(domain: i32, sock_type: i32, protocol: i32, sv_ptr: u64) -> Result<(), Errno> { + let ret = unsafe { + raw::syscall4(nr::SOCKETPAIR, domain as u64, sock_type as u64, protocol as u64, sv_ptr) + } as i64; + + if ret < 0 { + Err(Errno::from_raw(-ret)) + } else { + Ok(()) + } +} + +fn print_num(n: i64) { + let mut buf = [0u8; 21]; + let mut i = 20; + let negative = n < 0; + let mut n = if negative { (-n) as u64 } else { n as u64 }; + + if n == 0 { + io::print("0"); + return; + } + + while n > 0 { + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + i -= 1; + } + + if negative { + buf[i] = b'-'; + i -= 1; + } + + if let Ok(s) = core::str::from_utf8(&buf[i + 1..]) { + io::print(s); + } +} + +fn fail(msg: &str) -> ! { + io::print("UNIX_SOCKET: FAIL - "); + io::print(msg); + io::print("\n"); + process::exit(1); +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + io::print("=== Unix Socket Test ===\n"); + + // Phase 1: Create socket pair + io::print("Phase 1: Creating socket pair with socketpair()...\n"); + let (sv0, sv1) = match socketpair(AF_UNIX, SOCK_STREAM, 0) { + Ok(pair) => pair, + Err(e) => { + io::print(" socketpair() returned error: "); + print_num(e as i64); + io::print("\n"); + fail("socketpair() failed"); + } + }; + + io::print(" Socket pair created successfully\n"); + io::print(" sv[0] = "); + print_num(sv0 as i64); + io::print("\n sv[1] = "); + print_num(sv1 as i64); + io::print("\n"); + + // Validate fd numbers are reasonable (should be >= 3 after stdin/stdout/stderr) + if sv0 < 3 || sv1 < 3 { + fail("Socket fds should be >= 3 (after stdin/stdout/stderr)"); + } + if sv0 == sv1 { + fail("Socket fds should be different"); + } + io::print(" FD numbers are valid\n"); + + // Phase 2: Write from sv[0], read from sv[1] + io::print("Phase 2: Writing from sv[0], reading from sv[1]...\n"); + let test_data = b"Hello from sv[0]!"; + let write_ret = match write_fd(sv0, test_data) { + Ok(n) => n, + Err(e) => { + io::print(" write(sv[0]) returned error: "); + print_num(e as i64); + io::print("\n"); + fail("write to sv[0] failed"); + } + }; + + io::print(" Wrote "); + print_num(write_ret as i64); + io::print(" bytes to sv[0]\n"); + + if write_ret != test_data.len() { + fail("Did not write expected number of bytes"); + } + + // Read from sv[1] + let mut read_buf = [0u8; 32]; + let read_ret = match read_fd(sv1, &mut read_buf) { + Ok(n) => n, + Err(e) => { + io::print(" read(sv[1]) returned error: "); + print_num(e as i64); + io::print("\n"); + fail("read from sv[1] failed"); + } + }; + + io::print(" Read "); + print_num(read_ret as i64); + io::print(" bytes from sv[1]\n"); + + if read_ret != test_data.len() { + fail("Did not read expected number of bytes"); + } + + // Verify data matches + let read_slice = &read_buf[..read_ret]; + if read_slice != test_data { + fail("Data verification failed (sv[0] -> sv[1])"); + } + io::print(" Data verified: '"); + if let Ok(s) = core::str::from_utf8(read_slice) { + io::print(s); + } + io::print("'\n"); + + // Phase 3: Write from sv[1], read from sv[0] (reverse direction) + io::print("Phase 3: Writing from sv[1], reading from sv[0]...\n"); + let test_data2 = b"Reply from sv[1]!"; + let write_ret2 = match write_fd(sv1, test_data2) { + Ok(n) => n, + Err(e) => { + io::print(" write(sv[1]) returned error: "); + print_num(e as i64); + io::print("\n"); + fail("write to sv[1] failed"); + } + }; + + io::print(" Wrote "); + print_num(write_ret2 as i64); + io::print(" bytes to sv[1]\n"); + + // Read from sv[0] + let mut read_buf2 = [0u8; 32]; + let read_ret2 = match read_fd(sv0, &mut read_buf2) { + Ok(n) => n, + Err(e) => { + io::print(" read(sv[0]) returned error: "); + print_num(e as i64); + io::print("\n"); + fail("read from sv[0] failed"); + } + }; + + io::print(" Read "); + print_num(read_ret2 as i64); + io::print(" bytes from sv[0]\n"); + + let read_slice2 = &read_buf2[..read_ret2]; + if read_slice2 != test_data2 { + fail("Data verification failed (sv[1] -> sv[0])"); + } + io::print(" Bidirectional communication works!\n"); + + // Phase 4: Close sv[0], verify sv[1] sees EOF + io::print("Phase 4: Testing EOF on peer close...\n"); + let close_ret = close(sv0 as u64); + if close_ret < 0 { + io::print(" close(sv[0]) returned error: "); + print_num(close_ret); + io::print("\n"); + fail("close(sv[0]) failed"); + } + io::print(" Closed sv[0]\n"); + + // Read from sv[1] should return 0 (EOF) + let mut eof_buf = [0u8; 8]; + let eof_ret = match read_fd(sv1, &mut eof_buf) { + Ok(n) => n as i64, + Err(e) => -(e as i64), + }; + + io::print(" Read from sv[1] returned: "); + print_num(eof_ret); + io::print("\n"); + + if eof_ret != 0 { + fail("Expected EOF (0) after peer close"); + } + io::print(" EOF on peer close works!\n"); + + // Phase 5: Close sv[1] + io::print("Phase 5: Closing sv[1]...\n"); + let close_ret2 = close(sv1 as u64); + if close_ret2 < 0 { + io::print(" close(sv[1]) returned error: "); + print_num(close_ret2); + io::print("\n"); + fail("close(sv[1]) failed"); + } + io::print(" Closed sv[1]\n"); + + // Phase 6: Test SOCK_NONBLOCK - read should return EAGAIN when no data + io::print("Phase 6: Testing SOCK_NONBLOCK (EAGAIN on empty read)...\n"); + let (sv_nb0, sv_nb1) = match socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0) { + Ok(pair) => pair, + Err(e) => { + io::print(" socketpair(SOCK_NONBLOCK) returned error: "); + print_num(e as i64); + io::print("\n"); + fail("socketpair(SOCK_NONBLOCK) failed"); + } + }; + io::print(" Created non-blocking socket pair\n"); + io::print(" sv_nb[0] = "); + print_num(sv_nb0 as i64); + io::print(", sv_nb[1] = "); + print_num(sv_nb1 as i64); + io::print("\n"); + + // Try to read from empty socket - should return EAGAIN + let mut nb_buf = [0u8; 8]; + match read_fd(sv_nb1, &mut nb_buf) { + Ok(n) => { + io::print(" Read returned "); + print_num(n as i64); + io::print(" instead of EAGAIN\n"); + fail("Non-blocking read should return EAGAIN when no data available"); + } + Err(e) => { + io::print(" Read from empty non-blocking socket returned: "); + print_num(-(e as i64)); + io::print("\n"); + if e != Errno::EAGAIN { + io::print(" Expected EAGAIN, got different error\n"); + fail("Non-blocking read should return EAGAIN when no data available"); + } + } + } + io::print(" SOCK_NONBLOCK works correctly!\n"); + + // Clean up non-blocking sockets + close(sv_nb0 as u64); + close(sv_nb1 as u64); + + // Phase 7: Test EPIPE - write to socket after peer closed + io::print("Phase 7: Testing EPIPE (write to closed peer)...\n"); + let (sv_pipe0, sv_pipe1) = match socketpair(AF_UNIX, SOCK_STREAM, 0) { + Ok(pair) => pair, + Err(_) => fail("socketpair() for EPIPE test failed"), + }; + io::print(" Created socket pair for EPIPE test\n"); + + // Close the reader end + close(sv_pipe1 as u64); + io::print(" Closed sv_pipe[1] (reader)\n"); + + // Try to write to the socket whose peer is closed + let pipe_data = b"This should fail"; + match write_fd(sv_pipe0, pipe_data) { + Ok(n) => { + io::print(" Write returned "); + print_num(n as i64); + io::print(" instead of EPIPE\n"); + fail("Write to closed peer should return EPIPE"); + } + Err(e) => { + io::print(" Write to socket with closed peer returned: "); + print_num(-(e as i64)); + io::print("\n"); + if e != Errno::EPIPE { + io::print(" Expected EPIPE, got different error\n"); + fail("Write to closed peer should return EPIPE"); + } + } + } + io::print(" EPIPE works correctly!\n"); + + // Clean up + close(sv_pipe0 as u64); + + // Phase 8: Test error handling - wrong domain and type + io::print("Phase 8: Testing error handling (invalid domain/type)...\n"); + + // Test 8a: AF_INET should return EAFNOSUPPORT + match socketpair(AF_INET, SOCK_STREAM, 0) { + Ok(_) => fail("socketpair(AF_INET) should fail"), + Err(e) => { + io::print(" socketpair(AF_INET) returned: "); + print_num(-(e as i64)); + io::print("\n"); + // socketpair returns raw errno as i32 + if e != 97 { + // EAFNOSUPPORT = 97 + io::print(" Expected EAFNOSUPPORT (97)\n"); + fail("socketpair(AF_INET) should return EAFNOSUPPORT"); + } + } + } + io::print(" AF_INET correctly rejected with EAFNOSUPPORT\n"); + + // Test 8b: SOCK_DGRAM should return EINVAL (not yet implemented) + match socketpair(AF_UNIX, SOCK_DGRAM, 0) { + Ok(_) => fail("socketpair(SOCK_DGRAM) should fail"), + Err(e) => { + io::print(" socketpair(SOCK_DGRAM) returned: "); + print_num(-(e as i64)); + io::print("\n"); + // socketpair returns raw errno as i32 + if e != 22 { + // EINVAL = 22 + io::print(" Expected EINVAL (22)\n"); + fail("socketpair(SOCK_DGRAM) should return EINVAL"); + } + } + } + io::print(" SOCK_DGRAM correctly rejected with EINVAL\n"); + + io::print(" Error handling works correctly!\n"); + + // Phase 9: Test buffer-full scenario (EAGAIN on write when buffer is full) + io::print("Phase 9: Testing buffer-full (EAGAIN on non-blocking write)...\n"); + let (sv_buf0, sv_buf1) = match socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0) { + Ok(pair) => pair, + Err(_) => fail("socketpair() for buffer-full test failed"), + }; + io::print(" Created non-blocking socket pair for buffer test\n"); + + // Fill the buffer by writing chunks until EAGAIN + let chunk = [0x42u8; 4096]; // 4KB chunks + let mut total_written: usize = 0; + let mut eagain_received = false; + + // Write until we get EAGAIN (buffer full) + while total_written < UNIX_SOCKET_BUFFER_SIZE + 4096 { + match write_fd(sv_buf0, &chunk) { + Ok(n) => { + total_written += n; + } + Err(e) => { + if e == Errno::EAGAIN { + eagain_received = true; + io::print(" Got EAGAIN after writing "); + print_num(total_written as i64); + io::print(" bytes\n"); + break; + } else { + io::print(" Unexpected error during buffer fill: "); + print_num(-(e as i64)); + io::print("\n"); + fail("Unexpected error while filling buffer"); + } + } + } + } + + if !eagain_received { + io::print(" Wrote "); + print_num(total_written as i64); + io::print(" bytes without EAGAIN\n"); + fail("Expected EAGAIN when buffer is full"); + } + + // Verify we wrote at least UNIX_SOCKET_BUFFER_SIZE bytes before EAGAIN + if total_written < UNIX_SOCKET_BUFFER_SIZE { + io::print(" Only wrote "); + print_num(total_written as i64); + io::print(" bytes, expected at least "); + print_num(UNIX_SOCKET_BUFFER_SIZE as i64); + io::print("\n"); + fail("Buffer should hold at least UNIX_SOCKET_BUFFER_SIZE bytes"); + } + io::print(" Buffer-full test passed!\n"); + + // Clean up + close(sv_buf0 as u64); + close(sv_buf1 as u64); + + // Phase 10: Test NULL sv_ptr (should return EFAULT) + io::print("Phase 10: Testing NULL sv_ptr (EFAULT)...\n"); + match socketpair_raw(AF_UNIX, SOCK_STREAM, 0, 0) { + Ok(_) => fail("socketpair(NULL) should fail"), + Err(e) => { + io::print(" socketpair(NULL) returned: "); + print_num(-(e as i64)); + io::print("\n"); + if e != Errno::EFAULT { + io::print(" Expected EFAULT\n"); + fail("socketpair(NULL) should return EFAULT"); + } + } + } + io::print(" NULL sv_ptr correctly rejected with EFAULT\n"); + + // Phase 11: Test non-zero protocol (should return EINVAL) + io::print("Phase 11: Testing non-zero protocol (EINVAL)...\n"); + let mut sv_proto: [i32; 2] = [0, 0]; + match socketpair_raw(AF_UNIX, SOCK_STREAM, 1, sv_proto.as_mut_ptr() as u64) { + Ok(_) => fail("socketpair(protocol=1) should fail"), + Err(e) => { + io::print(" socketpair(protocol=1) returned: "); + print_num(-(e as i64)); + io::print("\n"); + if e != Errno::EINVAL { + io::print(" Expected EINVAL\n"); + fail("socketpair(protocol!=0) should return EINVAL"); + } + } + } + io::print(" Non-zero protocol correctly rejected with EINVAL\n"); + + // Phase 12: Test SOCK_CLOEXEC flag + io::print("Phase 12: Testing SOCK_CLOEXEC flag...\n"); + let (sv_cloexec0, sv_cloexec1) = match socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0) { + Ok(pair) => pair, + Err(e) => { + io::print(" socketpair(SOCK_CLOEXEC) returned error: "); + print_num(e as i64); + io::print("\n"); + fail("socketpair(SOCK_CLOEXEC) failed"); + } + }; + io::print(" Created socket pair with SOCK_CLOEXEC\n"); + + // Verify FD_CLOEXEC is set on both fds using fcntl(F_GETFD) + let flags0 = fcntl_getfd(sv_cloexec0 as u64); + let flags1 = fcntl_getfd(sv_cloexec1 as u64); + + io::print(" sv_cloexec[0] flags: "); + print_num(flags0); + io::print(", sv_cloexec[1] flags: "); + print_num(flags1); + io::print("\n"); + + if flags0 < 0 || flags1 < 0 { + io::print(" fcntl(F_GETFD) failed\n"); + fail("fcntl(F_GETFD) failed on SOCK_CLOEXEC socket"); + } + + if (flags0 & fd_flags::FD_CLOEXEC as i64) == 0 { + fail("sv_cloexec[0] should have FD_CLOEXEC set"); + } + if (flags1 & fd_flags::FD_CLOEXEC as i64) == 0 { + fail("sv_cloexec[1] should have FD_CLOEXEC set"); + } + io::print(" FD_CLOEXEC correctly set on both sockets\n"); + + // Clean up + close(sv_cloexec0 as u64); + close(sv_cloexec1 as u64); + + // All tests passed + io::print("=== Unix Socket Test PASSED ===\n"); + io::print("UNIX_SOCKET_TEST_PASSED\n"); + process::exit(0); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + io::print("PANIC in unix socket test!\n"); + process::exit(1); +} From ececec37f6c95fe455288d9afe1076ad2f589194 Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Sat, 24 Jan 2026 04:04:51 -0500 Subject: [PATCH 2/2] feat(graphics): add interactive graphics demos and syscalls Add framebuffer graphics support with interactive demos: Kernel: - Add sys_fb_info syscall to query framebuffer dimensions - Add sys_fb_draw_rect syscall for rectangle drawing - Extend graphics syscall infrastructure Userspace (libbreenix): - Add fb_info() and fb_draw_rect() wrappers - Add nanosleep() for timing control Interactive demos (not included in test builds): - demo.rs: Colorful rectangle animation demo - bounce.rs: Bouncing box animation Also adds run-interactive-native.sh for local testing. Co-Authored-By: Claude Opus 4.5 --- kernel/Cargo.toml | 2 +- kernel/build.rs | 2 + kernel/src/syscall/fs.rs | 9 ++ kernel/src/syscall/graphics.rs | 212 ++++++++++++++++++++++++++- kernel/src/syscall/handler.rs | 4 + libs/libbreenix/src/graphics.rs | 151 ++++++++++++++++++- libs/libbreenix/src/time.rs | 32 +++++ scripts/run-interactive-native.sh | 114 +++++++++++++++ userspace/examples/init_shell.rs | 10 ++ userspace/tests/Cargo.toml | 9 ++ userspace/tests/bounce.rs | 231 ++++++++++++++++++++++++++++++ userspace/tests/build.sh | 2 - userspace/tests/demo.rs | 220 ++++++++++++++++++++++++++++ 13 files changed, 993 insertions(+), 5 deletions(-) create mode 100755 scripts/run-interactive-native.sh create mode 100644 userspace/tests/bounce.rs create mode 100644 userspace/tests/demo.rs diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index ca22dba2..ccb2eec3 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -31,7 +31,7 @@ test_userspace = [] test_all_exceptions = [] external_test_bins = [] vmnet = [] # Use vmnet network config (192.168.64.x) instead of SLIRP (10.0.2.x) -interactive = [] # Boot into init_shell instead of running automated tests +interactive = ["testing"] # Boot into init_shell instead of running automated tests [dependencies] bootloader_api = { git = "https://github.com/rust-osdev/bootloader.git", branch = "main" } diff --git a/kernel/build.rs b/kernel/build.rs index b0690556..f7330cc8 100644 --- a/kernel/build.rs +++ b/kernel/build.rs @@ -103,6 +103,7 @@ fn main() { println!("cargo:rerun-if-changed={}/fork_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/clock_gettime_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/udp_socket_test.rs", userspace_tests); + println!("cargo:rerun-if-changed={}/unix_socket_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/tty_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/job_control_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/session_test.rs", userspace_tests); @@ -111,6 +112,7 @@ fn main() { println!("cargo:rerun-if-changed={}/pipeline_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/sigchld_job_test.rs", userspace_tests); println!("cargo:rerun-if-changed={}/cwd_test.rs", userspace_tests); + println!("cargo:rerun-if-changed={}/demo.rs", userspace_tests); println!("cargo:rerun-if-changed={}/lib.rs", libbreenix_dir.to_str().unwrap()); } } else { diff --git a/kernel/src/syscall/fs.rs b/kernel/src/syscall/fs.rs index ab8fe44d..63fc01d0 100644 --- a/kernel/src/syscall/fs.rs +++ b/kernel/src/syscall/fs.rs @@ -660,6 +660,15 @@ pub fn sys_fstat(fd: i32, statbuf: u64) -> SyscallResult { // Major 136 for PTY, minor is pty_num stat.st_rdev = make_dev(136, *pty_num as u64); } + FdKind::UnixStream(_) => { + // Unix domain sockets + static UNIX_SOCKET_INODE_COUNTER: core::sync::atomic::AtomicU64 = + core::sync::atomic::AtomicU64::new(5000); + stat.st_dev = 0; + stat.st_ino = UNIX_SOCKET_INODE_COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed); + stat.st_mode = S_IFSOCK | 0o755; // Socket with rwxr-xr-x + stat.st_nlink = 1; + } } // Copy stat structure to userspace diff --git a/kernel/src/syscall/graphics.rs b/kernel/src/syscall/graphics.rs index 887b2302..c38162ca 100644 --- a/kernel/src/syscall/graphics.rs +++ b/kernel/src/syscall/graphics.rs @@ -1,9 +1,11 @@ //! Graphics-related system calls. //! -//! Provides syscalls for querying framebuffer information. +//! Provides syscalls for querying and drawing to the framebuffer. #[cfg(feature = "interactive")] use crate::logger::SHELL_FRAMEBUFFER; +#[cfg(feature = "interactive")] +use crate::graphics::primitives::{Canvas, Color, Rect, fill_rect, draw_rect, fill_circle, draw_circle, draw_line}; use super::SyscallResult; /// Framebuffer info structure returned by sys_fbinfo. @@ -93,3 +95,211 @@ pub fn sys_fbinfo(_info_ptr: u64) -> SyscallResult { // No framebuffer available in non-interactive mode SyscallResult::Err(super::ErrorCode::InvalidArgument as u64) } + +/// Draw command operations for sys_fbdraw +#[cfg(feature = "interactive")] +#[repr(u32)] +#[allow(dead_code)] +pub enum FbDrawOp { + /// Clear the left pane with a color + Clear = 0, + /// Fill a rectangle: x, y, width, height, color + FillRect = 1, + /// Draw rectangle outline: x, y, width, height, color + DrawRect = 2, + /// Fill a circle: cx, cy, radius, color + FillCircle = 3, + /// Draw circle outline: cx, cy, radius, color + DrawCircle = 4, + /// Draw a line: x1, y1, x2, y2, color + DrawLine = 5, + /// Flush the framebuffer (for double-buffering) + Flush = 6, +} + +/// Draw command structure passed from userspace. +/// Must match the FbDrawCmd struct in libbreenix. +#[cfg(feature = "interactive")] +#[repr(C)] +pub struct FbDrawCmd { + /// Operation code (FbDrawOp) + pub op: u32, + /// First parameter (x, cx, x1, or unused) + pub p1: i32, + /// Second parameter (y, cy, y1, or unused) + pub p2: i32, + /// Third parameter (width, radius, x2, or unused) + pub p3: i32, + /// Fourth parameter (height, y2, or unused) + pub p4: i32, + /// Color as packed RGB (0x00RRGGBB) + pub color: u32, +} + +/// Get the width of the left (demo) pane +#[cfg(feature = "interactive")] +#[allow(dead_code)] +fn left_pane_width() -> usize { + if let Some(fb) = SHELL_FRAMEBUFFER.get() { + let fb_guard = fb.lock(); + fb_guard.width() / 2 + } else { + 0 + } +} + +/// Get the height of the framebuffer +#[cfg(feature = "interactive")] +#[allow(dead_code)] +fn fb_height() -> usize { + if let Some(fb) = SHELL_FRAMEBUFFER.get() { + let fb_guard = fb.lock(); + fb_guard.height() + } else { + 0 + } +} + +/// sys_fbdraw - Draw to the left pane of the framebuffer +/// +/// # Arguments +/// * `cmd_ptr` - Pointer to userspace FbDrawCmd structure +/// +/// # Returns +/// * 0 on success +/// * -EFAULT if cmd_ptr is invalid +/// * -ENODEV if no framebuffer is available +/// * -EINVAL if operation is invalid +#[cfg(feature = "interactive")] +pub fn sys_fbdraw(cmd_ptr: u64) -> SyscallResult { + // Validate pointer + if cmd_ptr == 0 || cmd_ptr >= USER_SPACE_MAX { + return SyscallResult::Err(super::ErrorCode::Fault as u64); + } + + // Validate the entire FbDrawCmd struct fits in userspace + let end_ptr = cmd_ptr.saturating_add(core::mem::size_of::() as u64); + if end_ptr > USER_SPACE_MAX { + return SyscallResult::Err(super::ErrorCode::Fault as u64); + } + + // Read the command from userspace + let cmd: FbDrawCmd = unsafe { core::ptr::read(cmd_ptr as *const FbDrawCmd) }; + + // Get framebuffer + let fb = match SHELL_FRAMEBUFFER.get() { + Some(fb) => fb, + None => return SyscallResult::Err(super::ErrorCode::InvalidArgument as u64), + }; + + let mut fb_guard = fb.lock(); + + // Get left pane dimensions (half the screen width) + let pane_width = fb_guard.width() / 2; + let pane_height = fb_guard.height(); + + // Parse color + let color = Color::rgb( + ((cmd.color >> 16) & 0xFF) as u8, + ((cmd.color >> 8) & 0xFF) as u8, + (cmd.color & 0xFF) as u8, + ); + + match cmd.op { + 0 => { + // Clear: fill entire left pane with color + fill_rect( + &mut *fb_guard, + Rect { + x: 0, + y: 0, + width: pane_width as u32, + height: pane_height as u32, + }, + color, + ); + } + 1 => { + // FillRect: x, y, width, height, color + // Clip to left pane + let x = cmd.p1.max(0) as i32; + let y = cmd.p2.max(0) as i32; + let w = cmd.p3.max(0) as u32; + let h = cmd.p4.max(0) as u32; + + // Only draw if within left pane + if (x as usize) < pane_width { + let clipped_w = w.min((pane_width as i32 - x) as u32); + fill_rect( + &mut *fb_guard, + Rect { x, y, width: clipped_w, height: h }, + color, + ); + } + } + 2 => { + // DrawRect: x, y, width, height, color + let x = cmd.p1.max(0) as i32; + let y = cmd.p2.max(0) as i32; + let w = cmd.p3.max(0) as u32; + let h = cmd.p4.max(0) as u32; + + if (x as usize) < pane_width { + draw_rect( + &mut *fb_guard, + Rect { x, y, width: w, height: h }, + color, + ); + } + } + 3 => { + // FillCircle: cx, cy, radius, color + let cx = cmd.p1; + let cy = cmd.p2; + let radius = cmd.p3.max(0) as u32; + + if (cx as usize) < pane_width { + fill_circle(&mut *fb_guard, cx, cy, radius, color); + } + } + 4 => { + // DrawCircle: cx, cy, radius, color + let cx = cmd.p1; + let cy = cmd.p2; + let radius = cmd.p3.max(0) as u32; + + if (cx as usize) < pane_width { + draw_circle(&mut *fb_guard, cx, cy, radius, color); + } + } + 5 => { + // DrawLine: x1, y1, x2, y2, color + let x1 = cmd.p1; + let y1 = cmd.p2; + let x2 = cmd.p3; + let y2 = cmd.p4; + + // Allow lines that start or end in left pane + if (x1 as usize) < pane_width || (x2 as usize) < pane_width { + draw_line(&mut *fb_guard, x1, y1, x2, y2, color); + } + } + 6 => { + // Flush: sync double buffer to screen + if let Some(db) = fb_guard.double_buffer_mut() { + db.flush_full(); + } + } + _ => { + return SyscallResult::Err(super::ErrorCode::InvalidArgument as u64); + } + } + + SyscallResult::Ok(0) +} + +/// sys_fbdraw - Stub for non-interactive mode +#[cfg(not(feature = "interactive"))] +pub fn sys_fbdraw(_cmd_ptr: u64) -> SyscallResult { + SyscallResult::Err(super::ErrorCode::InvalidArgument as u64) +} diff --git a/kernel/src/syscall/handler.rs b/kernel/src/syscall/handler.rs index d3b57fe2..c404a66c 100644 --- a/kernel/src/syscall/handler.rs +++ b/kernel/src/syscall/handler.rs @@ -261,6 +261,9 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { Some(SyscallNumber::Shutdown) => { super::socket::sys_shutdown(args.0, args.1) } + Some(SyscallNumber::Socketpair) => { + super::socket::sys_socketpair(args.0, args.1, args.2, args.3) + } Some(SyscallNumber::Poll) => super::handlers::sys_poll(args.0, args.1, args.2 as i32), Some(SyscallNumber::Select) => { super::handlers::sys_select(args.0 as i32, args.1, args.2, args.3, args.4) @@ -305,6 +308,7 @@ pub extern "C" fn rust_syscall_handler(frame: &mut SyscallFrame) { Some(SyscallNumber::Ptsname) => super::pty::sys_ptsname(args.0, args.1, args.2), // Graphics syscalls Some(SyscallNumber::FbInfo) => super::graphics::sys_fbinfo(args.0), + Some(SyscallNumber::FbDraw) => super::graphics::sys_fbdraw(args.0), None => { log::warn!("Unknown syscall number: {} - returning ENOSYS", syscall_num); SyscallResult::Err(super::ErrorCode::NoSys as u64) diff --git a/libs/libbreenix/src/graphics.rs b/libs/libbreenix/src/graphics.rs index f10a33bc..54d4a32b 100644 --- a/libs/libbreenix/src/graphics.rs +++ b/libs/libbreenix/src/graphics.rs @@ -1,6 +1,6 @@ //! Graphics syscall wrappers //! -//! Provides userspace API for querying framebuffer information. +//! Provides userspace API for querying framebuffer information and drawing. use crate::syscall::{nr, raw}; @@ -46,6 +46,11 @@ impl FbInfo { pub fn is_grayscale(&self) -> bool { self.pixel_format == 2 } + + /// Get the width of the left (demo) pane + pub fn left_pane_width(&self) -> u64 { + self.width / 2 + } } /// Get framebuffer information @@ -63,3 +68,147 @@ pub fn fbinfo() -> Result { Ok(info) } } + +/// Draw command structure for sys_fbdraw. +/// Must match kernel's FbDrawCmd in syscall/graphics.rs. +#[repr(C)] +pub struct FbDrawCmd { + /// Operation code + pub op: u32, + /// First parameter (x, cx, x1, or unused) + pub p1: i32, + /// Second parameter (y, cy, y1, or unused) + pub p2: i32, + /// Third parameter (width, radius, x2, or unused) + pub p3: i32, + /// Fourth parameter (height, y2, or unused) + pub p4: i32, + /// Color as packed RGB (0x00RRGGBB) + pub color: u32, +} + +/// Draw operation codes +pub mod draw_op { + /// Clear the left pane with a color + pub const CLEAR: u32 = 0; + /// Fill a rectangle: x, y, width, height, color + pub const FILL_RECT: u32 = 1; + /// Draw rectangle outline: x, y, width, height, color + pub const DRAW_RECT: u32 = 2; + /// Fill a circle: cx, cy, radius, color + pub const FILL_CIRCLE: u32 = 3; + /// Draw circle outline: cx, cy, radius, color + pub const DRAW_CIRCLE: u32 = 4; + /// Draw a line: x1, y1, x2, y2, color + pub const DRAW_LINE: u32 = 5; + /// Flush the framebuffer (for double-buffering) + pub const FLUSH: u32 = 6; +} + +/// Pack RGB color into u32 +#[inline] +pub const fn rgb(r: u8, g: u8, b: u8) -> u32 { + ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) +} + +/// Execute a draw command +fn fbdraw(cmd: &FbDrawCmd) -> Result<(), i32> { + let result = unsafe { raw::syscall1(nr::FBDRAW, cmd as *const FbDrawCmd as u64) }; + + if (result as i64) < 0 { + Err(-(result as i64) as i32) + } else { + Ok(()) + } +} + +/// Clear the left pane with a color +pub fn fb_clear(color: u32) -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::CLEAR, + p1: 0, + p2: 0, + p3: 0, + p4: 0, + color, + }; + fbdraw(&cmd) +} + +/// Fill a rectangle on the left pane +pub fn fb_fill_rect(x: i32, y: i32, width: i32, height: i32, color: u32) -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::FILL_RECT, + p1: x, + p2: y, + p3: width, + p4: height, + color, + }; + fbdraw(&cmd) +} + +/// Draw a rectangle outline on the left pane +pub fn fb_draw_rect(x: i32, y: i32, width: i32, height: i32, color: u32) -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::DRAW_RECT, + p1: x, + p2: y, + p3: width, + p4: height, + color, + }; + fbdraw(&cmd) +} + +/// Fill a circle on the left pane +pub fn fb_fill_circle(cx: i32, cy: i32, radius: i32, color: u32) -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::FILL_CIRCLE, + p1: cx, + p2: cy, + p3: radius, + p4: 0, + color, + }; + fbdraw(&cmd) +} + +/// Draw a circle outline on the left pane +pub fn fb_draw_circle(cx: i32, cy: i32, radius: i32, color: u32) -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::DRAW_CIRCLE, + p1: cx, + p2: cy, + p3: radius, + p4: 0, + color, + }; + fbdraw(&cmd) +} + +/// Draw a line on the left pane +pub fn fb_draw_line(x1: i32, y1: i32, x2: i32, y2: i32, color: u32) -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::DRAW_LINE, + p1: x1, + p2: y1, + p3: x2, + p4: y2, + color, + }; + fbdraw(&cmd) +} + +/// Flush the framebuffer (sync double buffer to screen) +pub fn fb_flush() -> Result<(), i32> { + let cmd = FbDrawCmd { + op: draw_op::FLUSH, + p1: 0, + p2: 0, + p3: 0, + p4: 0, + color: 0, + }; + fbdraw(&cmd) +} diff --git a/libs/libbreenix/src/time.rs b/libs/libbreenix/src/time.rs index 070c69be..21f3b21b 100644 --- a/libs/libbreenix/src/time.rs +++ b/libs/libbreenix/src/time.rs @@ -58,5 +58,37 @@ pub fn now_monotonic() -> Timespec { ts } +/// Sleep for the specified number of milliseconds. +/// +/// This is a busy-wait implementation since we don't have nanosleep yet. +/// It uses clock_gettime(CLOCK_MONOTONIC) for timing. +/// +/// # Arguments +/// * `ms` - Number of milliseconds to sleep +#[inline] +pub fn sleep_ms(ms: u64) { + let start = now_monotonic(); + let target_ns = ms * 1_000_000; + + loop { + let now = now_monotonic(); + let elapsed_sec = now.tv_sec - start.tv_sec; + let elapsed_nsec = if now.tv_nsec >= start.tv_nsec { + now.tv_nsec - start.tv_nsec + } else { + // Handle nanosecond underflow + 1_000_000_000 - (start.tv_nsec - now.tv_nsec) + }; + + let elapsed_ns = (elapsed_sec as u64) * 1_000_000_000 + (elapsed_nsec as u64); + if elapsed_ns >= target_ns { + break; + } + + // Yield to other processes while waiting + crate::process::yield_now(); + } +} + // Re-export clock constants for convenience pub use crate::types::clock::{MONOTONIC as CLOCK_MONOTONIC, REALTIME as CLOCK_REALTIME}; diff --git a/scripts/run-interactive-native.sh b/scripts/run-interactive-native.sh new file mode 100755 index 00000000..847095e8 --- /dev/null +++ b/scripts/run-interactive-native.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Run QEMU natively on macOS with Cocoa display for best performance +# Usage: ./run-interactive-native.sh +# +# This gives much better frame rates than VNC through Docker. +# Use for graphics demos like bounce and demo. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BREENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Find the UEFI image +UEFI_IMG=$(ls -t "$BREENIX_ROOT/target/release/build/breenix-"*/out/breenix-uefi.img 2>/dev/null | head -1) +if [ -z "$UEFI_IMG" ]; then + echo "Error: UEFI image not found. Build with:" + echo " cargo build --release --features interactive --bin qemu-uefi" + exit 1 +fi + +# Check for test_binaries.img +if [ ! -f "$BREENIX_ROOT/target/test_binaries.img" ]; then + echo "Error: test_binaries.img not found. Create with:" + echo " cargo run -p xtask -- create-test-disk" + exit 1 +fi + +# Check for ext2.img +if [ ! -f "$BREENIX_ROOT/target/ext2.img" ]; then + echo "Warning: ext2.img not found, filesystem features may not work" +fi + +# Create output directory +OUTPUT_DIR=$(mktemp -d) + +# Copy OVMF files +cp "$BREENIX_ROOT/target/ovmf/x64/code.fd" "$OUTPUT_DIR/OVMF_CODE.fd" +cp "$BREENIX_ROOT/target/ovmf/x64/vars.fd" "$OUTPUT_DIR/OVMF_VARS.fd" + +# Create empty serial output files +touch "$OUTPUT_DIR/serial_user.txt" +touch "$OUTPUT_DIR/serial_kernel.txt" + +echo "" +echo "=========================================" +echo "Starting QEMU with native Cocoa display" +echo "=========================================" +echo "Output: $OUTPUT_DIR" +echo "UEFI image: $UEFI_IMG" +echo "" +echo "A QEMU window will open with the Breenix display." +echo "Press Ctrl+C here or close the window to stop." +echo "" + +# Verify OVMF files were copied +if [ ! -f "$OUTPUT_DIR/OVMF_CODE.fd" ]; then + echo "Error: OVMF_CODE.fd not found" + exit 1 +fi + +# Build the QEMU command +QEMU_CMD=( + qemu-system-x86_64 + -drive "if=pflash,format=raw,readonly=on,file=$OUTPUT_DIR/OVMF_CODE.fd" + -drive "if=pflash,format=raw,file=$OUTPUT_DIR/OVMF_VARS.fd" + -drive "if=none,id=hd,format=raw,media=disk,readonly=on,file=$UEFI_IMG" + -device "virtio-blk-pci,drive=hd,bootindex=0,disable-modern=on,disable-legacy=off" + -drive "if=none,id=testdisk,format=raw,readonly=on,file=$BREENIX_ROOT/target/test_binaries.img" + -device "virtio-blk-pci,drive=testdisk,disable-modern=on,disable-legacy=off" + -machine "pc" + -accel "tcg,thread=multi,tb-size=512" + -cpu qemu64 + -smp 2 + -m 512 + -device virtio-vga + -display cocoa,show-cursor=on + -k en-us + -no-reboot + -device "isa-debug-exit,iobase=0xf4,iosize=0x04" + -netdev "user,id=net0" + -device "e1000,netdev=net0,mac=52:54:00:12:34:56" + -serial "file:$OUTPUT_DIR/serial_user.txt" + -serial "file:$OUTPUT_DIR/serial_kernel.txt" +) + +# Add ext2 disk if it exists +if [ -f "$BREENIX_ROOT/target/ext2.img" ]; then + QEMU_CMD+=( + -drive "if=none,id=ext2disk,format=raw,readonly=on,file=$BREENIX_ROOT/target/ext2.img" + -device "virtio-blk-pci,drive=ext2disk,disable-modern=on,disable-legacy=off" + ) +fi + +# Print the command for debugging +echo "Running: ${QEMU_CMD[*]}" +echo "" + +# Run QEMU +"${QEMU_CMD[@]}" + +echo "" +echo "=========================================" +echo "QEMU stopped" +echo "=========================================" +echo "" +echo "Serial output saved to:" +echo " User (COM1): $OUTPUT_DIR/serial_user.txt" +echo " Kernel (COM2): $OUTPUT_DIR/serial_kernel.txt" +echo "" +echo "=== Last 30 lines of kernel log ===" +tail -30 "$OUTPUT_DIR/serial_kernel.txt" 2>/dev/null || echo "(no output)" +echo "" +echo "=== Last 20 lines of user output ===" +tail -20 "$OUTPUT_DIR/serial_user.txt" 2>/dev/null || echo "(no output)" diff --git a/userspace/examples/init_shell.rs b/userspace/examples/init_shell.rs index be3ba0cc..8ac23945 100644 --- a/userspace/examples/init_shell.rs +++ b/userspace/examples/init_shell.rs @@ -510,6 +510,16 @@ static PROGRAM_REGISTRY: &[ProgramEntry] = &[ binary_name: b"spinner\0", description: "Display spinning animation", }, + ProgramEntry { + name: "demo", + binary_name: b"demo\0", + description: "Animated graphics demo on left pane", + }, + ProgramEntry { + name: "bounce", + binary_name: b"bounce\0", + description: "Bouncing balls with collision detection (for Gus!)", + }, ProgramEntry { name: "hello_time", binary_name: b"hello_time\0", diff --git a/userspace/tests/Cargo.toml b/userspace/tests/Cargo.toml index afe32ef2..34b8e48f 100644 --- a/userspace/tests/Cargo.toml +++ b/userspace/tests/Cargo.toml @@ -443,6 +443,15 @@ path = "pty_test.rs" name = "fbinfo_test" path = "fbinfo_test.rs" +# Interactive graphics demos (not included in test builds) +[[bin]] +name = "demo" +path = "demo.rs" + +[[bin]] +name = "bounce" +path = "bounce.rs" + # Coreutils argv integration tests [[bin]] name = "mkdir_argv_test" diff --git a/userspace/tests/bounce.rs b/userspace/tests/bounce.rs new file mode 100644 index 00000000..b7cf1aa8 --- /dev/null +++ b/userspace/tests/bounce.rs @@ -0,0 +1,231 @@ +//! Bouncing balls with collision detection demo for Breenix +//! +//! Balls bounce off walls and each other with elastic collisions. +//! Created for Gus! + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use libbreenix::graphics::{fb_clear, fb_fill_circle, fb_flush, fbinfo, rgb}; +use libbreenix::io::println; +use libbreenix::process::exit; +use libbreenix::time::sleep_ms; + +/// Ball state +struct Ball { + x: i32, // Position (fixed point, scaled by 100) + y: i32, + vx: i32, // Velocity (fixed point, scaled by 100) + vy: i32, + radius: i32, // Actual radius in pixels + color: u32, + mass: i32, // For collision response (proportional to radius) +} + +impl Ball { + fn new(x: i32, y: i32, vx: i32, vy: i32, radius: i32, color: u32) -> Self { + Self { + x: x * 100, // Scale up for fixed-point math + y: y * 100, + vx, + vy, + radius, + color, + mass: radius, // Mass proportional to radius + } + } + + /// Get pixel position + fn px(&self) -> i32 { + self.x / 100 + } + + fn py(&self) -> i32 { + self.y / 100 + } + + /// Update position based on velocity + fn update_position(&mut self) { + self.x += self.vx; + self.y += self.vy; + } + + /// Bounce off walls + fn bounce_walls(&mut self, width: i32, height: i32) { + let px = self.px(); + let py = self.py(); + + // Left wall + if px - self.radius < 0 { + self.x = self.radius * 100; + self.vx = -self.vx; + } + // Right wall + if px + self.radius >= width { + self.x = (width - self.radius - 1) * 100; + self.vx = -self.vx; + } + // Top wall + if py - self.radius < 0 { + self.y = self.radius * 100; + self.vy = -self.vy; + } + // Bottom wall + if py + self.radius >= height { + self.y = (height - self.radius - 1) * 100; + self.vy = -self.vy; + } + } + + fn draw(&self) { + let _ = fb_fill_circle(self.px(), self.py(), self.radius, self.color); + } +} + +/// Integer square root (for collision detection) +fn isqrt(n: i32) -> i32 { + if n < 0 { + return 0; + } + if n < 2 { + return n; + } + + let mut x = n; + let mut y = (x + 1) / 2; + + while y < x { + x = y; + y = (x + n / x) / 2; + } + x +} + +/// Check if two balls are colliding and handle the collision +fn check_collision(ball1: &mut Ball, ball2: &mut Ball) { + // Calculate distance between centers + let dx = ball2.px() - ball1.px(); + let dy = ball2.py() - ball1.py(); + let dist_sq = dx * dx + dy * dy; + + let min_dist = ball1.radius + ball2.radius; + let min_dist_sq = min_dist * min_dist; + + // Check if colliding + if dist_sq < min_dist_sq && dist_sq > 0 { + let dist = isqrt(dist_sq); + if dist == 0 { + return; + } + + // Normalize collision vector (scaled by 1000 for fixed-point) + let nx = (dx * 1000) / dist; + let ny = (dy * 1000) / dist; + + // Relative velocity + let dvx = ball1.vx - ball2.vx; + let dvy = ball1.vy - ball2.vy; + + // Relative velocity along collision normal (scaled) + let dvn = (dvx * nx + dvy * ny) / 1000; + + // Don't resolve if balls are moving apart + if dvn > 0 { + return; + } + + // Calculate impulse (simplified elastic collision) + let total_mass = ball1.mass + ball2.mass; + let impulse1 = (2 * ball2.mass * dvn) / total_mass; + let impulse2 = (2 * ball1.mass * dvn) / total_mass; + + // Apply impulse + ball1.vx -= (impulse1 * nx) / 1000; + ball1.vy -= (impulse1 * ny) / 1000; + ball2.vx += (impulse2 * nx) / 1000; + ball2.vy += (impulse2 * ny) / 1000; + + // Separate balls to prevent sticking + let overlap = min_dist - dist; + if overlap > 0 { + let sep = (overlap * 100) / 2 + 50; // Half overlap + small buffer, scaled + ball1.x -= (sep * nx) / 1000; + ball1.y -= (sep * ny) / 1000; + ball2.x += (sep * nx) / 1000; + ball2.y += (sep * ny) / 1000; + } + } +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println("Bounce demo starting (for Gus!)"); + + // Get framebuffer info + let info = match fbinfo() { + Ok(info) => info, + Err(e) => { + println("Error: Could not get framebuffer info"); + exit(e); + } + }; + + let width = info.left_pane_width() as i32; + let height = info.height as i32; + + println("Starting collision demo..."); + + // Create balls with different sizes, colors, and velocities + // Velocities are faster for more action! + let mut balls = [ + Ball::new(100, 100, 800, 600, 40, rgb(255, 50, 50)), // Red - large + Ball::new(300, 200, -700, 500, 35, rgb(50, 255, 50)), // Green + Ball::new(200, 400, 600, -700, 45, rgb(50, 50, 255)), // Blue - largest + Ball::new(400, 300, -500, -600, 30, rgb(255, 255, 50)), // Yellow + Ball::new(150, 300, 750, 400, 25, rgb(255, 50, 255)), // Magenta - small + Ball::new(350, 150, -650, 550, 28, rgb(50, 255, 255)), // Cyan + ]; + + // Animation loop + loop { + // Clear to dark background + let _ = fb_clear(rgb(15, 15, 30)); + + // Update positions + for ball in balls.iter_mut() { + ball.update_position(); + } + + // Check wall collisions + for ball in balls.iter_mut() { + ball.bounce_walls(width, height); + } + + // Check ball-ball collisions (all pairs) + for i in 0..balls.len() { + for j in (i + 1)..balls.len() { + // Split the array to get mutable references to both balls + let (left, right) = balls.split_at_mut(j); + check_collision(&mut left[i], &mut right[0]); + } + } + + // Draw all balls + for ball in balls.iter() { + ball.draw(); + } + + // Flush to screen + let _ = fb_flush(); + + // ~30 FPS (reduced for better performance on emulation) + sleep_ms(33); + } +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + println("Bounce demo panic!"); + exit(1); +} diff --git a/userspace/tests/build.sh b/userspace/tests/build.sh index 2b506dd9..a6362d55 100755 --- a/userspace/tests/build.sh +++ b/userspace/tests/build.sh @@ -140,8 +140,6 @@ BINARIES=( # Graphics syscall tests "fbinfo_test" "resolution" - "demo" - "bounce" # Coreutils argv integration tests "mkdir_argv_test" "cp_mv_argv_test" diff --git a/userspace/tests/demo.rs b/userspace/tests/demo.rs new file mode 100644 index 00000000..e37f678b --- /dev/null +++ b/userspace/tests/demo.rs @@ -0,0 +1,220 @@ +//! Animated graphics demo for Breenix +//! +//! This program draws animated graphics on the left pane of the screen. +//! Run it from the shell with: demo + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use libbreenix::graphics::{ + fb_clear, fb_draw_line, fb_draw_rect, fb_fill_circle, fb_fill_rect, fb_flush, fbinfo, rgb, +}; +use libbreenix::io::println; +use libbreenix::process::exit; +use libbreenix::time::sleep_ms; + +/// Pre-computed sine table (0-359 degrees, scaled by 1000) +/// sin(angle) * 1000 +const SIN_TABLE: [i32; 360] = [ + 0, 17, 35, 52, 70, 87, 105, 122, 139, 156, 174, 191, 208, 225, 242, 259, 276, 292, 309, 326, + 342, 358, 375, 391, 407, 423, 438, 454, 469, 485, 500, 515, 530, 545, 559, 574, 588, 602, 616, + 629, 643, 656, 669, 682, 695, 707, 719, 731, 743, 755, 766, 777, 788, 799, 809, 819, 829, 839, + 848, 857, 866, 875, 883, 891, 899, 906, 914, 921, 927, 934, 940, 946, 951, 956, 961, 966, 970, + 974, 978, 982, 985, 988, 990, 993, 995, 996, 998, 999, 999, 1000, 1000, 1000, 999, 999, 998, + 996, 995, 993, 990, 988, 985, 982, 978, 974, 970, 966, 961, 956, 951, 946, 940, 934, 927, 921, + 914, 906, 899, 891, 883, 875, 866, 857, 848, 839, 829, 819, 809, 799, 788, 777, 766, 755, 743, + 731, 719, 707, 695, 682, 669, 656, 643, 629, 616, 602, 588, 574, 559, 545, 530, 515, 500, 485, + 469, 454, 438, 423, 407, 391, 375, 358, 342, 326, 309, 292, 276, 259, 242, 225, 208, 191, 174, + 156, 139, 122, 105, 87, 70, 52, 35, 17, 0, -17, -35, -52, -70, -87, -105, -122, -139, -156, + -174, -191, -208, -225, -242, -259, -276, -292, -309, -326, -342, -358, -375, -391, -407, -423, + -438, -454, -469, -485, -500, -515, -530, -545, -559, -574, -588, -602, -616, -629, -643, -656, + -669, -682, -695, -707, -719, -731, -743, -755, -766, -777, -788, -799, -809, -819, -829, -839, + -848, -857, -866, -875, -883, -891, -899, -906, -914, -921, -927, -934, -940, -946, -951, -956, + -961, -966, -970, -974, -978, -982, -985, -988, -990, -993, -995, -996, -998, -999, -999, -1000, + -1000, -1000, -999, -999, -998, -996, -995, -993, -990, -988, -985, -982, -978, -974, -970, + -966, -961, -956, -951, -946, -940, -934, -927, -921, -914, -906, -899, -891, -883, -875, -866, + -857, -848, -839, -829, -819, -809, -799, -788, -777, -766, -755, -743, -731, -719, -707, -695, + -682, -669, -656, -643, -629, -616, -602, -588, -574, -559, -545, -530, -515, -500, -485, -469, + -454, -438, -423, -407, -391, -375, -358, -342, -326, -309, -292, -276, -259, -242, -225, -208, + -191, -174, -156, -139, -122, -105, -87, -70, -52, -35, -17, +]; + +/// Get sine value (scaled by 1000) for angle in degrees +fn sin(angle: i32) -> i32 { + let a = ((angle % 360) + 360) % 360; + SIN_TABLE[a as usize] +} + +/// Get cosine value (scaled by 1000) for angle in degrees +fn cos(angle: i32) -> i32 { + sin(angle + 90) +} + +/// Bouncing ball state +struct Ball { + x: i32, + y: i32, + vx: i32, + vy: i32, + radius: i32, + color: u32, +} + +impl Ball { + fn new(x: i32, y: i32, vx: i32, vy: i32, radius: i32, color: u32) -> Self { + Self { x, y, vx, vy, radius, color } + } + + fn update(&mut self, width: i32, height: i32) { + self.x += self.vx; + self.y += self.vy; + + // Bounce off walls + if self.x - self.radius < 0 { + self.x = self.radius; + self.vx = -self.vx; + } + if self.x + self.radius >= width { + self.x = width - self.radius - 1; + self.vx = -self.vx; + } + if self.y - self.radius < 0 { + self.y = self.radius; + self.vy = -self.vy; + } + if self.y + self.radius >= height { + self.y = height - self.radius - 1; + self.vy = -self.vy; + } + } + + fn draw(&self) { + let _ = fb_fill_circle(self.x, self.y, self.radius, self.color); + } +} + +/// Draw rotating lines from center +fn draw_rotating_lines(cx: i32, cy: i32, radius: i32, angle: i32, num_lines: i32) { + for i in 0..num_lines { + let a = angle + (i * 360 / num_lines); + let x2 = cx + (radius * cos(a)) / 1000; + let y2 = cy + (radius * sin(a)) / 1000; + + // Color based on angle + let hue = ((a % 360) + 360) % 360; + let color = hue_to_rgb(hue as u32); + let _ = fb_draw_line(cx, cy, x2, y2, color); + } +} + +/// Convert hue (0-359) to RGB color +fn hue_to_rgb(hue: u32) -> u32 { + let h = hue % 360; + let x = (255 * (60 - (h % 60).min(60 - (h % 60)))) / 60; + + match h / 60 { + 0 => rgb(255, x as u8, 0), + 1 => rgb(x as u8, 255, 0), + 2 => rgb(0, 255, x as u8), + 3 => rgb(0, x as u8, 255), + 4 => rgb(x as u8, 0, 255), + _ => rgb(255, 0, x as u8), + } +} + +/// Draw pulsing rectangles +fn draw_pulsing_rects(cx: i32, cy: i32, frame: i32) { + let pulse = (sin(frame * 3) + 1000) / 20; // 0-100 range + + for i in 0..5 { + let size = 20 + i * 15 + pulse / 5; + let alpha = 255 - i * 40; + let color = rgb(alpha as u8, (100 + i * 30) as u8, (200 - i * 20) as u8); + let _ = fb_draw_rect(cx - size, cy - size, size * 2, size * 2, color); + } +} + +/// Draw wave pattern +fn draw_wave(y_base: i32, width: i32, frame: i32, color: u32) { + let mut prev_y = y_base + (sin(frame) * 30) / 1000; + + for x in (0..width).step_by(4) { + let phase = frame + x * 2; + let y = y_base + (sin(phase) * 30) / 1000; + let _ = fb_draw_line(x - 4, prev_y, x, y, color); + prev_y = y; + } +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println("Breenix Graphics Demo starting..."); + + // Get framebuffer info + let info = match fbinfo() { + Ok(info) => info, + Err(e) => { + println("Error: Could not get framebuffer info"); + exit(e); + } + }; + + let width = info.left_pane_width() as i32; + let height = info.height as i32; + + println("Starting animation loop..."); + + // Create bouncing balls + let mut balls = [ + Ball::new(100, 100, 3, 2, 20, rgb(255, 100, 100)), + Ball::new(200, 150, -2, 3, 15, rgb(100, 255, 100)), + Ball::new(150, 200, 2, -2, 25, rgb(100, 100, 255)), + Ball::new(300, 100, -3, -2, 18, rgb(255, 255, 100)), + ]; + + let mut frame = 0i32; + let center_x = width / 2; + let center_y = height / 2; + + // Animation loop + loop { + // Clear to dark blue + let _ = fb_clear(rgb(10, 20, 40)); + + // Draw rotating lines in center + draw_rotating_lines(center_x, center_y - 100, 80, frame * 2, 12); + + // Draw pulsing rectangles + draw_pulsing_rects(center_x, center_y + 150, frame); + + // Draw wave patterns + draw_wave(height - 100, width, frame * 3, rgb(0, 150, 255)); + draw_wave(height - 130, width, frame * 3 + 60, rgb(0, 200, 150)); + draw_wave(height - 160, width, frame * 3 + 120, rgb(100, 100, 255)); + + // Update and draw bouncing balls + for ball in balls.iter_mut() { + ball.update(width, height); + ball.draw(); + } + + // Draw frame counter (simple rectangle indicator) + let indicator_width = (frame % 100) * 2; + let _ = fb_fill_rect(10, 10, indicator_width, 5, rgb(255, 255, 255)); + + // Flush to screen + let _ = fb_flush(); + + // Small delay for animation timing + sleep_ms(16); // ~60 FPS + + frame = frame.wrapping_add(1); + } +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + println("Demo panic!"); + exit(1); +}