From 89cbbb3aa6aea7ca7410096aeacc3316a3532344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Andr=C3=A9=20Tanner?= Date: Tue, 18 Feb 2014 20:06:51 +0100 Subject: [PATCH] Initial import --- .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(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 abduco.1 create mode 100644 abduco.c create mode 100644 client.c create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 debug.c create mode 100644 server.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ae5602 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# normal ignores +.* +*.[ao] +*.lo +*.so +tags +!.gitignore diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0928b6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013-2014 Marc André Tanner + +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 new file mode 100644 index 0000000..fd201cc --- /dev/null +++ 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 new file mode 100644 index 0000000..1b4f48c --- /dev/null +++ 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 new file mode 100644 index 0000000..611e35b --- /dev/null +++ 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 diff --git a/abduco.c b/abduco.c new file mode 100644 index 0000000..4d35d9b --- /dev/null +++ b/abduco.c @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2013-2014 Marc André Tanner + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__linux__) || defined(__CYGWIN__) +# include +#elif defined(__FreeBSD__) +# include +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) +# include +#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 new file mode 100644 index 0000000..5b9ae2c --- /dev/null +++ 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 new file mode 100644 index 0000000..f915943 --- /dev/null +++ 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 new file mode 100644 index 0000000..9c2d991 --- /dev/null +++ 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 new file mode 100644 index 0000000..806df97 --- /dev/null +++ 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 new file mode 100644 index 0000000..da1af89 --- /dev/null +++ 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); +}