Pseudo-terminals, and what tmux and htop needed around them
- /dev/ptmx and /dev/pts/N: Unix98 pairs with a full line discipline, window sizes, job control and hangup (kernel/dev/pty.c). - The console speaks scroll regions, insert/delete line, SU/SD, RI/IND/NEL, DECSC/DECRC; swallows charset designations and private CSI sequences; and LF only returns the carriage under ONLCR. - setsid() drops the controlling terminal, so a pane's pty becomes its ctty. - pipe2() honours O_NONBLOCK and O_CLOEXEC (libevent's signal pipe). - File modes are stored and enforced: chmod, fchmod(at), umask, creation modes; /tmp is 1777. - /proc/stat has real user/system/idle; /proc/<pid>/task; kthread flag. - SO_PEERCRED, IOV_MAX 1024. - /proc/strace: name a program and its syscalls go to the kernel log. - World gains htop, btop, tmux, mc, ncdu, fastfetch, games, X toys, NetSurf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
c65bfa3fa3
commit
fd9d392416
28 changed files with 1577 additions and 47 deletions
|
|
@ -7,6 +7,7 @@
|
|||
#include "fs/file.h"
|
||||
#include "arch/x86.h"
|
||||
#include "uapi/errno.h"
|
||||
#include "dev/pty.h"
|
||||
|
||||
extern int consoleread(struct inode*, uint64, char*, int);
|
||||
extern int consolewrite(struct inode*, uint64, char*, int);
|
||||
|
|
@ -216,4 +217,10 @@ struct devsw devsw[] = {
|
|||
// The close hook plays out what is still queued: a program that writes a
|
||||
// sound and exits expects to hear the end of it.
|
||||
[12] = { dspread, dspwrite, 0, dspclose },
|
||||
// Pseudo-terminals, kernel/dev/pty.c. /dev/ptmx (13) is never read or
|
||||
// written: open() swaps it for a new master (14) before it gets the chance.
|
||||
// The slaves (15) are /dev/pts/N.
|
||||
[PTMX_MAJOR] = { 0, 0 },
|
||||
[PTM_MAJOR] = { ptm_read, ptm_write, ptm_ready, ptm_close, ptm_open },
|
||||
[PTS_MAJOR] = { pts_read, pts_write, pts_ready, pts_close, pts_open },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -194,16 +194,36 @@ static void
|
|||
gen_stat(struct sbuf *sb)
|
||||
{
|
||||
// USER_HZ, like times() -- everything that parses /proc/stat divides by 100.
|
||||
uint t = (uint)((uint64)uptime() * USER_HZ / HZ);
|
||||
//
|
||||
// Busy is what processes were charged; idle is the rest of every online
|
||||
// processor's time. Calling it all user time, as this did, is a machine at
|
||||
// 100% forever in htop and top. The charge is not kept per processor, so
|
||||
// each cpuN line is an equal share of the total -- right on average, which
|
||||
// is all a meter can show anyway.
|
||||
uint64 n = ncpu_online > 0 ? ncpu_online : 1;
|
||||
uint64 all = (uint64)uptime() * USER_HZ / HZ * n;
|
||||
uint64 user, sys;
|
||||
proc_cputime(&user, &sys);
|
||||
user = user * USER_HZ / HZ;
|
||||
sys = sys * USER_HZ / HZ;
|
||||
if (user + sys > all)
|
||||
all = user + sys;
|
||||
uint64 idle = all - user - sys;
|
||||
|
||||
// user nice system idle iowait irq softirq steal guest guest_nice
|
||||
sb_puts(sb, "cpu ");
|
||||
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
|
||||
sb_putu(sb, user); sb_puts(sb, " 0 ");
|
||||
sb_putu(sb, sys); sb_putc(sb, ' ');
|
||||
sb_putu(sb, idle); sb_puts(sb, " 0 0 0 0 0 0\n");
|
||||
// Online, for the same reason as cpuinfo: top and friends count the cpuN
|
||||
// lines here to decide how many cores to draw.
|
||||
for (int i = 0; i < ncpu_online; i++) {
|
||||
sb_puts(sb, "cpu");
|
||||
sb_putu(sb, i);
|
||||
sb_putc(sb, ' ');
|
||||
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
|
||||
sb_putu(sb, user / n); sb_puts(sb, " 0 ");
|
||||
sb_putu(sb, sys / n); sb_putc(sb, ' ');
|
||||
sb_putu(sb, idle / n); sb_puts(sb, " 0 0 0 0 0 0\n");
|
||||
}
|
||||
sb_kv(sb, "processes ", (uint64)proc_num(), 0);
|
||||
sb_kv(sb, "procs_running ", 1, 0);
|
||||
|
|
@ -388,12 +408,42 @@ gen_filesystems(struct sbuf *sb)
|
|||
#define PROC_BOOTDEBUG 30
|
||||
#define PROC_SLAB 20
|
||||
#define PROC_PARTITIONS 21
|
||||
#define PROC_STRACE 31
|
||||
|
||||
// Render global file `id` (a PROC_* constant) into `buf`. Returns its length.
|
||||
//
|
||||
// Split out of procread() so the /proc *filesystem* (kernel/fs/procfs.c) can
|
||||
// use the same generators: that file owns the namespace, this one owns the
|
||||
// content, and they have no other business with each other.
|
||||
// /proc/strace: the name of the program to trace, or empty. Read by exec, so
|
||||
// a change takes effect at the next program started, not in running ones.
|
||||
static char strace_name[16];
|
||||
|
||||
int
|
||||
strace_wanted(const char *name)
|
||||
{
|
||||
return strace_name[0] && strncmp(name, strace_name, sizeof(strace_name)) == 0;
|
||||
}
|
||||
|
||||
int
|
||||
proc_write_strace(int user_src, uint64 src, uint n)
|
||||
{
|
||||
char tmp[16];
|
||||
uint len = n < sizeof(tmp) - 1 ? n : sizeof(tmp) - 1;
|
||||
|
||||
if (user_src) {
|
||||
if (copy_from_user(tmp, src, len) < 0)
|
||||
return -EFAULT;
|
||||
} else {
|
||||
memmove(tmp, (char *)src, len);
|
||||
}
|
||||
while (len > 0 && (tmp[len - 1] == '\n' || tmp[len - 1] == ' '))
|
||||
len--;
|
||||
tmp[len] = 0;
|
||||
memmove(strace_name, tmp, sizeof(strace_name));
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
int
|
||||
proc_gen_global(int id, char *buf, int cap)
|
||||
{
|
||||
|
|
@ -442,6 +492,10 @@ proc_gen_global(int id, char *buf, int cap)
|
|||
sb_puts(&sb, boot_debug ? "on\n" : "off\n");
|
||||
break;
|
||||
case PROC_DEVICES: gen_devices(&sb); break;
|
||||
case PROC_STRACE:
|
||||
sb_puts(&sb, strace_name);
|
||||
sb_putc(&sb, '\n');
|
||||
break;
|
||||
// The block layer renders its own table, like PCI and SchFS above.
|
||||
case PROC_PARTITIONS: sb.len = blkdev_info(buf, cap); break;
|
||||
default: return 0;
|
||||
|
|
|
|||
709
kernel/dev/pty.c
Normal file
709
kernel/dev/pty.c
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Pseudo-terminals.
|
||||
//
|
||||
// A terminal emulator, tmux, sshd and script(1) all want the same thing: a
|
||||
// terminal with a program on one side and *themselves* on the other, instead of
|
||||
// a keyboard and a screen. Unix98 spells that /dev/ptmx -- each open makes a new
|
||||
// pair and hands back its master -- and /dev/pts/N, the slave, which the program
|
||||
// gets as its stdin, stdout and controlling terminal. There were none of these,
|
||||
// so xterm had nothing to run a shell in and tmux could not start a server.
|
||||
//
|
||||
// Between the two sides sits a line discipline, the console's in miniature
|
||||
// (kernel/core/console.c): what the master writes is the slave's *input* --
|
||||
// canonical editing, echo, ^C as SIGINT to the foreground group -- and what the
|
||||
// slave writes is its *output*, NL turned into CR NL, read back by the master.
|
||||
//
|
||||
// Every pair shares one lock. The paths are short and a second lock per pair
|
||||
// would only be one more thing to take in the wrong order.
|
||||
|
||||
#include "core/types.h"
|
||||
#include "core/defs.h"
|
||||
#include "core/param.h"
|
||||
#include "core/spinlock.h"
|
||||
#include "core/sleeplock.h"
|
||||
#include "core/proc.h"
|
||||
#include "fs/fs.h"
|
||||
#include "fs/file.h"
|
||||
#include "core/uaccess.h"
|
||||
#include "uapi/posix.h"
|
||||
#include "uapi/errno.h"
|
||||
#include "dev/pty.h"
|
||||
|
||||
#define PTY_BUF 4096
|
||||
|
||||
#define TIOCGPTN 0x80045430
|
||||
#define TIOCSPTLCK 0x40045431
|
||||
#define TIOCGPTPEER 0x5441
|
||||
#define TIOCNOTTY 0x5422
|
||||
#define TIOCGSID 0x5429
|
||||
#define TIOCOUTQ 0x5411
|
||||
#define TIOCPKT 0x5420
|
||||
|
||||
#define POLLIN_ 0x001
|
||||
#define POLLOUT_ 0x004
|
||||
#define POLLHUP_ 0x010
|
||||
|
||||
struct pty {
|
||||
int used;
|
||||
int mopen, sopen; // open file descriptions on each side
|
||||
int slave_seen; // the slave has been opened at least once
|
||||
struct termios tio;
|
||||
struct winsize ws;
|
||||
int fg_pgrp, sid;
|
||||
|
||||
// The slave's input, after the line discipline. [r, w) is ready to read;
|
||||
// [w, e) is the line still being edited in canonical mode.
|
||||
char in[PTY_BUF];
|
||||
uint ir, iw, ie;
|
||||
int eof; // ^D on an empty line: the next read returns 0
|
||||
int abort_read; // ^C: wake the reader with EINTR
|
||||
|
||||
// The slave's output, for the master to read.
|
||||
char out[PTY_BUF];
|
||||
uint or, ow;
|
||||
};
|
||||
|
||||
static struct spinlock ptylock;
|
||||
static struct pty ptys[NPTY];
|
||||
|
||||
#define CTRL(x) ((x) & 0x1f)
|
||||
|
||||
static void
|
||||
termios_defaults(struct termios *t)
|
||||
{
|
||||
memset(t, 0, sizeof(*t));
|
||||
t->c_iflag = ICRNL | IUTF8;
|
||||
t->c_oflag = OPOST | ONLCR;
|
||||
t->c_cflag = CS8 | CREAD;
|
||||
t->c_lflag = ISIG | ICANON | ECHO | ECHOE | IEXTEN;
|
||||
t->c_cc[VINTR] = CTRL('C');
|
||||
t->c_cc[VQUIT] = CTRL('\\');
|
||||
t->c_cc[VERASE] = 0x7f;
|
||||
t->c_cc[VKILL] = CTRL('U');
|
||||
t->c_cc[VEOF] = CTRL('D');
|
||||
t->c_cc[VSUSP] = CTRL('Z');
|
||||
t->c_cc[VWERASE]= CTRL('W');
|
||||
t->c_cc[VMIN] = 1;
|
||||
t->c_ispeed = 15;
|
||||
t->c_ospeed = 15;
|
||||
}
|
||||
|
||||
void
|
||||
pty_init(void)
|
||||
{
|
||||
initlock(&ptylock, "pty");
|
||||
}
|
||||
|
||||
int
|
||||
pty_alloc(void)
|
||||
{
|
||||
acquire(&ptylock);
|
||||
for (int i = 0; i < NPTY; i++) {
|
||||
struct pty *p = &ptys[i];
|
||||
if (p->used)
|
||||
continue;
|
||||
memset(p, 0, sizeof(*p));
|
||||
p->used = 1;
|
||||
termios_defaults(&p->tio);
|
||||
p->ws.ws_row = 24;
|
||||
p->ws.ws_col = 80;
|
||||
release(&ptylock);
|
||||
return i;
|
||||
}
|
||||
release(&ptylock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
pty_used(int n)
|
||||
{
|
||||
return n >= 0 && n < NPTY && ptys[n].used;
|
||||
}
|
||||
|
||||
static struct pty *
|
||||
pty_of(struct inode *ip)
|
||||
{
|
||||
if (ip->minor < 0 || ip->minor >= NPTY)
|
||||
return 0;
|
||||
return &ptys[ip->minor];
|
||||
}
|
||||
|
||||
// /dev/ptmx, opened: a new pair, and the master in place of the ptmx inode.
|
||||
// Called with ip locked, as open() holds it; hands back the master the same way.
|
||||
struct inode *
|
||||
ptmx_open(struct inode *ip)
|
||||
{
|
||||
int n = pty_alloc();
|
||||
struct inode *m;
|
||||
|
||||
if (n < 0)
|
||||
return 0;
|
||||
m = devfs_pty_master(n);
|
||||
if (m == 0) {
|
||||
ptys[n].used = 0;
|
||||
return 0;
|
||||
}
|
||||
iunlockput(ip);
|
||||
ilock(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
static void
|
||||
maybe_free(struct pty *p)
|
||||
{
|
||||
if (p->mopen == 0 && p->sopen == 0)
|
||||
p->used = 0;
|
||||
}
|
||||
|
||||
// --- output: the slave writes, the master reads ------------------------------
|
||||
|
||||
static int
|
||||
out_room(struct pty *p)
|
||||
{
|
||||
return PTY_BUF - (int)(p->ow - p->or);
|
||||
}
|
||||
|
||||
// One byte of output, with OPOST processing. Caller holds the lock and has made
|
||||
// sure there is room for two.
|
||||
static void
|
||||
out_put(struct pty *p, int c)
|
||||
{
|
||||
if ((p->tio.c_oflag & OPOST) && (p->tio.c_oflag & ONLCR) && c == '\n')
|
||||
p->out[p->ow++ % PTY_BUF] = '\r';
|
||||
p->out[p->ow++ % PTY_BUF] = (char)c;
|
||||
}
|
||||
|
||||
// Echo goes into the same queue, and is dropped rather than waited for when
|
||||
// the master has stopped reading: the writer here is whoever typed, and
|
||||
// blocking it on its own echo would deadlock a master that writes and reads
|
||||
// from one thread.
|
||||
static void
|
||||
echo_put(struct pty *p, int c)
|
||||
{
|
||||
if (out_room(p) < 2)
|
||||
return;
|
||||
out_put(p, c);
|
||||
}
|
||||
|
||||
static void
|
||||
echo(struct pty *p, int c)
|
||||
{
|
||||
if (c == '\n') {
|
||||
if (p->tio.c_lflag & (ECHO | ECHONL))
|
||||
echo_put(p, '\n');
|
||||
return;
|
||||
}
|
||||
if (!(p->tio.c_lflag & ECHO))
|
||||
return;
|
||||
if (c < 32 && c != '\t' && c != '\r') {
|
||||
echo_put(p, '^');
|
||||
echo_put(p, c + '@');
|
||||
} else {
|
||||
echo_put(p, c);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
erase_one(struct pty *p)
|
||||
{
|
||||
if (p->ie == p->iw)
|
||||
return;
|
||||
p->ie--;
|
||||
if ((p->tio.c_lflag & ECHO) && (p->tio.c_lflag & ECHOE)) {
|
||||
echo_put(p, '\b');
|
||||
echo_put(p, ' ');
|
||||
echo_put(p, '\b');
|
||||
}
|
||||
}
|
||||
|
||||
// --- input: the master writes, the slave reads -------------------------------
|
||||
|
||||
// One byte through the line discipline. Returns a signal to send to the
|
||||
// foreground group, or 0. Caller holds the lock.
|
||||
static int
|
||||
input_byte(struct pty *p, int c)
|
||||
{
|
||||
struct termios *t = &p->tio;
|
||||
|
||||
if (t->c_iflag & ISTRIP)
|
||||
c &= 0x7f;
|
||||
if (c == '\r') {
|
||||
if (t->c_iflag & IGNCR)
|
||||
return 0;
|
||||
if (t->c_iflag & ICRNL)
|
||||
c = '\n';
|
||||
} else if (c == '\n' && (t->c_iflag & INLCR)) {
|
||||
c = '\r';
|
||||
}
|
||||
|
||||
if (t->c_lflag & ISIG) {
|
||||
int sig = 0;
|
||||
if (c == t->c_cc[VINTR]) sig = SIGINT;
|
||||
else if (c == t->c_cc[VQUIT]) sig = SIGQUIT;
|
||||
else if (c == t->c_cc[VSUSP]) sig = SIGTSTP;
|
||||
if (sig && c != 0) {
|
||||
echo(p, c);
|
||||
if (!(t->c_lflag & NOFLSH))
|
||||
p->ie = p->iw = p->ir;
|
||||
p->abort_read = 1;
|
||||
return sig;
|
||||
}
|
||||
}
|
||||
|
||||
if (t->c_lflag & ICANON) {
|
||||
if (c == t->c_cc[VERASE] || c == CTRL('H')) {
|
||||
erase_one(p);
|
||||
return 0;
|
||||
}
|
||||
if (c == t->c_cc[VKILL]) {
|
||||
while (p->ie != p->iw && p->in[(p->ie - 1) % PTY_BUF] != '\n')
|
||||
erase_one(p);
|
||||
return 0;
|
||||
}
|
||||
if (c == t->c_cc[VWERASE] && (t->c_lflag & IEXTEN)) {
|
||||
while (p->ie != p->iw && p->in[(p->ie - 1) % PTY_BUF] == ' ')
|
||||
erase_one(p);
|
||||
while (p->ie != p->iw && p->in[(p->ie - 1) % PTY_BUF] != ' ')
|
||||
erase_one(p);
|
||||
return 0;
|
||||
}
|
||||
if (c == t->c_cc[VEOF]) {
|
||||
if (p->ie == p->iw)
|
||||
p->eof = 1;
|
||||
p->iw = p->ie;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (p->ie - p->ir >= PTY_BUF)
|
||||
return 0; // full: dropped, as a console does
|
||||
p->in[p->ie++ % PTY_BUF] = (char)c;
|
||||
echo(p, c);
|
||||
if (!(t->c_lflag & ICANON) || c == '\n' || p->ie - p->ir >= PTY_BUF)
|
||||
p->iw = p->ie;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
ptm_write(struct inode *ip, uint64 off, char *src, int n)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
char kbuf[128];
|
||||
int done = 0;
|
||||
|
||||
(void)off;
|
||||
if (p == 0 || !p->used)
|
||||
return -EIO;
|
||||
iunlock(ip);
|
||||
while (done < n) {
|
||||
int chunk = n - done < (int)sizeof(kbuf) ? n - done : (int)sizeof(kbuf);
|
||||
if (copy_from_user(kbuf, (addr_t)(src + done), chunk) < 0) {
|
||||
ilock(ip);
|
||||
return done ? done : -EFAULT;
|
||||
}
|
||||
acquire(&ptylock);
|
||||
for (int i = 0; i < chunk; i++) {
|
||||
// Raw-mode input waits for room rather than vanishing: a paste into an
|
||||
// editor is a burst of thousands of bytes, and dropping the tail of it
|
||||
// is worse than making the writer wait for the reader.
|
||||
while (!(p->tio.c_lflag & ICANON) && p->ie - p->ir >= PTY_BUF &&
|
||||
p->sopen > 0) {
|
||||
if (myproc()->killed) {
|
||||
release(&ptylock);
|
||||
ilock(ip);
|
||||
return done + i ? done + i : -EINTR;
|
||||
}
|
||||
sleep(&p->ie, &ptylock);
|
||||
}
|
||||
int sig = input_byte(p, kbuf[i] & 0xff);
|
||||
if (sig) {
|
||||
int pg = p->fg_pgrp;
|
||||
wakeup(&p->ir);
|
||||
release(&ptylock);
|
||||
if (pg > 0)
|
||||
sigsend_pgrp(pg, sig);
|
||||
acquire(&ptylock);
|
||||
}
|
||||
}
|
||||
wakeup(&p->ir);
|
||||
wakeup(&p->or); // the echo
|
||||
release(&ptylock);
|
||||
done += chunk;
|
||||
}
|
||||
poll_kick();
|
||||
ilock(ip);
|
||||
return n;
|
||||
}
|
||||
|
||||
int
|
||||
ptm_read(struct inode *ip, uint64 off, char *dst, int n)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
char kbuf[256];
|
||||
int got = 0;
|
||||
|
||||
(void)off;
|
||||
if (p == 0 || !p->used)
|
||||
return -EIO;
|
||||
iunlock(ip);
|
||||
acquire(&ptylock);
|
||||
while (p->or == p->ow) {
|
||||
// Every slave descriptor closed, after there was one: the program on the
|
||||
// other side has gone. Linux says EIO, and that is what terminal
|
||||
// emulators and tmux take as "the shell exited".
|
||||
if (p->slave_seen && p->sopen == 0) {
|
||||
release(&ptylock);
|
||||
ilock(ip);
|
||||
return -EIO;
|
||||
}
|
||||
if (myproc()->killed) {
|
||||
release(&ptylock);
|
||||
ilock(ip);
|
||||
return -EINTR;
|
||||
}
|
||||
sleep(&p->or, &ptylock);
|
||||
}
|
||||
while (got < n && got < (int)sizeof(kbuf) && p->or != p->ow)
|
||||
kbuf[got++] = p->out[p->or++ % PTY_BUF];
|
||||
wakeup(&p->ow);
|
||||
release(&ptylock);
|
||||
poll_kick();
|
||||
ilock(ip);
|
||||
if (copy_to_user((addr_t)dst, kbuf, got) < 0)
|
||||
return -EFAULT;
|
||||
return got;
|
||||
}
|
||||
|
||||
int
|
||||
ptm_ready(struct inode *ip)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
return p && (p->or != p->ow || (p->slave_seen && p->sopen == 0));
|
||||
}
|
||||
|
||||
void
|
||||
ptm_open(struct inode *ip)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
if (p) {
|
||||
acquire(&ptylock);
|
||||
p->mopen++;
|
||||
release(&ptylock);
|
||||
}
|
||||
}
|
||||
|
||||
// The master is gone: the slave side has been hung up on. Its readers see end
|
||||
// of file, its writers EIO, and its session SIGHUP -- which is how closing an
|
||||
// xterm window ends the shell inside it.
|
||||
void
|
||||
ptm_close(struct inode *ip)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
int sid = 0, pg = 0;
|
||||
|
||||
if (p == 0)
|
||||
return;
|
||||
acquire(&ptylock);
|
||||
if (p->mopen > 0)
|
||||
p->mopen--;
|
||||
if (p->mopen == 0) {
|
||||
sid = p->sid;
|
||||
pg = p->fg_pgrp;
|
||||
wakeup(&p->ir);
|
||||
wakeup(&p->ow);
|
||||
wakeup(&p->ie);
|
||||
}
|
||||
maybe_free(p);
|
||||
release(&ptylock);
|
||||
if (sid > 0)
|
||||
sigsend_pgrp(sid, SIGHUP);
|
||||
if (pg > 0 && pg != sid)
|
||||
sigsend_pgrp(pg, SIGHUP);
|
||||
poll_kick();
|
||||
}
|
||||
|
||||
// --- the slave ---------------------------------------------------------------
|
||||
|
||||
int
|
||||
pts_read(struct inode *ip, uint64 off, char *dst, int n)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
char kbuf[256];
|
||||
int got = 0, c;
|
||||
|
||||
(void)off;
|
||||
if (p == 0 || !p->used)
|
||||
return -EIO;
|
||||
if (n > (int)sizeof(kbuf))
|
||||
n = sizeof(kbuf);
|
||||
iunlock(ip);
|
||||
acquire(&ptylock);
|
||||
while (got < n) {
|
||||
while (p->ir == p->iw) {
|
||||
if (p->eof) {
|
||||
p->eof = 0;
|
||||
goto done;
|
||||
}
|
||||
if (p->mopen == 0)
|
||||
goto done; // hung up: end of file
|
||||
// VMIN 0 is a poll, and a program that asked for one must not block.
|
||||
if (!(p->tio.c_lflag & ICANON) && p->tio.c_cc[VMIN] == 0)
|
||||
goto done;
|
||||
if (got > 0 && !(p->tio.c_lflag & ICANON))
|
||||
goto done;
|
||||
if (myproc()->killed || p->abort_read) {
|
||||
p->abort_read = 0;
|
||||
release(&ptylock);
|
||||
ilock(ip);
|
||||
return got ? got : -EINTR;
|
||||
}
|
||||
sleep(&p->ir, &ptylock);
|
||||
}
|
||||
c = p->in[p->ir++ % PTY_BUF];
|
||||
kbuf[got++] = (char)c;
|
||||
if ((p->tio.c_lflag & ICANON) && c == '\n')
|
||||
break;
|
||||
if (!(p->tio.c_lflag & ICANON) &&
|
||||
got >= (p->tio.c_cc[VMIN] ? p->tio.c_cc[VMIN] : 1) && p->ir == p->iw)
|
||||
break;
|
||||
}
|
||||
done:
|
||||
wakeup(&p->ie);
|
||||
release(&ptylock);
|
||||
poll_kick();
|
||||
ilock(ip);
|
||||
if (got && copy_to_user((addr_t)dst, kbuf, got) < 0)
|
||||
return -EFAULT;
|
||||
return got;
|
||||
}
|
||||
|
||||
int
|
||||
pts_write(struct inode *ip, uint64 off, char *src, int n)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
char kbuf[128];
|
||||
int done = 0;
|
||||
|
||||
(void)off;
|
||||
if (p == 0 || !p->used)
|
||||
return -EIO;
|
||||
iunlock(ip);
|
||||
while (done < n) {
|
||||
int chunk = n - done < (int)sizeof(kbuf) ? n - done : (int)sizeof(kbuf);
|
||||
if (copy_from_user(kbuf, (addr_t)(src + done), chunk) < 0) {
|
||||
ilock(ip);
|
||||
return done ? done : -EFAULT;
|
||||
}
|
||||
acquire(&ptylock);
|
||||
for (int i = 0; i < chunk; i++) {
|
||||
while (out_room(p) < 2) {
|
||||
if (p->mopen == 0 || myproc()->killed) {
|
||||
release(&ptylock);
|
||||
ilock(ip);
|
||||
return done + i ? done + i : (p->mopen == 0 ? -EIO : -EINTR);
|
||||
}
|
||||
wakeup(&p->or);
|
||||
sleep(&p->ow, &ptylock);
|
||||
}
|
||||
if (p->mopen == 0) {
|
||||
release(&ptylock);
|
||||
ilock(ip);
|
||||
return done + i ? done + i : -EIO;
|
||||
}
|
||||
out_put(p, kbuf[i] & 0xff);
|
||||
}
|
||||
wakeup(&p->or);
|
||||
release(&ptylock);
|
||||
done += chunk;
|
||||
}
|
||||
poll_kick();
|
||||
ilock(ip);
|
||||
return n;
|
||||
}
|
||||
|
||||
int
|
||||
pts_ready(struct inode *ip)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
return p && (p->ir != p->iw || p->eof || p->mopen == 0);
|
||||
}
|
||||
|
||||
// Opening a terminal makes it the caller's controlling terminal when the
|
||||
// caller leads a session that has none -- the System V rule, which Linux keeps
|
||||
// and which xterm relies on: its child calls setsid() and opens the slave, and
|
||||
// expects ^C to work from then on without a TIOCSCTTY.
|
||||
void
|
||||
pts_open(struct inode *ip)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
struct proc *me = myproc();
|
||||
|
||||
if (p == 0)
|
||||
return;
|
||||
acquire(&ptylock);
|
||||
p->sopen++;
|
||||
p->slave_seen = 1;
|
||||
if (me && me->sid == me->pid && me->ctty == 0 && p->sid == 0) {
|
||||
me->ctty = PTY_CTTY + ip->minor;
|
||||
p->sid = me->sid;
|
||||
p->fg_pgrp = me->pgid;
|
||||
}
|
||||
release(&ptylock);
|
||||
}
|
||||
|
||||
void
|
||||
pts_close(struct inode *ip)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
|
||||
if (p == 0)
|
||||
return;
|
||||
acquire(&ptylock);
|
||||
if (p->sopen > 0)
|
||||
p->sopen--;
|
||||
if (p->sopen == 0) {
|
||||
p->sid = 0;
|
||||
p->fg_pgrp = 0;
|
||||
wakeup(&p->or); // the master reads EIO now
|
||||
}
|
||||
maybe_free(p);
|
||||
release(&ptylock);
|
||||
poll_kick();
|
||||
}
|
||||
|
||||
// --- ioctl and poll ----------------------------------------------------------
|
||||
|
||||
int
|
||||
pty_ioctl(struct inode *ip, uint request, addr_t arg)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
int master = ip->major == PTM_MAJOR;
|
||||
int v;
|
||||
|
||||
if (p == 0 || !p->used)
|
||||
return -EIO;
|
||||
|
||||
switch (request) {
|
||||
case TIOCGPTN:
|
||||
if (!master)
|
||||
return -ENOTTY;
|
||||
v = ip->minor;
|
||||
return put_user(v, arg) < 0 ? -EFAULT : 0;
|
||||
case TIOCSPTLCK:
|
||||
case TIOCPKT:
|
||||
// Slaves are never locked here, so unlockpt() has nothing to undo; packet
|
||||
// mode is not offered, and nothing in the image asks for it.
|
||||
return master ? 0 : -ENOTTY;
|
||||
case TCGETS: {
|
||||
struct termios t;
|
||||
acquire(&ptylock);
|
||||
t = p->tio;
|
||||
release(&ptylock);
|
||||
return copy_to_user(arg, &t, sizeof(t)) < 0 ? -EFAULT : 0;
|
||||
}
|
||||
case TCSETS: case TCSETSW: case TCSETSF: {
|
||||
struct termios t;
|
||||
if (copy_from_user(&t, arg, sizeof(t)) < 0)
|
||||
return -EFAULT;
|
||||
acquire(&ptylock);
|
||||
p->tio = t;
|
||||
if (request == TCSETSF)
|
||||
p->ie = p->iw = p->ir;
|
||||
// Leaving canonical mode hands over the line being edited, as on the
|
||||
// console: a shell that switches to raw mid-line must not lose it.
|
||||
if (!(t.c_lflag & ICANON))
|
||||
p->iw = p->ie;
|
||||
wakeup(&p->ir);
|
||||
release(&ptylock);
|
||||
poll_kick();
|
||||
return 0;
|
||||
}
|
||||
case TCFLSH:
|
||||
acquire(&ptylock);
|
||||
if (arg == TCIFLUSH || arg == TCIOFLUSH) {
|
||||
p->ie = p->iw = p->ir;
|
||||
p->eof = 0;
|
||||
}
|
||||
if (arg == TCOFLUSH || arg == TCIOFLUSH)
|
||||
p->or = p->ow;
|
||||
wakeup(&p->ie);
|
||||
wakeup(&p->ow);
|
||||
release(&ptylock);
|
||||
return 0;
|
||||
case TIOCGWINSZ:
|
||||
return copy_to_user(arg, &p->ws, sizeof(p->ws)) < 0 ? -EFAULT : 0;
|
||||
case TIOCSWINSZ: {
|
||||
struct winsize ws;
|
||||
int changed, pg;
|
||||
if (copy_from_user(&ws, arg, sizeof(ws)) < 0)
|
||||
return -EFAULT;
|
||||
acquire(&ptylock);
|
||||
changed = ws.ws_row != p->ws.ws_row || ws.ws_col != p->ws.ws_col;
|
||||
p->ws = ws;
|
||||
pg = p->fg_pgrp;
|
||||
release(&ptylock);
|
||||
// The program inside learns of a resize the way it would on Linux.
|
||||
if (changed && pg > 0)
|
||||
sigsend_pgrp(pg, SIGWINCH);
|
||||
return 0;
|
||||
}
|
||||
case FIONREAD:
|
||||
v = master ? (int)(p->ow - p->or) : (int)(p->iw - p->ir);
|
||||
return put_user(v, arg) < 0 ? -EFAULT : 0;
|
||||
case TIOCOUTQ:
|
||||
v = master ? 0 : (int)(p->ow - p->or);
|
||||
return put_user(v, arg) < 0 ? -EFAULT : 0;
|
||||
case TIOCGPGRP:
|
||||
v = p->fg_pgrp;
|
||||
return put_user(v, arg) < 0 ? -EFAULT : 0;
|
||||
case TIOCSPGRP:
|
||||
if (copy_from_user(&v, arg, sizeof(v)) < 0)
|
||||
return -EFAULT;
|
||||
if (v <= 0)
|
||||
return -EINVAL;
|
||||
p->fg_pgrp = v;
|
||||
return 0;
|
||||
case TIOCGSID:
|
||||
if (p->sid == 0)
|
||||
return -ENOTTY;
|
||||
v = p->sid;
|
||||
return put_user(v, arg) < 0 ? -EFAULT : 0;
|
||||
case TIOCSCTTY:
|
||||
if (master)
|
||||
return -EINVAL;
|
||||
myproc()->ctty = PTY_CTTY + ip->minor;
|
||||
p->sid = myproc()->sid;
|
||||
p->fg_pgrp = myproc()->pgid;
|
||||
return 0;
|
||||
case TIOCNOTTY:
|
||||
if (myproc()->ctty == PTY_CTTY + ip->minor)
|
||||
myproc()->ctty = 0;
|
||||
return 0;
|
||||
}
|
||||
return -ENOTTY;
|
||||
}
|
||||
|
||||
int
|
||||
pty_poll(struct inode *ip, int events, int readable, int writable)
|
||||
{
|
||||
struct pty *p = pty_of(ip);
|
||||
int rev = 0;
|
||||
|
||||
if (p == 0 || !p->used)
|
||||
return POLLHUP_;
|
||||
if (ip->major == PTM_MAJOR) {
|
||||
if (readable && (events & POLLIN_) && p->or != p->ow)
|
||||
rev |= POLLIN_;
|
||||
if (p->slave_seen && p->sopen == 0)
|
||||
rev |= POLLHUP_ | (events & POLLIN_);
|
||||
} else {
|
||||
if (readable && (events & POLLIN_) && (p->ir != p->iw || p->eof))
|
||||
rev |= POLLIN_;
|
||||
if (p->mopen == 0)
|
||||
rev |= POLLHUP_ | (events & POLLIN_);
|
||||
}
|
||||
if (writable && (events & POLLOUT_))
|
||||
rev |= POLLOUT_;
|
||||
return rev;
|
||||
}
|
||||
177
kernel/dev/vt.c
177
kernel/dev/vt.c
|
|
@ -286,6 +286,99 @@ vt_scroll(struct vt *v)
|
|||
v->cy--;
|
||||
}
|
||||
|
||||
// --- the scrolling region ----------------------------------------------------
|
||||
//
|
||||
// tmux, htop, less and every curses program on TERM=linux scroll part of the
|
||||
// screen: a status line stays put while the rest moves. Without a region the
|
||||
// whole screen scrolled instead, and a pane's output smeared into its
|
||||
// neighbours and over the status bar.
|
||||
|
||||
static int
|
||||
region_bot(struct vt *v)
|
||||
{
|
||||
if (v->scroll_bot > v->scroll_top && v->scroll_bot < v->rows)
|
||||
return v->scroll_bot;
|
||||
return v->rows - 1;
|
||||
}
|
||||
|
||||
static int
|
||||
region_top(struct vt *v)
|
||||
{
|
||||
return v->scroll_top < region_bot(v) ? v->scroll_top : 0;
|
||||
}
|
||||
|
||||
static void
|
||||
draw_rows(struct vt *v, int r0, int r1)
|
||||
{
|
||||
if (!is_active(v))
|
||||
return;
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = 0; c < v->cols; c++)
|
||||
show(v, c, r);
|
||||
}
|
||||
|
||||
// Rows top..bot move up n; the n exposed at the bottom are blank.
|
||||
static void
|
||||
region_up(struct vt *v, int top, int bot, int n)
|
||||
{
|
||||
if (n <= 0 || top > bot)
|
||||
return;
|
||||
if (n > bot - top + 1)
|
||||
n = bot - top + 1;
|
||||
if (top == 0 && bot == v->rows - 1) {
|
||||
// The whole screen: the framebuffer can be moved instead of redrawn.
|
||||
while (n-- > 0) {
|
||||
memmove(v->cells, cell_at(v, 0, 1),
|
||||
(uint64)(v->rows - 1) * v->cols * sizeof(struct vtcell));
|
||||
blank_row(v, v->rows - 1);
|
||||
if (is_active(v))
|
||||
gfx_scroll_up(vt_rgb(v->bg));
|
||||
}
|
||||
return;
|
||||
}
|
||||
memmove(cell_at(v, 0, top), cell_at(v, 0, top + n),
|
||||
(uint64)(bot - top + 1 - n) * v->cols * sizeof(struct vtcell));
|
||||
for (int r = bot - n + 1; r <= bot; r++)
|
||||
blank_row(v, r);
|
||||
draw_rows(v, top, bot);
|
||||
}
|
||||
|
||||
// Rows top..bot move down n; the n exposed at the top are blank.
|
||||
static void
|
||||
region_down(struct vt *v, int top, int bot, int n)
|
||||
{
|
||||
if (n <= 0 || top > bot)
|
||||
return;
|
||||
if (n > bot - top + 1)
|
||||
n = bot - top + 1;
|
||||
memmove(cell_at(v, 0, top + n), cell_at(v, 0, top),
|
||||
(uint64)(bot - top + 1 - n) * v->cols * sizeof(struct vtcell));
|
||||
for (int r = top; r < top + n; r++)
|
||||
blank_row(v, r);
|
||||
draw_rows(v, top, bot);
|
||||
}
|
||||
|
||||
// Down a line, scrolling the region when the cursor is on its last row. Below
|
||||
// the region the cursor only moves, and stops at the bottom of the screen.
|
||||
static void
|
||||
linefeed(struct vt *v)
|
||||
{
|
||||
if (v->cy == region_bot(v))
|
||||
region_up(v, region_top(v), region_bot(v), 1);
|
||||
else if (v->cy < v->rows - 1)
|
||||
v->cy++;
|
||||
}
|
||||
|
||||
// Up a line, the reverse: the region scrolls down from its first row.
|
||||
static void
|
||||
reverse_index(struct vt *v)
|
||||
{
|
||||
if (v->cy == region_top(v))
|
||||
region_down(v, region_top(v), region_bot(v), 1);
|
||||
else if (v->cy > 0)
|
||||
v->cy--;
|
||||
}
|
||||
|
||||
void
|
||||
vt_cursor_set(struct vt *v, int col, int row)
|
||||
{
|
||||
|
|
@ -328,19 +421,23 @@ wrap_now(struct vt *v)
|
|||
return;
|
||||
v->wrap_pending = 0;
|
||||
v->cx = 0;
|
||||
v->cy++;
|
||||
while (v->cy >= v->rows)
|
||||
vt_scroll(v);
|
||||
linefeed(v);
|
||||
}
|
||||
|
||||
void
|
||||
vt_putc(struct vt *v, int glyph)
|
||||
{
|
||||
switch (glyph) {
|
||||
// LF is a line feed: down one row, same column. The carriage return a
|
||||
// shell expects with it is ONLCR's, so it happens only while the terminal
|
||||
// asks for it. Doing it always broke every program that turns OPOST off and
|
||||
// moves down with a bare LF -- tmux does, row after row, and each row of a
|
||||
// right-hand pane landed in column 0, where its ESC [ K erased the left one.
|
||||
case '\n':
|
||||
v->wrap_pending = 0;
|
||||
v->cx = 0;
|
||||
v->cy++;
|
||||
if ((v->tio.c_oflag & (OPOST | ONLCR)) == (OPOST | ONLCR))
|
||||
v->cx = 0;
|
||||
linefeed(v);
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
|
|
@ -759,6 +856,38 @@ vt_csi(struct vt *v, int final)
|
|||
case 's': v->saved_cx = col; v->saved_cy = row; break;
|
||||
case 'u': vt_cursor_set(v, v->saved_cx, v->saved_cy); break;
|
||||
|
||||
// DECSTBM: the scrolling region, 1-based and inclusive, and the cursor home.
|
||||
case 'r': {
|
||||
int t = v->esc_nparm > 0 && v->esc_parm[0] ? v->esc_parm[0] : 1;
|
||||
int b = v->esc_nparm > 1 && v->esc_parm[1] ? v->esc_parm[1] : v->rows;
|
||||
if (b > v->rows) b = v->rows;
|
||||
if (t < b) {
|
||||
v->scroll_top = t - 1;
|
||||
v->scroll_bot = b - 1;
|
||||
} else {
|
||||
v->scroll_top = v->scroll_bot = 0;
|
||||
}
|
||||
vt_cursor_set(v, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// Insert and delete lines at the cursor, within the region: what a curses
|
||||
// program sends to open or close a gap rather than repaint everything below.
|
||||
case 'L':
|
||||
if (row >= region_top(v) && row <= region_bot(v))
|
||||
region_down(v, row, region_bot(v), n1);
|
||||
v->wrap_pending = 0;
|
||||
break;
|
||||
case 'M':
|
||||
if (row >= region_top(v) && row <= region_bot(v))
|
||||
region_up(v, row, region_bot(v), n1);
|
||||
v->wrap_pending = 0;
|
||||
break;
|
||||
|
||||
// Scroll the region without moving the cursor.
|
||||
case 'S': region_up(v, region_top(v), region_bot(v), n1); break;
|
||||
case 'T': region_down(v, region_top(v), region_bot(v), n1); break;
|
||||
|
||||
// Cursor visibility (DECTCEM) and the other private modes. There is no
|
||||
// hardware cursor being drawn, so there is nothing to show or hide --
|
||||
// swallowing them silently is correct, and far better than printing
|
||||
|
|
@ -781,6 +910,37 @@ vt_feed_1(struct vt *v, int c)
|
|||
for (int i = 0; i < VT_MAXPARM; i++) v->esc_parm[i] = 0;
|
||||
return; }
|
||||
if (c == ']') { v->esc_state = VE_OSC; return; }
|
||||
v->esc_state = VE_NORMAL;
|
||||
switch (c) {
|
||||
case '7': // DECSC
|
||||
v->saved_cx = v->cx;
|
||||
v->saved_cy = v->cy;
|
||||
break;
|
||||
case '8': // DECRC
|
||||
vt_cursor_set(v, v->saved_cx, v->saved_cy);
|
||||
break;
|
||||
case 'M': // RI: up, scrolling back
|
||||
v->wrap_pending = 0;
|
||||
reverse_index(v);
|
||||
break;
|
||||
case 'D': // IND: down, scrolling
|
||||
v->wrap_pending = 0;
|
||||
linefeed(v);
|
||||
break;
|
||||
case 'E': // NEL: CR and IND
|
||||
v->wrap_pending = 0;
|
||||
v->cx = 0;
|
||||
linefeed(v);
|
||||
break;
|
||||
case '(': case ')': case '*': case '+':
|
||||
// A character set designation. The set itself is not offered, but the
|
||||
// byte naming it must be swallowed: ESC ( B printed a stray "B".
|
||||
v->esc_state = VE_CHARSET;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
||||
case VE_CHARSET:
|
||||
v->esc_state = VE_NORMAL;
|
||||
return;
|
||||
|
||||
|
|
@ -798,7 +958,12 @@ vt_feed_1(struct vt *v, int c)
|
|||
return;
|
||||
|
||||
case VE_CSI:
|
||||
if (c == '?') { v->esc_private = 1; return; }
|
||||
// Private markers (< = > ?) and intermediates (space to /): sequences
|
||||
// this terminal does not speak, but must still consume to the final byte.
|
||||
// Ending at the marker instead printed the rest -- tmux asking for
|
||||
// extended keys as ESC [ > 4 ; 1 m left "4;1m" on the screen.
|
||||
if (c >= '<' && c <= '?') { v->esc_private = 1; return; }
|
||||
if (c >= 0x20 && c <= 0x2F) { v->esc_private = 1; return; }
|
||||
if (c >= '0' && c <= '9') {
|
||||
if (v->esc_nparm == 0) v->esc_nparm = 1;
|
||||
if (v->esc_nparm <= VT_MAXPARM)
|
||||
|
|
|
|||
Loading…
Reference in a new issue