Redefine the TTY driver interface

This commit is contained in:
Ruihan Li
2025-05-03 23:31:54 +08:00
committed by Tate, Hongliang Tian
parent 5a9a63e1a7
commit 67065835ef
17 changed files with 469 additions and 523 deletions

View File

@ -24,7 +24,7 @@ pub struct FramebufferConsole {
state: SpinLock<ConsoleState, LocalIrqDisabled>,
}
pub static CONSOLE_NAME: &str = "Framebuffer-Console";
pub const CONSOLE_NAME: &str = "Framebuffer-Console";
pub static FRAMEBUFFER_CONSOLE: Once<Arc<FramebufferConsole>> = Once::new();

View File

@ -12,7 +12,7 @@ use ostd::Pod;
use crate::transport::{ConfigManager, VirtioTransport};
pub static DEVICE_NAME: &str = "Virtio-Block";
pub const DEVICE_NAME: &str = "Virtio-Block";
bitflags! {
/// features for virtio block device

View File

@ -3,4 +3,4 @@
pub mod config;
pub mod device;
pub static DEVICE_NAME: &str = "Virtio-Console";
pub const DEVICE_NAME: &str = "Virtio-Console";

View File

@ -32,7 +32,7 @@ use ostd::{io::IoMem, Pod};
use crate::transport::VirtioTransport;
pub static DEVICE_NAME: &str = "Virtio-Input";
pub const DEVICE_NAME: &str = "Virtio-Input";
/// Select value used for [`device::InputDevice::query_config_select()`].
#[repr(u8)]

View File

@ -4,4 +4,4 @@ pub mod config;
pub mod device;
pub mod header;
pub static DEVICE_NAME: &str = "Virtio-Net";
pub const DEVICE_NAME: &str = "Virtio-Net";

View File

@ -14,7 +14,8 @@ pub mod device;
pub mod error;
pub mod header;
pub static DEVICE_NAME: &str = "Virtio-Vsock";
pub const DEVICE_NAME: &str = "Virtio-Vsock";
pub trait VsockDeviceIrqHandler = Fn() + Send + Sync + 'static;
pub fn register_device(name: String, device: Arc<SpinLock<SocketDevice>>) {

View File

@ -11,11 +11,12 @@ mod zero;
#[cfg(all(target_arch = "x86_64", feature = "cvm_guest"))]
mod tdxguest;
use alloc::format;
pub use pty::{new_pty_pair, PtyMaster, PtySlave};
pub use random::Random;
pub use urandom::Urandom;
use self::tty::get_n_tty;
use crate::{
fs::device::{add_node, Device, DeviceId, DeviceType},
prelude::*,
@ -25,23 +26,37 @@ use crate::{
pub fn init() -> Result<()> {
let null = Arc::new(null::Null);
add_node(null, "null")?;
let zero = Arc::new(zero::Zero);
add_node(zero, "zero")?;
tty::init();
let console = get_n_tty().clone();
add_node(console, "console")?;
let tty = Arc::new(tty::TtyDevice);
add_node(tty, "tty")?;
let console = tty::system_console().clone();
add_node(console, "console")?;
for (index, tty) in tty::iter_n_tty().enumerate() {
add_node(tty.clone(), &format!("tty{}", index))?;
}
#[cfg(target_arch = "x86_64")]
ostd::if_tdx_enabled!({
add_node(Arc::new(tdxguest::TdxGuest), "tdx_guest")?;
});
let random = Arc::new(random::Random);
add_node(random, "random")?;
let urandom = Arc::new(urandom::Urandom);
add_node(urandom, "urandom")?;
pty::init()?;
shm::init()?;
Ok(())
}

View File

@ -0,0 +1,98 @@
// SPDX-License-Identifier: MPL-2.0
use ostd::sync::SpinLock;
use crate::{
device::tty::{Tty, TtyDriver},
events::IoEvents,
prelude::{return_errno_with_message, Errno, Result},
process::signal::Pollee,
util::ring_buffer::RingBuffer,
};
const BUFFER_CAPACITY: usize = 4096;
/// A pseudoterminal driver.
///
/// This is contained in the PTY slave, but it maintains the output buffer and the pollee of the
/// master. The pollee of the slave is part of the [`Tty`] structure (see the definition of
/// [`PtySlave`]).
pub struct PtyDriver {
output: SpinLock<RingBuffer<u8>>,
pollee: Pollee,
}
/// A pseudoterminal slave.
pub type PtySlave = Tty<PtyDriver>;
impl PtyDriver {
pub(super) fn new() -> Self {
Self {
output: SpinLock::new(RingBuffer::new(BUFFER_CAPACITY)),
pollee: Pollee::new(),
}
}
pub(super) fn try_read(&self, buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut output = self.output.lock();
if output.is_empty() {
return_errno_with_message!(Errno::EAGAIN, "the buffer is empty");
}
let read_len = output.len().min(buf.len());
output.pop_slice(&mut buf[..read_len]).unwrap();
Ok(read_len)
}
pub(super) fn pollee(&self) -> &Pollee {
&self.pollee
}
pub(super) fn buffer_len(&self) -> usize {
self.output.lock().len()
}
}
impl TtyDriver for PtyDriver {
fn push_output(&self, chs: &[u8]) {
let mut output = self.output.lock();
for ch in chs {
// TODO: This is termios-specific behavior and should be part of the TTY implementation
// instead of the TTY driver implementation. See the ONLCR flag for more details.
if *ch == b'\n' {
output.push_overwrite(b'\r');
output.push_overwrite(b'\n');
continue;
}
output.push_overwrite(*ch);
}
self.pollee.notify(IoEvents::IN);
}
fn drain_output(&self) {
self.output.lock().clear();
self.pollee.invalidate();
}
fn echo_callback(&self) -> impl FnMut(&[u8]) + '_ {
let mut output = self.output.lock();
let mut has_notified = false;
move |chs| {
for ch in chs {
output.push_overwrite(*ch);
}
if !has_notified {
self.pollee.notify(IoEvents::IN);
has_notified = true;
}
}
}
}

View File

@ -0,0 +1,155 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::format;
use ostd::task::Task;
use super::{driver::PtyDriver, PtySlave};
use crate::{
current_userspace,
events::IoEvents,
fs::{
devpts::DevPts,
file_table::FdFlags,
fs_resolver::FsPath,
inode_handle::FileIo,
utils::{AccessMode, Inode, InodeMode, IoctlCmd},
},
prelude::*,
process::{
posix_thread::{AsPosixThread, AsThreadLocal},
signal::{PollHandle, Pollable},
Terminal,
},
};
const IO_CAPACITY: usize = 4096;
/// A pseudoterminal master.
///
/// A pseudoterminal contains two buffers:
/// * The input buffer is written by the master and read by the slave, which is maintained in the
/// line discipline (part of [`PtySlave`], which is a [`Tty`]).
/// * The output buffer is written by the slave and read by the master, which is maintained in the
/// driver (i.e., [`PtyDriver`]).
///
/// [`Tty`]: crate::device::tty::Tty
pub struct PtyMaster {
ptmx: Arc<dyn Inode>,
slave: Arc<PtySlave>,
}
impl PtyMaster {
pub(super) fn new(ptmx: Arc<dyn Inode>, index: u32) -> Arc<Self> {
let slave = PtySlave::new(index, PtyDriver::new());
Arc::new(Self { ptmx, slave })
}
pub(super) fn slave(&self) -> &Arc<PtySlave> {
&self.slave
}
fn check_io_events(&self) -> IoEvents {
if self.slave().driver().buffer_len() > 0 {
IoEvents::IN | IoEvents::OUT
} else {
IoEvents::OUT
}
}
}
impl Pollable for PtyMaster {
fn poll(&self, mask: IoEvents, poller: Option<&mut PollHandle>) -> IoEvents {
self.slave
.driver()
.pollee()
.poll_with(mask, poller, || self.check_io_events())
}
}
impl FileIo for PtyMaster {
fn read(&self, writer: &mut VmWriter) -> Result<usize> {
// TODO: Add support for non-blocking mode and timeout
let mut buf = vec![0u8; writer.avail().min(IO_CAPACITY)];
let read_len = self.wait_events(IoEvents::IN, None, || {
self.slave.driver().try_read(&mut buf)
})?;
self.slave.driver().pollee().invalidate();
// TODO: Confirm what we should do if `write_fallible` fails in the middle.
writer.write_fallible(&mut buf[..read_len].into())?;
Ok(read_len)
}
fn write(&self, reader: &mut VmReader) -> Result<usize> {
let mut buf = vec![0u8; reader.remain().min(IO_CAPACITY)];
let write_len = reader.read_fallible(&mut buf.as_mut_slice().into())?;
self.slave.push_input(&buf[..write_len]);
Ok(write_len)
}
fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result<i32> {
match cmd {
IoctlCmd::TCGETS
| IoctlCmd::TCSETS
| IoctlCmd::TCSETSW
| IoctlCmd::TCSETSF
| IoctlCmd::TIOCGWINSZ
| IoctlCmd::TIOCSWINSZ
| IoctlCmd::TIOCGPTN => return self.slave.ioctl(cmd, arg),
IoctlCmd::TIOCSPTLCK => {
// TODO: Lock or unlock the PTY.
}
IoctlCmd::TIOCGPTPEER => {
let current_task = Task::current().unwrap();
let posix_thread = current_task.as_posix_thread().unwrap();
let thread_local = current_task.as_thread_local().unwrap();
// TODO: Deal with `open()` flags.
let slave = {
let slave_name = {
let devpts_path = super::DEV_PTS.get().unwrap().abs_path();
format!("{}/{}", devpts_path, self.slave.index())
};
let fs_path = FsPath::try_from(slave_name.as_str())?;
let inode_handle = {
let fs = posix_thread.fs().resolver().read();
let flags = AccessMode::O_RDWR as u32;
let mode = (InodeMode::S_IRUSR | InodeMode::S_IWUSR).bits();
fs.open(&fs_path, flags, mode)?
};
Arc::new(inode_handle)
};
let fd = {
let file_table = thread_local.borrow_file_table();
let mut file_table_locked = file_table.unwrap().write();
// TODO: Deal with the `O_CLOEXEC` flag.
file_table_locked.insert(slave, FdFlags::empty())
};
return Ok(fd);
}
IoctlCmd::FIONREAD => {
let len = self.slave.driver().buffer_len() as i32;
current_userspace!().write_val(arg, &len)?;
}
_ => (self.slave.clone() as Arc<dyn Terminal>).job_ioctl(cmd, arg, true)?,
}
Ok(0)
}
}
impl Drop for PtyMaster {
fn drop(&mut self) {
let fs = self.ptmx.fs();
let devpts = fs.downcast_ref::<DevPts>().unwrap();
let index = self.slave.index();
devpts.remove_slave(index);
}
}

View File

@ -10,10 +10,11 @@ use crate::{
prelude::*,
};
#[expect(clippy::module_inception)]
mod pty;
mod driver;
mod master;
pub use pty::{PtyMaster, PtySlave};
pub use driver::PtySlave;
pub use master::PtyMaster;
use spin::Once;
static DEV_PTS: Once<Dentry> = Once::new();

View File

@ -1,347 +0,0 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::format;
use ostd::task::Task;
use crate::{
current_userspace,
device::tty::line_discipline::LineDiscipline,
events::IoEvents,
fs::{
device::{Device, DeviceId, DeviceType},
devpts::DevPts,
file_table::FdFlags,
fs_resolver::FsPath,
inode_handle::FileIo,
utils::{AccessMode, Inode, InodeMode, IoctlCmd},
},
prelude::*,
process::{
broadcast_signal_async,
posix_thread::{AsPosixThread, AsThreadLocal},
signal::{PollHandle, Pollable, Pollee},
JobControl, Terminal,
},
util::ring_buffer::RingBuffer,
};
const BUFFER_CAPACITY: usize = 4096;
const IO_CAPACITY: usize = 4096;
/// Pseudo terminal master.
/// Internally, it has two buffers.
/// One is inside ldisc, which is written by master and read by slave,
/// the other is a ring buffer, which is written by slave and read by master.
pub struct PtyMaster {
ptmx: Arc<dyn Inode>,
index: u32,
slave: Arc<PtySlave>,
input: SpinLock<RingBuffer<u8>>,
pollee: Pollee,
}
impl PtyMaster {
pub fn new(ptmx: Arc<dyn Inode>, index: u32) -> Arc<Self> {
Arc::new_cyclic(move |master| {
let slave = Arc::new_cyclic(move |weak_self| PtySlave {
ldisc: SpinLock::new(LineDiscipline::new()),
job_control: JobControl::new(),
pollee: Pollee::new(),
master: master.clone(),
weak_self: weak_self.clone(),
});
PtyMaster {
ptmx,
index,
slave,
input: SpinLock::new(RingBuffer::new(BUFFER_CAPACITY)),
pollee: Pollee::new(),
}
})
}
pub fn index(&self) -> u32 {
self.index
}
pub fn ptmx(&self) -> &Arc<dyn Inode> {
&self.ptmx
}
pub fn slave(&self) -> &Arc<PtySlave> {
&self.slave
}
fn slave_push(&self, chs: &[u8]) {
let mut input = self.input.lock();
for ch in chs {
// TODO: This is termios-specific behavior and should be part of the TTY implementation
// instead of the TTY driver implementation. See the ONLCR flag for more details.
if *ch == b'\n' {
input.push_overwrite(b'\r');
input.push_overwrite(b'\n');
continue;
}
input.push_overwrite(*ch);
}
self.pollee.notify(IoEvents::IN);
}
fn slave_echo(&self) -> impl FnMut(&str) + '_ {
let mut input = self.input.lock();
let mut has_notified = false;
move |content| {
for byte in content.as_bytes() {
input.push_overwrite(*byte);
}
if !has_notified {
self.pollee.notify(IoEvents::IN);
has_notified = true;
}
}
}
fn try_read(&self, buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut input = self.input.lock();
if input.is_empty() {
return_errno_with_message!(Errno::EAGAIN, "the buffer is empty");
}
let read_len = input.len().min(buf.len());
input.pop_slice(&mut buf[..read_len]).unwrap();
self.pollee.invalidate();
Ok(read_len)
}
fn check_io_events(&self) -> IoEvents {
let input = self.input.lock();
if !input.is_empty() {
IoEvents::IN | IoEvents::OUT
} else {
IoEvents::OUT
}
}
}
impl Pollable for PtyMaster {
fn poll(&self, mask: IoEvents, poller: Option<&mut PollHandle>) -> IoEvents {
self.pollee
.poll_with(mask, poller, || self.check_io_events())
}
}
impl FileIo for PtyMaster {
fn read(&self, writer: &mut VmWriter) -> Result<usize> {
// TODO: Add support for non-blocking mode and timeout
let mut buf = vec![0u8; writer.avail().min(IO_CAPACITY)];
let read_len = self.wait_events(IoEvents::IN, None, || self.try_read(&mut buf))?;
writer.write_fallible(&mut buf[..read_len].into())?;
Ok(read_len)
}
fn write(&self, reader: &mut VmReader) -> Result<usize> {
let mut buf = vec![0u8; reader.remain().min(IO_CAPACITY)];
let write_len = reader.read_fallible(&mut buf.as_mut_slice().into())?;
self.slave.master_push(&buf[..write_len]);
Ok(write_len)
}
fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result<i32> {
match cmd {
IoctlCmd::TCGETS
| IoctlCmd::TCSETS
| IoctlCmd::TIOCGPTN
| IoctlCmd::TIOCGWINSZ
| IoctlCmd::TIOCSWINSZ => return self.slave.ioctl(cmd, arg),
IoctlCmd::TIOCSPTLCK => {
// TODO: lock/unlock pty
}
IoctlCmd::TIOCGPTPEER => {
let current_task = Task::current().unwrap();
let posix_thread = current_task.as_posix_thread().unwrap();
let thread_local = current_task.as_thread_local().unwrap();
// TODO: deal with open options
let slave = {
let slave_name = {
let devpts_path = super::DEV_PTS.get().unwrap().abs_path();
format!("{}/{}", devpts_path, self.index())
};
let fs_path = FsPath::try_from(slave_name.as_str())?;
let inode_handle = {
let fs = posix_thread.fs().resolver().read();
let flags = AccessMode::O_RDWR as u32;
let mode = (InodeMode::S_IRUSR | InodeMode::S_IWUSR).bits();
fs.open(&fs_path, flags, mode)?
};
Arc::new(inode_handle)
};
let fd = {
let file_table = thread_local.borrow_file_table();
let mut file_table_locked = file_table.unwrap().write();
// TODO: deal with the O_CLOEXEC flag
file_table_locked.insert(slave, FdFlags::empty())
};
return Ok(fd);
}
IoctlCmd::FIONREAD => {
let len = self.input.lock().len() as i32;
current_userspace!().write_val(arg, &len)?;
}
_ => (self.slave.clone() as Arc<dyn Terminal>).job_ioctl(cmd, arg, true)?,
}
Ok(0)
}
}
impl Drop for PtyMaster {
fn drop(&mut self) {
let fs = self.ptmx.fs();
let devpts = fs.downcast_ref::<DevPts>().unwrap();
let index = self.index;
devpts.remove_slave(index);
}
}
pub struct PtySlave {
ldisc: SpinLock<LineDiscipline>,
job_control: JobControl,
pollee: Pollee,
master: Weak<PtyMaster>,
weak_self: Weak<Self>,
}
impl PtySlave {
pub fn index(&self) -> u32 {
self.master().index()
}
fn master(&self) -> Arc<PtyMaster> {
self.master.upgrade().unwrap()
}
fn master_push(&self, chs: &[u8]) {
let mut ldisc = self.ldisc.lock();
let master = self.master();
let mut echo = master.slave_echo();
for ch in chs {
ldisc.push_char(
*ch,
|signum| {
if let Some(foreground) = self.job_control.foreground() {
broadcast_signal_async(Arc::downgrade(&foreground), signum);
}
},
&mut echo,
);
}
self.pollee.notify(IoEvents::IN);
}
fn check_io_events(&self) -> IoEvents {
if self.ldisc.lock().buffer_len() != 0 {
IoEvents::IN | IoEvents::OUT
} else {
IoEvents::OUT
}
}
}
impl Device for PtySlave {
fn type_(&self) -> DeviceType {
DeviceType::CharDevice
}
fn id(&self) -> crate::fs::device::DeviceId {
DeviceId::new(88, self.index())
}
}
impl Terminal for PtySlave {
fn job_control(&self) -> &JobControl {
&self.job_control
}
}
impl Pollable for PtySlave {
fn poll(&self, mask: IoEvents, poller: Option<&mut PollHandle>) -> IoEvents {
self.pollee
.poll_with(mask, poller, || self.check_io_events())
}
}
impl FileIo for PtySlave {
fn read(&self, writer: &mut VmWriter) -> Result<usize> {
self.job_control.wait_until_in_foreground()?;
// TODO: Add support for non-blocking mode and timeout
let mut buf = vec![0u8; writer.avail().min(IO_CAPACITY)];
let read_len =
self.wait_events(IoEvents::IN, None, || self.ldisc.lock().try_read(&mut buf))?;
self.pollee.invalidate();
writer.write_fallible(&mut buf[..read_len].into())?;
Ok(read_len)
}
fn write(&self, reader: &mut VmReader) -> Result<usize> {
let mut buf = vec![0u8; reader.remain().min(IO_CAPACITY)];
let write_len = reader.read_fallible(&mut buf.as_mut_slice().into())?;
self.master().slave_push(&buf[..write_len]);
Ok(write_len)
}
fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result<i32> {
match cmd {
IoctlCmd::TCGETS => {
let termios = *self.ldisc.lock().termios();
current_userspace!().write_val(arg, &termios)?;
}
IoctlCmd::TCSETS => {
let termios = current_userspace!().read_val(arg)?;
self.ldisc.lock().set_termios(termios);
}
IoctlCmd::TIOCGPTN => {
let idx = self.index();
current_userspace!().write_val(arg, &idx)?;
}
IoctlCmd::TIOCGWINSZ => {
let winsize = self.ldisc.lock().window_size();
current_userspace!().write_val(arg, &winsize)?;
}
IoctlCmd::TIOCSWINSZ => {
let winsize = current_userspace!().read_val(arg)?;
self.ldisc.lock().set_window_size(winsize);
}
IoctlCmd::FIONREAD => {
let buffer_len = self.ldisc.lock().buffer_len() as i32;
current_userspace!().write_val(arg, &buffer_len)?;
}
_ => (self.weak_self.upgrade().unwrap() as Arc<dyn Terminal>)
.job_ioctl(cmd, arg, false)?,
}
Ok(0)
}
}

View File

@ -1,89 +1,23 @@
// SPDX-License-Identifier: MPL-2.0
use ostd::mm::{Infallible, VmReader};
use spin::Once;
/// A TTY driver.
///
/// A driver exposes some device-specific behavior to [`Tty`]. For example, a device provides
/// methods to write to the output buffer (see [`Self::push_output`]), where the output buffer can
/// be the monitor if the underlying device is framebuffer, or just a ring buffer if the underlying
/// device is pseduoterminal).
///
/// [`Tty`]: super::Tty
pub trait TtyDriver: Send + Sync + 'static {
/// Pushes characters into the output buffer.
fn push_output(&self, chs: &[u8]);
use crate::{
device::tty::{get_n_tty, Tty},
prelude::*,
};
/// Drains the output buffer.
fn drain_output(&self);
pub static TTY_DRIVER: Once<Arc<TtyDriver>> = Once::new();
pub(super) fn init() {
for (_, device) in aster_console::all_devices() {
device.register_callback(&console_input_callback)
}
let tty_driver = Arc::new(TtyDriver::new());
// FIXME: install n_tty into tty_driver?
let n_tty = get_n_tty();
tty_driver.install(n_tty.clone());
TTY_DRIVER.call_once(|| tty_driver);
}
pub struct TtyDriver {
ttys: SpinLock<Vec<Arc<Tty>>>,
}
impl TtyDriver {
pub const fn new() -> Self {
Self {
ttys: SpinLock::new(Vec::new()),
}
}
/// Return the tty device in driver's internal table.
pub fn lookup(&self, index: usize) -> Result<Arc<Tty>> {
let ttys = self.ttys.disable_irq().lock();
// Return the tty device corresponding to idx
if index >= ttys.len() {
return_errno_with_message!(Errno::ENODEV, "lookup failed. No tty device");
}
let tty = ttys[index].clone();
drop(ttys);
Ok(tty)
}
/// Install a new tty into the driver's internal tables.
pub fn install(self: &Arc<Self>, tty: Arc<Tty>) {
tty.set_driver(Arc::downgrade(self));
self.ttys.disable_irq().lock().push(tty);
}
/// remove a new tty into the driver's internal tables.
pub fn remove(&self, index: usize) -> Result<()> {
let mut ttys = self.ttys.disable_irq().lock();
if index >= ttys.len() {
return_errno_with_message!(Errno::ENODEV, "lookup failed. No tty device");
}
let removed_tty = ttys.remove(index);
removed_tty.set_driver(Weak::new());
drop(ttys);
Ok(())
}
pub fn push_char(&self, ch: u8) {
// FIXME: should the char send to all ttys?
for tty in &*self.ttys.disable_irq().lock() {
tty.push_char(ch);
}
}
}
impl Default for TtyDriver {
fn default() -> Self {
Self::new()
}
}
fn console_input_callback(mut reader: VmReader<Infallible>) {
let tty_driver = get_tty_driver();
while reader.remain() > 0 {
let ch = reader.read_val().unwrap();
tty_driver.push_char(ch);
}
}
fn get_tty_driver() -> &'static TtyDriver {
TTY_DRIVER.get().unwrap()
/// Returns a callback function that echoes input characters to the output buffer.
///
/// Note that the implementation may choose to hold a lock during the life of the callback.
/// During this time, calls to other methods such as [`Self::push_output`] may cause deadlocks.
fn echo_callback(&self) -> impl FnMut(&[u8]) + '_;
}

View File

@ -1,7 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::format;
use super::termio::{KernelTermios, WinSize, CC_C_CHAR};
use crate::{
prelude::*,
@ -61,10 +59,6 @@ impl CurrentLine {
pub fn is_full(&self) -> bool {
self.buffer.is_full()
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
}
impl LineDiscipline {
@ -79,7 +73,7 @@ impl LineDiscipline {
}
/// Pushes a character to the line discipline.
pub fn push_char<F1: FnMut(SigNum), F2: FnMut(&str)>(
pub fn push_char<F1: FnMut(SigNum), F2: FnMut(&[u8])>(
&mut self,
ch: u8,
mut signal_callback: F1,
@ -135,19 +129,17 @@ impl LineDiscipline {
}
// TODO: respect output flags
fn output_char<F: FnMut(&str)>(&self, ch: u8, mut echo_callback: F) {
fn output_char<F: FnMut(&[u8])>(&self, ch: u8, mut echo_callback: F) {
match ch {
b'\n' => echo_callback("\n"),
b'\r' => echo_callback("\r\n"),
b'\n' => echo_callback(b"\n"),
b'\r' => echo_callback(b"\r\n"),
ch if ch == *self.termios.get_special_char(CC_C_CHAR::VERASE) => {
// Write a space to overwrite the current character
let backspace: &str = core::str::from_utf8(b"\x08 \x08").unwrap();
echo_callback(backspace);
echo_callback(b"\x08 \x08");
}
ch if is_printable_char(ch) => print!("{}", char::from(ch)),
ch if is_printable_char(ch) => echo_callback(&[ch]),
ch if is_ctrl_char(ch) && self.termios.contains_echo_ctl() => {
let ctrl_char = format!("^{}", ctrl_char_to_printable(ch));
echo_callback(&ctrl_char);
echo_callback(&[b'^', ctrl_char_to_printable(ch)]);
}
_ => {}
}
@ -267,7 +259,7 @@ fn char_to_signal(ch: u8, termios: &KernelTermios) -> Option<SigNum> {
}
}
fn ctrl_char_to_printable(ch: u8) -> char {
fn ctrl_char_to_printable(ch: u8) -> u8 {
debug_assert!(is_ctrl_char(ch));
char::from_u32((ch + b'A' - 1) as u32).unwrap()
ch + b'A' - 1
}

View File

@ -1,9 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
use ostd::{early_print, sync::LocalIrqDisabled};
use spin::Once;
use ostd::sync::LocalIrqDisabled;
use self::{driver::TtyDriver, line_discipline::LineDiscipline};
use self::line_discipline::LineDiscipline;
use crate::{
current_userspace,
events::IoEvents,
@ -21,65 +20,65 @@ use crate::{
};
mod device;
pub mod driver;
pub mod line_discipline;
pub mod termio;
mod driver;
mod line_discipline;
mod n_tty;
mod termio;
pub use device::TtyDevice;
static N_TTY: Once<Arc<Tty>> = Once::new();
pub(super) fn init() {
let name = CString::new("console").unwrap();
let tty = Tty::new(name);
N_TTY.call_once(|| tty);
driver::init();
}
pub use driver::TtyDriver;
pub(super) use n_tty::init;
pub use n_tty::{iter_n_tty, system_console};
const IO_CAPACITY: usize = 4096;
pub struct Tty {
/// tty_name
#[expect(unused)]
name: CString,
/// line discipline
/// A teletyper (TTY).
///
/// This abstracts the general functionality of a TTY in a way that
/// - Any input device driver can use [`Tty::push_input`] to push input characters, and users can
/// [`Tty::read`] from the TTY;
/// - Users can also [`Tty::write`] output characters to the TTY and the output device driver will
/// receive the characters from [`TtyDriver::push_output`] where the generic parameter `D` is
/// the [`TtyDriver`].
///
/// ```text
/// +------------+ +-------------+
/// |input device| |output device|
/// | driver | | driver |
/// +-----+------+ +------^------+
/// | |
/// | +-------+ |
/// +-----> TTY +-----+
/// +-------+
/// Tty::push_input D::push_output
/// ```
pub struct Tty<D> {
index: u32,
driver: D,
ldisc: SpinLock<LineDiscipline, LocalIrqDisabled>,
job_control: JobControl,
pollee: Pollee,
/// driver
driver: SpinLock<Weak<TtyDriver>>,
weak_self: Weak<Self>,
}
impl Tty {
pub fn new(name: CString) -> Arc<Self> {
impl<D> Tty<D> {
pub fn new(index: u32, driver: D) -> Arc<Self> {
Arc::new_cyclic(move |weak_ref| Tty {
name,
index,
driver,
ldisc: SpinLock::new(LineDiscipline::new()),
job_control: JobControl::new(),
pollee: Pollee::new(),
driver: SpinLock::new(Weak::new()),
weak_self: weak_ref.clone(),
})
}
pub fn set_driver(&self, driver: Weak<TtyDriver>) {
*self.driver.disable_irq().lock() = driver;
pub fn index(&self) -> u32 {
self.index
}
pub fn push_char(&self, ch: u8) {
// FIXME: Use `early_print` to avoid calling virtio-console.
// This is only a workaround
self.ldisc.lock().push_char(
ch,
|signum| {
if let Some(foreground) = self.job_control.foreground() {
broadcast_signal_async(Arc::downgrade(&foreground), signum);
}
},
|content| early_print!("{}", content),
);
self.pollee.notify(IoEvents::IN);
pub fn driver(&self) -> &D {
&self.driver
}
fn check_io_events(&self) -> IoEvents {
@ -91,14 +90,34 @@ impl Tty {
}
}
impl Pollable for Tty {
impl<D: TtyDriver> Tty<D> {
pub fn push_input(&self, chs: &[u8]) {
let mut ldisc = self.ldisc.lock();
let mut echo = self.driver.echo_callback();
for ch in chs {
ldisc.push_char(
*ch,
|signum| {
if let Some(foreground) = self.job_control.foreground() {
broadcast_signal_async(Arc::downgrade(&foreground), signum);
}
},
&mut echo,
);
}
self.pollee.notify(IoEvents::IN);
}
}
impl<D> Pollable for Tty<D> {
fn poll(&self, mask: IoEvents, poller: Option<&mut PollHandle>) -> IoEvents {
self.pollee
.poll_with(mask, poller, || self.check_io_events())
}
}
impl FileIo for Tty {
impl<D: TtyDriver> FileIo for Tty<D> {
fn read(&self, writer: &mut VmWriter) -> Result<usize> {
self.job_control.wait_until_in_foreground()?;
@ -117,11 +136,7 @@ impl FileIo for Tty {
let mut buf = vec![0u8; reader.remain().min(IO_CAPACITY)];
let write_len = reader.read_fallible(&mut buf.as_mut_slice().into())?;
if let Ok(content) = alloc::str::from_utf8(&buf[..write_len]) {
print!("{content}");
} else {
println!("Not utf-8 content: {:?}", buf);
}
self.driver.push_output(&buf[..write_len]);
Ok(write_len)
}
@ -140,8 +155,9 @@ impl FileIo for Tty {
IoctlCmd::TCSETSW => {
let termios = current_userspace!().read_val(arg)?;
self.ldisc.lock().set_termios(termios);
// TODO: Drain the output buffer
let mut ldisc = self.ldisc.lock();
ldisc.set_termios(termios);
self.driver.drain_output();
}
IoctlCmd::TCSETSF => {
let termios = current_userspace!().read_val(arg)?;
@ -149,7 +165,7 @@ impl FileIo for Tty {
let mut ldisc = self.ldisc.lock();
ldisc.set_termios(termios);
ldisc.drain_input();
// TODO: Drain the output buffer
self.driver.drain_output();
self.pollee.invalidate();
}
@ -163,6 +179,16 @@ impl FileIo for Tty {
self.ldisc.lock().set_window_size(winsize);
}
IoctlCmd::TIOCGPTN => {
let idx = self.index;
current_userspace!().write_val(arg, &idx)?;
}
IoctlCmd::FIONREAD => {
let buffer_len = self.ldisc.lock().buffer_len() as u32;
current_userspace!().write_val(arg, &buffer_len)?;
}
_ => (self.weak_self.upgrade().unwrap() as Arc<dyn Terminal>)
.job_ioctl(cmd, arg, false)?,
}
@ -171,23 +197,18 @@ impl FileIo for Tty {
}
}
impl Terminal for Tty {
impl<D: TtyDriver> Terminal for Tty<D> {
fn job_control(&self) -> &JobControl {
&self.job_control
}
}
impl Device for Tty {
impl<D: TtyDriver> Device for Tty<D> {
fn type_(&self) -> DeviceType {
DeviceType::CharDevice
}
fn id(&self) -> DeviceId {
// The same value as /dev/console in linux.
DeviceId::new(88, 0)
DeviceId::new(88, self.index)
}
}
pub fn get_n_tty() -> &'static Arc<Tty> {
N_TTY.get().unwrap()
}

View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: MPL-2.0
use alloc::{boxed::Box, sync::Arc, vec};
use aster_console::AnyConsoleDevice;
use ostd::mm::{Infallible, VmReader, VmWriter};
use spin::Once;
use super::{Tty, TtyDriver};
pub struct ConsoleDriver {
console: Arc<dyn AnyConsoleDevice>,
}
impl TtyDriver for ConsoleDriver {
fn push_output(&self, chs: &[u8]) {
self.console.send(chs);
}
fn drain_output(&self) {}
fn echo_callback(&self) -> impl FnMut(&[u8]) + '_ {
|chs| self.console.send(chs)
}
}
static N_TTY: Once<Box<[Arc<Tty<ConsoleDriver>>]>> = Once::new();
pub(in crate::device) fn init() {
let devices = {
let mut devices = aster_console::all_devices();
// Sort by priorities to ensure that the TTY for the virtio-console device comes first. Is
// there a better way than hardcoding this?
devices.sort_by_key(|(name, _)| match name.as_str() {
aster_virtio::device::console::DEVICE_NAME => 0,
aster_framebuffer::CONSOLE_NAME => 1,
_ => 2,
});
devices
};
let ttys = devices
.into_iter()
.enumerate()
.map(|(index, (_, device))| create_n_tty(index as _, device))
.collect();
N_TTY.call_once(|| ttys);
}
fn create_n_tty(index: u32, device: Arc<dyn AnyConsoleDevice>) -> Arc<Tty<ConsoleDriver>> {
let driver = ConsoleDriver {
console: device.clone(),
};
let tty = Tty::new(index, driver);
let tty_cloned = tty.clone();
device.register_callback(Box::leak(Box::new(
move |mut reader: VmReader<Infallible>| {
let mut chs = vec![0u8; reader.remain()];
reader.read(&mut VmWriter::from(chs.as_mut_slice()));
tty.push_input(chs.as_slice());
},
)));
tty_cloned
}
/// Returns the system console, i.e., `/dev/console`.
pub fn system_console() -> &'static Arc<Tty<ConsoleDriver>> {
&N_TTY.get().unwrap()[0]
}
/// Iterates all TTY devices, i.e., `/dev/tty1`, `/dev/tty2`, e.t.c.
pub fn iter_n_tty() -> impl Iterator<Item = &'static Arc<Tty<ConsoleDriver>>> {
N_TTY.get().unwrap().iter()
}

View File

@ -37,7 +37,7 @@ pub fn spawn_init_process(
set_session_and_group(&process);
// FIXME: This should be done by the userspace init process.
(crate::device::tty::get_n_tty().clone() as Arc<dyn Terminal>).set_control(&process)?;
(crate::device::tty::system_console().clone() as Arc<dyn Terminal>).set_control(&process)?;
process.run();

View File

@ -731,15 +731,14 @@ pub fn enqueue_signal_async(process: Weak<Process>, signum: SigNum) {
/// This is the asynchronous version of [`ProcessGroup::broadcast_signal`]. By asynchronous, this
/// method submits a work item and returns, so this method doesn't sleep and can be used in atomic
/// mode.
pub fn broadcast_signal_async(process_group: Weak<ProcessGroup>, signal: SigNum) {
pub fn broadcast_signal_async(process_group: Weak<ProcessGroup>, signum: SigNum) {
use super::signal::signals::kernel::KernelSignal;
use crate::thread::work_queue;
let signal = KernelSignal::new(signal);
work_queue::submit_work_func(
move || {
if let Some(process_group) = process_group.upgrade() {
process_group.broadcast_signal(signal);
process_group.broadcast_signal(KernelSignal::new(signum));
}
},
work_queue::WorkPriority::High,