//! Terminals ("ttys"): the console, /dev/hvc0, and pseudo-terminals. //! //! **Why terminals are weird.** A 1970s terminal was a keyboard and a //! screen on a serial line, and the kernel sat between that line and the //! programs. Typing went through a "line discipline" that let you fix //! typos *before* a program saw the line, echoed what you typed, and //! turned a few keys into signals (Ctrl+C = stop that program). All of //! that survives: every terminal -- the console, the virtio console, a //! terminal window -- is a `Tty` running the same line discipline, //! configured with a `termios` struct (TCGETS/TCSETS ioctls; libc's //! tcgetattr/tcsetattr): //! //! * **canonical** mode (ICANON, the default): typed bytes collect in a //! line buffer you can edit (Erase, ^U kills the line, ^D = end of //! file); `read` only sees whole lines. **Raw**/non-canonical mode: //! every byte is readable at once (editors, our shell's line editor). //! * ECHO: the tty prints what you type (passwords turn it off). //! * ISIG: ^C, ^\ and ^Z send SIGINT, SIGQUIT and SIGTSTP to the //! terminal's *foreground process group* -- the job the shell put in //! front (TIOCSPGRP). That's why ^C kills `cat` but not the shell. //! * Output: ONLCR turns "\n" into "\r\n" (a real terminal needs the //! carriage return to go back to column 0). //! //! **Sessions.** `setsid()` starts a session (a login, roughly); its first //! terminal opened becomes the session's *controlling terminal*, which //! `/dev/tty` always refers to, and whose ^C goes to the session's //! foreground group. When the session leader exits, the terminal is let //! go and the foreground job gets SIGHUP ("hang up", from modems). //! //! **Pseudo-terminals** (ptys) are terminals with a program instead of a //! keyboard and screen on the other side: opening /dev/ptmx gives the //! *master* end and creates /dev/pts/N, the *slave* end. Bytes written to //! the master are "typed" into the slave (through the line discipline); //! what programs write to the slave comes out of the master. Terminal //! emulators, ssh and tmux are all programs holding a pty master. use crate::errno::Errno; use crate::fs::file::{File, KIND_CONSOLE, KIND_DEVICE, Stat}; use crate::poll::{self, POLLHUP, POLLIN, POLLOUT}; use crate::process::Process; use crate::signal; use crate::thread::WaitQueue; use crate::{print, serial_print, vga_buffer}; use alloc::{collections::VecDeque, format, string::String, sync::Arc, vec::Vec}; use conquer_once::spin::OnceCell; use core::sync::atomic::{AtomicBool, Ordering}; use pc_keyboard::KeyCode; use spin::Mutex; use x86_64::instructions::interrupts::without_interrupts; // --- termios (Linux's x86_64 layout and values) ----------------------------- /// `struct termios`: 36 bytes. #[repr(C)] #[derive(Clone, Copy)] pub struct Termios { pub iflag: u32, pub oflag: u32, pub cflag: u32, pub lflag: u32, pub line: u8, pub cc: [u8; 19], } // iflag const INLCR: u32 = 0x40; // input '\n' -> '\r' const IGNCR: u32 = 0x80; // drop input '\r' const ICRNL: u32 = 0x100; // input '\r' -> '\n' (Enter sends '\r') const IXON: u32 = 0x400; // oflag const OPOST: u32 = 0x1; // process output at all const ONLCR: u32 = 0x4; // output '\n' -> "\r\n" // lflag const ISIG: u32 = 0x1; const ICANON: u32 = 0x2; const ECHO: u32 = 0x8; const ECHOE: u32 = 0x10; const ECHOK: u32 = 0x20; const NOFLSH: u32 = 0x80; const ECHOCTL: u32 = 0x200; const ECHOKE: u32 = 0x800; const IEXTEN: u32 = 0x8000; // c_cc indexes const VINTR: usize = 0; const VQUIT: usize = 1; const VERASE: usize = 2; const VKILL: usize = 3; const VEOF: usize = 4; const VTIME: usize = 5; const VMIN: usize = 6; const VSUSP: usize = 10; const VEOL: usize = 11; impl Termios { /// What a fresh Linux terminal starts with (`stty sane`). pub const fn sane() -> Termios { let mut cc = [0u8; 19]; cc[VINTR] = 3; // ^C cc[VQUIT] = 0x1c; // ^\ cc[VERASE] = 0x7f; // DEL (we also take ^H, what our keyboard sends) cc[VKILL] = 0x15; // ^U cc[VEOF] = 4; // ^D cc[VMIN] = 1; cc[8] = 0x11; // VSTART ^Q cc[9] = 0x13; // VSTOP ^S cc[VSUSP] = 0x1a; // ^Z cc[12] = 0x12; // VREPRINT ^R cc[14] = 0x17; // VWERASE ^W cc[15] = 0x16; // VLNEXT ^V Termios { iflag: ICRNL | IXON, oflag: OPOST | ONLCR, cflag: 0xbf, // B38400 | CS8 | CREAD | HUPCL lflag: ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN, line: 0, cc, } } fn bytes(&self) -> [u8; 36] { unsafe { core::mem::transmute(*self) } } fn from_bytes(b: &[u8]) -> Termios { unsafe { core::ptr::read_unaligned(b.as_ptr() as *const Termios) } } } // ioctl numbers pub const TCGETS: u64 = 0x5401; pub const TCSETS: u64 = 0x5402; // now pub const TCSETSW: u64 = 0x5403; // after output drains (ours never queues) pub const TCSETSF: u64 = 0x5404; // ...and throw away unread input pub const TIOCSCTTY: u64 = 0x540E; pub const TIOCGPGRP: u64 = 0x540F; pub const TIOCSPGRP: u64 = 0x5410; pub const TIOCGWINSZ: u64 = 0x5413; pub const TIOCSWINSZ: u64 = 0x5414; pub const FIONREAD: u64 = 0x541B; pub const FIONBIO: u64 = 0x5421; pub const TIOCNOTTY: u64 = 0x5422; pub const TIOCGSID: u64 = 0x5429; pub const TIOCGPTN: u64 = 0x8004_5430; pub const TIOCSPTLCK: u64 = 0x4004_5431; /// Ours (from stage 6): raw mode on/off in one call. Undone automatically /// if the process that switched it on exits. Returns the old setting. pub const TTY_SET_RAW: u64 = 1; /// How many input bytes a tty buffers (like Linux's N_TTY: 4 KiB). const INPUT_MAX: usize = 4096; /// How much pty output can wait for the master to read it. const OUTPUT_MAX: usize = 64 * 1024; // --- the tty ---------------------------------------------------------------- /// Where a tty's output goes / its input comes from. #[derive(Clone, Copy, PartialEq)] enum Driver { Console, // keyboard in; VGA/framebuffer + serial out Hvc, // virtio console (both ways) Pty, // the master end, through `output` } pub struct Tty { pub name: String, driver: Driver, /// Pty number (the N of /dev/pts/N). index: usize, /// Locked with interrupts off (touched in wait conditions). state: Mutex, readers: WaitQueue, writers: WaitQueue, } struct TtyState { termios: Termios, /// The canonical-mode line being edited (not yet readable). line: Vec, /// Readable input: finished lines (canonical) or bytes. An empty chunk /// is an end of file (^D on an empty line): `read` returns 0 once. input: VecDeque>, winsize: [u16; 4], // rows, cols, x pixels, y pixels /// Session it's the controlling terminal of (0 = none). session: u64, /// Foreground process group (gets ^C). pgrp: u64, /// TTY_SET_RAW: who switched raw on, and the settings before. raw_owner: Option<(u64, Termios)>, // --- ptys only --- output: VecDeque, // slave output, for the master to read master_open: bool, slaves_open: usize, slave_opened: bool, // (master reads EIO only once a slave came and went) locked: bool, // TIOCSPTLCK: slave can't be opened yet } /// What the line discipline decided while holding the lock; done after. #[derive(Default)] struct Effects { echo: Vec, signal: Option, } impl Tty { fn new(name: String, driver: Driver, index: usize) -> Arc { let (cols, rows) = vga_buffer::size(); Arc::new(Tty { name, driver, index, state: Mutex::new(TtyState { termios: Termios::sane(), line: Vec::new(), input: VecDeque::new(), winsize: if driver == Driver::Console { [rows as u16, cols as u16, 0, 0] } else { [24, 80, 0, 0] }, session: 0, pgrp: 0, raw_owner: None, output: VecDeque::new(), master_open: true, slaves_open: 0, slave_opened: false, locked: driver == Driver::Pty, }), readers: WaitQueue::new(), writers: WaitQueue::new(), }) } fn lock(&self) -> spin::MutexGuard<'_, TtyState> { self.state.lock() } /// Foreground process group. pub fn foreground(&self) -> u64 { without_interrupts(|| self.lock().pgrp) } pub fn set_foreground(&self, pgid: u64) { without_interrupts(|| self.lock().pgrp = pgid); } /// Session it's the controlling terminal of (0 = none). pub fn session(&self) -> u64 { without_interrupts(|| self.lock().session) } // --- input: the line discipline ----------------------------------------- /// Bytes arriving from the keyboard / the host / the pty master. pub fn input(&self, bytes: &[u8]) { let (fx, pgrp) = without_interrupts(|| { let mut s = self.lock(); let mut fx = Effects::default(); for &b in bytes { s.input_byte(b, &mut fx, self.driver); } (fx, s.pgrp) }); if !fx.echo.is_empty() { let _ = self.output(&fx.echo, false); // (echo into a full pty: dropped) } if let (Some(sig), true) = (fx.signal, pgrp != 0) { crate::process::signal_group(pgrp, sig); } self.readers.wake_all(); poll::notify(); } // --- output ------------------------------------------------------------- /// Send bytes towards the screen/host/master, with output processing /// (ONLCR). `block`: a program's write waits for room in a pty; /// echo doesn't (it's dropped instead). fn output(&self, bytes: &[u8], block: bool) -> Result { let termios = without_interrupts(|| self.lock().termios); let asked = bytes.len(); // what we report: the caller's count, not the cooked one let cooked: Vec; let onlcr = self.driver != Driver::Console && termios.oflag & (OPOST | ONLCR) == OPOST | ONLCR && bytes.contains(&b'\n'); let bytes = if onlcr { let mut v = Vec::with_capacity(bytes.len() + 16); for &b in bytes { if b == b'\n' { v.push(b'\r'); } v.push(b); } cooked = v; &cooked[..] } else { bytes }; match self.driver { Driver::Console => console_output(bytes), Driver::Hvc => { crate::virtio::console::write(bytes); } Driver::Pty => { let mut done = 0; let r = self.writers.wait_until_intr(|| { let mut s = self.lock(); if !s.master_open { return Some(Err(Errno::EIO)); // hung up } let n = (OUTPUT_MAX - s.output.len()).min(bytes.len() - done); s.output.extend(&bytes[done..done + n]); done += n; if n > 0 { self.readers.wake_all(); // (the master's readers) poll::notify(); } (done == bytes.len() || !block).then_some(Ok(())) }); match r { Ok(Err(e)) => return Err(e), Err(e) if done == 0 => return Err(e), _ => {} } if done < bytes.len() { // Only part of it fit (nonblocking, or a signal). Report // how many of the *caller's* bytes went out: every '\n' // sent cost two cooked bytes ("\r\n"), and a lone '\r' // added in front of an unsent '\n' doesn't count. if done == 0 { return Err(Errno::EAGAIN); } let mut sent = done; if onlcr { sent -= bytes[..done].iter().filter(|&&b| b == b'\n').count(); if bytes[done] == b'\n' { sent -= 1; } } return Ok(sent); } } } Ok(asked) } // --- the slave/terminal side, as a file --------------------------------- fn read(&self, buf: &mut [u8], nonblocking: bool) -> Result { if buf.is_empty() { return Ok(0); } let (vmin, vtime, canon) = without_interrupts(|| { let t = self.lock().termios; (t.cc[VMIN], t.cc[VTIME], t.lflag & ICANON != 0) }); // Non-canonical with VMIN = 0: don't wait (VTIME = 0) or wait at // most VTIME tenths of a second for something. let deadline = match (canon, vmin, vtime) { (false, 0, 0) => Some(0), (false, 0, t) => Some(crate::time::deadline_after_us(t as u64 * 100_000)), _ => None, }; let mut take = || { let mut s = self.lock(); if self.driver == Driver::Pty && !s.master_open && s.input.is_empty() { return Some(Ok(0)); // hung up: end of file } match s.take_input(buf, canon) { Some(n) => Some(Ok(n)), None if nonblocking => Some(Err(Errno::EAGAIN)), None => None, } }; let r = match deadline { None => self.readers.wait_until_intr(take)?, Some(0) => without_interrupts(|| take()).unwrap_or(Ok(0)), Some(d) => self.readers.wait_until_deadline_intr(d, take)?.unwrap_or(Ok(0)), }; poll::notify(); // (room in the input buffer) r } fn poll_slave(&self) -> u16 { without_interrupts(|| { let s = self.lock(); let mut events = if s.input.is_empty() { 0 } else { POLLIN }; if self.driver == Driver::Pty { if !s.master_open { events |= POLLHUP | POLLIN; } else if s.output.len() < OUTPUT_MAX { events |= POLLOUT; } } else { events |= POLLOUT; } events }) } // --- the pty master side ------------------------------------------------ fn master_read(&self, buf: &mut [u8], nonblocking: bool) -> Result { if buf.is_empty() { return Ok(0); } let r = self.readers.wait_until_intr(|| { let mut s = self.lock(); if s.output.is_empty() { return match () { _ if s.slave_opened && s.slaves_open == 0 => Some(Err(Errno::EIO)), // Linux: EIO _ if nonblocking => Some(Err(Errno::EAGAIN)), _ => None, }; } let n = buf.len().min(s.output.len()); for (dst, src) in buf.iter_mut().zip(s.output.drain(..n)) { *dst = src; } Some(Ok(n)) })?; self.writers.wake_all(); poll::notify(); r } fn poll_master(&self) -> u16 { without_interrupts(|| { let s = self.lock(); let mut events = POLLOUT; if !s.output.is_empty() { events |= POLLIN; } if s.slave_opened && s.slaves_open == 0 { events |= POLLHUP; } events }) } // --- ioctls --------------------------------------------------------------- fn ioctl(&self, file: &TtyFile, cmd: u64, arg: u64) -> Result { use crate::syscall::{copy_to_user as copy_out, user_slice}; let p = crate::thread::current_process().ok_or(Errno::EINVAL)?; let read_u32 = || -> Result { Ok(u32::from_le_bytes(user_slice(arg, 4)?.try_into().unwrap())) }; match cmd { TCGETS => copy_out(arg, &without_interrupts(|| self.lock().termios).bytes()).map(|_| 0), TCSETS | TCSETSW | TCSETSF => { let t = Termios::from_bytes(user_slice(arg, 36)?); without_interrupts(|| self.lock().set_termios(t, cmd == TCSETSF)); self.readers.wake_all(); Ok(0) } TTY_SET_RAW => { let was_raw = without_interrupts(|| { let mut s = self.lock(); let was_raw = s.termios.lflag & ICANON == 0; if arg != 0 && !was_raw { let old = s.termios; s.raw_owner = Some((p.pid, old)); let mut raw = old; raw.lflag &= !(ICANON | ECHO | ISIG | IEXTEN); raw.iflag &= !(ICRNL | INLCR | IGNCR | IXON); raw.cc[VMIN] = 1; raw.cc[VTIME] = 0; s.set_termios(raw, false); } else if arg == 0 && was_raw { let old = s.raw_owner.take().map_or(Termios::sane(), |(_, t)| t); s.set_termios(old, false); } was_raw }); self.readers.wake_all(); Ok(was_raw as u64) } TIOCGWINSZ => copy_out(arg, &without_interrupts(|| self.lock().winsize)).map(|_| 0), TIOCSWINSZ => { let b = user_slice(arg, 8)?; let ws = [0, 2, 4, 6].map(|i| u16::from_le_bytes([b[i], b[i + 1]])); let pgrp = without_interrupts(|| { let mut s = self.lock(); s.winsize = ws; s.pgrp }); if pgrp != 0 { crate::process::signal_group(pgrp, signal::SIGWINCH); } Ok(0) } FIONREAD => { let n: usize = without_interrupts(|| { let s = self.lock(); if file.master { s.output.len() } else { s.input.iter().map(|c| c.len()).sum() } }); copy_out(arg, &(n as u32)).map(|_| 0) } FIONBIO => { file.set_nonblocking(read_u32()? != 0); Ok(0) } TIOCGPTN if file.master => copy_out(arg, &(self.index as u32)).map(|_| 0), TIOCSPTLCK if file.master => { let lock = read_u32()? != 0; without_interrupts(|| self.lock().locked = lock); Ok(0) } TIOCGPGRP => copy_out(arg, &(self.foreground() as u32)).map(|_| 0), TIOCSPGRP => { let pgid = read_u32()? as u64; let session = self.session(); if session != 0 && session != p.sid() { return Err(Errno::ENOTTY); // not our terminal } if crate::process::group_members(pgid).is_empty() { return Err(Errno::ESRCH); } self.set_foreground(pgid); Ok(0) } TIOCGSID => match self.session() { 0 => Err(Errno::ENOTTY), sid => copy_out(arg, &(sid as u32)).map(|_| 0), }, TIOCSCTTY => attach(&p, self.self_arc()?, arg == 1).map(|_| 0), TIOCNOTTY => { let mine = p.ctty.lock().as_ref().is_some_and(|t| core::ptr::eq(&**t, self)); if !mine { return Err(Errno::ENOTTY); } if p.sid() == p.pid { hang_up(self); } *p.ctty.lock() = None; Ok(0) } _ => Err(Errno::ENOTTY), } } /// The Arc for `self` (from the registry). fn self_arc(&self) -> Result, Errno> { all_ttys().into_iter().find(|t| core::ptr::eq(&**t, self)).ok_or(Errno::ENOTTY) } } impl TtyState { fn set_termios(&mut self, t: Termios, flush: bool) { let was_canon = self.termios.lflag & ICANON != 0; self.termios = t; if flush { self.input.clear(); self.line.clear(); } // Leaving canonical mode: a half-edited line becomes readable. if was_canon && t.lflag & ICANON == 0 && !self.line.is_empty() { let line = core::mem::take(&mut self.line); self.input.push_back(line); } } fn buffered(&self) -> usize { self.line.len() + self.input.iter().map(|c| c.len()).sum::() } /// One input byte through the line discipline. fn input_byte(&mut self, mut b: u8, fx: &mut Effects, driver: Driver) { let t = self.termios; // Input translations: Enter sends '\r'; programs want '\n'. if b == b'\r' { if t.iflag & IGNCR != 0 { return; } if t.iflag & ICRNL != 0 { b = b'\n'; } } else if b == b'\n' && t.iflag & INLCR != 0 { b = b'\r'; } // ^C ^\ ^Z: signals for the foreground job. if t.lflag & ISIG != 0 { let sig = match b { _ if b == t.cc[VINTR] && b != 0 => Some(signal::SIGINT), _ if b == t.cc[VQUIT] && b != 0 => Some(signal::SIGQUIT), _ if b == t.cc[VSUSP] && b != 0 => Some(signal::SIGTSTP), _ => None, }; if let Some(sig) = sig { if t.lflag & NOFLSH == 0 { self.line.clear(); // the half-typed line is thrown away self.input.clear(); } if t.lflag & ECHO != 0 { // "^C" -- plus a newline, so the next prompt starts // on a fresh line (Linux leaves that to the shell). fx.echo.extend_from_slice(&[b'^', b + 0x40, b'\n']); } fx.signal = Some(sig); return; } } if t.lflag & ICANON == 0 { // Raw: readable at once. if self.buffered() < INPUT_MAX { match self.input.back_mut() { Some(chunk) if !chunk.is_empty() => chunk.push(b), _ => self.input.push_back(alloc::vec![b]), } if t.lflag & ECHO != 0 { fx.echo.push(b); } } return; } // Canonical: edit the line. let echo = t.lflag & ECHO != 0; let erase: &[u8] = if driver == Driver::Console { b"\x08" } else { b"\x08 \x08" }; match b { _ if b == t.cc[VERASE] || b == 0x08 => { if self.line.pop().is_some() && echo && t.lflag & ECHOE != 0 { fx.echo.extend_from_slice(erase); } } _ if b == t.cc[VKILL] => { for _ in 0..self.line.len() { if echo && t.lflag & ECHOK != 0 { fx.echo.extend_from_slice(erase); } } self.line.clear(); } _ if b == t.cc[VEOF] => { // ^D: hand over the line so far (without the ^D); on an // empty line that's a 0-byte read = end of file. let line = core::mem::take(&mut self.line); self.input.push_back(line); } b'\n' => { if echo { fx.echo.push(b'\n'); } self.line.push(b'\n'); let line = core::mem::take(&mut self.line); self.input.push_back(line); } _ if b == t.cc[VEOL] && b != 0 => { self.line.push(b); let line = core::mem::take(&mut self.line); self.input.push_back(line); } _ if self.buffered() >= INPUT_MAX - 1 => {} // full (room kept for '\n') _ => { self.line.push(b); if echo { if b < 0x20 && b != b'\t' && t.lflag & ECHOCTL != 0 { fx.echo.extend_from_slice(&[b'^', b + 0x40]); // ^X } else { fx.echo.push(b); } } } } } /// Copy readable input to `buf`: canonical = at most one line (or an /// EOF = 0), raw = whatever is there. None = nothing to read. fn take_input(&mut self, buf: &mut [u8], canon: bool) -> Option { let mut n = 0; while n < buf.len() { let Some(chunk) = self.input.front_mut() else { break }; if chunk.is_empty() { // An end of file: returned on its own (as 0) if first. if n == 0 { self.input.pop_front(); return Some(0); } break; } let k = chunk.len().min(buf.len() - n); buf[n..n + k].copy_from_slice(&chunk[..k]); chunk.drain(..k); n += k; if chunk.is_empty() { self.input.pop_front(); if canon { break; // one line per read } } } (n > 0).then_some(n) } } /// Print to the screen and serial port (the console's output). '\x08' /// erases the character before the cursor and '\x0c' clears the screen. fn console_output(buf: &[u8]) { // One write = one unbroken piece of output, even when programs on // other CPUs write at the same time. Taken in thread context only, so // while waiting we just yield. let _turn = loop { match CONSOLE_WRITE.try_lock() { Some(guard) => break guard, None => crate::thread::yield_now(), } }; for chunk in buf.utf8_chunks() { for piece in chunk.valid().split_inclusive(['\x08', '\x0c']) { let (text, control) = match piece.strip_suffix(['\x08', '\x0c']) { Some(text) => (text, piece.chars().last()), None => (piece, None), }; print!("{}", text); serial_print!("{}", text); match control { Some('\x08') => { vga_buffer::backspace(); serial_print!("\x08"); } Some(_) => vga_buffer::clear_screen(), // serial: nothing to clear None => {} } } if !chunk.invalid().is_empty() { print!("?"); serial_print!("?"); } } } static CONSOLE_WRITE: Mutex<()> = Mutex::new(()); // --- the registry: console, hvc0, ptys -------------------------------------- static CONSOLE: OnceCell> = OnceCell::uninit(); static HVC: OnceCell> = OnceCell::uninit(); /// Ptys by number; None = free. static PTYS: Mutex>>> = Mutex::new(Vec::new()); /// Most ptys at once. pub const MAX_PTYS: usize = 64; /// The console tty (keyboard + screen). pub fn console() -> Arc { CONSOLE.get_or_init(|| Tty::new(String::from("console"), Driver::Console, 0)).clone() } /// The console as a file (fds 0/1/2 of programs started by the kernel). pub fn console_file() -> Arc { Arc::new(TtyFile::new(console(), false)) } /// /dev/hvc0's tty. Its input is fed by the `hvcd` kernel thread, started /// the first time hvc0 is opened (until then, tests can read the raw /// virtio console directly). pub fn hvc() -> Arc { HVC.get_or_init(|| { crate::thread::spawn("hvcd", hvc_input_thread); Tty::new(String::from("hvc0"), Driver::Hvc, 0) }) .clone() } fn hvc_input_thread() { let mut buf = [0u8; 256]; loop { if let Ok(n) = crate::virtio::console::read(&mut buf) { hvc().input(&buf[..n]); } } } fn all_ttys() -> Vec> { let mut v: Vec> = without_interrupts(|| PTYS.lock().iter().flatten().cloned().collect()); v.extend(CONSOLE.get().cloned()); v.extend(HVC.get().cloned()); v } /// Open /dev/ptmx: a new pty; returns the master end. pub fn open_ptmx() -> Result, Errno> { let tty = without_interrupts(|| { let mut ptys = PTYS.lock(); let index = match ptys.iter().position(|p| p.is_none()) { Some(i) => i, None if ptys.len() < MAX_PTYS => { ptys.push(None); ptys.len() - 1 } None => return Err(Errno::ENOSPC), }; let tty = Tty::new(format!("pts/{}", index), Driver::Pty, index); ptys[index] = Some(tty.clone()); Ok(tty) })?; Ok(Arc::new(TtyFile::new(tty, true))) } /// Pty numbers in use (the /dev/pts listing). pub fn pty_numbers() -> Vec { without_interrupts(|| PTYS.lock().iter().enumerate().filter(|(_, p)| p.is_some()).map(|(i, _)| i).collect()) } /// Open /dev/pts/N. pub fn open_pts(n: usize) -> Result, Errno> { let tty = without_interrupts(|| PTYS.lock().get(n).cloned().flatten()).ok_or(Errno::ENOENT)?; let ok = without_interrupts(|| { let s = tty.lock(); s.master_open && !s.locked }); if !ok { return Err(Errno::EIO); // locked (not unlockpt'ed yet) or hung up } Ok(Arc::new(TtyFile::new(tty, false))) } /// Open /dev/hvc0 as a terminal. pub fn open_hvc() -> Arc { Arc::new(TtyFile::new(hvc(), false)) } /// Open /dev/tty: the caller's controlling terminal. pub fn open_ctty() -> Result, Errno> { let p = crate::thread::current_process().ok_or(Errno::ENXIO)?; let tty = p.ctty.lock().clone().ok_or(Errno::ENXIO)?; Ok(Arc::new(TtyFile::new(tty, false))) } /// How many ptys exist (leak checks). pub fn pty_count() -> usize { pty_numbers().len() } // --- sessions and controlling terminals --------------------------------------- /// Make `tty` the controlling terminal of `p`'s session. Only a session /// leader without one may, and only if the tty is free (or `steal`). pub fn attach(p: &Process, tty: Arc, steal: bool) -> Result<(), Errno> { if p.sid() != p.pid || p.ctty.lock().is_some() { return Err(Errno::EPERM); } without_interrupts(|| { let mut s = tty.lock(); if s.session != 0 && s.session != p.sid() && !steal { return Err(Errno::EPERM); } s.session = p.sid(); s.pgrp = p.pgid(); Ok(()) })?; *p.ctty.lock() = Some(tty); Ok(()) } /// open() of a terminal without O_NOCTTY: a session leader without a /// controlling terminal gets this one (System V style, what Linux does). pub fn opened(p: &Process, file: &dyn File) { if let Some(tf) = file.as_tty() { if !tf.master && p.sid() == p.pid && p.ctty.lock().is_none() && tf.tty.session() == 0 { let _ = attach(p, tf.tty.clone(), false); } } } /// The session leader let go of `tty`: no session owns it any more, and /// its foreground job is told with SIGHUP (+ SIGCONT in case it's stopped). fn hang_up(tty: &Tty) { let pgrp = without_interrupts(|| { let mut s = tty.lock(); s.session = 0; core::mem::replace(&mut s.pgrp, 0) }); if pgrp != 0 { crate::process::signal_group(pgrp, signal::SIGHUP); crate::process::signal_group(pgrp, signal::SIGCONT); } } /// A process ended: undo its TTY_SET_RAW, and if it led the session of /// its controlling terminal, hang that up. pub fn process_exited(p: &Process) { for tty in all_ttys() { let restored = without_interrupts(|| { let mut s = tty.lock(); match s.raw_owner { Some((pid, old)) if pid == p.pid => { s.raw_owner = None; s.set_termios(old, false); true } _ => false, } }); if restored { tty.readers.wake_all(); } } if let Some(tty) = p.ctty.lock().take() { if tty.session() == p.pid { hang_up(&tty); } } } // --- the old console API (keyboard driver, tests) --------------------------- pub fn foreground() -> u64 { console().foreground() } pub fn set_foreground(pgid: u64) { console().set_foreground(pgid); } /// Called by the keyboard task for every typed character. pub fn input_char(c: char) { let mut utf8 = [0; 4]; console().input(c.encode_utf8(&mut utf8).as_bytes()); } /// Keys that aren't characters: the ANSI/VT100 sequences a Linux terminal /// sends for them. pub fn input_key(key: KeyCode) { let seq: &[u8] = match key { KeyCode::ArrowUp => b"\x1b[A", KeyCode::ArrowDown => b"\x1b[B", KeyCode::ArrowRight => b"\x1b[C", KeyCode::ArrowLeft => b"\x1b[D", KeyCode::Home => b"\x1b[H", KeyCode::End => b"\x1b[F", KeyCode::Delete => b"\x1b[3~", _ => return, }; console().input(seq); } // --- a terminal as a file ----------------------------------------------------- /// An open terminal: the console, hvc0, a pty slave -- or a pty master. pub struct TtyFile { pub tty: Arc, pub master: bool, nonblocking: AtomicBool, } impl TtyFile { fn new(tty: Arc, master: bool) -> TtyFile { without_interrupts(|| { let mut s = tty.lock(); if master { s.master_open = true; } else if tty.driver == Driver::Pty { s.slaves_open += 1; s.slave_opened = true; } }); if tty.driver == Driver::Hvc && !master { crate::virtio::console::OPEN_COUNT.fetch_add(1, Ordering::Relaxed); } TtyFile { tty, master, nonblocking: AtomicBool::new(false) } } } impl File for TtyFile { fn read(&self, buf: &mut [u8]) -> Result { if self.master { self.tty.master_read(buf, self.nonblocking()) } else { self.tty.read(buf, self.nonblocking()) } } fn write(&self, buf: &[u8]) -> Result { if self.master { self.tty.input(buf); // "typed" into the slave Ok(buf.len()) } else { self.tty.output(buf, !self.nonblocking()) } } fn stat(&self) -> Result { // Terminals look like the console (that's what isatty checks). Ok(Stat { kind: if self.master { KIND_DEVICE } else { KIND_CONSOLE }, size: 0, mode: 0o620 }) } fn poll(&self) -> u16 { if self.master { self.tty.poll_master() } else { self.tty.poll_slave() } } fn set_nonblocking(&self, on: bool) { self.nonblocking.store(on, Ordering::Relaxed); } fn nonblocking(&self) -> bool { self.nonblocking.load(Ordering::Relaxed) } fn ioctl(&self, cmd: u64, arg: u64) -> Result { self.tty.ioctl(self, cmd, arg) } fn as_tty(&self) -> Option<&TtyFile> { Some(self) } fn proc_link(&self) -> Option { Some(if self.master { String::from("/dev/ptmx") } else { format!("/dev/{}", self.tty.name) }) } } impl Drop for TtyFile { fn drop(&mut self) { let tty = &self.tty; if tty.driver == Driver::Hvc && !self.master { crate::virtio::console::OPEN_COUNT.fetch_sub(1, Ordering::Relaxed); } if tty.driver != Driver::Pty { return; } let master_gone = without_interrupts(|| { let mut s = tty.lock(); if self.master { s.master_open = false; } else { s.slaves_open -= 1; } self.master }); if master_gone { // The terminal "hung up": its session hears SIGHUP, readers // get EOF, writers EIO; the number is free again. hang_up(tty); without_interrupts(|| PTYS.lock()[tty.index] = None); } tty.readers.wake_all(); tty.writers.wake_all(); poll::notify(); } }