forked from NotAShelf/beer
pty: run the shell and read its output
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ib472cd1bd66ffbba1725d4576eedffff6a6a6964
This commit is contained in:
parent
c68d3445e7
commit
740aefffa8
5 changed files with 165 additions and 1 deletions
|
|
@ -1,5 +1,6 @@
|
|||
//! beer, a fast, software-rendered, Wayland-native terminal emulator.
|
||||
|
||||
mod pty;
|
||||
mod wayland;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
|
|
|||
112
src/pty.rs
Normal file
112
src/pty.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Pseudo-terminal: open a master/slave pair and run the user's shell on it.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::os::fd::{AsFd, OwnedFd};
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, ExitStatus, Stdio};
|
||||
|
||||
use anyhow::Context;
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{Winsize, tcsetwinsize};
|
||||
|
||||
/// A running child process attached to a PTY master.
|
||||
#[derive(Debug)]
|
||||
pub struct Pty {
|
||||
master: OwnedFd,
|
||||
child: Child,
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
/// Open a PTY, size it to `cols`x`rows`, and exec the user's login shell on
|
||||
/// the slave end with `TERM=beer`.
|
||||
pub fn spawn(cols: u16, rows: u16) -> anyhow::Result<Self> {
|
||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
|
||||
.context("open pty master")?;
|
||||
grantpt(&master).context("grantpt")?;
|
||||
unlockpt(&master).context("unlockpt")?;
|
||||
let slave = ioctl_tiocgptpeer(&master, OpenptFlags::RDWR | OpenptFlags::NOCTTY)
|
||||
.context("open pty slave")?;
|
||||
|
||||
set_winsize(&master, cols, rows)?;
|
||||
|
||||
let shell = std::env::var_os("SHELL").unwrap_or_else(|| "/bin/sh".into());
|
||||
let argv0 = login_argv0(&shell);
|
||||
|
||||
// Hand the slave to the child's stdio. try_clone gives O_CLOEXEC dups, so
|
||||
// the parent's copies and the controlling-tty handle vanish at exec.
|
||||
let ctty = slave.try_clone().context("dup slave")?;
|
||||
let (stdin, stdout, stderr) = (
|
||||
slave.try_clone().context("dup slave")?,
|
||||
slave.try_clone().context("dup slave")?,
|
||||
slave,
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(&shell);
|
||||
cmd.arg0(&argv0)
|
||||
.env("TERM", "beer")
|
||||
.env_remove("COLUMNS")
|
||||
.env_remove("LINES")
|
||||
.env_remove("TERMCAP")
|
||||
.stdin(Stdio::from(stdin))
|
||||
.stdout(Stdio::from(stdout))
|
||||
.stderr(Stdio::from(stderr));
|
||||
|
||||
// SAFETY: setsid and the TIOCSCTTY ioctl are async-signal-safe raw
|
||||
// syscalls; ctty is a valid fd captured by move. We touch no parent
|
||||
// heap state, satisfying pre_exec's contract.
|
||||
unsafe {
|
||||
cmd.pre_exec(move || {
|
||||
rustix::process::setsid()?;
|
||||
rustix::process::ioctl_tiocsctty(&ctty)?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let child = cmd.spawn().context("spawn shell")?;
|
||||
Ok(Self { master, child })
|
||||
}
|
||||
|
||||
/// The PTY master, for reading child output and writing input.
|
||||
pub fn master(&self) -> &OwnedFd {
|
||||
&self.master
|
||||
}
|
||||
|
||||
/// Reap the child if it has exited.
|
||||
pub fn wait(&mut self) -> io::Result<ExitStatus> {
|
||||
self.child.wait()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_winsize(master: &OwnedFd, cols: u16, rows: u16) -> anyhow::Result<()> {
|
||||
let ws = Winsize {
|
||||
ws_row: rows,
|
||||
ws_col: cols,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
tcsetwinsize(master.as_fd(), ws).context("set pty winsize")
|
||||
}
|
||||
|
||||
/// Login-shell argv[0] is the shell's basename with a leading '-'.
|
||||
fn login_argv0(shell: &OsStr) -> std::ffi::OsString {
|
||||
let name = Path::new(shell)
|
||||
.file_name()
|
||||
.unwrap_or_else(|| OsStr::new("sh"));
|
||||
let mut argv0 = std::ffi::OsString::from("-");
|
||||
argv0.push(name);
|
||||
argv0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn argv0_is_dash_prefixed_basename() {
|
||||
assert_eq!(login_argv0(OsStr::new("/usr/bin/bash")), "-bash");
|
||||
assert_eq!(login_argv0(OsStr::new("zsh")), "-zsh");
|
||||
assert_eq!(login_argv0(OsStr::new("")), "-sh");
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,11 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use calloop::EventLoop;
|
||||
use calloop::generic::Generic;
|
||||
use calloop::{EventLoop, Interest, Mode, PostAction};
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
|
||||
use crate::pty::Pty;
|
||||
use smithay_client_toolkit::{
|
||||
compositor::{CompositorHandler, CompositorState},
|
||||
delegate_compositor, delegate_output, delegate_registry, delegate_seat, delegate_shm,
|
||||
|
|
@ -36,6 +39,9 @@ const DEFAULT_W: u32 = 800;
|
|||
const DEFAULT_H: u32 = 600;
|
||||
/// Background fill, 0xAARRGGBB. Foot-ish dark grey.
|
||||
const BG: u32 = 0xFF18_1818;
|
||||
/// Terminal size handed to the shell until cell geometry drives it.
|
||||
const COLS: u16 = 80;
|
||||
const ROWS: u16 = 24;
|
||||
|
||||
/// Run a single window until it is closed.
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
|
|
@ -65,6 +71,38 @@ pub fn run() -> anyhow::Result<()> {
|
|||
let pool = SlotPool::new(DEFAULT_W as usize * DEFAULT_H as usize * 4, &shm)
|
||||
.context("create shm slot pool")?;
|
||||
|
||||
let pty = Pty::spawn(COLS, ROWS).context("spawn shell on pty")?;
|
||||
|
||||
// Read child output off a clone of the master; the original stays in `pty`
|
||||
// for writes and resizing.
|
||||
let read_fd = pty.master().try_clone().context("clone pty master")?;
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(
|
||||
Generic::new(read_fd, Interest::READ, Mode::Level),
|
||||
|_, fd, app: &mut App| {
|
||||
let mut buf = [0u8; 4096];
|
||||
match rustix::io::read(&*fd, &mut buf) {
|
||||
Ok(0) => {
|
||||
app.child_exited();
|
||||
Ok(PostAction::Remove)
|
||||
}
|
||||
Ok(n) => {
|
||||
tracing::debug!("pty -> {n} bytes: {:02x?}", &buf[..n]);
|
||||
Ok(PostAction::Continue)
|
||||
}
|
||||
Err(rustix::io::Errno::INTR | rustix::io::Errno::AGAIN) => {
|
||||
Ok(PostAction::Continue)
|
||||
}
|
||||
Err(_) => {
|
||||
app.child_exited();
|
||||
Ok(PostAction::Remove)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("register pty in event loop: {e}"))?;
|
||||
|
||||
let mut app = App {
|
||||
registry_state: RegistryState::new(&globals),
|
||||
output_state: OutputState::new(&globals, &qh),
|
||||
|
|
@ -72,6 +110,7 @@ pub fn run() -> anyhow::Result<()> {
|
|||
shm,
|
||||
pool,
|
||||
window,
|
||||
pty,
|
||||
width: DEFAULT_W,
|
||||
height: DEFAULT_H,
|
||||
configured: false,
|
||||
|
|
@ -95,6 +134,7 @@ struct App {
|
|||
shm: Shm,
|
||||
pool: SlotPool,
|
||||
window: Window,
|
||||
pty: Pty,
|
||||
width: u32,
|
||||
height: u32,
|
||||
configured: bool,
|
||||
|
|
@ -102,6 +142,15 @@ struct App {
|
|||
}
|
||||
|
||||
impl App {
|
||||
/// The child shell has gone away; reap it and tear the window down.
|
||||
fn child_exited(&mut self) {
|
||||
match self.pty.wait() {
|
||||
Ok(status) => tracing::info!("shell exited: {status}"),
|
||||
Err(err) => tracing::warn!("reap shell: {err}"),
|
||||
}
|
||||
self.exit = true;
|
||||
}
|
||||
|
||||
/// Fill the surface with the background colour and present it.
|
||||
fn draw(&mut self) {
|
||||
let (w, h) = (self.width, self.height);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue