abduco

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | LICENSE

commit 89cbbb3aa6aea7ca7410096aeacc3316a3532344
Author: Marc André Tanner <mat@brain-dump.org>
Date:   Tue, 18 Feb 2014 20:06:51 +0100

Initial import

Diffstat:
.gitignore | 7+++++++
LICENSE | 13+++++++++++++
Makefile | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
README | 8++++++++
abduco.1 | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
abduco.c | 429+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
client.c | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
config.def.h | 2++
config.mk | 17+++++++++++++++++
debug.c | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.c | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 1093 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,7 @@ +# normal ignores +.* +*.[ao] +*.lo +*.so +tags +!.gitignore diff --git a/LICENSE b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013-2014 Marc André Tanner <mat at brain-dump.org> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,61 @@ +include config.mk + +SRC += abduco.c +OBJ = ${SRC:.c=.o} + +all: clean options abduco + +options: + @echo abduco build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +config.h: + cp config.def.h config.h + +.c.o: + @echo CC $< + @${CC} -c ${CFLAGS} $< + +${OBJ}: config.h config.mk + +abduco: ${OBJ} + @echo CC -o $@ + @${CC} -o $@ ${OBJ} ${LDFLAGS} + +debug: clean + @make CFLAGS='${DEBUG_CFLAGS}' + +clean: + @echo cleaning + @rm -f abduco ${OBJ} abduco-${VERSION}.tar.gz + +dist: clean + @echo creating dist tarball + @mkdir -p abduco-${VERSION} + @cp -R LICENSE Makefile README config.def.h config.mk \ + ${SRC} client.c server.c forkpty-aix.c abduco.1 abduco-${VERSION} + @tar -cf abduco-${VERSION}.tar abduco-${VERSION} + @gzip abduco-${VERSION}.tar + @rm -rf abduco-${VERSION} + +install: abduco + @echo stripping executable + @strip -s abduco + @echo installing executable file to ${DESTDIR}${PREFIX}/bin + @mkdir -p ${DESTDIR}${PREFIX}/bin + @cp -f abduco ${DESTDIR}${PREFIX}/bin + @chmod 755 ${DESTDIR}${PREFIX}/bin/abduco + @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 + @mkdir -p ${DESTDIR}${MANPREFIX}/man1 + @sed "s/VERSION/${VERSION}/g" < abduco.1 > ${DESTDIR}${MANPREFIX}/man1/abduco.1 + @chmod 644 ${DESTDIR}${MANPREFIX}/man1/abduco.1 + +uninstall: + @echo removing executable file from ${DESTDIR}${PREFIX}/bin + @rm -f ${DESTDIR}${PREFIX}/bin/abduco + @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 + @rm -f ${DESTDIR}${MANPREFIX}/man1/abduco.1 + +.PHONY: all options clean dist install uninstall debug diff --git a/README b/README @@ -0,0 +1,8 @@ +abduco +====== + +abduco provides the session management and attach/detach functionality +of screen(1) and tmux(1) together with dvtm(1) it is a nearly complete +replacement of the before mentioned tools. + +See http://www.brain-dump.org/projects/abduco for the latest version. diff --git a/abduco.1 b/abduco.1 @@ -0,0 +1,71 @@ +.TH ABDUCO 1 abduco\-VERSION +.nh +.SH NAME +abduco +.SH SYNOPSIS +.B abduco +.RB [ \-e +.IR detachkey ] +.RB \-c +.RB name +.RB command +.RI [ args \ ... "" ] +.br +.B abduco +.RB [ \-e +.IR detachkey ] +.RB \-n +.RB name +.RB command +.RI [ args \ ... "" ] +.br +.B abduco +.RB [ \-e +.IR detachkey ] +.RB \-A +.RB name +.RB command +.RI [ args \ ... "" ] +.br +.B abduco +.RB [ \-e +.IR detachkey ] +.RB \-a +.RB name +.br +.SH DESCRIPTION +.B abduco +provides a way to disconnect a given application from it's controlling +terminal thus it provides roughly the same session attach/detach support as +.BR screen(1) , " tmux(1)" " or" " dtach(1)". + +By default all session related information is stored in +.B $HOME/.abduco +or as a fallback in +.BR /tmp/.abduco . +However if a given session name starts either with a dot or a forward slash +it is interpreted as a path name and used unmodified i.e. relatively to the +current working directory. +.SH OPTIONS +If no command line arguments are given all currently active sessions are +printed together with their respective creation date. +.TP +.B \-v +Print version information to standard output and exit. +.TP +.BI \-e \ detachkey +Set the key to detach which by default is set to CTRL+\\ i.e. ^\\ to detachkey. +.TP +.BI \-c +Create a new session and attach immediately to it. +.TP +.BI \-n +Create a new session but don't attach to it. +.TP +.BI \-A +Try to connect to an existing session, upon failure create said session and attach immediately to it. +.TP +.BI \-a +Attach to an existing session. +.SH AUTHOR +abduco is written by Marc André Tanner <mat at brain-dump.org> diff --git a/abduco.c b/abduco.c @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2013-2014 Marc André Tanner <mat at brain-dump.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdarg.h> +#include <stdlib.h> +#include <stdbool.h> +#include <stddef.h> +#include <signal.h> +#include <libgen.h> +#include <string.h> +#include <limits.h> +#include <dirent.h> +#include <termios.h> +#include <time.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/socket.h> +#include <sys/un.h> +#if defined(__linux__) || defined(__CYGWIN__) +# include <pty.h> +#elif defined(__FreeBSD__) +# include <libutil.h> +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) +# include <util.h> +#endif + +#if defined CTRL && defined _AIX + #undef CTRL +#endif +#ifndef CTRL + #define CTRL(k) ((k) & 0x1F) +#endif + +#include "config.h" + +#ifdef _AIX +# include "forkpty-aix.c" +#endif +#define CLIENT_TIMEOUT 100 +#define countof(arr) (sizeof(arr) / sizeof((arr)[0])) + +enum PacketType { + MSG_CONTENT = 0, + MSG_ATTACH = 1, + MSG_DETACH = 2, + MSG_RESIZE = 3, + MSG_REDRAW = 4, +}; + +/* packet sent from client to server */ +typedef struct { + unsigned char type; + unsigned char len; + union { + char msg[sizeof(struct winsize)]; + struct winsize ws; + int i; + } u; +} ClientPacket; + +/* packet sent from server to all clients */ +typedef struct { + char buf[BUFSIZ]; + ssize_t len; +} ServerPacket; + +typedef struct { + ClientPacket pkt; + size_t off; +} ClientPacketState; + +typedef struct { + ServerPacket *pkt; + size_t off; +} ServerPacketState; + +typedef struct Client Client; +struct Client { + ServerPacketState output; /* display output as received from server */ + ClientPacketState input; /* input as sent to the server */ + int socket; + enum { + STATE_CONNECTED, + STATE_ATTACHED, + STATE_DETACHED, + STATE_DISCONNECTED, + } state; + time_t last_activity; + bool need_resize; + Client *next; +}; + +typedef struct { + Client *clients; + int client_count; + int socket; + ServerPacket pty_output; + ClientPacketState pty_input; + ClientPacket queue[10]; + int queue_count; + int queue_insert; + int queue_remove; + int pty; + int exit_status; + struct termios term; + pid_t pid; + volatile sig_atomic_t running; + const char *name; +} Server; + +static Server server = { .running = true, .exit_status = -1 }; +static struct termios orig_term, cur_term; +bool has_term; + +static struct sockaddr_un sockaddr = { + .sun_family = AF_UNIX, +}; + +static int create_socket(const char *name); +static void die(const char *s); +static void info(const char *str, ...); + +static bool is_client_packet_complete(ClientPacketState *pkt) { + return pkt->off == sizeof pkt->pkt; +} + +static bool is_server_packet_complete(ServerPacketState *pkt) { + return pkt->pkt && pkt->off == pkt->pkt->len; +} + +static bool is_server_packet_nonempty(ServerPacketState *pkt) { + return pkt->pkt && pkt->pkt->len > 0; +} + +#include "debug.c" +#include "client.c" +#include "server.c" + +static void info(const char *str, ...) { + va_list ap; + va_start(ap, str); + fprintf(stdout, "\e[999H\r\n"); + if (str) { + fprintf(stdout, "%s: ", server.name); + vfprintf(stdout, str, ap); + fprintf(stdout, "\r\n"); + } + fflush(stdout); + va_end(ap); +} + +static void die(const char *s) { + perror(s); + exit(EXIT_FAILURE); +} + +static void usage() { + fprintf(stderr, "usage: abduco [-a|-A|-c|-n] [-e detachkey] name command\n"); + exit(EXIT_FAILURE); +} + +static int create_socket_dir() { + size_t maxlen = sizeof(sockaddr.sun_path); + char *dir = getenv("HOME"); + if (!dir) + dir = "/tmp"; + int len = snprintf(sockaddr.sun_path, maxlen, "%s/.%s/", dir, server.name); + if (len >= maxlen) + return -1; + if (mkdir(sockaddr.sun_path, 0750) == -1 && errno != EEXIST) + return -1; + return len; +} + +static int create_socket(const char *name) { + size_t maxlen = sizeof(sockaddr.sun_path); + if (name[0] == '.' || name[0] == '/') { + strncpy(sockaddr.sun_path, name, maxlen); + if (sockaddr.sun_path[maxlen-1]) + return -1; + } else { + int len = create_socket_dir(), rem = strlen(name); + if (len == -1 || maxlen - len - rem <= 0) + return -1; + strncat(sockaddr.sun_path, name, maxlen - len - 1); + } + return socket(AF_LOCAL, SOCK_STREAM, 0); +} + +static bool create_session(const char *name, char * const argv[]) { + int pipefds[2]; + if (pipe(pipefds) == -1) + return false; + if ((server.socket = create_socket(name)) == -1) + return false; + socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1; + mode_t mode = S_IRUSR|S_IWUSR; + fchmod(server.socket, mode); + if (bind(server.socket, (struct sockaddr*)&sockaddr, socklen) == -1) + return false; + if (listen(server.socket, 5) == -1) + goto error; + if (fchmod(server.socket, mode) == -1 && chmod(sockaddr.sun_path, mode) == -1) + goto error; + + pid_t pid; + char errormsg[255]; + struct sigaction sa; + + switch ((pid = fork())) { + case 0: /* child process */ + setsid(); + close(pipefds[0]); + switch ((pid = fork())) { + case 0: /* child process */ + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sa.sa_handler = server_pty_died_handler; + sigaction(SIGCHLD, &sa, NULL); + switch (server.pid = forkpty(&server.pty, NULL, has_term ? &server.term : NULL, NULL)) { + case 0: /* child process */ + fcntl(pipefds[1], F_SETFD, FD_CLOEXEC); + close(server.socket); + execvp(argv[0], argv); + snprintf(errormsg, sizeof(errormsg), "server-execvp: %s\n", strerror(errno)); + write_all(pipefds[1], errormsg, strlen(errormsg)); + close(pipefds[1]); + exit(EXIT_FAILURE); + break; + case -1: + die("server-forkpty"); + break; + default: + /* SIGTTIN, SIGTTU */ + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + sa.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &sa, NULL); + sigaction(SIGHUP, &sa, NULL); + chdir("/"); + #ifdef NDEBUG + int fd = open("/dev/null", O_RDWR); + dup2(fd, 0); + dup2(fd, 1); + dup2(fd, 2); + #endif /* NDEBUG */ + close(pipefds[1]); + server_mainloop(); + break; + } + break; + default: + close(pipefds[1]); + exit(EXIT_SUCCESS); + break; + } + break; + case -1: /* fork failed */ + return false; + default: /* parent */ + close(pipefds[1]); + int status; + wait(&status); /* wait for first fork */ + if ((status = read_all(pipefds[0], errormsg, sizeof(errormsg))) > 0) { + write_all(STDERR_FILENO, errormsg, status); + exit(EXIT_FAILURE); + } + close(pipefds[0]); + } + return true; +error: + unlink(sockaddr.sun_path); + return false; +} + +static bool attach_session(const char *name) { + if (server.socket > 0) + close(server.socket); + if ((server.socket = create_socket(name)) == -1) + return false; + socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1; + if (connect(server.socket, (struct sockaddr*)&sockaddr, socklen) == -1) + return false; + if (server_set_socket_non_blocking(server.socket) == -1) + return false; + + struct sigaction sa; + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sa.sa_handler = client_sigwinch_handler; + sigaction(SIGWINCH, &sa, NULL); + sa.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &sa, NULL); + atexit(client_restore_terminal); + + cur_term = orig_term; + cur_term.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXOFF); + cur_term.c_oflag &= ~(OPOST); + cur_term.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); + cur_term.c_cflag &= ~(CSIZE|PARENB); + cur_term.c_cflag |= CS8; + cur_term.c_cc[VLNEXT] = _POSIX_VDISABLE; + cur_term.c_cc[VMIN] = 1; + cur_term.c_cc[VTIME] = 0; + tcsetattr(0, TCSADRAIN, &cur_term); + + client_clear_screen(); + switch (client_mainloop()) { + case -1: + info("detached"); + break; + case EIO: + info("exited due to I/O errors: %s", strerror(errno)); + break; + } + + return true; +} + +static int list_session() { + if (create_socket_dir() == -1) + return 1; + chdir(sockaddr.sun_path); + DIR *d = opendir(sockaddr.sun_path); + if (!d) + return 1; + puts("Active sessions"); + struct dirent *e; + while ((e = readdir(d))) { + if (e->d_name[0] != '.') { + struct stat sb; char buf[255]; + if (stat(e->d_name, &sb) == 0) { + strftime(buf, sizeof(buf), "%A%t %d.%m.%Y %T", localtime(&sb.st_atime)); + printf(" %s\t%s\n", buf, e->d_name); + } + } + } + closedir(d); + return 0; +} + +int main(int argc, char *argv[]) { + char *session = NULL, **cmd = NULL, action = '\0'; + server.name = basename(argv[0]); + if (argc == 1) + exit(list_session()); + for (int arg = 1; arg < argc; arg++) { + if (argv[arg][0] != '-') { + if (!session) { + session = argv[arg]; + continue; + } else if (!cmd) { + cmd = &argv[arg]; + break; + } + } + if (session) + usage(); + switch (argv[arg][1]) { + case 'a': + case 'A': + case 'c': + case 'n': + action = argv[arg][1]; + break; + case 'e': + if (arg + 1 >= argc) + usage(); + char *esc = argv[++arg]; + if (esc[0] == '^' && esc[1]) + *esc = CTRL(esc[1]); + KEY_DETACH = *esc; + break; + case 'v': + puts("abduco-"VERSION" © 2013-2014 Marc André Tanner"); + exit(EXIT_SUCCESS); + default: + usage(); + } + } + + if (!action || !session || (action != 'a' && !cmd)) + usage(); + + if (tcgetattr(STDIN_FILENO, &orig_term) != -1) { + server.term = orig_term; + has_term = true; + } + + switch (action) { + redo: + case 'n': + case 'c': + if (!create_session(session, cmd)) + die("create-session"); + if (action == 'n') + break; + case 'a': + case 'A': + if (!attach_session(session)) { + if (action == 'A') { + action = 'c'; + goto redo; + } + die("attach-session"); + } + } + + return 0; +} diff --git a/client.c b/client.c @@ -0,0 +1,143 @@ +static Client client; + +static void client_sigwinch_handler(int sig) { + client.need_resize = true; +} + +static ssize_t write_all(int fd, const char *buf, size_t len) { + ssize_t ret = len; + while (len > 0) { + int res = write(fd, buf, len); + if (res < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + continue; + return -1; + } + if (res == 0) + return ret - len; + buf += res; + len -= res; + } + return ret; +} + +static ssize_t read_all(int fd, char *buf, size_t len) { + ssize_t ret = len; + while (len > 0) { + int res = read(fd, buf, len); + if (res < 0) { + if (errno == EWOULDBLOCK) + return ret - len; + if (errno == EAGAIN || errno == EINTR) + continue; + return -1; + } + if (res == 0) + return ret - len; + buf += res; + len -= res; + } + return ret; +} + +static bool client_send_packet(ClientPacket *pkt) { + print_client_packet("client-send:", pkt); + if (write_all(server.socket, (char *)pkt, sizeof(ClientPacket)) != sizeof(ClientPacket)) { + debug("FAILED\n"); + server.running = false; + return false; + } + return true; +} + +static bool client_recv_packet(ServerPacket *pkt) { + pkt->len = read_all(server.socket, pkt->buf, sizeof(pkt->buf)); + print_server_packet("client-recv:", pkt); + if (pkt->len <= 0) { + debug("FAILED\n"); + server.running = false; + return false; + } + return true; +} + +static void client_clear_screen() { + printf("\e[H\e[J"); + fflush(stdout); +} + +static void client_show_cursor() { + printf("\e[?25h"); + fflush(stdout); +} + +static void client_restore_terminal() { + if (has_term) + tcsetattr(0, TCSADRAIN, &orig_term); + client_show_cursor(); +} + +static int client_mainloop() { + client.need_resize = true; + while (server.running) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + FD_SET(server.socket, &fds); + + if (client.need_resize) { + struct winsize ws; + if (ioctl(0, TIOCGWINSZ, &ws) != -1) { + ClientPacket pkt = { + .type = MSG_RESIZE, + .u = { .ws = ws }, + .len = sizeof(ws), + }; + if (client_send_packet(&pkt)) + client.need_resize = false; + } + } + + if (select(server.socket + 1, &fds, NULL, NULL, NULL) == -1) { + if (errno == EINTR) + continue; + die("client-mainloop"); + } + + if (FD_ISSET(server.socket, &fds)) { + ServerPacket pkt; + if (client_recv_packet(&pkt)) + write_all(STDOUT_FILENO, pkt.buf, pkt.len); + } + + if (FD_ISSET(STDIN_FILENO, &fds)) { + ClientPacket pkt = { .type = MSG_CONTENT }; + ssize_t len = read(STDIN_FILENO, pkt.u.msg, sizeof(pkt.u.msg)); + if (len == -1 && errno != EAGAIN && errno != EINTR) + die("client-stdin"); + if (len > 0) { + pkt.len = len; + if (pkt.u.msg[0] == KEY_REDRAW) { + client.need_resize = true; + } else if (pkt.u.msg[0] == KEY_DETACH) { + pkt.type = MSG_DETACH; + client_send_packet(&pkt); + return -1; + } else if (pkt.u.msg[0] == cur_term.c_cc[VSUSP]) { + pkt.type = MSG_DETACH; + client_send_packet(&pkt); + tcsetattr(0, TCSADRAIN, &orig_term); + client_show_cursor(); + info(NULL); + kill(getpid(), SIGTSTP); + tcsetattr(0, TCSADRAIN, &cur_term); + client.need_resize = true; + } else { + client_send_packet(&pkt); + } + } + } + } + + return 0; +} diff --git a/config.def.h b/config.def.h @@ -0,0 +1,2 @@ +static char KEY_DETACH = CTRL('q'); +static char KEY_REDRAW = CTRL('L'); diff --git a/config.mk b/config.mk @@ -0,0 +1,17 @@ +# abduco version +VERSION = 0.1 + +# Customize below to fit your system + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +INCS = -I. -I/usr/include -I/usr/local/include +LIBS = -lc -lutil + +CFLAGS += -std=c99 -Os ${INCS} -DVERSION=\"${VERSION}\" -DNDEBUG +LDFLAGS += -L/usr/lib -L/usr/local/lib ${LIBS} + +DEBUG_CFLAGS = ${CFLAGS} -UNDEBUG -O0 -g -ggdb -Wall + +CC = cc diff --git a/debug.c b/debug.c @@ -0,0 +1,62 @@ +#ifdef NDEBUG +static void debug(const char *errstr, ...) { } +static void print_client_packet(const char *prefix, ClientPacket *pkt) { } +static void print_client_packet_state(const char *prefix, ClientPacketState *pkt) { } +static void print_server_packet(const char *prefix, ServerPacket *pkt) { } +static void print_server_packet_state(const char *prefix, ServerPacketState *pkt) { } +#else + +static void debug(const char *errstr, ...) { + va_list ap; + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); +} + +static void print_client_packet(const char *prefix, ClientPacket *pkt) { + char *s = "UNKNOWN"; + switch (pkt->type) { + case MSG_CONTENT: + s = "CONTENT"; + break; + case MSG_ATTACH: + s = "ATTACH"; + break; + case MSG_DETACH: + s = "DETACH"; + break; + case MSG_RESIZE: + s = "RESIZE"; + break; + case MSG_REDRAW: + s = "REDRAW"; + break; + } + + if (pkt->type == MSG_CONTENT) { + fprintf(stderr, "%s %s len: %d content: ", prefix, s, pkt->len); + for (int i = 0; i < pkt->len && i < sizeof(pkt->u.msg); i++) + fprintf(stderr, "%c", pkt->u.msg[i]); + fprintf(stderr, "\n"); + } else { + fprintf(stderr, "%s %s\n", prefix, s); + } +} + +static void print_client_packet_state(const char *prefix, ClientPacketState *pkt) { + fprintf(stderr, "%s %d/%d\n", prefix, pkt->off, sizeof(ClientPacket)); + if (is_client_packet_complete(pkt)) + print_client_packet(prefix, &pkt->pkt); +} + +static void print_server_packet(const char *prefix, ServerPacket *pkt) { + fprintf(stderr, "%s len: %d buf: \n\t%s\n", prefix, pkt->len, pkt->buf); +} + +static void print_server_packet_state(const char *prefix, ServerPacketState *pkt) { + fprintf(stderr, "%s %d/%d\n", prefix, pkt->off, pkt->pkt->len); + if (is_server_packet_complete(pkt)) + print_server_packet(prefix, pkt->pkt); +} + +#endif /* NDEBUG */ diff --git a/server.c b/server.c @@ -0,0 +1,280 @@ +#define FD_SET_MAX(fd, set, maxfd) do { \ + FD_SET(fd, set); \ + if (fd > maxfd) \ + maxfd = fd; \ + } while (0) + +static Client *client_malloc(int socket) { + Client *c = calloc(1, sizeof(Client)); + if (!c) + return NULL; + c->socket = socket; + return c; +} + +static void client_free(Client *c) { + if (c && c->socket > 0) + close(c->socket); + free(c); +} + +static int server_set_socket_non_blocking(int sock) { + int flags; + if ((flags = fcntl(sock, F_GETFL, 0)) == -1) + flags = 0; + return fcntl(sock, F_SETFL, flags | O_NONBLOCK); +} + +static Client *server_accept_client(time_t now) { + int newfd = accept(server.socket, NULL, NULL); + if (newfd == -1) + return NULL; + Client *c = client_malloc(newfd); + if (!c) + return NULL; + server_set_socket_non_blocking(newfd); + c->socket = newfd; + c->state = STATE_CONNECTED; + c->last_activity = now; + c->next = server.clients; + server.clients = c; + server.client_count++; + return c; +} + +static bool server_read_pty(ServerPacket *pkt) { + ssize_t len = read(server.pty, pkt->buf, sizeof(pkt->buf)); + if (len != -1) + pkt->len = len; + else if (errno != EAGAIN && errno != EINTR) + server.running = false; + print_server_packet("server-read-pty:", pkt); + return len > 0; +} + +static bool server_write_pty(ClientPacketState *pkt) { + int count = pkt->pkt.len - pkt->off; + ssize_t len = write(server.pty, pkt->pkt.u.msg + pkt->off, count); + if (len == -1) { + if (errno != EAGAIN && errno != EINTR) + server.running = false; + } else { + pkt->off += len; + } + print_client_packet_state("server-write-pty:", pkt); + return len == count; +} + +static void server_place_packet(Client *c, ServerPacket *pkt) { + c->output.pkt = pkt; + c->output.off = 0; +} + +static bool server_recv_packet(Client *c) { + ClientPacketState *pkt = &c->input; + if (is_client_packet_complete(pkt)) + return true; + int count = sizeof(ClientPacket) - pkt->off; + ssize_t len = recv(c->socket, ((char *)&pkt->pkt) + pkt->off, count, 0); + switch (len) { + case -1: + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { + case 0: + c->state = STATE_DISCONNECTED; + } + break; + default: + pkt->off += len; + break; + } + print_client_packet_state("server-recv:", pkt); + return len == count; +} + +static bool server_send_packet(Client *c) { + ServerPacketState *pkt = &c->output; + if (is_server_packet_complete(pkt)) + return true; + int count = pkt->pkt->len - pkt->off; + ssize_t len = send(c->socket, pkt->pkt->buf + pkt->off, count, 0); + switch (len) { + case -1: + if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { + case 0: + c->state = STATE_DISCONNECTED; + } + break; + default: + pkt->off += len; + break; + } + print_server_packet_state("server-send:", pkt); + return len == count; +} + +static void server_pty_died_handler(int sig) { + int errsv = errno; + pid_t pid; + + while ((pid = waitpid(-1, &server.exit_status, WNOHANG)) != 0) { + if (pid == -1) + break; + server.exit_status = WEXITSTATUS(server.exit_status); + } + + debug("server pty died: %d\n", server.exit_status); + errno = errsv; +} + +static void server_atexit_handler() { + unlink(sockaddr.sun_path); +} + +static bool server_queue_empty() { + return server.queue_count == 0; +} + +static bool server_queue_packet(ClientPacket *pkt) { + if (server.queue_count >= countof(server.queue)) + return false; + server.queue[server.queue_insert] = *pkt; + server.queue_insert++; + server.queue_insert %= countof(server.queue); + server.queue_count++; + return true; +} + +static ClientPacket *server_peek_packet() { + return &server.queue[server.queue_remove]; +} + +static void server_dequeue_packet() { + server.queue_remove++; + server.queue_remove %= countof(server.queue); + server.queue_count--; +} + +static void server_mainloop() { + atexit(server_atexit_handler); + fd_set new_readfds, new_writefds; + FD_ZERO(&new_readfds); + FD_ZERO(&new_writefds); + FD_SET(server.socket, &new_readfds); + int new_fdmax = server.socket; + + for (;;) { + int fdmax = new_fdmax; + fd_set readfds = new_readfds; + fd_set writefds = new_writefds; + + if (select(fdmax+1, &readfds, &writefds, NULL, NULL) == -1) { + if (errno == EINTR) + continue; + die("server-mainloop"); + } + + FD_ZERO(&new_readfds); + FD_SET(server.socket, &new_readfds); + FD_ZERO(&new_writefds); + new_fdmax = server.socket; + + time_t now = time(NULL); + time_t timeout = now - CLIENT_TIMEOUT; + bool pty_data = false, clients_ready = true; + + if (FD_ISSET(server.socket, &readfds)) + server_accept_client(now); + + if (FD_ISSET(server.pty, &readfds)) { + pty_data = server_read_pty(&server.pty_output); + clients_ready = !pty_data; + } + + for (Client **prev_next = &server.clients, *c = server.clients; c;) { + if (c->state == STATE_DISCONNECTED) { + Client *t = c->next; + client_free(c); + *prev_next = c = t; + server.client_count--; + continue; + } + + if (FD_ISSET(c->socket, &readfds)) + server_recv_packet(c); + if (is_client_packet_complete(&c->input)) { + bool packet_handled = true; + switch (c->input.pkt.type) { + case MSG_CONTENT: + packet_handled = server_queue_packet(&c->input.pkt); + break; + case MSG_ATTACH: + case MSG_RESIZE: + c->state = STATE_ATTACHED; + ioctl(server.pty, TIOCSWINSZ, &c->input.pkt.u.ws); + case MSG_REDRAW: + kill(-server.pid, SIGWINCH); + break; + case MSG_DETACH: + c->state = STATE_DETACHED; + break; + default: /* ignore package */ + break; + } + + if (packet_handled) { + c->input.off = 0; + FD_SET_MAX(c->socket, &new_readfds, new_fdmax); + } + } else { + FD_SET_MAX(c->socket, &new_readfds, new_fdmax); + } + + if (pty_data) { + server_place_packet(c, &server.pty_output); + c->last_activity = now; + } + + if (FD_ISSET(c->socket, &writefds)) { + server_send_packet(c); + c->last_activity = now; + } + + if (!is_server_packet_complete(&c->output)) { + if (c->last_activity < timeout) { + c->state = STATE_DISCONNECTED; + } else if (is_server_packet_nonempty(&c->output)) { + clients_ready = false; + FD_SET_MAX(c->socket, &new_writefds, new_fdmax); + } + } + + if (c->state != STATE_ATTACHED) + clients_ready = false; + prev_next = &c->next; + c = c->next; + } + + if (clients_ready && server.clients) { + if (server.running) + FD_SET_MAX(server.pty, &new_readfds, new_fdmax); + else + break; + } + + if (FD_ISSET(server.pty, &writefds)) { + while (!server_queue_empty()) { + if (!server.pty_input.off) + server.pty_input.pkt = *server_peek_packet(); + if (!server_write_pty(&server.pty_input)) + break; + server_dequeue_packet(); + server.pty_input.off = 0; + } + } + + if (!server_queue_empty()) + FD_SET_MAX(server.pty, &new_writefds, new_fdmax); + } + + exit(EXIT_SUCCESS); +}