Support pty packet mode

This commit is contained in:
jiangjianfeng 2025-12-08 06:36:29 +00:00 committed by Tate, Hongliang Tian
parent b552bdbc51
commit 3235175fcf
10 changed files with 242 additions and 46 deletions

View File

@ -5,7 +5,13 @@ use ostd::sync::SpinLock;
use super::file::PtySlaveFile;
use crate::{
device::tty::{Tty, TtyDriver, TtyFlags},
device::{
pty::packet::{PacketCtrl, PacketStatus},
tty::{
termio::{CCtrlCharId, CInputFlags, CLocalFlags, CTermios},
Tty, TtyDriver, TtyFlags,
},
},
events::IoEvents,
fs::inode_handle::FileIo,
prelude::*,
@ -25,6 +31,7 @@ pub struct PtyDriver {
pollee: Pollee,
opened_slaves: SpinLock<usize>,
tty_flags: TtyFlags,
packet_ctrl: PacketCtrl,
}
/// A pseudoterminal slave.
@ -39,6 +46,7 @@ impl PtyDriver {
pollee: Pollee::new(),
opened_slaves: SpinLock::new(0),
tty_flags,
packet_ctrl: PacketCtrl::new(),
}
}
@ -47,6 +55,27 @@ impl PtyDriver {
return Ok(0);
}
// Reference: <https://elixir.bootlin.com/linux/v6.17/source/drivers/tty/n_tty.c#L2245>.
match self.packet_ctrl.take_status() {
None => {
// Packet mode is disabled.
self.read_output(buf)
}
Some(packet_status) if !packet_status.is_empty() => {
// Some packet status is pending.
buf[0] = packet_status.bits();
Ok(1)
}
Some(_) => {
// There's no pending packet status.
let data_len = self.read_output(&mut buf[1..])?;
buf[0] = 0;
Ok(data_len + 1)
}
}
}
fn read_output(&self, buf: &mut [u8]) -> Result<usize> {
let mut output = self.output.lock();
if output.is_empty() {
if self.tty_flags.is_other_closed() {
@ -57,7 +86,6 @@ impl PtyDriver {
let read_len = output.len().min(buf.len());
output.pop_slice(&mut buf[..read_len]).unwrap();
Ok(read_len)
}
@ -76,6 +104,10 @@ impl PtyDriver {
pub(super) fn tty_flags(&self) -> &TtyFlags {
&self.tty_flags
}
pub(super) fn packet_ctrl(&self) -> &PacketCtrl {
&self.packet_ctrl
}
}
impl TtyDriver for PtyDriver {
@ -110,7 +142,7 @@ impl TtyDriver for PtyDriver {
len += 1;
}
self.pollee.notify(IoEvents::IN);
self.pollee.notify(IoEvents::IN | IoEvents::RDNORM);
Ok(len)
}
@ -129,7 +161,7 @@ impl TtyDriver for PtyDriver {
}
if !has_notified {
self.pollee.notify(IoEvents::IN);
self.pollee.notify(IoEvents::IN | IoEvents::RDNORM);
has_notified = true;
}
}
@ -147,4 +179,40 @@ impl TtyDriver for PtyDriver {
fn console(&self) -> Option<&dyn AnyConsoleDevice> {
None
}
fn on_termios_change(&self, old_termios: &CTermios, new_termios: &CTermios) {
// Reference: <https://elixir.bootlin.com/linux/v6.17/source/drivers/tty/pty.c#L246>.
let extproc = old_termios.local_flags().contains(CLocalFlags::EXTPROC)
|| new_termios.local_flags().contains(CLocalFlags::EXTPROC);
let old_flow = old_termios.input_flags().contains(CInputFlags::IXON)
&& old_termios.special_char(CCtrlCharId::VSTOP) == 0o23
&& old_termios.special_char(CCtrlCharId::VSTART) == 0o21;
let new_flow = new_termios.input_flags().contains(CInputFlags::IXON)
&& new_termios.special_char(CCtrlCharId::VSTOP) == 0o23
&& new_termios.special_char(CCtrlCharId::VSTART) == 0o21;
if (old_flow == new_flow) && !extproc {
return;
}
let has_set = self.packet_ctrl.set_status(|packet_status| {
if old_flow != new_flow {
*packet_status &= !(PacketStatus::DOSTOP | PacketStatus::NOSTOP);
if new_flow {
*packet_status |= PacketStatus::DOSTOP;
} else {
*packet_status |= PacketStatus::NOSTOP;
}
}
if extproc {
*packet_status |= PacketStatus::IOCTL;
}
});
if has_set {
self.pollee
.notify(IoEvents::PRI | IoEvents::IN | IoEvents::RDNORM);
}
}
}

View File

@ -63,7 +63,7 @@ impl PtyMaster {
let mut events = IoEvents::empty();
if self.slave().driver().buffer_len() > 0 {
events |= IoEvents::IN;
events |= IoEvents::IN | IoEvents::RDNORM;
}
if self.slave().can_push() {
@ -74,6 +74,12 @@ impl PtyMaster {
events |= IoEvents::HUP;
}
// Deal with packet mode.
let packet_ctrl = self.slave.driver().packet_ctrl();
if packet_ctrl.has_status() {
events |= IoEvents::PRI | IoEvents::IN | IoEvents::RDNORM;
}
events
}
}
@ -144,6 +150,9 @@ mod ioctl_defs {
pub(super) type GetPtyLock = ioc!(TIOCGPTLCK, b'T', 0x39, OutData<i32>);
pub(super) type OpenPtySlave = ioc!(TIOCGPTPEER, b'T', 0x41, NoData);
pub(super) type SetPktMode = ioc!(TIOCPKT, 0x5420, InData<i32>);
pub(super) type GetPktMode = ioc!(TIOCGPKT, b'T', 0x38, OutData<i32>);
}
impl FileIo for PtyMaster {
@ -223,6 +232,20 @@ impl FileIo for PtyMaster {
cmd.write(&len)?;
}
cmd @ SetPktMode => {
let new_mode = cmd.read()? != 0;
self.slave.driver().packet_ctrl().set_mode(new_mode);
}
cmd @ GetPktMode => {
let packet_mode = if self.slave.driver().packet_ctrl().mode() {
1
} else {
0
};
cmd.write(&packet_mode)?;
}
_ => (self.slave.clone() as Arc<dyn Terminal>).job_ioctl(raw_ioctl, true)?,
});

View File

@ -13,6 +13,7 @@ use crate::{
mod driver;
mod file;
mod master;
mod packet;
pub use driver::PtySlave;
pub use master::PtyMaster;

View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: MPL-2.0
use core::sync::atomic::{AtomicBool, Ordering};
use crate::prelude::*;
/// Control states related to packet mode.
///
/// Reference: <https://man7.org/linux/man-pages/man2/TIOCPKT.2const.html>.
pub(super) struct PacketCtrl {
mode: AtomicBool,
status: SpinLock<PacketStatus>,
}
impl PacketCtrl {
// Creates a new `PacketCtrl`.
pub(super) fn new() -> Self {
Self {
mode: AtomicBool::new(false),
status: SpinLock::new(PacketStatus::empty()),
}
}
/// Returns whether packet mode is enabled.
pub(super) fn mode(&self) -> bool {
self.mode.load(Ordering::Relaxed)
}
/// Sets whether packet mode is enabled.
pub(super) fn set_mode(&self, mode: bool) {
// Reference: <https://elixir.bootlin.com/linux/v6.17/source/drivers/tty/pty.c#L158>.
let mut status = self.status.lock();
let old_mode = self.mode.load(Ordering::Relaxed);
self.mode.store(mode, Ordering::Relaxed);
if mode && !old_mode {
*status = PacketStatus::empty();
}
}
/// Sets packet status if packet mode is enabled.
pub(super) fn set_status(&self, set_packet_status: impl FnOnce(&mut PacketStatus)) -> bool {
// Fast path: Packet mode is disabled.
if !self.mode.load(Ordering::Relaxed) {
return false;
}
// Packet mode is enabled.
let mut packet_status = self.status.lock();
set_packet_status(&mut packet_status);
true
}
/// Checks if packet mode is enabled and there is pending packet status.
pub(super) fn has_status(&self) -> bool {
// Fast path: Packet mode is disabled.
if !self.mode.load(Ordering::Relaxed) {
return false;
}
// Packet mode is enabled.
!self.status.lock().is_empty()
}
/// Takes out packet status if packet mode is enabled.
pub(super) fn take_status(&self) -> Option<PacketStatus> {
// Fast path: Packed mode is disabled.
if !self.mode.load(Ordering::Relaxed) {
return None;
}
// Packet mode is enabled.
let mut status = self.status.lock();
let old_status = *status;
*status = PacketStatus::empty();
Some(old_status)
}
}
bitflags! {
/// Reference: <https://elixir.bootlin.com/linux/v6.18/source/include/uapi/asm-generic/ioctls.h#L110>.
pub(super) struct PacketStatus: u8 {
const DATA = 0;
const FLUSHREAD = 1 << 0;
const FLUSHWRITE = 1 << 1;
const STOP = 1 << 2;
const START = 1 << 3;
const NOSTOP = 1 << 4;
const DOSTOP = 1 << 5;
const IOCTL = 1 << 6;
}
}

View File

@ -2,7 +2,11 @@
use aster_console::AnyConsoleDevice;
use crate::{device::tty::Tty, fs::inode_handle::FileIo, prelude::*};
use crate::{
device::tty::{termio::CTermios, Tty},
fs::inode_handle::FileIo,
prelude::*,
};
/// A TTY driver.
///
@ -58,4 +62,9 @@ pub trait TtyDriver: Send + Sync + 'static {
///
/// If the TTY is not associated with any console device, this method will return `None`.
fn console(&self) -> Option<&dyn AnyConsoleDevice>;
/// Notifies that the TTY termios is changed.
///
/// This method will be called with a spin lock held, so it cannot break atomic mode.
fn on_termios_change(&self, old_termios: &CTermios, new_termios: &CTermios);
}

View File

@ -4,6 +4,7 @@ use ostd::const_assert;
use super::termio::{CCtrlCharId, CTermios, CWinSize};
use crate::{
device::tty::termio::{CInputFlags, CLocalFlags},
prelude::*,
process::signal::{
constants::{SIGINT, SIGQUIT},
@ -98,7 +99,7 @@ impl LineDiscipline {
mut signal_callback: F1,
echo_callback: F2,
) -> Result<()> {
let ch = if self.termios.contains_icrnl() && ch == b'\r' {
let ch = if self.termios.input_flags().contains(CInputFlags::ICRNL) && ch == b'\r' {
b'\n'
} else {
ch
@ -111,7 +112,7 @@ impl LineDiscipline {
// Typically, a TTY in raw mode does not echo. But the TTY can also be in a CBREAK mode,
// with ICANON closed and ECHO opened.
if self.termios.contain_echo() {
if self.termios.local_flags().contains(CLocalFlags::ECHO) {
self.output_char(ch, echo_callback);
}
@ -166,7 +167,7 @@ impl LineDiscipline {
echo_callback(b"\x08");
}
ch if is_printable_char(ch) => echo_callback(&[ch]),
ch if is_ctrl_char(ch) && self.termios.contains_echo_ctl() => {
ch if is_ctrl_char(ch) && self.termios.local_flags().contains(CLocalFlags::ECHOCTL) => {
echo_callback(&[b'^', ctrl_char_to_printable(ch)]);
}
_ => {}
@ -269,7 +270,9 @@ fn is_line_terminator(ch: u8, termios: &CTermios) -> bool {
return true;
}
if termios.contains_iexten() && ch == termios.special_char(CCtrlCharId::VEOL2) {
if termios.local_flags().contains(CLocalFlags::IEXTEN)
&& ch == termios.special_char(CCtrlCharId::VEOL2)
{
return true;
}
@ -293,7 +296,7 @@ fn is_ctrl_char(ch: u8) -> bool {
}
fn char_to_signal(ch: u8, termios: &CTermios) -> Option<SigNum> {
if !termios.is_canonical_mode() || !termios.contains_isig() {
if !termios.is_canonical_mode() || !termios.local_flags().contains(CLocalFlags::ISIG) {
return None;
}

View File

@ -31,7 +31,7 @@ mod flags;
pub(super) mod ioctl_defs;
mod line_discipline;
mod n_tty;
mod termio;
pub(super) mod termio;
pub use device::SystemConsole;
pub use driver::TtyDriver;
@ -152,7 +152,7 @@ impl<D: TtyDriver> Tty<D> {
}
}
self.pollee.notify(IoEvents::IN);
self.pollee.notify(IoEvents::IN | IoEvents::RDNORM);
Ok(len)
}
@ -160,7 +160,7 @@ impl<D: TtyDriver> Tty<D> {
let mut events = IoEvents::empty();
if self.ldisc.lock().buffer_len() > 0 {
events |= IoEvents::IN;
events |= IoEvents::IN | IoEvents::RDNORM;
}
if self.driver.can_push() {
@ -315,12 +315,17 @@ impl<D: TtyDriver> Tty<D> {
cmd @ SetTermios => {
let termios = cmd.read()?;
self.ldisc.lock().set_termios(termios);
let mut ldisc = self.ldisc.lock();
let old_termios = ldisc.termios();
self.driver().on_termios_change(old_termios, &termios);
ldisc.set_termios(termios);
}
cmd @ SetTermiosDrain => {
let termios = cmd.read()?;
let mut ldisc = self.ldisc.lock();
let old_termios = ldisc.termios();
self.driver().on_termios_change(old_termios, &termios);
ldisc.set_termios(termios);
self.driver.drain_output();
}
@ -328,6 +333,8 @@ impl<D: TtyDriver> Tty<D> {
let termios = cmd.read()?;
let mut ldisc = self.ldisc.lock();
let old_termios = ldisc.termios();
self.driver().on_termios_change(old_termios, &termios);
ldisc.set_termios(termios);
ldisc.drain_input();
self.driver.drain_output();

View File

@ -10,7 +10,7 @@ use spin::Once;
use super::{Tty, TtyDriver};
use crate::{
device::registry::char,
device::{registry::char, tty::termio::CTermios},
events::IoEvents,
fs::{
inode_handle::FileIo,
@ -61,6 +61,8 @@ impl TtyDriver for VtDriver {
fn console(&self) -> Option<&dyn AnyConsoleDevice> {
Some(&*self.console)
}
fn on_termios_change(&self, _old_termios: &CTermios, _new_termios: &CTermios) {}
}
/// The driver for hypervisor console devices.
@ -101,6 +103,8 @@ impl TtyDriver for HvcDriver {
fn console(&self) -> Option<&dyn AnyConsoleDevice> {
Some(&*self.console)
}
fn on_termios_change(&self, _old_termios: &CTermios, _new_termios: &CTermios) {}
}
struct TtyFile<D>(Arc<Tty<D>>);

View File

@ -11,7 +11,7 @@ bitflags! {
/// The input flags; `c_iflags` bits in Linux.
#[derive(Pod)]
#[repr(C)]
pub(super) struct CInputFlags: u32 {
pub struct CInputFlags: u32 {
// https://elixir.bootlin.com/linux/v6.0.9/source/include/uapi/asm-generic/termbits-common.h
const IGNBRK = 0x001; /* Ignore break condition */
const BRKINT = 0x002; /* Signal interrupt on break */
@ -136,7 +136,7 @@ bitflags! {
/// The local flags; `c_lflags` bits in Linux.
#[repr(C)]
#[derive(Pod)]
pub(super) struct CLocalFlags: u32 {
pub struct CLocalFlags: u32 {
// https://elixir.bootlin.com/linux/v6.0.9/source/include/uapi/asm-generic/termbits.h#L127
const ISIG = 0x00001;
const ICANON = 0x00002;
@ -174,7 +174,7 @@ impl Default for CLocalFlags {
#[repr(u32)]
#[derive(Debug, Clone, Copy, TryFromInt)]
#[expect(clippy::upper_case_acronyms)]
pub(super) enum CCtrlCharId {
pub enum CCtrlCharId {
// https://elixir.bootlin.com/linux/v6.0.9/source/include/uapi/asm-generic/termbits.h#L42
VINTR = 0,
VQUIT = 1,
@ -276,7 +276,7 @@ impl CTermios {
/// Reference: <https://elixir.bootlin.com/linux/v6.0.9/source/include/uapi/asm-generic/termbits.h#L9>.
const NUM_CTRL_CHARS: usize = 19;
pub(super) fn special_char(&self, id: CCtrlCharId) -> CCtrlChar {
pub fn special_char(&self, id: CCtrlCharId) -> CCtrlChar {
self.c_cc[id as usize]
}
@ -292,27 +292,14 @@ impl CTermios {
self.c_lflags.contains(CLocalFlags::ICANON)
}
/// Returns whether the input flags contain `ICRNL`.
///
/// The `ICRNL` flag means the `\r` characters in the input should be mapped to `\n`.
pub(super) fn contains_icrnl(&self) -> bool {
self.c_iflags.contains(CInputFlags::ICRNL)
/// Returns the input flags.
pub fn input_flags(&self) -> &CInputFlags {
&self.c_iflags
}
pub(super) fn contains_isig(&self) -> bool {
self.c_lflags.contains(CLocalFlags::ISIG)
}
pub(super) fn contain_echo(&self) -> bool {
self.c_lflags.contains(CLocalFlags::ECHO)
}
pub(super) fn contains_echo_ctl(&self) -> bool {
self.c_lflags.contains(CLocalFlags::ECHOCTL)
}
pub(super) fn contains_iexten(&self) -> bool {
self.c_lflags.contains(CLocalFlags::IEXTEN)
/// Returns the local flags.
pub fn local_flags(&self) -> &CLocalFlags {
&self.c_lflags
}
}

View File

@ -5,13 +5,14 @@ use crate::prelude::*;
bitflags! {
pub struct IoEvents: u32 {
const IN = 0x0001;
const PRI = 0x0002;
const OUT = 0x0004;
const ERR = 0x0008;
const HUP = 0x0010;
const NVAL = 0x0020;
const RDHUP = 0x2000;
const IN = 0x0001;
const PRI = 0x0002;
const OUT = 0x0004;
const ERR = 0x0008;
const HUP = 0x0010;
const NVAL = 0x0020;
const RDNORM = 0x0040;
const RDHUP = 0x2000;
/// Events that are always polled even without specifying them.
const ALWAYS_POLL = Self::ERR.bits | Self::HUP.bits;
}