abduco-0.1/0000755000175000017500000000000012355725670011251 5ustar marcmarcabduco-0.1/config.mk0000644000175000017500000000045412355725670013052 0ustar marcmarc# abduco version VERSION = 0.1 # Customize below to fit your system PREFIX = /usr/local MANPREFIX = ${PREFIX}/share/man INCS = -I. LIBS = -lc -lutil CFLAGS += -std=c99 -Os ${INCS} -DVERSION=\"${VERSION}\" -DNDEBUG LDFLAGS += ${LIBS} DEBUG_CFLAGS = ${CFLAGS} -UNDEBUG -O0 -g -ggdb -Wall CC = cc abduco-0.1/testsuite.sh0000755000175000017500000000773712355725670013657 0ustar marcmarc#!/bin/bash ABDUCO="./abduco" # set detach key explicitly in case it was changed in config.h ABDUCO_OPTS="-e ^\\" [ ! -z "$1" ] && ABDUCO="$1" [ ! -x "$ABDUCO" ] && echo "usage: $0 /path/to/abduco" && exit 1 detach() { sleep 1 printf "" } dvtm_cmd() { printf "$1\n" sleep 1 } dvtm_session() { sleep 1 dvtm_cmd 'c' dvtm_cmd 'c' dvtm_cmd 'c' sleep 1 dvtm_cmd ' ' dvtm_cmd ' ' dvtm_cmd ' ' sleep 1 dvtm_cmd 'q' } # $1 => session-name, $2 => exit status expected_abduco_output() { echo "[?25habduco: $1: session terminated with exit status $2" } check_environment() { [ "`$ABDUCO | wc -l`" -gt 1 ] && echo Abduco session exists && exit 1; pgrep abduco && echo Abduco process exists && exit 1; return 0; } test_non_existing_command() { check_environment || return 1; $ABDUCO -c test ./non-existing-command &> /dev/null check_environment || return 1; } # $1 => session-name, $2 => command to execute run_test_attached() { check_environment || return 1; local name="$1" local cmd="$2" local output="$name.out" local output_expected="$name.expected" echo -n "Running test attached: $name " $cmd &> "$output_expected" expected_abduco_output "$name" $? >> "$output_expected" $ABDUCO -c "$name" $cmd 2>&1 | head -n -1 | sed 's/.$//' > "$output" if diff -u "$output_expected" "$output" && check_environment; then rm "$output" "$output_expected" echo "OK" return 0 else echo "FAIL" return 1 fi } # $1 => session-name, $2 => command to execute run_test_detached() { check_environment || return 1; local name="$1" local cmd="$2" local output="$name.out" local output_expected="$name.expected" echo -n "Running test detached: $name " $cmd &> /dev/null expected_abduco_output "$name" $? > "$output_expected" if $ABDUCO -n "$name" $cmd &> /dev/null && sleep 1 && $ABDUCO -a "$name" 2>&1 | head -n -1 | sed 's/.$//' > "$output" && diff -u "$output_expected" "$output" && check_environment; then rm "$output" "$output_expected" echo "OK" return 0 else echo "FAIL" return 1 fi } # $1 => session-name, $2 => command to execute run_test_attached_detached() { check_environment || return 1; local name="$1" local cmd="$2" local output="$name.out" local output_expected="$name.expected" echo -n "Running test: $name " $cmd &> /dev/null expected_abduco_output "$name" $? > "$output_expected" if detach | $ABDUCO $ABDUCO_OPTS -c "$name" $cmd &> /dev/null && sleep 3 && $ABDUCO -a "$name" 2>&1 | head -n -1 | tail -1 | sed 's/.$//' > "$output" && diff -u "$output_expected" "$output" && check_environment; then rm "$output" "$output_expected" echo "OK" return 0 else echo "FAIL" return 1 fi } run_test_dvtm() { echo -n "Running dvtm test: " if ! which dvtm &> /dev/null; then echo "SKIPPED" return 0; fi local name="dvtm" local output="$name.out" local output_expected="$name.expected" echo exit | dvtm &> /dev/null expected_abduco_output "$name" $? > "$output_expected" local len=`wc -c "$output_expected" | awk '{ print $1 }'` len=$((len+1)) if dvtm_session | $ABDUCO -c "$name" 2>&1 | head -n -1 | tail -c $len | sed 's/.$//' > "$output" && diff -u "$output_expected" "$output" && check_environment; then rm "$output" "$output_expected" echo "OK" return 0 else echo "FAIL" return 1 fi } test_non_existing_command || echo "Execution of non existing command FAILED" run_test_attached "seq" "seq 1 1000" run_test_detached "seq" "seq 1 1000" run_test_attached "false" "false" run_test_detached "false" "false" run_test_attached "true" "true" run_test_detached "true" "true" cat > exit-status.sh <<-EOT #!/bin/sh exit 42 EOT chmod +x exit-status.sh run_test_attached "exit-status" "./exit-status.sh" run_test_detached "exit-status" "./exit-status.sh" rm ./exit-status.sh cat > long-running.sh <<-EOT #!/bin/sh echo Start date sleep 3 echo Hello World sleep 3 echo End date exit 1 EOT chmod +x long-running.sh run_test_attached_detached "attach-detach" "./long-running.sh" rm ./long-running.sh run_test_dvtm abduco-0.1/abduco.c0000644000175000017500000003323212355725670012655 0ustar marcmarc/* * 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 countof(arr) (sizeof(arr) / sizeof((arr)[0])) enum PacketType { MSG_CONTENT = 0, MSG_ATTACH = 1, MSG_DETACH = 2, MSG_RESIZE = 3, MSG_REDRAW = 4, MSG_EXIT = 5, }; typedef struct { unsigned int type; size_t len; union { char msg[BUFSIZ]; struct winsize ws; int i; bool b; } u; } Packet; typedef struct Client Client; struct Client { int socket; enum { STATE_CONNECTED, STATE_ATTACHED, STATE_DETACHED, STATE_DISCONNECTED, } state; bool need_resize; bool readonly; Client *next; }; typedef struct { Client *clients; int socket; Packet pty_output; int pty; int exit_status; struct termios term; struct winsize winsize; pid_t pid; volatile sig_atomic_t running; const char *name; const char *session_name; bool read_pty; } Server; static Server server = { .running = true, .exit_status = -1 }; static Client client; 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, ...); #include "debug.c" static inline size_t packet_header_size() { return offsetof(Packet, u); } static size_t packet_size(Packet *pkt) { return packet_header_size() + pkt->len; } static ssize_t write_all(int fd, const char *buf, size_t len) { debug("write_all(%d)\n", len); ssize_t ret = len; while (len > 0) { ssize_t 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) { debug("read_all(%d)\n", len); ssize_t ret = len; while (len > 0) { ssize_t 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 send_packet(int socket, Packet *pkt) { size_t size = packet_size(pkt); return write_all(socket, (char *)pkt, size) == size; } static bool recv_packet(int socket, Packet *pkt) { ssize_t len = read_all(socket, (char*)pkt, packet_header_size()); if (len <= 0 || len != packet_header_size()) return false; if (pkt->len > 0) { len = read_all(socket, pkt->u.msg, pkt->len); if (len <= 0 || len != pkt->len) return false; } return true; } #include "client.c" #include "server.c" static void info(const char *str, ...) { va_list ap; va_start(ap, str); fprintf(stderr, "\e[999H"); if (str) { fprintf(stderr, "%s: %s: ", server.name, server.session_name); vfprintf(stderr, str, ap); fprintf(stderr, "\r\n"); } fflush(stderr); 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] [-r] [-e detachkey] name command\n"); exit(EXIT_FAILURE); } static int create_socket_dir() { size_t maxlen = sizeof(sockaddr.sun_path); char *dirs[] = { getenv("HOME"), getenv("TMPDIR"), "/tmp" }; int socketfd = socket(AF_LOCAL, SOCK_STREAM, 0); if (socketfd == -1) return -1; for (unsigned int i = 0; i < countof(dirs); i++) { char *dir = dirs[i]; if (!dir) continue; int len = snprintf(sockaddr.sun_path, maxlen, "%s/.%s/", dir, server.name); if (len < 0 || (size_t)len >= maxlen) continue; if (mkdir(sockaddr.sun_path, 0750) == -1 && errno != EEXIST) continue; int len2 = snprintf(sockaddr.sun_path, maxlen, "%s/.%s/.abduco-%d", dir, server.name, getpid()); if (len2 < 0 || (size_t)len2 >= maxlen) continue; socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1; if (bind(socketfd, (struct sockaddr*)&sockaddr, socklen) == -1) continue; unlink(sockaddr.sun_path); close(socketfd); sockaddr.sun_path[len] = '\0'; return len; } close(socketfd); return -1; } static int create_socket(const char *name) { size_t maxlen = sizeof(sockaddr.sun_path); if (name[0] == '/') { strncpy(sockaddr.sun_path, name, maxlen); if (sockaddr.sun_path[maxlen-1]) return -1; } else if (name[0] == '.' && (name[1] == '.' || name[1] == '/')) { char buf[maxlen], *cwd = getcwd(buf, sizeof buf); if (!cwd) return -1; int len = snprintf(sockaddr.sun_path, maxlen, "%s/%s", cwd, name); if (len < 0 || (size_t)len >= maxlen) 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[]) { /* this uses the well known double fork strategy as described in section 1.7 of * * http://www.faqs.org/faqs/unix-faq/programmer/faq/ * * pipes are used for synchronization and error reporting i.e. the child sets * the close on exec flag before calling execvp(3) the parent blocks on a read(2) * in case of failure the error message is written to the pipe, success is * indicated by EOF on the pipe. */ int client_pipe[2], server_pipe[2]; pid_t pid; char errormsg[255]; struct sigaction sa; if (pipe(client_pipe) == -1) return false; if ((server.socket = server_create_socket(name)) == -1) return false; switch ((pid = fork())) { case 0: /* child process */ setsid(); close(client_pipe[0]); switch ((pid = fork())) { case 0: /* child process */ if (pipe(server_pipe) == -1) { snprintf(errormsg, sizeof(errormsg), "server-pipe: %s\n", strerror(errno)); write_all(client_pipe[1], errormsg, strlen(errormsg)); close(client_pipe[1]); _exit(EXIT_FAILURE); } 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, &server.winsize)) { case 0: /* child = user application process */ close(server.socket); close(server_pipe[0]); fcntl(client_pipe[1], F_SETFD, FD_CLOEXEC); fcntl(server_pipe[1], F_SETFD, FD_CLOEXEC); execvp(argv[0], argv); snprintf(errormsg, sizeof(errormsg), "server-execvp: %s\n", strerror(errno)); write_all(client_pipe[1], errormsg, strlen(errormsg)); write_all(server_pipe[1], errormsg, strlen(errormsg)); close(client_pipe[1]); close(server_pipe[1]); _exit(EXIT_FAILURE); break; case -1: /* forkpty failed */ snprintf(errormsg, sizeof(errormsg), "server-forkpty: %s\n", strerror(errno)); write_all(client_pipe[1], errormsg, strlen(errormsg)); close(client_pipe[1]); close(server_pipe[0]); close(server_pipe[1]); _exit(EXIT_FAILURE); break; default: /* parent = server process */ sa.sa_handler = server_sigterm_handler; sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL); sa.sa_handler = server_sigusr1_handler; sigaction(SIGUSR1, &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(client_pipe[1]); close(server_pipe[1]); if (read_all(server_pipe[0], errormsg, sizeof(errormsg)) > 0) _exit(EXIT_FAILURE); close(server_pipe[0]); server_mainloop(); break; } break; case -1: /* fork failed */ snprintf(errormsg, sizeof(errormsg), "server-fork: %s\n", strerror(errno)); write_all(client_pipe[1], errormsg, strlen(errormsg)); close(client_pipe[1]); _exit(EXIT_FAILURE); break; default: /* parent = intermediate process */ close(client_pipe[1]); _exit(EXIT_SUCCESS); break; } break; case -1: /* fork failed */ close(client_pipe[0]); close(client_pipe[1]); return false; default: /* parent = client process */ close(client_pipe[1]); int status; wait(&status); /* wait for first fork */ ssize_t len = read_all(client_pipe[0], errormsg, sizeof(errormsg)); if (len > 0) { write_all(STDERR_FILENO, errormsg, len); unlink(sockaddr.sun_path); exit(EXIT_FAILURE); } close(client_pipe[0]); } return true; } 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(STDIN_FILENO, TCSADRAIN, &cur_term); int status = client_mainloop(); client_restore_terminal(); if (status == -1) { info("detached"); } else if (status == -EIO) { info("exited due to I/O errors"); } else { info("session terminated with exit status %d", status); exit(status); } return true; } static int session_filter(const struct dirent *d) { return d->d_name[0] != '.'; } static int session_comparator(const struct dirent **a, const struct dirent **b) { struct stat sa, sb; if (stat((*a)->d_name, &sa) != 0) return -1; if (stat((*b)->d_name, &sb) != 0) return 1; return sa.st_atime < sb.st_atime ? -1 : 1; } static int list_session() { if (create_socket_dir() == -1) return 1; chdir(sockaddr.sun_path); struct dirent **namelist; int n = scandir(sockaddr.sun_path, &namelist, session_filter, session_comparator); if (n < 0) return 1; puts("Active sessions"); while (n--) { struct stat sb; char buf[255]; if (stat(namelist[n]->d_name, &sb) == 0 && S_ISSOCK(sb.st_mode)) { strftime(buf, sizeof(buf), "%a%t %F %T", localtime(&sb.st_atime)); char status = ' '; if (sb.st_mode & S_IXUSR) status = '*'; else if (sb.st_mode & S_IXGRP) status = '+'; printf("%c %s\t%s\n", status, buf, namelist[n]->d_name); } free(namelist[n]); } free(namelist); return 0; } int main(int argc, char *argv[]) { char **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 (!server.session_name) { server.session_name = argv[arg]; continue; } else if (!cmd) { cmd = &argv[arg]; break; } } if (server.session_name) 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 'r': client.readonly = true; break; case 'v': puts("abduco-"VERSION" © 2013-2014 Marc André Tanner"); exit(EXIT_SUCCESS); default: usage(); } } if (!cmd) { cmd = (char*[]){ getenv("ABDUCO_CMD"), NULL }; if (!cmd[0]) cmd[0] = "dvtm"; } if (!action || !server.session_name || ((action == 'c' || action == 'A') && client.readonly)) usage(); if (tcgetattr(STDIN_FILENO, &orig_term) != -1) { server.term = orig_term; has_term = true; } if (ioctl(STDIN_FILENO, TIOCGWINSZ, &server.winsize) == -1) { server.winsize.ws_col = 80; server.winsize.ws_row = 25; } server.read_pty = (action == 'n'); switch (action) { redo: case 'n': case 'c': if (!create_session(server.session_name, cmd)) die("create-session"); if (action == 'n') break; case 'a': case 'A': if (!attach_session(server.session_name)) { if (action == 'A') { action = 'c'; goto redo; } die("attach-session"); } } return 0; } abduco-0.1/abduco.10000644000175000017500000000441312355725670012572 0ustar marcmarc.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 [ \-r ] .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 [ \-r ] .RB [ \-e .IR detachkey ] .RB \-a .RB name .br .SH DESCRIPTION .B abduco provides a way to disconnect a given application from its controlling terminal, thus it provides roughly the same session attach/detach support as .BR screen(1) , " tmux(1)" " or" " dtach(1)". If the .BR command to execute is not specified, the environment variable .BR $ABDUCO_CMD is examined, if it is not set .BR dvtm(1) is executed. By default all session related information is stored in .B $HOME/.abduco with .BR $TMPDIR/.abduco as a fallback and .BR /tmp/.abduco as a last resort. However if a given session name represents either a relative or absolute path it is used unmodified. If for some reason the .BR unix(7) domain socket representing a session is deleted, sending .BR SIGUSR1 to the server process will recreate it. .SH OPTIONS If no command line arguments are given all currently active sessions are printed sorted by their respective creation date. Lines starting with an asterik .BR * indicate that at least one client is connected. A plus sign .BR + indicates that the command terminated while no client was connected, attach to get its exit status. .TP .B \-v Print version information to standard output and exit. .TP .B \-r Readonly session, i.e. user input is ignored. .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 do not 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 EXAMPLE Start a new session (assuming .BR dvtm(1) is in .BR $PATH ) with .nf .B abduco -c my-session .fi do some work, then detach by pressing .B CTRL+\e and later reattach with .nf .B abduco -a my-session .fi .SH AUTHOR abduco is written by Marc André Tanner abduco-0.1/client.c0000644000175000017500000000461212355725670012676 0ustar marcmarcstatic void client_sigwinch_handler(int sig) { client.need_resize = true; } static bool client_send_packet(Packet *pkt) { print_packet("client-send:", pkt); if (send_packet(server.socket, pkt)) return true; debug("FAILED\n"); server.running = false; return false; } static bool client_recv_packet(Packet *pkt) { if (recv_packet(server.socket, pkt)) { print_packet("client-recv:", pkt); return true; } debug("client-recv: FAILED\n"); server.running = false; return false; } static void client_show_cursor() { printf("\e[?25h"); fflush(stdout); } static void client_restore_terminal() { if (has_term) tcsetattr(STDIN_FILENO, TCSADRAIN, &orig_term); client_show_cursor(); } static int client_mainloop() { client.need_resize = true; Packet pkt = { .type = MSG_ATTACH, .u = { .b = client.readonly }, .len = sizeof(pkt.u.b), }; client_send_packet(&pkt); 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(STDIN_FILENO, TIOCGWINSZ, &ws) != -1) { Packet 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)) { Packet pkt; if (client_recv_packet(&pkt)) { switch (pkt.type) { case MSG_CONTENT: write_all(STDOUT_FILENO, pkt.u.msg, pkt.len); break; case MSG_RESIZE: client.need_resize = true; break; case MSG_EXIT: client_send_packet(&pkt); close(server.socket); return pkt.u.i; } } } if (FD_ISSET(STDIN_FILENO, &fds)) { Packet 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) { debug("client-stdin: %c\n", pkt.u.msg[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; pkt.len = 0; client_send_packet(&pkt); close(server.socket); return -1; } else if (!client.readonly) { client_send_packet(&pkt); } } } } return -EIO; } abduco-0.1/LICENSE0000644000175000017500000000137512355725670012264 0ustar marcmarcCopyright (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. abduco-0.1/server.c0000644000175000017500000001425612355725670012733 0ustar marcmarc#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_mark_socket_exec(bool exec, bool usr) { struct stat sb; if (stat(sockaddr.sun_path, &sb) == -1) return -1; mode_t mode = sb.st_mode; mode_t flag = usr ? S_IXUSR : S_IXGRP; if (exec) mode |= flag; else mode &= ~flag; return chmod(sockaddr.sun_path, mode); } static int server_create_socket(const char *name) { int socket = create_socket(name); if (socket == -1) return -1; socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1; mode_t mode = S_IRUSR|S_IWUSR; fchmod(socket, mode); if (bind(socket, (struct sockaddr*)&sockaddr, socklen) == -1) return -1; if (fchmod(socket, mode) == -1 && chmod(sockaddr.sun_path, mode) == -1) goto error; if (listen(socket, 5) == -1) goto error; return socket; error: unlink(sockaddr.sun_path); return -1; } 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() { int newfd = accept(server.socket, NULL, NULL); if (newfd == -1) return NULL; Client *c = client_malloc(newfd); if (!c) return NULL; if (!server.clients) server_mark_socket_exec(true, true); server_set_socket_non_blocking(newfd); c->socket = newfd; c->state = STATE_CONNECTED; c->next = server.clients; server.clients = c; server.read_pty = true; return c; } static bool server_read_pty(Packet *pkt) { pkt->type = MSG_CONTENT; ssize_t len = read(server.pty, pkt->u.msg, sizeof(pkt->u.msg)); if (len != -1) pkt->len = len; else if (errno != EAGAIN && errno != EINTR) server.running = false; print_packet("server-read-pty:", pkt); return len > 0; } static bool server_write_pty(Packet *pkt) { print_packet("server-write-pty:", pkt); size_t size = pkt->len; if (write_all(server.pty, pkt->u.msg, size) == size) return true; debug("FAILED\n"); server.running = false; return false; } static bool server_recv_packet(Client *c, Packet *pkt) { if (recv_packet(c->socket, pkt)) { print_packet("server-recv:", pkt); return true; } debug("server-recv: FAILED\n"); c->state = STATE_DISCONNECTED; return false; } static bool server_send_packet(Client *c, Packet *pkt) { print_packet("server-send:", pkt); if (send_packet(c->socket, pkt)) return true; debug("FAILED\n"); c->state = STATE_DISCONNECTED; return false; } 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); server_mark_socket_exec(true, false); } debug("server pty died: %d\n", server.exit_status); errno = errsv; } static void server_sigterm_handler(int sig) { exit(EXIT_FAILURE); /* invoke atexit handler */ } static void server_sigusr1_handler(int sig) { int socket = server_create_socket(server.session_name); if (socket != -1) { if (server.socket) close(server.socket); server.socket = socket; } } static void server_atexit_handler() { unlink(sockaddr.sun_path); } 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; bool exit_packet_delivered = false; if (server.read_pty) FD_SET_MAX(server.pty, &new_readfds, new_fdmax); while (server.clients || !exit_packet_delivered) { int fdmax = new_fdmax; fd_set readfds = new_readfds; fd_set writefds = new_writefds; FD_SET_MAX(server.socket, &readfds, fdmax); if (select(fdmax+1, &readfds, &writefds, NULL, NULL) == -1) { if (errno == EINTR) continue; die("server-mainloop"); } FD_ZERO(&new_readfds); FD_ZERO(&new_writefds); new_fdmax = server.socket; bool pty_data = false; Packet server_packet, client_packet; if (FD_ISSET(server.socket, &readfds)) server_accept_client(); if (FD_ISSET(server.pty, &readfds)) pty_data = server_read_pty(&server_packet); for (Client **prev_next = &server.clients, *c = server.clients; c;) { if (FD_ISSET(c->socket, &readfds) && server_recv_packet(c, &client_packet)) { switch (client_packet.type) { case MSG_CONTENT: server_write_pty(&client_packet); break; case MSG_ATTACH: c->readonly = client_packet.u.b; break; case MSG_RESIZE: c->state = STATE_ATTACHED; case MSG_REDRAW: if (!c->readonly && (client_packet.type == MSG_REDRAW || c == server.clients)) { debug("server-ioct: TIOCSWINSZ\n"); ioctl(server.pty, TIOCSWINSZ, &client_packet.u.ws); } kill(-server.pid, SIGWINCH); break; case MSG_DETACH: case MSG_EXIT: c->state = STATE_DISCONNECTED; break; default: /* ignore package */ break; } } if (c->state == STATE_DISCONNECTED) { bool first = (c == server.clients); Client *t = c->next; client_free(c); *prev_next = c = t; if (first && server.clients) { Packet pkt = { .type = MSG_RESIZE, .len = 0, }; server_send_packet(server.clients, &pkt); } else if (!server.clients) { server_mark_socket_exec(false, true); } continue; } FD_SET_MAX(c->socket, &new_readfds, new_fdmax); if (pty_data) server_send_packet(c, &server_packet); if (!server.running) { if (server.exit_status != -1) { Packet pkt = { .type = MSG_EXIT, .u.i = server.exit_status, .len = sizeof(pkt.u.i), }; if (server_send_packet(c, &pkt)) exit_packet_delivered = true; else FD_SET_MAX(c->socket, &new_writefds, new_fdmax); } else { FD_SET_MAX(c->socket, &new_writefds, new_fdmax); } } prev_next = &c->next; c = c->next; } if (server.running && server.read_pty) FD_SET_MAX(server.pty, &new_readfds, new_fdmax); } exit(EXIT_SUCCESS); } abduco-0.1/debug.c0000644000175000017500000000202212355725670012477 0ustar marcmarc#ifdef NDEBUG static void debug(const char *errstr, ...) { } static void print_packet(const char *prefix, Packet *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_packet(const char *prefix, Packet *pkt) { static const char *msgtype[] = { [MSG_CONTENT] = "CONTENT", [MSG_ATTACH] = "ATTACH", [MSG_DETACH] = "DETACH", [MSG_RESIZE] = "RESIZE", [MSG_REDRAW] = "REDRAW", [MSG_EXIT] = "EXIT", }; const char *type = "UNKNOWN"; if (pkt->type < countof(msgtype) && msgtype[pkt->type]) type = msgtype[pkt->type]; fprintf(stderr, "%s: %s ", prefix, type); switch (pkt->type) { case MSG_CONTENT: for (size_t i = 0; i < pkt->len && i < sizeof(pkt->u.msg); i++) fprintf(stderr, "%c", pkt->u.msg[i]); break; case MSG_RESIZE: fprintf(stderr, "%dx%d", pkt->u.ws.ws_col, pkt->u.ws.ws_row); break; default: fprintf(stderr, "len: %zu", pkt->len); break; } fprintf(stderr, "\n"); } #endif /* NDEBUG */ abduco-0.1/forkpty-aix.c0000644000175000017500000000477312355725670013705 0ustar marcmarc/* * Copyright (c) 2009 Nicholas Marriott * Copyright (c) 2012 Ross Palmer Mohn * * Permission to use, copy, modify, and 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 MIND, 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. */ #include #include #include #include #include #include #include /* Fatal errors. */ #ifdef NDEBUG #define debug(format, args...) #else #define debug eprint #endif #define fatal(msg) debug("%s: %s", __func__, msg); pid_t forkpty(int *master, char *name, struct termios *tio, struct winsize *ws) { int slave, fd; char *path; pid_t pid; struct termios tio2; if ((*master = open("/dev/ptc", O_RDWR|O_NOCTTY)) == -1) return (-1); if ((path = ttyname(*master)) == NULL) goto out; if ((slave = open(path, O_RDWR|O_NOCTTY)) == -1) goto out; switch (pid = fork()) { case -1: goto out; case 0: close(*master); fd = open(_PATH_TTY, O_RDWR|O_NOCTTY); if (fd >= 0) { ioctl(fd, TIOCNOTTY, NULL); close(fd); } if (setsid() < 0) fatal("setsid"); fd = open(_PATH_TTY, O_RDWR|O_NOCTTY); if (fd >= 0) fatal("open succeeded (failed to disconnect)"); fd = open(path, O_RDWR); if (fd < 0) fatal("open failed"); close(fd); fd = open("/dev/tty", O_WRONLY); if (fd < 0) fatal("open failed"); close(fd); if (tcgetattr(slave, &tio2) != 0) fatal("tcgetattr failed"); if (tio != NULL) memcpy(tio2.c_cc, tio->c_cc, sizeof tio2.c_cc); tio2.c_cc[VERASE] = '\177'; if (tcsetattr(slave, TCSAFLUSH, &tio2) == -1) fatal("tcsetattr failed"); if (ioctl(slave, TIOCSWINSZ, ws) == -1) fatal("ioctl failed"); dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); if (slave > 2) close(slave); return (0); } close(slave); return (pid); out: if (*master != -1) close(*master); if (slave != -1) close(slave); return (-1); } abduco-0.1/README0000644000175000017500000000041712355725670012133 0ustar marcmarcabduco ====== 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. abduco-0.1/config.def.h0000644000175000017500000000010112355725670013414 0ustar marcmarcstatic char KEY_DETACH = CTRL('\\'); static char KEY_REDRAW = 0; abduco-0.1/Makefile0000644000175000017500000000303512355725670012712 0ustar marcmarcinclude 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 testsuite.sh config.def.h config.mk \ ${SRC} debug.c 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