From 1793360992821464b3d789fbe47a81dca0ed5b04 Mon Sep 17 00:00:00 2001 From: Stone749990226 <749990226@qq.com> Date: Mon, 24 Feb 2025 21:18:44 +0800 Subject: [PATCH 1/2] refactor: Extract RingBuffer from PipeRingBuffer for better reusability --- Cargo.lock | 5 + Cargo.toml | 1 + api/ruxos_posix_api/Cargo.toml | 5 +- api/ruxos_posix_api/src/imp/pipe.rs | 73 +----- crates/ringbuffer/Cargo.toml | 11 + crates/ringbuffer/src/lib.rs | 394 ++++++++++++++++++++++++++++ 6 files changed, 419 insertions(+), 70 deletions(-) create mode 100644 crates/ringbuffer/Cargo.toml create mode 100644 crates/ringbuffer/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5322cdb0b..b2327e3f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,6 +1491,10 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "ringbuffer" +version = "0.1.0" + [[package]] name = "riscv" version = "0.10.1" @@ -1820,6 +1824,7 @@ dependencies = [ "lazy_static", "memory_addr", "page_table", + "ringbuffer", "ruxconfig", "ruxfdtable", "ruxfeat", diff --git a/Cargo.toml b/Cargo.toml index 38925402e..838611483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "crates/spinlock", "crates/timer_list", "crates/tuple_for_each", + "crates/ringbuffer", "crates/tty", "modules/axalloc", diff --git a/api/ruxos_posix_api/Cargo.toml b/api/ruxos_posix_api/Cargo.toml index 85a078c43..95e9f3137 100644 --- a/api/ruxos_posix_api/Cargo.toml +++ b/api/ruxos_posix_api/Cargo.toml @@ -47,13 +47,16 @@ ruxfdtable = { path = "../../modules/ruxfdtable" } ruxmm = { path = "../../modules/ruxmm", optional = true } ruxfutex = { path = "../../modules/ruxfutex", optional = true } axalloc = { path = "../../modules/axalloc", optional = true } -ruxtask = { path = "../../modules/ruxtask", features = ["notest"], optional = true } +ruxtask = { path = "../../modules/ruxtask", features = [ + "notest", +], optional = true } ruxfs = { path = "../../modules/ruxfs", optional = true } ruxnet = { path = "../../modules/ruxnet", optional = true } # Other crates axio = { path = "../../crates/axio" } axerrno = { path = "../../crates/axerrno" } +ringbuffer = { path = "../../crates/ringbuffer" } memory_addr = "0.1.0" static_assertions = "1.1.0" spin = { version = "0.9" } diff --git a/api/ruxos_posix_api/src/imp/pipe.rs b/api/ruxos_posix_api/src/imp/pipe.rs index 87c1dbdd9..24a94e377 100644 --- a/api/ruxos_posix_api/src/imp/pipe.rs +++ b/api/ruxos_posix_api/src/imp/pipe.rs @@ -9,6 +9,7 @@ use alloc::sync::{Arc, Weak}; use core::ffi::c_int; +use ringbuffer::RingBuffer; use ruxfs::{fops, AbsPath}; use axerrno::{LinuxError, LinuxResult}; @@ -19,82 +20,16 @@ use ruxfdtable::{FileLike, RuxStat}; use crate::{ctypes, sys_fcntl}; use ruxtask::fs::{add_file_like, close_file_like}; -#[derive(Copy, Clone, PartialEq)] -enum RingBufferStatus { - Full, - Empty, - Normal, -} - -const RING_BUFFER_SIZE: usize = ruxconfig::PIPE_BUFFER_SIZE; - -pub struct PipeRingBuffer { - arr: [u8; RING_BUFFER_SIZE], - head: usize, - tail: usize, - status: RingBufferStatus, -} - -impl PipeRingBuffer { - pub const fn new() -> Self { - Self { - arr: [0; RING_BUFFER_SIZE], - head: 0, - tail: 0, - status: RingBufferStatus::Empty, - } - } - - pub fn write_byte(&mut self, byte: u8) { - self.status = RingBufferStatus::Normal; - self.arr[self.tail] = byte; - self.tail = (self.tail + 1) % RING_BUFFER_SIZE; - if self.tail == self.head { - self.status = RingBufferStatus::Full; - } - } - - pub fn read_byte(&mut self) -> u8 { - self.status = RingBufferStatus::Normal; - let c = self.arr[self.head]; - self.head = (self.head + 1) % RING_BUFFER_SIZE; - if self.head == self.tail { - self.status = RingBufferStatus::Empty; - } - c - } - - /// Get the length of remaining data in the buffer - pub const fn available_read(&self) -> usize { - if matches!(self.status, RingBufferStatus::Empty) { - 0 - } else if self.tail > self.head { - self.tail - self.head - } else { - self.tail + RING_BUFFER_SIZE - self.head - } - } - - /// Get the length of remaining space in the buffer - pub const fn available_write(&self) -> usize { - if matches!(self.status, RingBufferStatus::Full) { - 0 - } else { - RING_BUFFER_SIZE - self.available_read() - } - } -} - pub struct Pipe { readable: bool, - buffer: Arc>, + buffer: Arc>, // to find the write end when the read end is closed - _write_end_closed: Option>>, + _write_end_closed: Option>>, } impl Pipe { pub fn new() -> (Pipe, Pipe) { - let buffer = Arc::new(Mutex::new(PipeRingBuffer::new())); + let buffer = Arc::new(Mutex::new(RingBuffer::new(ruxconfig::PIPE_BUFFER_SIZE))); let read_end = Pipe { readable: true, buffer: buffer.clone(), diff --git a/crates/ringbuffer/Cargo.toml b/crates/ringbuffer/Cargo.toml new file mode 100644 index 000000000..56daa0c36 --- /dev/null +++ b/crates/ringbuffer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ringbuffer" +version = "0.1.0" +edition = "2021" +authors = ["RuiWei Cheng <1982833213@qq.com>", "Quan Shi <749990226@qq.com>"] +description = "A data structure that uses a single, fixed-size buffer as if it were connected end-to-end" +license = "GPL-3.0-or-later OR Apache-2.0" +homepage = "https://github.com/syswonder/ruxos" +repository = "https://github.com/syswonder/ruxos/tree/dev/crates/ringbuffer" + +[build-dependencies] diff --git a/crates/ringbuffer/src/lib.rs b/crates/ringbuffer/src/lib.rs new file mode 100644 index 000000000..519a7ab71 --- /dev/null +++ b/crates/ringbuffer/src/lib.rs @@ -0,0 +1,394 @@ +//! A circular buffer (ring buffer) implementation for efficient FIFO operations. +#![cfg_attr(not(test), no_std)] + +extern crate alloc; + +use alloc::{vec, vec::Vec}; +use core::cmp; + +/// Represents the current state of the ring buffer +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +enum RingBufferState { + #[default] + /// Buffer contains no data + Empty, + /// Buffer is completely full + Full, + /// Buffer has data but isn't full + Normal, +} + +/// A circular buffer implementation using a Vec as backing storage +pub struct RingBuffer { + /// Underlying data storage + arr: Vec, + // NOTE: When and only when `head` equals `tail`, `state` can only be `Full` or `Empty`. + /// Index of the next element to read + head: usize, + /// Index of the next element to write + tail: usize, + /// Current buffer state + state: RingBufferState, +} + +impl RingBuffer { + /// Creates a new RingBuffer with the specified capacity + /// + /// # Arguments + /// * `len` - Capacity of the buffer (must be greater than 0) + /// + /// # Panics + /// Panics if `len` is 0 + pub fn new(len: usize) -> Self { + assert!(len > 0, "Buffer length must be positive"); + Self { + arr: vec![0; len], + head: 0, + tail: 0, + state: RingBufferState::Empty, + } + } + + /// Returns true if the buffer contains no data + pub fn is_empty(&self) -> bool { + self.state == RingBufferState::Empty + } + + /// Returns true if the buffer has no free space + pub fn is_full(&self) -> bool { + self.state == RingBufferState::Full + } + + /// Read as much as possible to fill `buf`. + /// + /// # Arguments + /// * `buf` - Destination buffer for read data + /// + /// # Returns + /// Number of bytes actually written + pub fn read(&mut self, buf: &mut [u8]) -> usize { + if self.state == RingBufferState::Empty || buf.is_empty() { + return 0; + } + + let ret_len; + let n = self.arr.len(); + if self.head < self.tail { + ret_len = cmp::min(self.tail - self.head, buf.len()); + buf[..ret_len].copy_from_slice(&self.arr[self.head..self.head + ret_len]); + } else { + // also handles full + ret_len = cmp::min(n - self.head + self.tail, buf.len()); + if ret_len <= (n - self.head) { + buf[..ret_len].copy_from_slice(&self.arr[self.head..self.head + ret_len]); + } else { + let right_len = n - self.head; + buf[..right_len].copy_from_slice(&self.arr[self.head..]); + buf[right_len..ret_len].copy_from_slice(&self.arr[..(ret_len - right_len)]); + } + } + self.head = (self.head + ret_len) % n; + + if self.head == self.tail { + self.state = RingBufferState::Empty; + } else { + self.state = RingBufferState::Normal; + } + + ret_len + } + + /// Write as much as possible to fill the ring buffer. + /// + /// # Arguments + /// * `buf` - Source buffer containing data to write + /// + /// # Returns + /// Number of bytes actually written + pub fn write(&mut self, buf: &[u8]) -> usize { + if self.state == RingBufferState::Full || buf.is_empty() { + return 0; + } + + let ret_len; + let n = self.arr.len(); + if self.head <= self.tail { + // also handles empty + ret_len = cmp::min(n - (self.tail - self.head), buf.len()); + if ret_len <= (n - self.tail) { + self.arr[self.tail..self.tail + ret_len].copy_from_slice(&buf[..ret_len]); + } else { + self.arr[self.tail..].copy_from_slice(&buf[..n - self.tail]); + self.arr[..(ret_len - (n - self.tail))] + .copy_from_slice(&buf[n - self.tail..ret_len]); + } + } else { + ret_len = cmp::min(self.head - self.tail, buf.len()); + self.arr[self.tail..self.tail + ret_len].copy_from_slice(&buf[..ret_len]); + } + self.tail = (self.tail + ret_len) % n; + + if self.head == self.tail { + self.state = RingBufferState::Full; + } else { + self.state = RingBufferState::Normal; + } + + ret_len + } + + /// Removes and returns the next byte from the buffer + /// + /// # Returns + /// `Some(byte)` if available, `None` if buffer is empty + pub fn dequeue(&mut self) -> Option { + if self.is_empty() { + return None; + } + + let n = self.arr.len(); + let c = self.arr[self.head]; + self.head = (self.head + 1) % n; + if self.head == self.tail { + self.state = RingBufferState::Empty; + } else { + self.state = RingBufferState::Normal; + } + Some(c) + } + + /// Adds a single byte to the buffer + /// + /// # Arguments + /// * `byte` - Byte to add to the buffer + /// + /// # Returns + /// `Some(())` if successful, `None` if buffer is full + pub fn enqueue(&mut self, byte: u8) -> Option<()> { + if self.is_full() { + return None; + } + + let n = self.arr.len(); + self.arr[self.tail] = byte; + self.tail = (self.tail + 1) % n; + if self.head == self.tail { + self.state = RingBufferState::Full; + } else { + self.state = RingBufferState::Normal; + } + Some(()) + } + + /// Adds a single byte to the buffer, panics if full + /// + /// # Panics + /// Panics if the buffer is full + pub fn write_byte(&mut self, byte: u8) { + self.enqueue(byte).expect("Ring buffer is full"); + } + + /// Removes and returns a single byte from the buffer, panics if empty + /// + /// # Panics + /// Panics if the buffer is empty + pub fn read_byte(&mut self) -> u8 { + self.dequeue().expect("Ring buffer is empty") + } + + /// Returns the number of bytes available for reading + pub fn available_read(&self) -> usize { + match self.state { + RingBufferState::Empty => 0, + RingBufferState::Full => self.arr.len(), + RingBufferState::Normal => { + let n = self.arr.len(); + if self.head < self.tail { + self.tail - self.head + } else { + (n - self.head) + self.tail + } + } + } + } + + /// Returns the number of bytes available for writing + pub fn available_write(&self) -> usize { + self.arr.len() - self.available_read() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new() { + let rb = RingBuffer::new(5); + assert_eq!(rb.arr.len(), 5); + assert!(rb.is_empty()); + assert!(!rb.is_full()); + } + + #[test] + fn test_enqueue_dequeue_single() { + let mut rb = RingBuffer::new(3); + assert_eq!(rb.enqueue(1), Some(())); + assert!(!rb.is_empty()); + assert!(!rb.is_full()); + assert_eq!(rb.dequeue(), Some(1)); + assert!(rb.is_empty()); + } + + #[test] + fn test_full_condition() { + let mut rb = RingBuffer::new(2); + assert_eq!(rb.enqueue(1), Some(())); + assert_eq!(rb.enqueue(2), Some(())); + assert!(rb.is_full()); + assert_eq!(rb.enqueue(3), None); + } + + #[test] + fn test_empty_condition() { + let mut rb = RingBuffer::new(2); + assert_eq!(rb.dequeue(), None); + rb.enqueue(1).unwrap(); + rb.dequeue().unwrap(); + assert_eq!(rb.dequeue(), None); + } + + #[test] + fn test_wrap_around() { + let mut rb = RingBuffer::new(3); + rb.enqueue(1).unwrap(); + rb.enqueue(2).unwrap(); + rb.enqueue(3).unwrap(); + assert!(rb.is_full()); + assert_eq!(rb.dequeue().unwrap(), 1); + assert_eq!(rb.dequeue().unwrap(), 2); + assert_eq!(rb.dequeue().unwrap(), 3); + assert!(rb.is_empty()); + + rb.enqueue(4).unwrap(); + rb.enqueue(5).unwrap(); + assert_eq!(rb.dequeue().unwrap(), 4); + rb.enqueue(6).unwrap(); + assert_eq!(rb.dequeue().unwrap(), 5); + assert_eq!(rb.dequeue().unwrap(), 6); + } + + #[test] + fn test_read_write_basic() { + let mut rb = RingBuffer::new(5); + let data = [1, 2, 3]; + assert_eq!(rb.write(&data), 3); + assert_eq!(rb.head, 0); + assert_eq!(rb.tail, 3); + + let mut buf = [0; 5]; + assert_eq!(rb.read(&mut buf), 3); + assert_eq!(&buf[..3], &[1, 2, 3]); + assert!(rb.is_empty()); + } + + #[test] + fn test_read_write_wrap() { + let mut rb = RingBuffer::new(5); + + assert_eq!(rb.write(&[1, 2, 3, 4]), 4); + assert_eq!(rb.head, 0); + assert_eq!(rb.tail, 4); + assert!(!rb.is_full()); + + let mut buf = [0; 3]; + assert_eq!(rb.read(&mut buf), 3); + assert_eq!(buf, [1, 2, 3]); + assert_eq!(rb.head, 3); + assert_eq!(rb.tail, 4); + + assert_eq!(rb.write(&[5, 6, 7]), 3); + assert_eq!(rb.tail, (4 + 3) % 5); + assert_eq!(rb.tail, 2); + + let mut buf = [0; 5]; + assert_eq!(rb.read(&mut buf), 4); + assert_eq!(&buf[..4], &[4, 5, 6, 7]); + assert!(rb.is_empty()); + } + + #[test] + fn test_full_read_write() { + let mut rb = RingBuffer::new(5); + assert_eq!(rb.write(&[1, 2, 3, 4, 5]), 5); + assert!(rb.is_full()); + assert_eq!(rb.write(&[6]), 0); + let mut buf = [0; 5]; + assert_eq!(rb.read(&mut buf), 5); + assert_eq!(buf, [1, 2, 3, 4, 5]); + assert!(rb.is_empty()); + } + + #[test] + fn test_partial_read_write() { + let mut rb = RingBuffer::new(5); + assert_eq!(rb.write(&[1, 2]), 2); + let mut buf = [0; 3]; + assert_eq!(rb.read(&mut buf), 2); + assert_eq!(&buf[..2], &[1, 2]); + } + + #[test] + fn test_buffer_edge_cases() { + let mut rb = RingBuffer::new(1); + assert!(rb.is_empty()); + rb.enqueue(42).unwrap(); + assert!(rb.is_full()); + assert_eq!(rb.dequeue(), Some(42)); + assert!(rb.is_empty()); + } + + #[test] + fn test_complex_operations() { + let mut rb = RingBuffer::new(5); + rb.write(&[1, 2, 3]); + let mut buf = [0; 2]; + rb.read(&mut buf); + rb.write(&[4, 5, 6, 7]); + let mut buf = [0; 5]; + assert_eq!(rb.read(&mut buf), 5); + } + + #[test] + fn test_state_transitions() { + let mut rb = RingBuffer::new(3); + // Empty -> Normal + rb.enqueue(1).unwrap(); + assert_eq!(rb.state, RingBufferState::Normal); + // Normal -> Full + rb.enqueue(2).unwrap(); + rb.enqueue(3).unwrap(); + assert_eq!(rb.state, RingBufferState::Full); + // Full -> Normal + rb.dequeue().unwrap(); + assert_eq!(rb.state, RingBufferState::Normal); + // Normal -> Empty + rb.dequeue().unwrap(); + rb.dequeue().unwrap(); + assert_eq!(rb.state, RingBufferState::Empty); + } + + #[test] + fn test_available() { + let mut rb = RingBuffer::new(5); + rb.write_byte(1); + rb.write_byte(2); + assert_eq!(rb.available_read(), 2); + assert_eq!(rb.available_write(), 3); + + let byte = rb.read_byte(); + assert_eq!(byte, 1); + assert_eq!(rb.available_read(), 1); + assert_eq!(rb.available_write(), 4); + } +} From f3bfb67994fe72e09294931f59d0408e01c61810 Mon Sep 17 00:00:00 2001 From: Stone749990226 <749990226@qq.com> Date: Sat, 1 Mar 2025 15:40:05 +0800 Subject: [PATCH 2/2] feat(tty): enhance TTY support with Termios implementation - Introduce Termios structure with c_cflags, c_iflags and other POSIX-compliant fields - Add ioctl methods for terminal attribute control - Remove optional 'tty' feature flag, make TTY support a core component --- Cargo.lock | 32 +- Cargo.toml | 2 +- api/ruxfeat/Cargo.toml | 20 +- api/ruxos_posix_api/Cargo.toml | 1 + api/ruxos_posix_api/src/imp/fs.rs | 8 +- api/ruxos_posix_api/src/imp/io.rs | 11 +- api/ruxos_posix_api/src/imp/ioctl.rs | 45 +- api/ruxos_posix_api/src/imp/pipe.rs | 6 +- api/ruxos_posix_api/src/imp/stdio.rs | 148 +++---- crates/ringbuffer/src/lib.rs | 136 ++++-- crates/tty/Cargo.toml | 9 - crates/tty/src/buffer.rs | 149 ------- crates/tty/src/constant.rs | 29 -- crates/tty/src/driver.rs | 167 -------- crates/tty/src/ldisc.rs | 221 ---------- crates/tty/src/lib.rs | 72 ---- crates/tty/src/tty.rs | 104 ----- modules/ruxfdtable/src/lib.rs | 6 + modules/ruxhal/Cargo.toml | 17 +- modules/ruxhal/src/lib.rs | 55 +-- .../src/platform/aarch64_common/pl011.rs | 39 +- modules/ruxruntime/Cargo.toml | 4 +- modules/ruxruntime/src/lib.rs | 3 +- modules/ruxtty/Cargo.toml | 20 + modules/ruxtty/src/driver.rs | 71 ++++ modules/ruxtty/src/ldisc.rs | 343 +++++++++++++++ modules/ruxtty/src/lib.rs | 137 ++++++ modules/ruxtty/src/termios.rs | 401 ++++++++++++++++++ modules/ruxtty/src/tty.rs | 153 +++++++ 29 files changed, 1369 insertions(+), 1040 deletions(-) delete mode 100644 crates/tty/Cargo.toml delete mode 100644 crates/tty/src/buffer.rs delete mode 100644 crates/tty/src/constant.rs delete mode 100644 crates/tty/src/driver.rs delete mode 100644 crates/tty/src/ldisc.rs delete mode 100644 crates/tty/src/lib.rs delete mode 100644 crates/tty/src/tty.rs create mode 100644 modules/ruxtty/Cargo.toml create mode 100644 modules/ruxtty/src/driver.rs create mode 100644 modules/ruxtty/src/ldisc.rs create mode 100644 modules/ruxtty/src/lib.rs create mode 100644 modules/ruxtty/src/termios.rs create mode 100644 modules/ruxtty/src/tty.rs diff --git a/Cargo.lock b/Cargo.lock index b2327e3f0..4b6d38fc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1643,6 +1643,7 @@ dependencies = [ "ruxnet", "ruxruntime", "ruxtask", + "ruxtty", "spinlock", ] @@ -1692,6 +1693,8 @@ dependencies = [ "arm_gic", "arm_pl011", "axalloc", + "axerrno", + "axio", "axlog", "bitflags 2.6.0", "cfg-if", @@ -1715,11 +1718,11 @@ dependencies = [ "raw-cpuid 11.1.0", "riscv", "ruxconfig", + "ruxtty", "sbi-rt", "spinlock", "static_assertions", "tock-registers", - "tty", "virtio-drivers", "x2apic", "x86", @@ -1835,6 +1838,7 @@ dependencies = [ "ruxnet", "ruxruntime", "ruxtask", + "ruxtty", "spin", "spinlock", "static_assertions", @@ -1881,7 +1885,7 @@ dependencies = [ "ruxnet", "ruxrand", "ruxtask", - "tty", + "ruxtty", ] [[package]] @@ -1916,6 +1920,21 @@ dependencies = [ "timer_list", ] +[[package]] +name = "ruxtty" +version = "0.0.1" +dependencies = [ + "axerrno", + "axio", + "axlog", + "bitflags 2.6.0", + "lazy_init", + "num_enum", + "ringbuffer", + "spin", + "spinlock", +] + [[package]] name = "ryu" version = "1.0.18" @@ -2178,15 +2197,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tty" -version = "0.0.1" -dependencies = [ - "lazy_init", - "log", - "spinlock", -] - [[package]] name = "tuple_for_each" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 838611483..f8a7ee0e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ members = [ "crates/timer_list", "crates/tuple_for_each", "crates/ringbuffer", - "crates/tty", "modules/axalloc", "modules/axlog", @@ -54,6 +53,7 @@ members = [ "modules/ruxtask", "modules/ruxfutex", "modules/ruxrand", + "modules/ruxtty", "api/ruxfeat", "api/arceos_api", diff --git a/api/ruxfeat/Cargo.toml b/api/ruxfeat/Cargo.toml index 7ba90ce99..4960f22de 100644 --- a/api/ruxfeat/Cargo.toml +++ b/api/ruxfeat/Cargo.toml @@ -31,7 +31,13 @@ alloc = ["axalloc", "ruxruntime/alloc", "ruxfs/alloc", "ruxhal/alloc"] alloc-tlsf = ["axalloc/tlsf"] alloc-slab = ["axalloc/slab"] alloc-buddy = ["axalloc/buddy"] -paging = ["alloc", "ruxhal/paging", "ruxtask/paging", "ruxruntime/paging", "ruxmm/paging"] +paging = [ + "alloc", + "ruxhal/paging", + "ruxtask/paging", + "ruxruntime/paging", + "ruxmm/paging", +] tls = ["alloc", "ruxhal/tls", "ruxruntime/tls", "ruxtask?/tls"] # Multi-threading and scheduler @@ -78,7 +84,13 @@ virtio-9p = [ net-9p = ["9pfs", "net", "rux9p/net-9p", "ruxruntime/net-9p"] # virtio console -virtio_console = ["ruxhal/virtio_console", "ruxruntime/virtio_console", "alloc", "ruxdriver/virtio_console", "ruxdriver/virtio"] +virtio_console = [ + "ruxhal/virtio_console", + "ruxruntime/virtio_console", + "alloc", + "ruxdriver/virtio_console", + "ruxdriver/virtio", +] # Device drivers bus-mmio = ["ruxdriver?/bus-mmio"] @@ -95,11 +107,13 @@ log-level-info = ["axlog/log-level-info"] log-level-debug = ["axlog/log-level-debug"] log-level-trace = ["axlog/log-level-trace"] -tty = ["ruxhal/tty", "ruxruntime/tty", "alloc", "irq"] +# this feature has already been deprecated +tty = [] [dependencies] ruxruntime = { path = "../../modules/ruxruntime" } ruxhal = { path = "../../modules/ruxhal" } +ruxtty = { path = "../../modules/ruxtty" } ruxmm = { path = "../../modules/ruxmm" } axlog = { path = "../../modules/axlog" } axalloc = { path = "../../modules/axalloc", optional = true } diff --git a/api/ruxos_posix_api/Cargo.toml b/api/ruxos_posix_api/Cargo.toml index 95e9f3137..31b2c865c 100644 --- a/api/ruxos_posix_api/Cargo.toml +++ b/api/ruxos_posix_api/Cargo.toml @@ -52,6 +52,7 @@ ruxtask = { path = "../../modules/ruxtask", features = [ ], optional = true } ruxfs = { path = "../../modules/ruxfs", optional = true } ruxnet = { path = "../../modules/ruxnet", optional = true } +ruxtty = { path = "../../modules/ruxtty" } # Other crates axio = { path = "../../crates/axio" } diff --git a/api/ruxos_posix_api/src/imp/fs.rs b/api/ruxos_posix_api/src/imp/fs.rs index b0428f523..7f4ee643e 100644 --- a/api/ruxos_posix_api/src/imp/fs.rs +++ b/api/ruxos_posix_api/src/imp/fs.rs @@ -24,7 +24,7 @@ use ruxfs::{ use crate::ctypes; use ruxtask::fs::{get_file_like, Directory, File}; -use super::stdio::{stdin, stdout}; +use super::stdio::{Stdin, Stdout}; struct InitFsImpl; @@ -33,9 +33,9 @@ impl ruxtask::fs::InitFs for InitFsImpl { fn add_stdios_to_fd_table(fs: &mut ruxtask::fs::FileSystem) { debug!("init initial process's fd_table"); let fd_table = &mut fs.fd_table; - fd_table.add_at(0, Arc::new(stdin()) as _).unwrap(); // stdin - fd_table.add_at(1, Arc::new(stdout()) as _).unwrap(); // stdout - fd_table.add_at(2, Arc::new(stdout()) as _).unwrap(); // stderr + fd_table.add_at(0, Arc::new(Stdin::default()) as _).unwrap(); // stdin + fd_table.add_at(1, Arc::new(Stdout {}) as _).unwrap(); // stdout + fd_table.add_at(2, Arc::new(Stdout {}) as _).unwrap(); // stderr } } diff --git a/api/ruxos_posix_api/src/imp/io.rs b/api/ruxos_posix_api/src/imp/io.rs index 21ba68eeb..e067a45f6 100644 --- a/api/ruxos_posix_api/src/imp/io.rs +++ b/api/ruxos_posix_api/src/imp/io.rs @@ -11,10 +11,13 @@ use crate::ctypes; use axerrno::LinuxError; use core::ffi::{c_int, c_void}; -#[cfg(not(feature = "fd"))] -use axio::prelude::*; #[cfg(feature = "fd")] use ruxtask::fs::get_file_like; +#[cfg(not(feature = "fd"))] +use { + axio::prelude::*, + imp::stdio::{Stdin, Stdout}, +}; /// Read data from the file indicated by `fd`. /// @@ -32,7 +35,7 @@ pub fn sys_read(fd: c_int, buf: *mut c_void, count: usize) -> ctypes::ssize_t { } #[cfg(not(feature = "fd"))] match fd { - 0 => Ok(super::stdio::stdin().read(dst)? as ctypes::ssize_t), + 0 => Ok(Stdin::default()._read(dst)? as ctypes::ssize_t), 1 | 2 => Err(LinuxError::EPERM), _ => Err(LinuxError::EBADF), } @@ -56,7 +59,7 @@ pub fn sys_write(fd: c_int, buf: *const c_void, count: usize) -> ctypes::ssize_t #[cfg(not(feature = "fd"))] match fd { 0 => Err(LinuxError::EPERM), - 1 | 2 => Ok(super::stdio::stdout().write(src)? as ctypes::ssize_t), + 1 | 2 => Ok(Stdout {}.write(src)? as ctypes::ssize_t), _ => Err(LinuxError::EBADF), } }) diff --git a/api/ruxos_posix_api/src/imp/ioctl.rs b/api/ruxos_posix_api/src/imp/ioctl.rs index e0d8e5b4d..dbde59c4c 100644 --- a/api/ruxos_posix_api/src/imp/ioctl.rs +++ b/api/ruxos_posix_api/src/imp/ioctl.rs @@ -7,27 +7,12 @@ * See the Mulan PSL v2 for more details. */ -use crate::sys_getpgid; -use axerrno::LinuxError; use core::ffi::c_int; use ruxtask::fs::get_file_like; -/// IOCTL oprations -pub const TCGETS: usize = 0x5401; -pub const TIOCGPGRP: usize = 0x540F; -pub const TIOCSPGRP: usize = 0x5410; -pub const TIOCGWINSZ: usize = 0x5413; pub const FIONBIO: usize = 0x5421; pub const FIOCLEX: usize = 0x5451; -#[derive(Clone, Copy, Default)] -pub struct ConsoleWinSize { - pub ws_row: u16, - pub ws_col: u16, - pub ws_xpixel: u16, - pub ws_ypixel: u16, -} - /// ioctl implementation, /// currently only support fd = 1 pub fn sys_ioctl(fd: c_int, request: usize, data: usize) -> c_int { @@ -40,35 +25,11 @@ pub fn sys_ioctl(fd: c_int, request: usize, data: usize) -> c_int { } Ok(0) } - // TODO: a temporary solution for TIOCGWINSZ. - TIOCGWINSZ => { - let winsize = data as *mut ConsoleWinSize; - unsafe { - *winsize = ConsoleWinSize::default(); - } - if fd == 0 || fd == 1 || fd == 2 { - Ok(0) - } else { - Ok(-1) - } - } - TCGETS => { - debug!("sys_ioctl: tty TCGETS"); - Ok(0) - } - TIOCSPGRP => { - warn!("stdout pretend to be tty"); - Ok(0) - } - TIOCGPGRP => { - warn!("stdout TIOCGPGRP, pretend to be have a tty process group."); - unsafe { - *(data as *mut u32) = sys_getpgid(0) as _; - } + FIOCLEX => Ok(0), + _ => { + get_file_like(fd)?.ioctl(request, data)?; Ok(0) } - FIOCLEX => Ok(0), - _ => Err(LinuxError::EINVAL), } }) } diff --git a/api/ruxos_posix_api/src/imp/pipe.rs b/api/ruxos_posix_api/src/imp/pipe.rs index 24a94e377..b953b1c47 100644 --- a/api/ruxos_posix_api/src/imp/pipe.rs +++ b/api/ruxos_posix_api/src/imp/pipe.rs @@ -96,7 +96,8 @@ impl FileLike for Pipe { if read_size == max_len { return Ok(read_size); } - buf[read_size] = ring_buffer.read_byte(); + debug_assert!(!ring_buffer.is_empty()); + buf[read_size] = ring_buffer.dequeue().unwrap(); read_size += 1; } Ok(read_size) @@ -121,7 +122,8 @@ impl FileLike for Pipe { if write_size == max_len { return Ok(write_size); } - ring_buffer.write_byte(buf[write_size]); + debug_assert!(!ring_buffer.is_full()); + ring_buffer.enqueue(buf[write_size]); write_size += 1; } } diff --git a/api/ruxos_posix_api/src/imp/stdio.rs b/api/ruxos_posix_api/src/imp/stdio.rs index f6ddfb0c1..73c986313 100644 --- a/api/ruxos_posix_api/src/imp/stdio.rs +++ b/api/ruxos_posix_api/src/imp/stdio.rs @@ -7,128 +7,73 @@ * See the Mulan PSL v2 for more details. */ -use axerrno::AxResult; -use axio::{prelude::*, BufReader}; -use axsync::Mutex; - #[cfg(feature = "fd")] use { alloc::sync::Arc, - axerrno::{AxError, LinuxError, LinuxResult}, + axerrno::{LinuxError, LinuxResult}, axio::PollState, core::sync::atomic::{AtomicBool, Ordering}, ruxfs::AbsPath, }; -struct StdinRaw; -struct StdoutRaw; - -#[cfg(feature = "alloc")] -extern crate alloc; -#[cfg(feature = "alloc")] -static STDIO_TTY_NAME: lazy_init::LazyInit = lazy_init::LazyInit::new(); -#[cfg(not(feature = "alloc"))] -static STDIO_TTY_NAME: &str = "dummy"; - -fn get_stdio_tty_name() -> &'static str { - #[cfg(feature = "alloc")] - { - if !STDIO_TTY_NAME.is_init() { - let name = ruxhal::get_all_device_names().first().unwrap().clone(); - STDIO_TTY_NAME.init_by(name); - } - } - &STDIO_TTY_NAME -} - -impl Read for StdinRaw { - // Non-blocking read, returns number of bytes read. - fn read(&mut self, buf: &mut [u8]) -> AxResult { - Ok(ruxhal::tty_read(buf, get_stdio_tty_name())) - } -} - -impl Write for StdoutRaw { - fn write(&mut self, buf: &[u8]) -> AxResult { - Ok(ruxhal::tty_write(buf, get_stdio_tty_name())) - } - - fn flush(&mut self) -> AxResult { - Ok(()) - } -} +use axerrno::{AxError, AxResult}; +use axio::prelude::*; +#[derive(Default)] pub struct Stdin { - inner: &'static Mutex>, #[cfg(feature = "fd")] nonblocking: AtomicBool, } impl Stdin { - // Block until at least one byte is read. - fn read_blocked(&self, buf: &mut [u8]) -> AxResult { - let read_len = self.inner.lock().read(buf)?; - if buf.is_empty() || read_len > 0 { - return Ok(read_len); - } - // try again until we get something + pub fn _read(&self, buf: &mut [u8]) -> AxResult { loop { - let read_len = self.inner.lock().read(buf)?; - if read_len > 0 { - return Ok(read_len); + #[cfg(not(all(feature = "irq", target_arch = "aarch64")))] + { + // Only the aarch64 architecture implements UART IRQ Handler, which asynchronously + // transmits characters to Tty with higher efficiency. Current implementations for + // x86_64 and riscv architectures lack this capability, requiring polling-based + // reads from the console instead. + while let Some(c) = ruxhal::console::getchar() { + ruxtty::tty_receive_char(c); + } } - crate::sys_sched_yield(); - } - } - - // Attempt a non-blocking read operation. - #[cfg(feature = "fd")] - fn read_nonblocked(&self, buf: &mut [u8]) -> AxResult { - if let Some(mut inner) = self.inner.try_lock() { - let read_len = inner.read(buf)?; - Ok(read_len) - } else { - Err(AxError::WouldBlock) + match ruxtty::tty_read(buf) { + Ok(len) => return Ok(len), + Err(AxError::WouldBlock) => { + #[cfg(feature = "fd")] + if self.nonblocking.load(Ordering::Relaxed) { + return Err(AxError::WouldBlock); + } + crate::sys_sched_yield(); + } + Err(_) => unreachable!(), + }; } } } +pub struct Stdout; + +#[cfg(feature = "alloc")] +extern crate alloc; + impl Read for Stdin { fn read(&mut self, buf: &mut [u8]) -> AxResult { - self.read_blocked(buf) + self._read(buf) } } -pub struct Stdout { - inner: &'static Mutex, -} - impl Write for Stdout { fn write(&mut self, buf: &[u8]) -> AxResult { - self.inner.lock().write(buf) + ruxtty::tty_write(buf) } fn flush(&mut self) -> AxResult { - self.inner.lock().flush() - } -} - -/// Constructs a new handle to the standard input of the current process. -pub fn stdin() -> Stdin { - static INSTANCE: Mutex> = Mutex::new(BufReader::new(StdinRaw)); - Stdin { - inner: &INSTANCE, - #[cfg(feature = "fd")] - nonblocking: AtomicBool::from(false), + Ok(()) } } -/// Constructs a new handle to the standard output of the current process. -pub fn stdout() -> Stdout { - static INSTANCE: Mutex = Mutex::new(StdoutRaw); - Stdout { inner: &INSTANCE } -} - #[cfg(feature = "fd")] impl ruxfdtable::FileLike for Stdin { fn path(&self) -> AbsPath { @@ -136,10 +81,7 @@ impl ruxfdtable::FileLike for Stdin { } fn read(&self, buf: &mut [u8]) -> LinuxResult { - match self.nonblocking.load(Ordering::Relaxed) { - true => Ok(self.read_nonblocked(buf)?), - false => Ok(self.read_blocked(buf)?), - } + self._read(buf).map_err(|_| LinuxError::EAGAIN) } fn write(&self, _buf: &[u8]) -> LinuxResult { @@ -165,17 +107,23 @@ impl ruxfdtable::FileLike for Stdin { } fn poll(&self) -> LinuxResult { - Ok(PollState { - readable: true, - writable: true, - pollhup: false, - }) + #[cfg(not(all(feature = "irq", target_arch = "aarch64")))] + { + while let Some(c) = ruxhal::console::getchar() { + ruxtty::tty_receive_char(c); + } + } + Ok(ruxtty::tty_poll()) } fn set_nonblocking(&self, nonblocking: bool) -> LinuxResult { self.nonblocking.store(nonblocking, Ordering::Relaxed); Ok(()) } + + fn ioctl(&self, cmd: usize, arg: usize) -> LinuxResult { + ruxtty::tty_ioctl(cmd, arg).map_err(LinuxError::from) + } } #[cfg(feature = "fd")] @@ -189,7 +137,7 @@ impl ruxfdtable::FileLike for Stdout { } fn write(&self, buf: &[u8]) -> LinuxResult { - Ok(self.inner.lock().write(buf)?) + ruxtty::tty_write(buf).map_err(LinuxError::from) } fn flush(&self) -> LinuxResult { @@ -221,4 +169,8 @@ impl ruxfdtable::FileLike for Stdout { fn set_nonblocking(&self, _nonblocking: bool) -> LinuxResult { Ok(()) } + + fn ioctl(&self, cmd: usize, arg: usize) -> LinuxResult { + ruxtty::tty_ioctl(cmd, arg).map_err(LinuxError::from) + } } diff --git a/crates/ringbuffer/src/lib.rs b/crates/ringbuffer/src/lib.rs index 519a7ab71..087b2ab58 100644 --- a/crates/ringbuffer/src/lib.rs +++ b/crates/ringbuffer/src/lib.rs @@ -18,7 +18,7 @@ enum RingBufferState { Normal, } -/// A circular buffer implementation using a Vec as backing storage +/// A circular buffer implementation using a `Vec` as backing storage pub struct RingBuffer { /// Underlying data storage arr: Vec, @@ -59,32 +59,32 @@ impl RingBuffer { self.state == RingBufferState::Full } - /// Read as much as possible to fill `buf`. + /// Read as much as possible to fill `dst`. /// /// # Arguments - /// * `buf` - Destination buffer for read data + /// * `dst` - Destination buffer for read data /// /// # Returns /// Number of bytes actually written - pub fn read(&mut self, buf: &mut [u8]) -> usize { - if self.state == RingBufferState::Empty || buf.is_empty() { + pub fn read(&mut self, dst: &mut [u8]) -> usize { + if self.state == RingBufferState::Empty || dst.is_empty() { return 0; } let ret_len; let n = self.arr.len(); if self.head < self.tail { - ret_len = cmp::min(self.tail - self.head, buf.len()); - buf[..ret_len].copy_from_slice(&self.arr[self.head..self.head + ret_len]); + ret_len = cmp::min(self.tail - self.head, dst.len()); + dst[..ret_len].copy_from_slice(&self.arr[self.head..self.head + ret_len]); } else { // also handles full - ret_len = cmp::min(n - self.head + self.tail, buf.len()); + ret_len = cmp::min(n - self.head + self.tail, dst.len()); if ret_len <= (n - self.head) { - buf[..ret_len].copy_from_slice(&self.arr[self.head..self.head + ret_len]); + dst[..ret_len].copy_from_slice(&self.arr[self.head..self.head + ret_len]); } else { let right_len = n - self.head; - buf[..right_len].copy_from_slice(&self.arr[self.head..]); - buf[right_len..ret_len].copy_from_slice(&self.arr[..(ret_len - right_len)]); + dst[..right_len].copy_from_slice(&self.arr[self.head..]); + dst[right_len..ret_len].copy_from_slice(&self.arr[..(ret_len - right_len)]); } } self.head = (self.head + ret_len) % n; @@ -101,12 +101,12 @@ impl RingBuffer { /// Write as much as possible to fill the ring buffer. /// /// # Arguments - /// * `buf` - Source buffer containing data to write + /// * `src` - Source buffer containing data to write /// /// # Returns /// Number of bytes actually written - pub fn write(&mut self, buf: &[u8]) -> usize { - if self.state == RingBufferState::Full || buf.is_empty() { + pub fn write(&mut self, src: &[u8]) -> usize { + if self.state == RingBufferState::Full || src.is_empty() { return 0; } @@ -114,17 +114,17 @@ impl RingBuffer { let n = self.arr.len(); if self.head <= self.tail { // also handles empty - ret_len = cmp::min(n - (self.tail - self.head), buf.len()); + ret_len = cmp::min(n - (self.tail - self.head), src.len()); if ret_len <= (n - self.tail) { - self.arr[self.tail..self.tail + ret_len].copy_from_slice(&buf[..ret_len]); + self.arr[self.tail..self.tail + ret_len].copy_from_slice(&src[..ret_len]); } else { - self.arr[self.tail..].copy_from_slice(&buf[..n - self.tail]); + self.arr[self.tail..].copy_from_slice(&src[..n - self.tail]); self.arr[..(ret_len - (n - self.tail))] - .copy_from_slice(&buf[n - self.tail..ret_len]); + .copy_from_slice(&src[n - self.tail..ret_len]); } } else { - ret_len = cmp::min(self.head - self.tail, buf.len()); - self.arr[self.tail..self.tail + ret_len].copy_from_slice(&buf[..ret_len]); + ret_len = cmp::min(self.head - self.tail, src.len()); + self.arr[self.tail..self.tail + ret_len].copy_from_slice(&src[..ret_len]); } self.tail = (self.tail + ret_len) % n; @@ -180,20 +180,33 @@ impl RingBuffer { Some(()) } - /// Adds a single byte to the buffer, panics if full + /// Forces a byte into the buffer by overwriting the oldest element if full /// - /// # Panics - /// Panics if the buffer is full - pub fn write_byte(&mut self, byte: u8) { - self.enqueue(byte).expect("Ring buffer is full"); + /// # Arguments + /// * `byte` - Byte to be inserted into the buffer + pub fn force_enqueue(&mut self, byte: u8) { + if self.is_full() { + self.dequeue(); + } + self.enqueue(byte); } - /// Removes and returns a single byte from the buffer, panics if empty + /// Clears the buffer, resetting it to an empty state. /// - /// # Panics - /// Panics if the buffer is empty - pub fn read_byte(&mut self) -> u8 { - self.dequeue().expect("Ring buffer is empty") + /// This method resets the read and write pointers to the start of the buffer + /// and marks the buffer as empty. All existing data in the buffer is logically + /// discarded, and the full capacity becomes available for new writes. + pub fn clear(&mut self) { + self.head = 0; + self.tail = 0; + self.state = RingBufferState::Empty; + } + + /// Reads and removes all available data from the buffer, returning the collected bytes + pub fn drain(&mut self) -> Vec { + let mut ret = vec![0u8; self.available_read()]; + self.read(ret.as_mut_slice()); + ret } /// Returns the number of bytes available for reading @@ -202,11 +215,10 @@ impl RingBuffer { RingBufferState::Empty => 0, RingBufferState::Full => self.arr.len(), RingBufferState::Normal => { - let n = self.arr.len(); if self.head < self.tail { self.tail - self.head } else { - (n - self.head) + self.tail + (self.arr.len() - self.head) + self.tail } } } @@ -381,14 +393,68 @@ mod tests { #[test] fn test_available() { let mut rb = RingBuffer::new(5); - rb.write_byte(1); - rb.write_byte(2); + rb.enqueue(1); + rb.enqueue(2); assert_eq!(rb.available_read(), 2); assert_eq!(rb.available_write(), 3); - let byte = rb.read_byte(); + let byte = rb.dequeue().unwrap(); assert_eq!(byte, 1); assert_eq!(rb.available_read(), 1); assert_eq!(rb.available_write(), 4); } + + #[test] + fn test_clear() { + let mut rb = RingBuffer::new(4); + + // Test empty buffer + rb.clear(); + assert!(rb.is_empty()); + assert_eq!(rb.available_read(), 0); + assert_eq!(rb.available_write(), 4); + + // Test after writes + rb.write(&[1, 2, 3]); + assert_eq!(rb.available_read(), 3); + rb.clear(); + assert!(rb.is_empty()); + assert_eq!(rb.available_write(), 4); + + // Test after wrap-around + rb.write(&[1, 2, 3, 4]); // Fill buffer + rb.dequeue(); // head = 1 + rb.enqueue(5); // tail wraps to 0 + rb.clear(); + assert_eq!(rb.head, 0); + assert_eq!(rb.tail, 0); + assert_eq!(rb.available_write(), 4); + + // Verify post-clear functionality + rb.write(&[6, 7]); + let mut buf = [0u8; 2]; + assert_eq!(rb.read(&mut buf), 2); + assert_eq!(buf, [6, 7]); + } + + #[test] + fn test_clear_edge_cases() { + let mut rb = RingBuffer::new(3); + + // Clear full buffer + rb.write(&[1, 2, 3]); + assert!(rb.is_full()); + rb.clear(); + assert!(rb.is_empty()); + rb.write(&[4, 5, 6]); + assert_eq!(rb.drain(), vec![4, 5, 6]); + + // Clear partially read buffer + rb.write(&[7, 8, 9]); + rb.dequeue(); // head = 1 + rb.dequeue(); // head = 2 + rb.clear(); + rb.write(&[10]); + assert_eq!(rb.dequeue().unwrap(), 10); + } } diff --git a/crates/tty/Cargo.toml b/crates/tty/Cargo.toml deleted file mode 100644 index 8d8da252c..000000000 --- a/crates/tty/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "tty" -version = "0.0.1" -edition = "2021" - -[dependencies] -spinlock = { path = "../spinlock" } -lazy_init = { path = "../lazy_init" } -log = "0.4" diff --git a/crates/tty/src/buffer.rs b/crates/tty/src/buffer.rs deleted file mode 100644 index 2c63d2324..000000000 --- a/crates/tty/src/buffer.rs +++ /dev/null @@ -1,149 +0,0 @@ -/* Copyright (c) [2023] [Syswonder Community] - * [Ruxos] is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * http://license.coscl.org.cn/MulanPSL2 - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -//! functions for tty buffer. -//! Drivers should fill the buffer by functions below. -//! then the data will be passed to line discipline for processing. - -/// tty buffer size. -const TTY_BUF_SIZE: usize = 4096; - -/// ring buffer. -#[derive(Debug)] -struct RingBuffer { - /// data. - buf: [u8; TTY_BUF_SIZE], - - /// the first element or empty slot if buffer is empty. - head: usize, - - /// the first empty slot. - tail: usize, - - /// number of elements. - len: usize, -} - -/// tty buffer. -/// TODO: use flip buffer. -#[derive(Debug)] -pub struct TtyBuffer { - /// use ring buffer to save chars. - buffer: spinlock::SpinNoIrq, -} - -impl TtyBuffer { - pub fn new() -> Self { - Self { - buffer: spinlock::SpinNoIrq::new(RingBuffer { - buf: [0u8; TTY_BUF_SIZE], - head: 0, - tail: 0, - len: 0, - }), - } - } - - /// get `index`th element without changing buffer. - pub fn see(&self, index: usize) -> u8 { - let buf = self.buffer.lock(); - if index < buf.len { - buf.buf[(index + buf.head) % TTY_BUF_SIZE] - } else { - 0 - } - } - - /// push a char to tail. - pub fn push(&self, ch: u8) { - let mut buf = self.buffer.lock(); - if buf.len != TTY_BUF_SIZE { - buf.len += 1; - let idx = buf.tail; - buf.buf[idx] = ch; - buf.tail = (buf.tail + 1) % TTY_BUF_SIZE; - } - } - - /// delete and return the heading char. - pub fn pop(&self) -> u8 { - self.delete(0) - } - - /// insert `ch` to `index`th position. - pub fn insert(&self, ch: u8, index: usize) { - let mut buf = self.buffer.lock(); - // if not full and index is right - if buf.len != TTY_BUF_SIZE && index <= buf.len { - // shift buffer[index..move_len+index] one slot right. - let move_len = buf.len - index; - let mut i = buf.tail; - for _ in 0..move_len { - i -= 1; - buf.buf[(i + 1) % TTY_BUF_SIZE] = buf.buf[i % TTY_BUF_SIZE]; - } - // insert - let idx = (buf.head + index) % TTY_BUF_SIZE; - buf.buf[idx] = ch; - buf.len += 1; - buf.tail = (buf.tail + 1) % TTY_BUF_SIZE; - } - } - - /// delete and return the `index`th element. - pub fn delete(&self, index: usize) -> u8 { - let mut buf = self.buffer.lock(); - // if not empty and index is right - if buf.len != 0 && index < buf.len { - let move_len = buf.len - index; - let mut i = index + buf.head; - - // save retval - let ret = buf.buf[i % TTY_BUF_SIZE]; - - // copy move_len elements from buffer[index+head] to buffer[index+head-1]; - for _ in 0..move_len { - buf.buf[i % TTY_BUF_SIZE] = buf.buf[(i + 1) % TTY_BUF_SIZE]; - i += 1; - } - - // len -= 1 - buf.len -= 1; - buf.tail -= 1; - ret - } else { - 0 - } - } - - /// get current length of buffer. - pub fn len(&self) -> usize { - self.buffer.lock().len - } -} - -/// a buffer for echo of line discipline. -/// additionally saving the cursor position. -#[derive(Debug)] -pub struct EchoBuffer { - /// chars buffer. - pub buffer: TtyBuffer, - - /// current column of cursor. - pub col: usize, -} - -impl EchoBuffer { - pub fn new() -> Self { - Self { - buffer: TtyBuffer::new(), - col: 0, - } - } -} diff --git a/crates/tty/src/constant.rs b/crates/tty/src/constant.rs deleted file mode 100644 index 9a134764f..000000000 --- a/crates/tty/src/constant.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright (c) [2023] [Syswonder Community] - * [Ruxos] is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * http://license.coscl.org.cn/MulanPSL2 - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -pub const LF: u8 = b'\n'; -pub const CR: u8 = b'\r'; - -pub const DEL: u8 = b'\x7f'; -pub const BS: u8 = b'\x08'; - -pub const SPACE: u8 = b' '; - -/// escape -pub const ESC: u8 = 27; -/// [ -pub const LEFT_BRACKET: u8 = 91; - -/// an arrow char is `ARROW_PREFIX` + `UP/DOWN/RIGHT/LEFT` -pub const ARROW_PREFIX: [u8; 2] = [ESC, LEFT_BRACKET]; - -// const UP: u8 = 65; -// const DOWN: u8 = 66; -pub const RIGHT: u8 = 67; -pub const LEFT: u8 = 68; diff --git a/crates/tty/src/driver.rs b/crates/tty/src/driver.rs deleted file mode 100644 index dfe106a31..000000000 --- a/crates/tty/src/driver.rs +++ /dev/null @@ -1,167 +0,0 @@ -/* Copyright (c) [2023] [Syswonder Community] - * [Ruxos] is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * http://license.coscl.org.cn/MulanPSL2 - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -//! the first thing a driver should do is registering itself by `register_driver()`, -//! which will allocate an index for this driver. -//! -//! then, driver should register every device it has by `register_device()`, -//! which will allocate an index for this device. - -use crate::tty::TtyStruct; -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::sync::Arc; -use alloc::{vec, vec::Vec}; -use lazy_init::LazyInit; -use spinlock::SpinNoIrq; - -/// all tty drivers. -/// only be written when registering a driver. -pub(super) static ALL_DRIVERS: LazyInit>>> = LazyInit::new(); - -/// the operations a tty driver must implement. -/// passed by driver when registering itself. -#[derive(Debug)] -pub struct TtyDriverOps { - /// push a char to device. - pub putchar: fn(u8), -} - -/// tty driver. -#[derive(Debug)] -pub struct TtyDriver { - /// driver operations. - pub ops: TtyDriverOps, - - /// driver's devices. - /// TODO: maybe use rwlock for dynamicly adding devices is better. - ttys: SpinNoIrq>>, - - /// index of driver. - index: usize, - - /// name of driver. - name: String, -} - -impl TtyDriver { - pub fn new(ops: TtyDriverOps, name: &str) -> Self { - Self { - ops, - ttys: SpinNoIrq::new(BTreeMap::new()), - index: 0, - name: String::from(name), - } - } - - /// add a device, return its index, -1 means failure. - fn add_one_device(&self, tty: Arc) -> isize { - let mut index = 0; - if let Some(k) = self.ttys.lock().last_key_value() { - index = *k.0; - } - - // set index of device - tty.set_index(index); - - // set name of device - let mut name = self.name.clone(); - name.push(core::char::from_digit(index as _, 16).unwrap()); - tty.set_name(&name); - - // save this device - self.ttys.lock().insert(index, tty.clone()); - - // return device's index - index as _ - } - - pub fn name(&self) -> String { - self.name.clone() - } - - pub fn index(&self) -> usize { - self.index - } - - /// get all devices' name - pub fn get_all_device_names(&self) -> Vec { - let mut ret = vec![]; - for (_, tty) in self.ttys.lock().iter() { - ret.push(tty.name()); - } - ret - } - - /// get device - pub fn get_device_by_name(&self, name: &str) -> Option> { - for (_, tty) in self.ttys.lock().iter() { - if tty.name() == name { - return Some(tty.clone()); - } - } - None - } - - /// get device - pub fn get_device_by_index(&self, index: usize) -> Option> { - self.ttys.lock().get(&index).cloned() - } -} - -pub fn init() { - ALL_DRIVERS.init_by(SpinNoIrq::new(vec![])); -} - -/// get driver by index. -pub fn get_driver_by_index(index: usize) -> Option> { - let lock = ALL_DRIVERS.lock(); - for driver in lock.iter() { - if driver.index == index { - return Some(driver.clone()); - } - } - None -} - -/// called by driver to register itself. -/// return driver's index. -pub fn register_driver(ops: TtyDriverOps, name: &str) -> usize { - // create a tty driver structure - let mut driver = TtyDriver::new(ops, name); - - // lock - let mut lock = ALL_DRIVERS.lock(); - - // grant an index to the driver - let index = lock.len(); - driver.index = index; - - // push - lock.push(Arc::new(driver)); - - // return index - index -} - -/// called by driver to register device. -/// return device's index, or -1 on failure. -pub fn register_device(driver_index: usize) -> isize { - let mut index = -1; - // if driver is found - if let Some(driver) = get_driver_by_index(driver_index) { - // create a tty structure - let tty = Arc::new(TtyStruct::new(driver.clone())); - - // save this structure - index = driver.add_one_device(tty.clone()); - crate::tty::add_one_device(tty.clone()); - } - index -} diff --git a/crates/tty/src/ldisc.rs b/crates/tty/src/ldisc.rs deleted file mode 100644 index e7394ac42..000000000 --- a/crates/tty/src/ldisc.rs +++ /dev/null @@ -1,221 +0,0 @@ -/* Copyright (c) [2023] [Syswonder Community] - * [Ruxos] is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * http://license.coscl.org.cn/MulanPSL2 - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -//! TTY line discipline process all incoming and outgoing chars from/to a tty device. -//! the currently implemented line discipline is N_TTY. -//! line disciplines are registered when a device is registered. - -use alloc::sync::Arc; -use spinlock::SpinNoIrq; - -use crate::{ - buffer::{EchoBuffer, TtyBuffer}, - tty::TtyStruct, -}; - -/// tty line discipline. -#[derive(Debug)] -pub struct TtyLdisc { - /// chars that can be read by kernel. - read_buf: TtyBuffer, - - /// chars being echoed on the screen. - echo_buf: SpinNoIrq, - - /// chars from driver, and not yet been processed. - rec_buf: TtyBuffer, -} - -/// implement N_TTY. -impl TtyLdisc { - pub fn new() -> Self { - Self { - read_buf: TtyBuffer::new(), - echo_buf: SpinNoIrq::new(EchoBuffer::new()), - rec_buf: TtyBuffer::new(), - } - } - - /// kernel reads data. - pub fn read(&self, buf: &mut [u8]) -> usize { - let read_buf = &self.read_buf; - - // len of this reading - let len = buf.len().min(read_buf.len()); - - // return if nothing can be read - if len == 0 { - return 0; - } - - // copy data from read_buf to `buf` - for ch in buf.iter_mut().take(len) { - *ch = read_buf.pop(); - } - - len - } - - /// driver sends data from device for processing and echoing. - /// running in irq. - pub fn receive_buf(&self, tty: Arc, buf: &[u8]) { - use crate::constant::*; - - let rec_buf = &self.rec_buf; - - // save data to receive buffer - for ch in buf { - rec_buf.push(*ch); - } - - // process chars in receive buffer - while rec_buf.len() > 0 { - let ch = rec_buf.see(0); - - // if char may be arrow char - if ch == ARROW_PREFIX[0] { - // no enough len, just break, waitting for next time - if rec_buf.len() < 3 { - break; - } - - // enough len, but not a arrow char, just ignore - if rec_buf.see(1) != ARROW_PREFIX[1] { - rec_buf.pop(); - rec_buf.pop(); - break; - } - - // it is an arrow char, get it - rec_buf.pop(); - rec_buf.pop(); - let ch = rec_buf.pop(); - - // deal with arrow char - match ch { - LEFT => { - let mut lock = self.echo_buf.lock(); - // if can go left - if lock.col > 0 { - self.write(tty.clone(), &[ARROW_PREFIX[0], ARROW_PREFIX[1], ch]); - lock.col -= 1; - } - } - RIGHT => { - let mut lock = self.echo_buf.lock(); - // if can go right - if lock.col < lock.buffer.len() { - self.write(tty.clone(), &[ARROW_PREFIX[0], ARROW_PREFIX[1], ch]); - lock.col += 1; - } - } - _ => { - // it is UP/DOWN, just ignore - } - } - // not a arrow char, handle it as a normal char - } else { - let ch = rec_buf.pop(); - match ch { - CR | LF => { - // always '\n' - let ch = LF; - - // echo - self.write(tty.clone(), &[ch]); - - // push this char to echo buffer - let mut lock = self.echo_buf.lock(); - lock.buffer.push(ch); - - // copy echo buffer to read buffer - // FIXME: currently will push all data to read_buf - let len = lock.buffer.len(); - for _ in 0..len { - self.read_buf.push(lock.buffer.pop()); - } - - // echo buffer's column is set to 0 - lock.col = 0; - } - BS | DEL => { - let mut lock = self.echo_buf.lock(); - let col = lock.col; - let len = lock.buffer.len(); - // if can delete - if col > 0 { - // perform a backspace - self.write(tty.clone(), &[BS, SPACE, BS]); - - // if cursor is not on the rightmost - if col != len { - for i in col..len { - let ch = lock.buffer.see(i); - self.write(tty.clone(), &[ch]); - } - self.write(tty.clone(), &[SPACE]); - for _ in 0..(len - col + 1) { - self.write( - tty.clone(), - &[ARROW_PREFIX[0], ARROW_PREFIX[1], LEFT], - ); - } - } - - // modify echo buffer - lock.buffer.delete(col - 1); - lock.col -= 1; - } - } - _ => { - // process normal chars. - let mut echo_buf = self.echo_buf.lock(); - let col = echo_buf.col; - let len = echo_buf.buffer.len(); - - // echo - self.write(tty.clone(), &[ch]); - - // if cursor is not on the rightmost - if col != len { - for i in col..len { - self.write(tty.clone(), &[echo_buf.buffer.see(i)]); - } - for _ in 0..(len - col) { - self.write(tty.clone(), &[ARROW_PREFIX[0], ARROW_PREFIX[1], LEFT]); - } - } - - // modify echo buffer - echo_buf.buffer.insert(ch, col); - echo_buf.col += 1; - } - } - } - } - } - - /// kernel writes data to device. - pub fn write(&self, tty: Arc, buf: &[u8]) -> usize { - let mut len = 0; - let driver = tty.driver(); - for ch in buf { - len += 1; - // call driver's method - (driver.ops.putchar)(*ch); - } - len - } -} - -impl Default for TtyLdisc { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/tty/src/lib.rs b/crates/tty/src/lib.rs deleted file mode 100644 index e6e30a134..000000000 --- a/crates/tty/src/lib.rs +++ /dev/null @@ -1,72 +0,0 @@ -/* Copyright (c) [2023] [Syswonder Community] - * [Ruxos] is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * http://license.coscl.org.cn/MulanPSL2 - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -//! Init -//! -//! firstly, a driver registers itself to get its index. -//! next, the driver registers all devices it found to get their indices. -//! -//! Read -//! -//! when a device receives data, it will cause a irq. -//! then the driver sends the data to tty layer using their indices. -//! finally, kernel will get the data using the device's name. -//! -//! Write -//! -//! kernel writes data to a device using its name. - -#![no_std] - -extern crate alloc; - -mod buffer; -mod constant; -mod driver; -mod ldisc; -mod tty; - -use driver::get_driver_by_index; - -pub use driver::{register_device, register_driver, TtyDriverOps}; -pub use tty::{get_all_device_names, get_device_by_name}; - -/// called by driver when irq, to send data from hardware. -pub fn tty_receive_buf(driver_index: usize, device_index: usize, buf: &[u8]) { - // check the validation of index - if let Some(driver) = get_driver_by_index(driver_index) { - if let Some(tty) = driver.get_device_by_index(device_index) { - tty.ldisc().receive_buf(tty.clone(), buf); - } - } -} - -/// called by kernel to read a tty device. -pub fn tty_read(buf: &mut [u8], dev_name: &str) -> usize { - if let Some(tty) = get_device_by_name(dev_name) { - tty.ldisc().read(buf) - } else { - 0 - } -} - -/// called by kernel to write a tty device. -pub fn tty_write(buf: &[u8], dev_name: &str) -> usize { - if let Some(tty) = get_device_by_name(dev_name) { - tty.ldisc().write(tty.clone(), buf) - } else { - 0 - } -} - -/// init -pub fn init() { - driver::init(); - tty::init(); -} diff --git a/crates/tty/src/tty.rs b/crates/tty/src/tty.rs deleted file mode 100644 index 2a54c2164..000000000 --- a/crates/tty/src/tty.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* Copyright (c) [2023] [Syswonder Community] - * [Ruxos] is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * http://license.coscl.org.cn/MulanPSL2 - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -use core::sync::atomic::AtomicUsize; - -use alloc::{string::String, sync::Arc, vec, vec::Vec}; -use lazy_init::LazyInit; -use spinlock::SpinNoIrq; - -use crate::{driver::TtyDriver, ldisc::TtyLdisc}; - -/// all registered devices. -pub(super) static ALL_DEVICES: LazyInit>>> = LazyInit::new(); - -/// tty device. -#[derive(Debug)] -pub struct TtyStruct { - /// driver of device. - driver: Arc, - - /// device's line discipline. - ldisc: Arc, - - /// index of device. - index: AtomicUsize, - - /// name of device. - name: SpinNoIrq, -} - -impl TtyStruct { - pub fn new(driver: Arc) -> Self { - Self { - driver: driver.clone(), - ldisc: Arc::new(TtyLdisc::new()), - index: AtomicUsize::new(0), - name: SpinNoIrq::new(String::new()), - } - } - - /// get tty line discipline. - pub fn ldisc(&self) -> Arc { - self.ldisc.clone() - } - - /// set device index. - pub fn set_index(&self, index: usize) { - self.index - .store(index, core::sync::atomic::Ordering::Relaxed); - } - - /// set name of device - pub fn set_name(&self, name: &str) { - let mut lock = self.name.lock(); - lock.clone_from(&String::from(name)); - } - - /// Convert a tty structure into a name, reflecting the kernel naming policy. - pub fn name(&self) -> String { - self.name.lock().clone() - } - - /// get device's driver. - pub fn driver(&self) -> Arc { - self.driver.clone() - } -} - -/// called by kernel to get a device. -pub fn get_device_by_name(name: &str) -> Option> { - let lock = ALL_DEVICES.lock(); - for tty in lock.iter() { - if tty.name() == name { - return Some(tty.clone()); - } - } - None -} - -/// called by kernel to get all devices' names. -/// usually used in init to get the view of tty. -pub fn get_all_device_names() -> Vec { - let mut ret = vec![]; - let alldev = ALL_DEVICES.lock(); - for dev in alldev.iter() { - ret.push(dev.name()); - } - ret -} - -/// save a device when registered. -pub fn add_one_device(tty: Arc) { - ALL_DEVICES.lock().push(tty); -} - -pub fn init() { - ALL_DEVICES.init_by(SpinNoIrq::new(vec![])); -} diff --git a/modules/ruxfdtable/src/lib.rs b/modules/ruxfdtable/src/lib.rs index afdda7441..f34463b3d 100644 --- a/modules/ruxfdtable/src/lib.rs +++ b/modules/ruxfdtable/src/lib.rs @@ -11,6 +11,7 @@ #![no_std] extern crate alloc; use alloc::sync::Arc; +use axerrno::LinuxError; use axfs_vfs::VfsNodeAttr; use core::marker::Send; use core::marker::Sync; @@ -196,4 +197,9 @@ pub trait FileLike: Send + Sync { /// Sets or clears the non-blocking I/O mode for the file-like object. fn set_nonblocking(&self, nonblocking: bool) -> LinuxResult; + + /// Handles ioctl commands for the device. + fn ioctl(&self, _cmd: usize, _arg: usize) -> LinuxResult { + Err(LinuxError::ENOTTY) + } } diff --git a/modules/ruxhal/Cargo.toml b/modules/ruxhal/Cargo.toml index c12254a7a..0898fdd0c 100644 --- a/modules/ruxhal/Cargo.toml +++ b/modules/ruxhal/Cargo.toml @@ -12,7 +12,7 @@ homepage = "https://github.com/syswonder/ruxos" repository = "https://github.com/syswonder/ruxos/tree/main/modules/ruxhal" [features] -virtio = ["driver_virtio","virtio-drivers","axalloc"] +virtio = ["driver_virtio", "virtio-drivers", "axalloc"] smp = [] alloc = [] fp_simd = [] @@ -23,7 +23,16 @@ tls = ["alloc"] default = [] musl = [] signal = [] -virtio_console = ["driver_console", "driver_virtio", "driver_virtio/console", "driver_common", "virtio-drivers", "axalloc", "alloc", "virtio"] +virtio_console = [ + "driver_console", + "driver_virtio", + "driver_virtio/console", + "driver_common", + "virtio-drivers", + "axalloc", + "alloc", + "virtio", +] [dependencies] @@ -50,7 +59,9 @@ lazy_static = { version = "1.4", features = ["spin_no_std"] } memory_addr = "0.1.0" handler_table = "0.1.0" crate_interface = "0.1.1" -tty = { path = "../../crates/tty", optional = true } +axerrno = { path = "../../crates/axerrno" } +axio = { path = "../../crates/axio" } +ruxtty = { path = "../ruxtty" } [target.'cfg(target_arch = "x86_64")'.dependencies] x86 = "0.52" diff --git a/modules/ruxhal/src/lib.rs b/modules/ruxhal/src/lib.rs index d0c0fd94c..2704fb6eb 100644 --- a/modules/ruxhal/src/lib.rs +++ b/modules/ruxhal/src/lib.rs @@ -43,6 +43,9 @@ #[macro_use] extern crate log; +#[cfg(feature = "alloc")] +extern crate alloc; + pub mod arch; pub mod cpu; pub mod mem; @@ -94,55 +97,3 @@ pub use self::platform::platform_init_secondary; /// so we should save cmdline in a buf before this memory is set free #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] pub static mut COMLINE_BUF: [u8; 256] = [0; 256]; - -#[allow(unused)] -/// read a tty device specified by its name. -pub fn tty_read(buf: &mut [u8], dev_name: &str) -> usize { - #[cfg(not(feature = "tty"))] - { - let mut read_len = 0; - while read_len < buf.len() { - if let Some(c) = console::getchar().map(|c| if c == b'\r' { b'\n' } else { c }) { - buf[read_len] = c; - read_len += 1; - } else { - break; - } - } - read_len - } - - #[cfg(feature = "tty")] - { - tty::tty_read(buf, dev_name) - } -} - -#[cfg(feature = "alloc")] -extern crate alloc; - -/// get all tty devices' names. -#[cfg(feature = "alloc")] -pub fn get_all_device_names() -> alloc::vec::Vec { - #[cfg(feature = "tty")] - { - tty::get_all_device_names() - } - #[cfg(not(feature = "tty"))] - { - alloc::vec![alloc::string::String::from("notty")] - } -} - -/// write a tty device specified by its name. -pub fn tty_write(buf: &[u8], _dev_name: &str) -> usize { - #[cfg(feature = "tty")] - { - tty::tty_write(buf, _dev_name) - } - #[cfg(not(feature = "tty"))] - { - console::write_bytes(buf); - return buf.len(); - } -} diff --git a/modules/ruxhal/src/platform/aarch64_common/pl011.rs b/modules/ruxhal/src/platform/aarch64_common/pl011.rs index d67aec6a2..c1265f1de 100644 --- a/modules/ruxhal/src/platform/aarch64_common/pl011.rs +++ b/modules/ruxhal/src/platform/aarch64_common/pl011.rs @@ -37,7 +37,8 @@ impl RxRingBuffer { empty: true, } } - #[cfg(not(feature = "tty"))] + + #[allow(unused)] fn push(&mut self, n: u8) { if self.tail != self.head || self.empty { self.buffer[self.tail] = n; @@ -100,24 +101,10 @@ pub fn init_early() { UART.inner.lock().init(); } -#[cfg(feature = "tty")] -static DRIVER_INDEX: lazy_init::LazyInit = lazy_init::LazyInit::new(); -#[cfg(feature = "tty")] -static DEV_INDEX: lazy_init::LazyInit = lazy_init::LazyInit::new(); - /// Set UART IRQ Enable pub fn init() { #[cfg(feature = "irq")] { - #[cfg(feature = "tty")] - { - let ops = tty::TtyDriverOps { putchar }; - let driver_index = tty::register_driver(ops, "ttyS"); - let dev_index = tty::register_device(driver_index); - assert_ne!(dev_index, -1); - DRIVER_INDEX.init_by(driver_index); - DEV_INDEX.init_by(dev_index as _); - } crate::irq::register_handler(crate::platform::irq::UART_IRQ_NUM, irq_handler); crate::irq::set_enable(crate::platform::irq::UART_IRQ_NUM, true); } @@ -130,22 +117,14 @@ pub fn irq_handler() { let is_receive_interrupt = dev.is_receive_interrupt(); if is_receive_interrupt { dev.ack_interrupts(); - #[cfg(not(feature = "tty"))] + + let mut buf = [0u8; 128]; + let mut len = 0; + while let Some(c) = dev.getchar() { - UART.buffer.lock().push(c); - } - #[cfg(feature = "tty")] - { - let mut buf = [0u8; 128]; - let mut len = 0; - - while let Some(c) = dev.getchar() { - buf[len] = c; - len += 1; - } - let drv_idx = *DRIVER_INDEX.try_get().unwrap(); - let dev_idx = *DEV_INDEX.try_get().unwrap(); - tty::tty_receive_buf(drv_idx, dev_idx, &buf[..len]); + buf[len] = c; + len += 1; } + ruxtty::tty_receive_buf(&buf[..len]); } } diff --git a/modules/ruxruntime/Cargo.toml b/modules/ruxruntime/Cargo.toml index 67de95808..e160df91c 100644 --- a/modules/ruxruntime/Cargo.toml +++ b/modules/ruxruntime/Cargo.toml @@ -44,7 +44,7 @@ fatfs = ["fs", "multitask", "alloc", "blkfs", "ruxfs/fatfs", "ruxtask/test"] cfg-if = "1.0" ruxhal = { path = "../ruxhal" } axlog = { path = "../axlog" } -ruxconfig = { path = "../ruxconfig"} +ruxconfig = { path = "../ruxconfig" } axalloc = { path = "../axalloc", optional = true } ruxdriver = { path = "../ruxdriver", optional = true } ruxfs = { path = "../ruxfs", optional = true } @@ -56,6 +56,7 @@ ruxmm = { path = "../ruxmm" } axsync = { path = "../axsync", optional = true } ruxfutex = { path = "../ruxfutex", optional = true } ruxrand = { path = "../ruxrand", optional = true } +ruxtty = { path = "../ruxtty" } crate_interface = "0.1.1" percpu = { path = "../../crates/percpu", optional = true } @@ -63,7 +64,6 @@ kernel_guard = { version = "0.1.0", optional = true } lazy_init = { path = "../../crates/lazy_init", optional = true } dtb = { path = "../../crates/dtb", optional = true } -tty = { path = "../../crates/tty", optional = true } [dev-dependencies] log = "0.4" diff --git a/modules/ruxruntime/src/lib.rs b/modules/ruxruntime/src/lib.rs index 0a8c3784c..a7e4ac9f1 100644 --- a/modules/ruxruntime/src/lib.rs +++ b/modules/ruxruntime/src/lib.rs @@ -194,8 +194,7 @@ pub extern "C" fn rust_main(cpu_id: usize, dtb: usize) -> ! { #[cfg(feature = "alloc")] init_allocator(); - #[cfg(feature = "tty")] - tty::init(); + ruxtty::init_tty(); info!("Initialize platform devices..."); ruxhal::platform_init(); diff --git a/modules/ruxtty/Cargo.toml b/modules/ruxtty/Cargo.toml new file mode 100644 index 000000000..1a2865888 --- /dev/null +++ b/modules/ruxtty/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ruxtty" +version = "0.0.1" +edition = "2021" +authors = ["Quan Shi <749990226@qq.com>"] +description = "The TTY module provides a terminal interface, managing data flow through layers like TTY Core, Line Discipline, and Driver" +license = "Mulan PSL v2" +homepage = "https://github.com/syswonder/ruxos" +repository = "https://github.com/syswonder/ruxos/tree/dev/modules/ruxtty" + +[dependencies] +spinlock = { path = "../../crates/spinlock" } +lazy_init = { path = "../../crates/lazy_init" } +axlog = { path = "../axlog" } +bitflags = "2.2" +ringbuffer = { path = "../../crates/ringbuffer" } +spin = { version = "0.9" } +axerrno = { path = "../../crates/axerrno" } +axio = { path = "../../crates/axio" } +num_enum = { version = "0.5.11", default-features = false } diff --git a/modules/ruxtty/src/driver.rs b/modules/ruxtty/src/driver.rs new file mode 100644 index 000000000..7d3745e03 --- /dev/null +++ b/modules/ruxtty/src/driver.rs @@ -0,0 +1,71 @@ +/* Copyright (c) [2023] [Syswonder Community] + * [Ruxos] is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//! the first thing a driver should do is registering itself by `register_driver()`, +//! which will allocate an index for this driver. +//! +//! then, driver should register every device it has by `register_device()`, +//! which will allocate an index for this device. + +use alloc::{sync::Arc, vec::Vec}; +use lazy_init::LazyInit; + +use crate::tty::Tty; +use spinlock::SpinNoIrq; +/// all tty drivers. +/// only be written when registering a driver. +/// +/// Current now there is only ttyS device +pub(crate) static TTY_DRIVER: LazyInit> = LazyInit::new(); + +/// TTY device controller managing registered TTY instances +pub struct TtyDriver { + /// When registering a tty device(e.g. ttyS), it will be put in tty device list `ttys` + ttys: SpinNoIrq>>, +} + +impl Default for TtyDriver { + fn default() -> Self { + Self { + ttys: SpinNoIrq::new(Vec::new()), + } + } +} + +impl TtyDriver { + /// Creates a new TTY driver with empty device list + pub fn new() -> Arc { + Arc::new(Self { + ttys: SpinNoIrq::new(Vec::new()), + }) + } + + /// Registers a TTY device to the driver's management list + pub fn add_tty(&self, tty: Arc) { + self.ttys.lock().push(tty) + } + + /// Broadcasts input bytes to all registered TTY devices + /// + /// Sequentially sends each byte to every managed TTY + pub fn push_slice(&self, slice: &[u8]) { + for tty in &*self.ttys.lock() { + for ch in slice { + tty.push_char(*ch); + } + } + } + + /// Broadcasts input byte to all registered TTY devices + pub fn push_char(&self, ch: u8) { + for tty in &*self.ttys.lock() { + tty.push_char(ch); + } + } +} diff --git a/modules/ruxtty/src/ldisc.rs b/modules/ruxtty/src/ldisc.rs new file mode 100644 index 000000000..af6ecb3fc --- /dev/null +++ b/modules/ruxtty/src/ldisc.rs @@ -0,0 +1,343 @@ +/* Copyright (c) [2023] [Syswonder Community] + * [Ruxos] is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//! TTY line discipline process all incoming and outgoing chars from/to a tty device. +//! the currently implemented line discipline is N_TTY. +//! line disciplines are registered when a device is registered. + +use crate::termios::{Termios, CC_C_CHAR}; +use alloc::{format, sync::Arc}; +use axerrno::AxResult; +use axio::PollState; +use axlog::ax_print; +use ringbuffer::RingBuffer; +use spinlock::SpinNoIrq; + +const BUFFER_CAPACITY: usize = 4096; + +/// Line discipline managing terminal input processing +/// +/// Handles raw input buffering, line editing, and terminal control settings +pub struct Ldisc { + /// Buffer for current line being edited + current_line: SpinNoIrq, + /// Buffer for completed lines ready for reading + read_buffer: SpinNoIrq, + /// Terminal control settings + termios: SpinNoIrq, + /// Terminal window dimensions + winsize: SpinNoIrq, +} + +impl Ldisc { + /// Creates a new line discipline instance with default buffers and terminal settings + pub fn new() -> Arc { + Arc::new(Self { + current_line: SpinNoIrq::new(RingBuffer::new(BUFFER_CAPACITY)), + read_buffer: SpinNoIrq::new(RingBuffer::new(BUFFER_CAPACITY)), + termios: SpinNoIrq::new(Termios::default()), + winsize: SpinNoIrq::new(WinSize::default()), + }) + } + + /// Reads bytes from terminal, dispatching to raw/canonical mode handlers + pub fn read(&self, dst: &mut [u8]) -> AxResult { + let termios = self.termios.lock(); + if termios.is_raw_mode() { + return self.raw_mode_read(dst, &termios); + } else { + return self.canonical_mode_read(dst, &termios); + } + } + + /// Canonical mode is enabled by setting the `ICANON` flag in terminal attributes. + /// It provides line-oriented input processing with the following characteristics: + /// + /// 1. **Line Assembly**: + /// Input is buffered into lines terminated by any of these characters: + /// - `NL` (newline, ASCII `\n`) + /// - `EOL` (end-of-line, configurable via `VEOL`) + /// - `EOL2` (secondary EOL, configurable via `VEOL2` if `IEXTEN` is set) + /// - `EOF` (end-of-file, typically `Ctrl+D` via `VEOF`, but ignored if at line start) + /// + /// All terminators except `EOF` are included in the input line passed to the reading process. + /// + /// 2. **Line Editing**: + /// Supports in-line editing with these control characters: + /// - `ERASE` (backspace, deletes previous character) + /// - `KILL` (deletes entire line) + /// - `WERASE` (word erase, deletes previous word if `IEXTEN` is set) + /// + /// 3. **Read Behavior**: + /// - A `read()` call returns only when a complete line is available. + /// - If the requested byte count is smaller than the line length, partial data is returned; subsequent reads fetch remaining bytes. + /// - (unimplemented) If interrupted by a signal without `SA_RESTART` flag, `read()` terminates early (partial read or `EINTR` error). + fn canonical_mode_read(&self, dst: &mut [u8], termios: &Termios) -> AxResult { + let mut buf = self.read_buffer.lock(); + let available_read = buf.available_read(); + if available_read == 0 { + return Err(axio::Error::WouldBlock); + } + let max_read_len = available_read.min(dst.len()); + let mut read_len = 0; + while read_len < max_read_len { + let Some(next_char) = buf.dequeue() else { + break; + }; + + if is_line_terminator(next_char, &termios) { + if !is_eof(next_char, &termios) { + dst[read_len] = next_char; + read_len += 1; + } + break; // Stop at line terminator + } + + dst[read_len] = next_char; + read_len += 1; + } + Ok(read_len) + } + + /// Non-canonical mode(raw mode, enabled by unsetting `ICANON`) allows applications like `vi` and `less` + /// to read input immediately without line termination. + /// + /// In this mode: + /// - Input bypasses line assembly and becomes visible to applications byte-by-byte. + /// - Special editing characters (e.g., `ERASE`, `KILL`) are disabled. + /// - Read completion is controlled by `VMIN` (minimum bytes) and `VTIME` (timeout) in `termios.c_cc`. + /// + /// ### **VMIN and VTIME Behavior** + /// The interaction of `VMIN` and `VTIME` defines four distinct read modes: + /// + /// #### **1. `VMIN = 0`, `VTIME = 0` (Non-Blocking Poll)** + /// - **Behavior**: + /// - Returns immediately with available bytes (up to requested count) or `0` if no data. + /// - **Use Case**: + /// - Non-blocking input checks (similar to `O_NONBLOCK` flag, but returns `0` instead of `EAGAIN`). + /// + /// #### **2. `VMIN > 0`, `VTIME = 0` (Blocking Read)** + /// - **Behavior**: + /// - Blocks indefinitely until at least `min(VMIN, requested_bytes)` are read. + /// - **Use Case**: + /// - Efficient single-byte input (e.g., `less` sets `VMIN=1` to wait for keystrokes without CPU polling). + /// + /// #### **3. `VMIN = 0`, `VTIME > 0` (Timeout-Based Read)** (TODO) + /// - **Behavior**: + /// - Starts a timer (`VTIME × 0.1 seconds`) on `read()` call. + /// - Returns immediately if ≥1 byte arrives or timer expires (returns `0` on timeout). + /// - **Use Case**: + /// - Handling serial devices (e.g., modems) to avoid indefinite hangs. + /// + /// #### **4. `VMIN > 0`, `VTIME > 0` (Inter-Byte Timeout)** (TODO) + /// - **Behavior**: + /// - After receiving the first byte, a timer resets for each subsequent byte. + /// - Returns when: + /// - `min(VMIN, requested_bytes)` are read, **OR** + /// - Inter-byte gap exceeds `VTIME × 0.1 seconds` (returns available bytes ≥1). + /// - **Use Case**: + /// - Detecting escape sequences (e.g., terminal arrow keys generating multi-byte sequences like `←` → `\x1B[D`). + /// - Applications like `vi` use short timeouts (e.g., 0.2s) to distinguish keystrokes from manual input. + fn raw_mode_read(&self, dst: &mut [u8], termios: &Termios) -> AxResult { + let vmin = *termios.special_char(CC_C_CHAR::VMIN); + let vtime = *termios.special_char(CC_C_CHAR::VTIME); + let read_len = { + if vmin == 0 && vtime == 0 { + self.polling_read(dst) + } else if vmin > 0 && vtime == 0 { + self.blocking_read(dst, vmin)? + } else if vmin == 0 && vtime > 0 { + todo!() + } else if vmin > 0 && vtime > 0 { + todo!() + } else { + unreachable!() + } + }; + Ok(read_len) + } + + /// Used in non-canonical mode read + /// + /// Returns immediately with available bytes (up to requested count) or `0` if no data + fn polling_read(&self, dst: &mut [u8]) -> usize { + let mut buf = self.read_buffer.lock(); + let max_read_len = buf.available_read().min(dst.len()); + buf.read(&mut dst[..max_read_len]) + } + + /// Used in non-canonical mode read + /// + /// Blocks indefinitely until at least `min(VMIN, requested_bytes)` are read. + fn blocking_read(&self, dst: &mut [u8], vmin: u8) -> AxResult { + let mut buf = self.read_buffer.lock(); + let buffer_len = buf.available_read(); + if buffer_len >= dst.len() { + return Ok(buf.read(dst)); + } + if buffer_len < vmin as usize { + return Err(axio::Error::WouldBlock); + } + Ok(buf.read(&mut dst[..buffer_len])) + } + + /// Processes an input character through terminal line discipline + /// + /// Applies termios settings for character conversion, echo handling, and + /// buffering in either raw or canonical mode. + pub fn push_char(&self, mut ch: u8, mut echo: F) { + let termios = self.termios.lock(); + + // Convert CR to LF if ICRNL is enabled + if termios.contain_icrnl() && ch == b'\r' { + ch = b'\n'; + } + + // Echo handling + if termios.contain_echo() { + match ch { + b'\n' => echo("\n"), // Standard newline echo + b'\r' => echo("\r\n"), // Carriage return expands to CR+LF + ch if ch == *termios.special_char(CC_C_CHAR::VERASE) => { + // Visual backspace sequence: + // 1. `\x08` (Backspace) moves cursor left + // 2. ` ` (Space) overwrites character + // 3. `\x08` moves cursor left again to final position + // This achieves character erasure in terminal display + echo(core::str::from_utf8(b"\x08 \x08").unwrap()); + } + ch if is_printable_char(ch) => { + // Direct echo for printable + ax_print!("{}", char::from(ch)); + } + ch if is_ctrl_char(ch) && termios.contain_echo_ctl() => { + // Convert control character (0x01-0x1F) to ^X notation: + // 0x01 → 1 + 'A' - 1 = 65 → 'A' → "^A" + let ctrl_char = format!("^{}", char::from_u32((ch + b'A' - 1) as u32).unwrap()); + echo(&ctrl_char); + } + _ => {} + } + } + // Raw mode + if termios.is_raw_mode() { + self.read_buffer.lock().force_enqueue(ch); + return; + } + + // Canonical mode (line-editing) + if ch == *termios.special_char(CC_C_CHAR::VKILL) { + // Erase current line + self.current_line.lock().clear(); + } + + if ch == *termios.special_char(CC_C_CHAR::VERASE) { + // Type backspace + let mut current_line = self.current_line.lock(); + if !current_line.is_empty() { + current_line.dequeue(); + } + } + + if is_line_terminator(ch, &termios) { + // If a new line is met, all bytes in current_line will be moved to read_buffer + let mut current_line = self.current_line.lock(); + current_line.force_enqueue(ch); + let current_line_chars = current_line.drain(); + for char in current_line_chars { + self.read_buffer.lock().force_enqueue(char); + } + } + + if is_printable_char(ch) { + self.current_line.lock().enqueue(ch); + } + } + + pub fn poll(&self) -> PollState { + let readable = !self.read_buffer.lock().is_empty(); + PollState { + readable, + writable: true, + pollhup: false, + } + } + + pub fn termios(&self) -> Termios { + *self.termios.lock() + } + + pub fn set_termios(&self, termios: &Termios) { + *self.termios.lock() = *termios + } + + pub fn winsize(&self) -> WinSize { + *self.winsize.lock() + } + + pub fn set_winsize(&self, winsize: &WinSize) { + *self.winsize.lock() = *winsize + } + + pub fn clear_input(&self) { + self.current_line.lock().clear(); + self.read_buffer.lock().clear(); + } +} + +/// Terminal window size information (rows/columns) +/// +/// Follows POSIX `struct winsize` convention, pixel fields unused in most TTY implementations +#[derive(Debug, Clone, Copy, Default)] +#[repr(C)] +pub struct WinSize { + /// Number of terminal rows + ws_row: u16, + /// Number of terminal columns + ws_col: u16, + /// Horizontal size in pixels + ws_xpixel: u16, + /// Vertical size in pixels + ws_ypixel: u16, +} + +/// printable characters​ refer to characters that can be visually displayed in terminals +/// +/// ASCII Printable Characters: Ranging from decimal 32 (space) to 126 (the ~ symbol), +/// covering letters (uppercase and lowercase), digits, punctuation marks (e.g., !@#$%), and spaces. +fn is_printable_char(ch: u8) -> bool { + (0x20..0x7f).contains(&ch) +} + +/// Checks if the character is a control character (0x00-0x1F), excluding \r and \n +fn is_ctrl_char(ch: u8) -> bool { + if ch == b'\r' || ch == b'\n' { + return false; + } + (0..0x20).contains(&ch) +} + +/// Checks if the character matches the EOF control character (VEOF) +fn is_eof(ch: u8, termios: &Termios) -> bool { + ch == *termios.special_char(CC_C_CHAR::VEOF) +} + +/// Checks if the character is a line terminator (\n, VEOF, VEOL, or VEOL2 with IEXTEN) +fn is_line_terminator(ch: u8, termios: &Termios) -> bool { + if ch == b'\n' + || ch == *termios.special_char(CC_C_CHAR::VEOF) + || ch == *termios.special_char(CC_C_CHAR::VEOL) + { + return true; + } + if termios.contain_iexten() && ch == *termios.special_char(CC_C_CHAR::VEOL2) { + return true; + } + false +} diff --git a/modules/ruxtty/src/lib.rs b/modules/ruxtty/src/lib.rs new file mode 100644 index 000000000..04806a003 --- /dev/null +++ b/modules/ruxtty/src/lib.rs @@ -0,0 +1,137 @@ +/* Copyright (c) [2023] [Syswonder Community] + * [Ruxos] is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +//! Init +//! +//! firstly, a driver registers itself to get its index. +//! next, the driver registers all devices it found to get their indices. +//! +//! Read +//! +//! when a device receives data, it will cause a irq. +//! then the driver sends the data to tty layer using their indices. +//! finally, kernel will get the data using the device's name. +//! +//! Write +//! +//! kernel writes data to a device using its name. +//! +//! Portions of this code are inspired by the design of Asterinas OS + +#![no_std] +#![feature(lint_reasons)] +extern crate alloc; + +mod driver; +mod ldisc; +mod termios; +mod tty; + +use alloc::ffi::CString; +use axerrno::AxResult; +use axio::PollState; +use axlog::{ax_print, ax_println, warn}; +use driver::{TtyDriver, TTY_DRIVER}; +use ldisc::WinSize; +use num_enum::FromPrimitive; +use termios::Termios; +use tty::{Tty, TtyIoctlCmd, N_TTY}; + +/// Initializes the TTY subsystem, creates a new TTY device and registers it +pub fn init_tty() { + let tty = Tty::new(CString::new("ttyS").unwrap()); + N_TTY.call_once(|| tty.clone()); + let driver = TtyDriver::new(); + driver.add_tty(tty); + TTY_DRIVER.init_by(driver); +} + +/// Pushes received data into the TTY driver's input buffer +/// # Note: This implementation is ​**only enabled for `aarch64` architecture** since others haven't implement irq +pub fn tty_receive_buf(buf: &[u8]) { + TTY_DRIVER.try_get().unwrap().push_slice(buf); +} + +/// Pushes received char into the TTY driver's input buffer +pub fn tty_receive_char(ch: u8) { + TTY_DRIVER.try_get().unwrap().push_char(ch); +} + +/// Reads data from TTY line discipline into the destination buffer +pub fn tty_read(dst: &mut [u8]) -> AxResult { + N_TTY.get().unwrap().ldisc.read(dst) +} + +/// Checks if TTY has data available for reading +pub fn tty_poll() -> PollState { + N_TTY.get().unwrap().ldisc.poll() +} + +/// Writes data to TTY output, handles UTF-8 and binary content +pub fn tty_write(src: &[u8]) -> AxResult { + if let Ok(content) = alloc::str::from_utf8(&src) { + ax_print!("{}", content); + } else { + ax_println!("Not utf-8 content: {:?}", src); + } + Ok(src.len()) +} + +/// Handles TTY-specific ioctl commands +pub fn tty_ioctl(cmd: usize, arg: usize) -> AxResult { + match TtyIoctlCmd::from_primitive(cmd as u32) { + TtyIoctlCmd::TCGETS | TtyIoctlCmd::TCGETA => { + let termios = N_TTY.get().unwrap().ldisc.termios(); + unsafe { *(arg as *mut Termios) = termios }; + Ok(()) + } + TtyIoctlCmd::TIOCGPGRP => { + let fg_pgid = N_TTY.get().unwrap().fg_pgid() as u32; + unsafe { + *(arg as *mut u32) = fg_pgid; + } + Ok(()) + } + TtyIoctlCmd::TIOCSPGRP => { + let fg_pgid = unsafe { *(arg as *const u32) } as usize; + N_TTY.get().unwrap().set_fg_pgid(fg_pgid); + Ok(()) + } + TtyIoctlCmd::TCSETS | TtyIoctlCmd::TCSETSW => { + let termios = unsafe { *(arg as *const Termios) }; + N_TTY.get().unwrap().ldisc.set_termios(&termios); + Ok(()) + } + TtyIoctlCmd::TCSETSF => { + let termios = unsafe { *(arg as *const Termios) }; + let tty = N_TTY.get().unwrap(); + tty.ldisc.set_termios(&termios); + tty.ldisc.clear_input(); + Ok(()) + } + TtyIoctlCmd::TIOCGWINSZ => { + let winsize = N_TTY.get().unwrap().ldisc.winsize(); + unsafe { *(arg as *mut WinSize) = winsize }; + Ok(()) + } + TtyIoctlCmd::TIOCSWINSZ => { + let winsize = unsafe { *(arg as *const WinSize) }; + N_TTY.get().unwrap().ldisc.set_winsize(&winsize); + Ok(()) + } + TtyIoctlCmd::TIOCSCTTY => { + warn!("TtyIoctlCmd::TIOCSCTTY not implemented"); + Ok(()) + } + _ => { + warn!("unimplemented tty ioctl, cmd {cmd}"); + Ok(()) + } + } +} diff --git a/modules/ruxtty/src/termios.rs b/modules/ruxtty/src/termios.rs new file mode 100644 index 000000000..1ee722524 --- /dev/null +++ b/modules/ruxtty/src/termios.rs @@ -0,0 +1,401 @@ +#![expect(non_camel_case_types)] +#![allow(dead_code)] +use bitflags::bitflags; +const NCCS: usize = 19; +type CcT = u8; + +/// Represents terminal I/O settings and control characters. +/// +/// This structure mirrors the behavior of the POSIX `termios` structure, +/// providing configuration for serial communication, line discipline, and +/// terminal control. It is typically used with `tcgetattr` and `tcsetattr` +/// for configuring TTY devices. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Termios { + /// Input mode flags + c_iflags: C_IFLAGS, + /// Output mode flags + c_oflags: C_OFLAGS, + /// Control mode flags (e.g., baud rate) + c_cflags: C_CFLAGS, + /// Local mode flags (e.g., echo, canonical mode). + c_lflags: C_LFLAGS, + /// Line discipline type + c_line: CcT, + /// Array of control characters + c_cc: [CcT; NCCS], +} + +impl Default for Termios { + fn default() -> Self { + Self { + c_iflags: C_IFLAGS::default(), + c_oflags: C_OFLAGS::default(), + c_cflags: C_CFLAGS::default(), + c_lflags: C_LFLAGS::default(), + c_line: 0, + c_cc: [ + 3, // VINTR Ctrl-C + 28, // VQUIT + 127, // VERASE + 21, // VKILL + 4, // VEOF Ctrl-D + 0, // VTIME + 1, // VMIN + 0, // VSWTC + 17, // VSTART + 19, // VSTOP + 26, // VSUSP Ctrl-Z + 255, // VEOL + 18, // VREPAINT + 15, // VDISCARD + 23, // VWERASE + 22, // VLNEXT + 255, // VEOL2 + 0, 0, + ], + } + } +} + +impl Termios { + /// Gets the value of a specific control character (e.g. VINTR, VERASE) + pub fn special_char(&self, cc_c_char: CC_C_CHAR) -> &CcT { + &self.c_cc[cc_c_char as usize] + } + + /// Checks if terminal is in raw mode (non-canonical input processing) + pub fn is_raw_mode(&self) -> bool { + !self.c_lflags.contains(C_LFLAGS::ICANON) + } + + /// Checks if carriage return to newline conversion is enabled (\r → \n) + pub fn contain_icrnl(&self) -> bool { + self.c_iflags.contains(C_IFLAGS::ICRNL) + } + + /// Checks if signal-generating characters (e.g. Ctrl+C) are enabled + pub fn contain_isig(&self) -> bool { + self.c_lflags.contains(C_LFLAGS::ISIG) + } + + /// Checks if input character echoing is enabled + pub fn contain_echo(&self) -> bool { + self.c_lflags.contains(C_LFLAGS::ECHO) + } + + /// Checks if control characters are echoed as ^X + pub fn contain_echo_ctl(&self) -> bool { + self.c_lflags.contains(C_LFLAGS::ECHOCTL) + } + + /// Checks if extended input character processing is enabled + pub fn contain_iexten(&self) -> bool { + self.c_lflags.contains(C_LFLAGS::IEXTEN) + } +} +bitflags! { + #[repr(C)] + #[derive(Debug, Clone, Copy)] + /// Input mode flags + pub struct C_IFLAGS: u32 { + // ============================================== + // Break condition handling + // ============================================== + + /// IGNBRK: IGNore BReaK condition + /// Ignore break conditions (BREAK treated as valid NULL byte) + const IGNBRK = 0x001; + + /// BRKINT: BReaK INTerrupt + /// Generate SIGINT on break condition (when not ignored by IGNBRK) + const BRKINT = 0x002; + + // ============================================== + // Parity/error handling + // ============================================== + + /// IGNPAR: IGNore PARity errors + /// Ignore bytes with framing/parity errors (keep invalid bytes) + const IGNPAR = 0x004; + + /// PARMRK: PARity MarK + /// Insert error marker bytes (0xFF + 0x00) before invalid bytes + const PARMRK = 0x008; + + /// INPCK: INput Parity ChecK + /// Enable parity checking for incoming bytes + const INPCK = 0x010; + + // ============================================== + // Character processing + // ============================================== + + /// ISTRIP: STRIP high bit + /// Strip 8th bit of input characters (force 7-bit processing) + const ISTRIP = 0x020; + + /// INLCR: INput NL to CR + /// Map received newline (NL, 0x0A) to carriage return (CR, 0x0D) + const INLCR = 0x040; + + /// IGNCR: IGNore CR + /// Ignore received carriage return (CR, 0x0D) characters + const IGNCR = 0x080; + + /// ICRNL: Input CR to NL + /// Map received carriage return (CR, 0x0D) to newline (NL, 0x0A) + const ICRNL = 0x100; + + /// IUCLC: Input UpperCase to LowerCase + /// Map uppercase letters (A-Z) to lowercase (a-z) on input + const IUCLC = 0x0200; + + // ============================================== + // Flow control + // ============================================== + + /// IXON: Enable XON/XOFF output control + /// Enable software flow control for output (Ctrl-Q/Ctrl-S) + const IXON = 0x0400; + + /// IXOFF: Enable XON/XOFF input control + /// Enable software flow control for input (Ctrl-Q/Ctrl-S) + const IXOFF = 0x1000; + + /// IXANY: Enable any character to restart output + /// Allow any character (not just XON) to resume paused output + const IXANY = 0x800; + + // ============================================== + // Special behaviors + // ============================================== + + /// IMAXBEL: Input MAX buffer BEL + /// Ring terminal bell when input buffer is full + const IMAXBEL = 0x2000; + + /// IUTF8: Input UTF-8 + /// Enable UTF-8 input processing (required for canonical mode) + const IUTF8 = 0x4000; + } +} +impl Default for C_IFLAGS { + fn default() -> Self { + C_IFLAGS::ICRNL | C_IFLAGS::IXON + } +} + +bitflags! { + #[repr(C)] + #[derive(Debug, Clone, Copy)] + /// Output mode flags + pub struct C_OFLAGS: u32 { + // ============================================== + // Output processing control + // ============================================== + + /// OPOST: Output POST-process + /// Enable output post-processing (required for other transformations) + const OPOST = 1 << 0; + + /// ONLCR: Output NL to CR-NL + /// Map newline (NL, 0x0A) to CR-NL sequence (0x0D 0x0A) + const ONLCR = 1 << 2; + + /// OCRNL: Output CR to NL + /// Map carriage return (CR, 0x0D) to newline (NL, 0x0A) + const OCRNL = 1 << 3; + + /// ONOCR: Output No CR at column 0 + /// Discard CR characters when at start of line (column 0) + const ONOCR = 1 << 4; + + /// ONLRET: Output NL performs RETurn + /// Newline (NL) moves cursor to column 0 (carriage return behavior) + const ONLRET = 1 << 5; + + // ============================================== + // Character case conversion + // ============================================== + + /// OLCUC: Output LowerCase UpperCase + /// Convert lowercase letters (a-z) to uppercase (A-Z) on output + const OLCUC = 1 << 1; + + // ============================================== + // Fill/delay handling (legacy systems) + // ============================================== + + /// OFILL: Output FILL characters + /// Send fill characters for timing delays (obsolete on modern systems) + const OFILL = 1 << 6; + + /// OFDEL: Output DEL as fill character + /// Use DEL (0x7F) instead of NUL (0x00) for fill (requires OFILL) + const OFDEL = 1 << 7; + } +} + +impl Default for C_OFLAGS { + fn default() -> Self { + C_OFLAGS::OPOST | C_OFLAGS::ONLCR + } +} + +/// Control mode flags +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct C_CFLAGS(u32); + +impl Default for C_CFLAGS { + /// Creates default c_cflags configuration: + /// - Baud rate: 38400 (B38400) + /// - Data bits: 8 (CS8) + /// - Enable receiver (CREAD) + fn default() -> Self { + /// Enable receiver flag (termios CREAD) + const CREAD: u32 = 0x00000080; + let cbaud = C_CFLAGS_BAUD::B38400 as u32; + let csize = C_CFLAGS_CSIZE::CS8 as u32; + let c_cflags = cbaud | csize | CREAD; + Self(c_cflags) + } +} + +/// Baud rate constants for termios c_cflags field +/// +/// These values correspond to the `B*` constants in Unix termios implementations. +/// Used to set serial communication speed. The special value `B0` indicates hang-up. +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +#[doc(hidden)] +pub enum C_CFLAGS_BAUD { + /// Hang up/discard data + B0 = 0x00000000, + B50 = 0x00000001, + B75 = 0x00000002, + B110 = 0x00000003, + B134 = 0x00000004, + B150 = 0x00000005, + B200 = 0x00000006, + B300 = 0x00000007, + B600 = 0x00000008, + B1200 = 0x00000009, + B1800 = 0x0000000a, + B2400 = 0x0000000b, + B4800 = 0x0000000c, + B9600 = 0x0000000d, + B19200 = 0x0000000e, + B38400 = 0x0000000f, +} + +bitflags! { + #[repr(C)] + #[derive(Debug, Clone, Copy)] + /// Local mode flags (e.g., echo, canonical mode). + pub struct C_LFLAGS: u32 { + /// Generate signals (SIGINT/SIGQUIT/SIGSTOP) for control characters. + const ISIG = 0x00001; + /// Enable canonical input mode (line-by-line input with editing). + const ICANON = 0x00002; + /// Obsolete flag for uppercase/lowercase input processing (use `IUTF8` instead). + const XCASE = 0x00004; + /// Echo input characters back to the terminal. + const ECHO = 0x00008; + /// Echo ERASE character as backspace-space-backspace sequence (visual erase). + const ECHOE = 0x00010; + /// Echo KILL character (line deletion) by erasing the entire line. + const ECHOK = 0x00020; + /// Echo newline (`NL`) even if `ECHO` is disabled. + const ECHONL = 0x00040; + /// Disable flushing after SIGINT/SIGQUIT signals. + const NOFLSH = 0x00080; + /// Send SIGTTOU when background processes write to terminal (job control). + const TOSTOP = 0x00100; + /// Echo control characters as `^X` (Ctrl-X notation). + const ECHOCTL = 0x00200; + /// Echo erased characters visually (between `\` and `/` during erase). + const ECHOPRT = 0x00400; + /// Erase entire line on KILL character (enhanced `ECHOK` behavior). + const ECHOKE = 0x00800; + /// Output is being flushed (set internally; do not configure manually). + const FLUSHO = 0x01000; + /// Redraw pending input after next read (typeahead handling for line editing). + const PENDIN = 0x04000; + /// Enable extended input character processing (non-canonical mode features). + const IEXTEN = 0x08000; + /// External processing mode (used with `ICANON` for shell job control). + const EXTPROC = 0x10000; + } +} + +impl Default for C_LFLAGS { + fn default() -> Self { + C_LFLAGS::ICANON + | C_LFLAGS::ECHO + | C_LFLAGS::ISIG + | C_LFLAGS::ECHOE + | C_LFLAGS::ECHOK + | C_LFLAGS::ECHOCTL + | C_LFLAGS::ECHOKE + | C_LFLAGS::IEXTEN + } +} + +#[repr(u32)] +#[doc(hidden)] +#[derive(Clone, Copy)] +/// These values correspond to the `CS*` constants in Unix termios implementations. +/// Used with `CSIZE_MASK` to set/clear the number of data bits per character. +pub enum C_CFLAGS_CSIZE { + CS5 = 0x00000000, + CS6 = 0x00000010, + CS7 = 0x00000020, + CS8 = 0x00000030, +} + +/* c_cc characters index*/ +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +/// Represents indices for terminal control characters in the `c_cc` array of [`Termios`]. +/// +/// These constants define positions in the control character array (`c_cc`), which configure +/// terminal behavior (e.g., signal triggers, line editing). Values correspond to the POSIX standard +pub enum CC_C_CHAR { + /// Interrupt character (e.g., `Ctrl+C`). Sends `SIGINT` to the foreground process group. + VINTR = 0, + /// Quit character (e.g., `Ctrl+\`). Sends `SIGQUIT` to the foreground process group. + VQUIT = 1, + /// Erase character (e.g., `Backspace` or `Ctrl+?`). Deletes the preceding character in line-editing mode. + VERASE = 2, + /// Kill character (e.g., `Ctrl+U`). Deletes the entire line in line-editing mode. + VKILL = 3, + /// End-of-file character (e.g., `Ctrl+D`). Signals EOF in canonical input mode. + VEOF = 4, + /// Timeout value (in tenths of a second) for non-canonical reads. Used with [`VMIN`](Self::VMIN). + VTIME = 5, + /// Minimum number of bytes to read in non-canonical mode. Used with [`VTIME`](Self::VTIME). + VMIN = 6, + /// Switch process group character (obsolete; unused in modern systems). + VSWTC = 7, + /// Resume output character (e.g., `Ctrl+Q`). Restarts paused output (XON). + VSTART = 8, + /// Suspend output character (e.g., `Ctrl+S`). Pauses terminal output (XOFF). + VSTOP = 9, + /// Suspend character (e.g., `Ctrl+Z`). Sends `SIGTSTP` to the foreground process group. + VSUSP = 10, + /// End-of-line character (alternate; `\0` by default). Terminates input in canonical mode. + VEOL = 11, + /// Reprint character (e.g., `Ctrl+R`). Redisplays the current line in line-editing mode. + VREPRINT = 12, + /// Discard character (e.g., `Ctrl+O`). Toggles discarding of pending output. + VDISCARD = 13, + /// Word-erase character (e.g., `Ctrl+W`). Deletes the preceding word in line-editing mode. + VWERASE = 14, + /// Literal-next character (e.g., `Ctrl+V`). Quotes the next input character (disables special handling). + VLNEXT = 15, + /// Secondary end-of-line character (rarely used). Alternative to [`VEOL`](Self::VEOL). + VEOL2 = 16, +} diff --git a/modules/ruxtty/src/tty.rs b/modules/ruxtty/src/tty.rs new file mode 100644 index 000000000..4f1dd08b2 --- /dev/null +++ b/modules/ruxtty/src/tty.rs @@ -0,0 +1,153 @@ +/* Copyright (c) [2023] [Syswonder Community] + * [Ruxos] is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ +#![expect(dead_code)] +use core::sync::atomic::{AtomicUsize, Ordering}; + +use alloc::{ffi::CString, sync::Arc}; +use axlog::ax_print; + +use crate::ldisc::Ldisc; +use num_enum::FromPrimitive; +use spin::once::Once; + +/// Global singleton instance for the default TTY device +pub(crate) static N_TTY: Once> = Once::new(); + +/// Terminal device representation with line discipline and process control +/// +/// Manages character input/output processing and foreground process group +pub struct Tty { + /// Device name + name: CString, + /// Line discipline handling input processing + pub ldisc: Arc, + // TODO: Implement process group management + fg_pgid: AtomicUsize, +} + +impl Tty { + /// Creates new TTY device + pub fn new(name: CString) -> Arc { + Arc::new(Self { + name, + ldisc: Ldisc::new(), + fg_pgid: AtomicUsize::new(1000), + }) + } + + // Handles incoming character with line discipline processing + /// + /// Invokes callback for immediate echo when applicable + pub fn push_char(&self, ch: u8) { + self.ldisc.push_char(ch, |content| ax_print!("{}", content)); + } + + /// Retrieves current foreground process group ID + pub fn fg_pgid(&self) -> usize { + self.fg_pgid.load(Ordering::Relaxed) + } + + /// Updates foreground process group ID with release ordering + pub fn set_fg_pgid(&self, fg_pgid: usize) { + self.fg_pgid.store(fg_pgid, Ordering::Release); + } +} + +#[derive(Debug, FromPrimitive, Clone, Copy, Eq, PartialEq)] +#[repr(u32)] +/// Tty IO control command +pub enum TtyIoctlCmd { + /// Default variant to catch invalid/unknown ioctl commands + #[num_enum(default)] + InvalidCommand, + + // ============================================== + // Terminal control commands (termios structure) + // ============================================== + /// TCGETS: Terminal Control GET Settings + /// + /// Gets current terminal settings using termios structure + TCGETS = 0x5401, + + /// TCSETS: Terminal Control SET Settings + /// + /// Sets terminal settings immediately using termios structure + TCSETS = 0x5402, + + /// TCSETSW: Terminal Control SET Settings and Wait + /// + /// Sets terminal settings after draining output buffer + TCSETSW = 0x5403, + + /// TCSETSF: Terminal Control SET Settings and Flush + /// + /// Sets terminal settings after flushing input/output buffers + TCSETSF = 0x5404, + + // ============================================== + // Terminal control commands (termio structure - legacy BSD) + // ============================================== + /// TCGETA: Terminal Control GET Attributes + /// + /// Gets current terminal settings using legacy termio structure + TCGETA = 0x5405, + + /// TCSETA: Terminal Control SET Attributes + /// + /// Sets terminal settings immediately using termio structure + TCSETA = 0x5406, + + /// TCSETAW: Terminal Control SET Attributes and Wait + /// + /// Sets termio settings after draining output buffer + TCSETAW = 0x5407, + + /// TCSETAF: Terminal Control SET Attributes and Flush + /// + /// Sets termio settings after flushing input/output buffers + TCSETAF = 0x5408, + + // ============================================== + // Special control commands + // ============================================== + /// TCSBRK: Terminal Control Send BReaK + /// + /// Sends a break sequence (stream of zero bits) for 0.25-0.5 seconds + TCSBRK = 0x5409, + + /// TIOCSCTTY: Terminal IOCtl Set Controlling TTY + /// + /// Makes the given terminal the controlling terminal of the calling process + TIOCSCTTY = 0x540E, + + /// TIOCGPGRP: Terminal IOCtl Get Process GRouP + /// + /// Gets foreground process group ID associated with terminal + TIOCGPGRP = 0x540F, + + /// TIOCSPGRP: Terminal IOCtl Set Process GRouP + /// + /// Sets foreground process group ID associated with terminal + TIOCSPGRP = 0x5410, + + /// TIOCGWINSZ: Terminal IOCtl Get WINdow SiZe + /// + /// Gets terminal window size (rows/columns) + TIOCGWINSZ = 0x5413, + + /// TIOCSWINSZ: Terminal IOCtl Set WINdow SiZe + /// + /// Sets terminal window size (rows/columns) + TIOCSWINSZ = 0x5414, + + /// TIOCNOTTY: Terminal IOCtl No TTY + /// + /// Disassociates from controlling terminal (used by daemons/sshd) + TIOCNOTTY = 0x5422, +}