blob: 985f281f6dc0f1c8ddf074c6b721995dee29a8ae [file] [log] [blame]
/*
* SHL - PTY Helpers
*
* Copyright (c) 2011-2013 David Herrmann <dh.herrmann@gmail.com>
* Dedicated to the Public Domain
*/
/*
* PTY Helpers
*/
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pty.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/uio.h>
#include <termios.h>
#include <unistd.h>
#include "shl_pty.h"
#define SHL_PTY_BUFSIZE 16384
/*
* Ring Buffer
* Our PTY helper buffers outgoing data so the caller can rely on write
* operations to always succeed (except for OOM). To buffer data in a PTY we
* use a fast ring buffer to avoid heavy re-allocations on every write.
*
* Note that this allows users to use pty-writes for small data without
* causing heavy allocations in the PTY layer. This is quite important for
* keyboard-handling or other DEC-VT emulations.
*/
struct ring {
char *buf;
size_t size;
size_t start;
size_t end;
};
#define RING_MASK(_r, _v) ((_v) & ((_r)->size - 1))
/*
* Resize ring-buffer to size @nsize. @nsize must be a power-of-2, otherwise
* ring operations will behave incorrectly.
*/
static int ring_resize(struct ring *r, size_t nsize)
{
char *buf;
buf = malloc(nsize);
if (!buf)
return -ENOMEM;
if (r->end == r->start) {
r->end = 0;
r->start = 0;
} else if (r->end > r->start) {
memcpy(buf, &r->buf[r->start], r->end - r->start);
r->end -= r->start;
r->start = 0;
} else {
memcpy(buf, &r->buf[r->start], r->size - r->start);
memcpy(&buf[r->size - r->start], r->buf, r->end);
r->end += r->size - r->start;
r->start = 0;
}
free(r->buf);
r->buf = buf;
r->size = nsize;
return 0;
}
/* Compute next higher power-of-2 of @v. Returns 4096 in case v is 0. */
static size_t ring_pow2(size_t v)
{
size_t i;
if (!v)
return 4096;
--v;
for (i = 1; i < 8 * sizeof (size_t); i *= 2)
v |= v >> i;
return ++v;
}
/*
* Resize ring-buffer to provide enough room for @add bytes of new data. This
* resizes the buffer if it is too small. It returns -ENOMEM on OOM and 0 on
* success.
*/
static int ring_grow(struct ring *r, size_t add)
{
size_t len;
/*
* Note that "end == start" means "empty buffer". Hence, we can never
* fill the last byte of a buffer. That means, we must account for an
* additional byte here ("end == start"-byte).
*/
if (r->end < r->start)
len = r->start - r->end;
else
len = r->start + r->size - r->end;
/* don't use ">=" as "end == start" would be ambigious */
if (len > add)
return 0;
/* +1 for additional "end == start" byte */
len = r->size + add - len + 1;
len = ring_pow2(len);
if (len <= r->size)
return -ENOMEM;
return ring_resize(r, len);
}
/*
* Push @len bytes from @u8 into the ring buffer. The buffer is resized if it
* is too small. -ENOMEM is returned on OOM, 0 on success.
*/
static int ring_push(struct ring *r, const char *u8, size_t len)
{
int err;
size_t l;
err = ring_grow(r, len);
if (err < 0)
return err;
if (r->start <= r->end) {
l = r->size - r->end;
if (l > len)
l = len;
memcpy(&r->buf[r->end], u8, l);
r->end = RING_MASK(r, r->end + l);
len -= l;
u8 += l;
}
if (!len)
return 0;
memcpy(&r->buf[r->end], u8, len);
r->end = RING_MASK(r, r->end + len);
return 0;
}
/*
* Get data pointers for current ring-buffer data. @vec must be an array of 2
* iovec objects. They are filled according to the data available in the
* ring-buffer. 0, 1 or 2 is returned according to the number of iovec objects
* that were filled (0 meaning buffer is empty).
*
* Hint: "struct iovec" is defined in <sys/uio.h> and looks like this:
* struct iovec {
* void *iov_base;
* size_t iov_len;
* };
*/
static size_t ring_peek(struct ring *r, struct iovec *vec)
{
if (r->end > r->start) {
vec[0].iov_base = &r->buf[r->start];
vec[0].iov_len = r->end - r->start;
return 1;
} else if (r->end < r->start) {
vec[0].iov_base = &r->buf[r->start];
vec[0].iov_len = r->size - r->start;
vec[1].iov_base = r->buf;
vec[1].iov_len = r->end;
return 2;
} else {
return 0;
}
}
/*
* Remove @len bytes from the start of the ring-buffer. Note that we protect
* against overflows so removing more bytes than available is safe.
*/
static void ring_pop(struct ring *r, size_t len)
{
size_t l;
if (r->start > r->end) {
l = r->size - r->start;
if (l > len)
l = len;
r->start = RING_MASK(r, r->start + l);
len -= l;
}
if (!len)
return;
l = r->end - r->start;
if (l > len)
l = len;
r->start = RING_MASK(r, r->start + l);
}
/*
* PTY
* A PTY object represents a single PTY connection between a master and a
* child. The child process is fork()ed so the caller controls what program
* will be run.
*
* Programs like /bin/login tend to perform a vhangup() on their TTY
* before running the login procedure. This also causes the pty master
* to get a EPOLLHUP event as long as no client has the TTY opened.
* This means, we cannot use the TTY connection as reliable way to track
* the client. Instead, we _must_ rely on the PID of the client to track
* them.
* However, this has the side effect that if the client forks and the
* parent exits, we loose them and restart the client. But this seems to
* be the expected behavior so we implement it here.
*
* Unfortunately, epoll always polls for EPOLLHUP so as long as the
* vhangup() is ongoing, we will _always_ get EPOLLHUP and cannot sleep.
* This gets worse if the client closes the TTY but doesn't exit.
* Therefore, we the fd must be edge-triggered in the epoll-set so we
* only get the events once they change. This has to be taken into by the
* user of shl_pty. As many event-loops don't support edge-triggered
* behavior, you can use the shl_pty_bridge interface.
*
* Note that shl_pty does not track SIGHUP, you need to do that yourself
* and call shl_pty_close() once the client exited.
*/
struct shl_pty {
unsigned long ref;
int fd;
pid_t child;
char in_buf[SHL_PTY_BUFSIZE];
struct ring out_buf;
shl_pty_input_cb cb;
void *data;
};
enum shl_pty_msg {
SHL_PTY_FAILED,
SHL_PTY_SETUP,
};
static char pty_recv(int fd)
{
int r;
char d;
do {
r = read(fd, &d, 1);
}
while (r < 0 && (errno == EINTR || errno == EAGAIN));
return (r <= 0) ? SHL_PTY_FAILED : d;
}
static int pty_send(int fd, char d)
{
int r;
do {
r = write(fd, &d, 1);
}
while (r < 0 && (errno == EINTR || errno == EAGAIN));
return (r == 1) ? 0 : -EINVAL;
}
static int
pty_setup_child(int slave, unsigned short term_width,
unsigned short term_height)
{
struct termios attr;
struct winsize ws;
/* get terminal attributes */
if (tcgetattr(slave, &attr) < 0)
return -errno;
/* erase character should be normal backspace, PLEASEEE! */
attr.c_cc[VERASE] = 010;
/* set changed terminal attributes */
if (tcsetattr(slave, TCSANOW, &attr) < 0)
return -errno;
memset(&ws, 0, sizeof (ws));
ws.ws_col = term_width;
ws.ws_row = term_height;
if (ioctl(slave, TIOCSWINSZ, &ws) < 0)
return -errno;
if (dup2(slave, STDIN_FILENO) != STDIN_FILENO
|| dup2(slave, STDOUT_FILENO) != STDOUT_FILENO
|| dup2(slave, STDERR_FILENO) != STDERR_FILENO)
return -errno;
return 0;
}
static int pty_init_child(int fd)
{
int r;
sigset_t sigset;
char *slave_name;
int slave, i;
pid_t pid;
/* unlockpt() requires unset signal-handlers */
sigemptyset(&sigset);
r = sigprocmask(SIG_SETMASK, &sigset, NULL);
if (r < 0)
return -errno;
for (i = 1; i < SIGSYS; ++i)
signal(i, SIG_DFL);
r = grantpt(fd);
if (r < 0)
return -errno;
r = unlockpt(fd);
if (r < 0)
return -errno;
slave_name = ptsname(fd);
if (!slave_name)
return -errno;
/* open slave-TTY */
slave = open(slave_name, O_RDWR | O_CLOEXEC | O_NOCTTY);
if (slave < 0)
return -errno;
/* open session so we loose our controlling TTY */
pid = setsid();
if (pid < 0) {
close(slave);
return -errno;
}
/* set controlling TTY */
r = ioctl(slave, TIOCSCTTY, 0);
if (r < 0) {
close(slave);
return -errno;
}
return slave;
}
pid_t
shl_pty_open(struct shl_pty ** out, shl_pty_input_cb cb, void *data,
unsigned short term_width, unsigned short term_height,
int pts_fd)
{
struct shl_pty *pty;
pid_t pid;
int fd, comm[2], slave, r;
char d;
pty = calloc(1, sizeof (*pty));
if (!pty)
return -ENOMEM;
if (pts_fd >= 0)
fd = pts_fd;
else
fd = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) {
free(pty);
return -errno;
}
r = pipe2(comm, O_CLOEXEC);
if (r < 0) {
r = -errno;
close(fd);
free(pty);
return r;
}
pid = fork();
if (pid < 0) {
/* error */
pid = -errno;
close(comm[0]);
close(comm[1]);
close(fd);
free(pty);
return pid;
} else if (!pid) {
/* child */
close(comm[0]);
free(pty);
slave = pty_init_child(fd);
close(fd);
if (slave < 0)
exit(1);
r = pty_setup_child(slave, term_width, term_height);
if (r < 0)
exit(1);
/* close slave if it's not one of the std-fds */
if (slave > 2)
close(slave);
/* wake parent */
pty_send(comm[1], SHL_PTY_SETUP);
close(comm[1]);
*out = NULL;
return pid;
}
/* parent */
close(comm[1]);
pty->ref = 1;
pty->fd = fd;
pty->child = pid;
pty->cb = cb;
pty->data = data;
/* wait for child setup */
d = pty_recv(comm[0]);
if (d != SHL_PTY_SETUP) {
close(comm[0]);
close(fd);
free(pty);
return -EINVAL;
}
close(comm[0]);
*out = pty;
return pid;
}
void shl_pty_ref(struct shl_pty *pty)
{
if (!pty || !pty->ref)
return;
++pty->ref;
}
void shl_pty_unref(struct shl_pty *pty)
{
if (!pty || !pty->ref || --pty->ref)
return;
shl_pty_close(pty);
free(pty->out_buf.buf);
free(pty);
}
void shl_pty_close(struct shl_pty *pty)
{
if (pty->fd < 0)
return;
close(pty->fd);
pty->fd = -1;
}
bool shl_pty_is_open(struct shl_pty *pty)
{
return pty->fd >= 0;
}
int shl_pty_get_fd(struct shl_pty *pty)
{
return pty->fd;
}
pid_t shl_pty_get_child(struct shl_pty *pty)
{
return pty->child;
}
static void pty_write(struct shl_pty *pty)
{
struct iovec vec[2];
size_t num;
ssize_t r;
num = ring_peek(&pty->out_buf, vec);
if (!num)
return;
/* ignore errors in favor of SIGCHLD; (we're edge-triggered, anyway) */
r = writev(pty->fd, vec, (int) num);
if (r >= 0)
ring_pop(&pty->out_buf, (size_t) r);
}
static int pty_read(struct shl_pty *pty)
{
ssize_t len, num;
/* We're edge-triggered, means we need to read the whole queue. This,
* however, might cause us to stall if the writer is faster than we
* are. Therefore, we have some rather arbitrary limit on how fast
* we read. If we reach it, we simply return EAGAIN to the caller and
* let them deal with it. */
/* Setting this as low as possible. It will slow down long
* file dumps, but, will respond to ctrl-c quicker, which is better
* for our usage model
*/
num = 1;
do {
len = read(pty->fd, pty->in_buf, sizeof (pty->in_buf));
if (len > 0)
pty->cb(pty, pty->in_buf, len, pty->data);
}
while (len > 0 && --num);
return !num ? -EAGAIN : 0;
}
int shl_pty_dispatch(struct shl_pty *pty)
{
int r;
r = pty_read(pty);
pty_write(pty);
return r;
}
int shl_pty_write(struct shl_pty *pty, const char *u8, size_t len)
{
if (!shl_pty_is_open(pty))
return -ENODEV;
return ring_push(&pty->out_buf, u8, len);
}
int shl_pty_signal(struct shl_pty *pty, int sig)
{
int r;
if (!shl_pty_is_open(pty))
return -ENODEV;
r = ioctl(pty->fd, TIOCSIG, sig);
return (r < 0) ? -errno : 0;
}
int
shl_pty_resize(struct shl_pty *pty, unsigned short term_width,
unsigned short term_height)
{
struct winsize ws;
int r;
if (!shl_pty_is_open(pty))
return -ENODEV;
memset(&ws, 0, sizeof (ws));
ws.ws_col = term_width;
ws.ws_row = term_height;
/*
* This will send SIGWINCH to the pty slave foreground process group.
* We will also get one, but we don't need it.
*/
r = ioctl(pty->fd, TIOCSWINSZ, &ws);
return (r < 0) ? -errno : 0;
}
/*
* PTY Bridge
* The PTY bridge wraps multiple ptys in a single file-descriptor. It is
* enough for the caller to listen for read-events on the fd.
*
* This interface is provided to allow integration of PTYs into event-loops
* that do not support edge-triggered interfaces. There is no other reason
* to use this bridge.
*/
int shl_pty_bridge_new(void)
{
int fd;
fd = epoll_create1(EPOLL_CLOEXEC);
if (fd < 0)
return -errno;
return fd;
}
void shl_pty_bridge_free(int bridge)
{
close(bridge);
}
int shl_pty_bridge_dispatch(int bridge, int timeout)
{
struct epoll_event up, ev;
struct shl_pty *pty;
int fd, r;
r = epoll_wait(bridge, &ev, 1, timeout);
if (r < 0) {
if (errno == EAGAIN || errno == EINTR)
return 0;
return -errno;
}
if (!r)
return 0;
pty = ev.data.ptr;
r = shl_pty_dispatch(pty);
if (r == -EAGAIN) {
/* EAGAIN means we couldn't dispatch data fast enough. Modify
* the fd in the epoll-set so we get edge-triggered events
* next round. */
memset(&up, 0, sizeof (up));
up.events = EPOLLIN | EPOLLOUT | EPOLLET;
up.data.ptr = pty;
fd = shl_pty_get_fd(pty);
epoll_ctl(bridge, EPOLL_CTL_ADD, fd, &up);
}
return 0;
}
int shl_pty_bridge_add(int bridge, struct shl_pty *pty)
{
struct epoll_event ev;
int r, fd;
memset(&ev, 0, sizeof (ev));
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
ev.data.ptr = pty;
fd = shl_pty_get_fd(pty);
r = epoll_ctl(bridge, EPOLL_CTL_ADD, fd, &ev);
if (r < 0)
return -errno;
return 0;
}
void shl_pty_bridge_remove(int bridge, struct shl_pty *pty)
{
int fd;
fd = shl_pty_get_fd(pty);
epoll_ctl(bridge, EPOLL_CTL_DEL, fd, NULL);
}