commit 23074d3425d3fbcefa2bf6b94bdfc2c46ca4c5e7
Author: AndrewLockVI <andrew@laack.co>
Date: Fri, 21 Feb 2025 05:16:08 -0600
Init commit of abduco
Diffstat:
| A | .gitignore | | | 9 | +++++++++ |
| A | LICENSE | | | 15 | +++++++++++++++ |
| A | Makefile | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | README.md | | | 185 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | abduco.1 | | | 226 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | abduco.c | | | 718 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | client.c | | | 144 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | config.def.h | | | 19 | +++++++++++++++++++ |
| A | configure | | | 244 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | contrib/abduco.zsh | | | 35 | +++++++++++++++++++++++++++++++++++ |
| A | debug.c | | | 52 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | forkpty-aix.c | | | 96 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | forkpty-sunos.c | | | 88 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | server.c | | | 295 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | testsuite.sh | | | 215 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
15 files changed, 2408 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,9 @@
+/config.h
+/config.mk
+/abduco
+*.css
+*.gcda
+*.gcno
+*.gcov
+*.html
+*.o
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2013-2018 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,67 @@
+-include config.mk
+
+VERSION = 0.6
+
+CFLAGS_STD ?= -std=c99 -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -DNDEBUG
+CFLAGS_STD += -DVERSION=\"${VERSION}\"
+
+LDFLAGS_STD ?= -lc -lutil
+
+STRIP ?= strip
+INSTALL ?= install
+
+PREFIX ?= /usr/local
+SHAREDIR ?= ${PREFIX}/share
+
+SRC = abduco.c
+
+all: abduco
+
+config.h:
+ cp config.def.h config.h
+
+config.mk:
+ @touch $@
+
+abduco: config.h config.mk *.c
+ ${CC} ${CFLAGS} ${CFLAGS_STD} ${CFLAGS_AUTO} ${CFLAGS_EXTRA} ${SRC} ${LDFLAGS} ${LDFLAGS_STD} ${LDFLAGS_AUTO} -o $@
+
+debug: clean
+ make CFLAGS_EXTRA='${CFLAGS_DEBUG}'
+
+clean:
+ @echo cleaning
+ @rm -f abduco abduco-*.tar.gz
+
+dist: clean
+ @echo creating dist tarball
+ @git archive --prefix=abduco-${VERSION}/ -o abduco-${VERSION}.tar.gz HEAD
+
+installdirs:
+ @${INSTALL} -d ${DESTDIR}${PREFIX}/bin \
+ ${DESTDIR}${MANPREFIX}/man1
+
+install: abduco installdirs
+ @echo installing executable file to ${DESTDIR}${PREFIX}/bin
+ @${INSTALL} -m 0755 abduco ${DESTDIR}${PREFIX}/bin
+ @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
+
+install-strip: install
+ ${STRIP} ${DESTDIR}${PREFIX}/bin/abduco
+
+install-completion:
+ @echo installing zsh completion file to ${DESTDIR}${SHAREDIR}/zsh/site-functions
+ @install -Dm644 contrib/abduco.zsh ${DESTDIR}${SHAREDIR}/zsh/site-functions/_abduco
+
+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
+ @echo removing zsh completion file from ${DESTDIR}${SHAREDIR}/zsh/site-functions
+ @rm -f ${DESTDIR}${SHAREDIR}/zsh/site-functions/_abduco
+
+.PHONY: all clean dist install installdirs install-strip install-completion uninstall debug
diff --git a/README.md b/README.md
@@ -0,0 +1,185 @@
+# abduco a tool for session {at,de}tach support
+
+[abduco](https://www.brain-dump.org/projects/abduco) provides
+session management i.e. it allows programs to be run independently
+from their controlling terminal. That is programs can be detached -
+run in the background - and then later reattached. Together with
+[dvtm](https://www.brain-dump.org/projects/dvtm) it provides a
+simpler and cleaner alternative to tmux or screen.
+
+
+
+abduco is in many ways very similar to [dtach](http://dtach.sf.net)
+but is a completely independent implementation which is actively maintained,
+contains no legacy code, provides a few additional features, has a
+cleaner, more robust implementation and is distributed under the
+[ISC license](https://raw.githubusercontent.com/martanne/abduco/master/LICENSE)
+
+## News
+
+ * [abduco-0.6](https://www.brain-dump.org/projects/abduco/abduco-0.6.tar.gz)
+ [released](https://lists.suckless.org/dev/1603/28589.html) (24.03.2016)
+ * [abduco-0.5](https://www.brain-dump.org/projects/abduco/abduco-0.5.tar.gz)
+ [released](https://lists.suckless.org/dev/1601/28094.html) (09.01.2016)
+ * [abduco-0.4](https://www.brain-dump.org/projects/abduco/abduco-0.4.tar.gz)
+ [released](https://lists.suckless.org/dev/1503/26027.html) (18.03.2015)
+ * [abduco-0.3](https://www.brain-dump.org/projects/abduco/abduco-0.3.tar.gz)
+ [released](https://lists.suckless.org/dev/1502/25557.html) (19.02.2015)
+ * [abduco-0.2](https://www.brain-dump.org/projects/abduco/abduco-0.2.tar.gz)
+ [released](https://lists.suckless.org/dev/1411/24447.html) (15.11.2014)
+ * [abduco-0.1](https://www.brain-dump.org/projects/abduco/abduco-0.1.tar.gz)
+ [released](https://lists.suckless.org/dev/1407/22703.html) (05.07.2014)
+ * [Initial announcement](https://lists.suckless.org/dev/1403/20372.html)
+ on the suckless development mailing list (08.03.2014)
+
+## Download
+
+Either download the latest [source tarball](https://github.com/martanne/abduco/releases),
+compile and install it
+
+ ./configure && make && sudo make install
+
+or use one of the distribution provided
+[binary packages](https://repology.org/project/abduco/packages).
+
+## Quickstart
+
+In order to create a new session `abduco` requires a session name
+as well as an command which will be run. If no command is given
+the environment variable `$ABDUCO_CMD` is examined and if not set
+`dvtm` is executed. Therefore assuming `dvtm` is located somewhere
+in `$PATH` a new session named *demo* is created with:
+
+ $ abduco -c demo
+
+An arbitrary application can be started as follows:
+
+ $ abduco -c session-name your-application
+
+`CTRL-\` detaches from the active session. This detach key can be
+changed by means of the `-e` command line option, `-e ^q` would
+for example set it to `CTRL-q`.
+
+To get an overview of existing session run `abduco` without any
+arguments.
+
+ $ abduco
+ Active sessions (on host debbook)
+ * Thu 2015-03-12 12:05:20 demo-active
+ + Thu 2015-03-12 12:04:50 demo-finished
+ Thu 2015-03-12 12:03:30 demo
+
+A leading asterisk `*` indicates that at least one client is
+connected. A leading plus `+` denotes that the session terminated,
+attaching to it will print its exit status.
+
+A session can be reattached by using the `-a` command line option
+in combination with the session name which was used during session
+creation.
+
+ $ abduco -a demo
+
+If you encounter problems with incomplete redraws or other
+incompatibilities it is recommended to run your applications
+within [dvtm](https://github.com/martanne/dvtm) under abduco:
+
+ $ abduco -c demo dvtm your-application
+
+Check out the manual page for further information and all available
+command line options.
+
+## Improvements over dtach
+
+ * **session list**, available by executing `abduco` without any arguments,
+ indicating whether clients are connected or the command has already
+ terminated.
+
+ * the **session exit status** of the command being run is always kept and
+ reported either upon command termination or on reconnection
+ e.g. the following works:
+
+ $ abduco -n demo true && abduco -a demo
+ abduco: demo: session terminated with exit status 0
+
+ * **read only sessions** if the `-r` command line argument is used when
+ attaching to a session, then all keyboard input is ignored and the
+ client is a passive observer only.
+
+ Note that this is not a security feature, but only a convenient way to
+ avoid accidental keyboard input.
+
+ If you want to make your abduco session available to another user
+ in a read only fashion, use [socat](http://www.dest-unreach.org/socat/)
+ to proxy the abduco socket in a unidirectional (from the abduco server
+ to the client, but not vice versa) way.
+
+ Start your to be shared session, make sure only you have access to
+ the `private` directory:
+
+ $ abduco -c /tmp/abduco/private/session
+
+ Then proxy the socket in unidirectional mode `-u` to a directory
+ where the desired observers have sufficient access rights:
+
+ $ socat -u unix-connect:/tmp/abduco/private/session unix-listen:/tmp/abduco/public/read-only &
+
+ Now the observers can connect to the read-only side of the socket:
+
+ $ abduco -a /tmp/abduco/public/read-only
+
+ communication in the other direction will not be possible and keyboard
+ input will hence be discarded.
+
+ * **better resize handling** on shared sessions, resize request are only
+ processed if they are initiated by the most recently connected, non
+ read only client.
+
+ * **socket recreation** by sending the `SIGUSR1` signal to the server
+ process. In case the unix domain socket was removed by accident it
+ can be recreated. The simplest way to find out the server process
+ id is to look for abduco processes which are reparented to the init
+ process.
+
+ $ pgrep -P 1 abduco
+
+ After finding the correct PID the socket can be recreated with
+
+ $ kill -USR1 $PID
+
+ If the abduco binary itself has also been deleted, but a session is
+ still running, use the following command to bring back the session:
+
+ $ /proc/$PID/exe
+
+ * **improved socket permissions** the session sockets are by default either
+ stored in `$HOME/.abduco` or `/tmp/abduco/$USER` in both cases it is
+ made sure that only the owner has access to the respective directory.
+
+## Development
+
+You can always fetch the current code base from the git repository
+located at [Github](https://github.com/martanne/abduco/) or
+[Sourcehut](https://git.sr.ht/~martanne/abduco).
+
+If you have comments, suggestions, ideas, a bug report, a patch or something
+else related to abduco then write to the
+[suckless developer mailing list](https://suckless.org/community)
+or contact me directly.
+
+### Debugging
+
+The protocol content exchanged between client and server can be dumped
+to temporary files as follows:
+
+ $ make debug
+ $ ./abduco -n debug [command-to-debug] 2> server-log
+ $ ./abduco -a debug 2> client-log
+
+If you want to run client and server with one command (e.g. using the `-c`
+option) then within `gdb` the option `set follow-fork-mode {child,parent}`
+might be useful. Similarly to get a syscall trace `strace -o abduco -ff
+[abduco-cmd]` proved to be handy.
+
+## License
+
+abduco is licensed under the [ISC license](https://raw.githubusercontent.com/martanne/abduco/master/LICENSE)
diff --git a/abduco.1 b/abduco.1
@@ -0,0 +1,226 @@
+.Dd March 18, 2018
+.Dt ABDUCO 1
+.Os abduco VERSION
+.
+.Sh NAME
+.Nm abduco
+.Nd terminal session manager
+.
+.Sh SYNOPSIS
+.Nm
+.Fl a
+.Op options ...
+.Cm name
+.
+.Nm
+.Fl A
+.Op options ...
+.Cm name
+.Cm command Op args ...
+.
+.Nm
+.Fl c
+.Op options ...
+.Cm name
+.Cm command Op args ...
+.
+.Nm
+.Fl n
+.Op options ...
+.Cm name
+.Cm command Op args ...
+.
+.Sh DESCRIPTION
+.
+.Nm
+disassociates a given application from its controlling
+terminal, thereby providing roughly the same session attach/detach support as
+.Xr screen 1 ,
+.Xr tmux 1 ,
+or
+.Xr dtach 1 .
+.Pp
+A session comprises of an
+.Nm
+server process which spawns a user
+command in its own pseudo terminal
+.Pq see Xr pty 7 .
+Each session is given a name represented by a unix domain socket
+.Pq see Xr unix 7
+stored in the local file system.
+.Nm
+clients can connect to it and their standard input output streams
+are relayed to the command supervised by the server.
+.Pp
+.Nm
+operates on the raw I/O byte stream without interpreting any terminal
+escape sequences.
+As a consequence the terminal state is not preserved across sessions.
+If this functionality is desired, it should be provided by another
+utility such as
+.Xr dvtm 1 .
+.
+.Ss ACTIONS
+.
+If no command line arguments are given, all currently active sessions are
+listed sorted by their respective creation date.
+Lines starting with an asterisk
+.Pq *
+indicate that at least one client is currently connected.
+A plus sign
+.Pq +
+signals that the command terminated while no client was connected.
+Attaching to the session will print its exit status.
+The next column shows the PID of the server process, followed by the session
+.Ic name .
+.Pp
+.Nm
+provides different actions of which one must be provided.
+.
+.Bl -tag -width indent
+.It Fl a
+Attach to an existing session.
+.It Fl A
+Try to connect to an existing session, upon failure create said session and attach immediately to it.
+.It Fl c
+Create a new session and attach immediately to it.
+.It Fl n
+Create a new session but do not attach to it.
+.El
+.
+.Ss OPTIONS
+.
+Additionally the following options can be provided to further tweak
+the behavior.
+.Bl -tag -width indent
+.It Fl e Ar detachkey
+Set the key to detach.
+Defaults to
+.Aq Ctrl+\e
+which is specified as ^\\ i.e. Ctrl is represented as a caret
+.Pq ^ .
+.It Fl f
+Force creation of session when there is an already terminated session of the same name,
+after showing its exit status.
+.It Fl l
+Attach with the lowest priority, meaning this client will be the last to control the size.
+.It Fl p
+Pass through content of standard input to the session.
+Implies the
+.Fl q
+and
+.Fl l
+options.
+.It Fl q
+Be quiet, do not print informative messages.
+.It Fl r
+Read-only session, user input is ignored.
+.It Fl v
+Print version information and exit.
+.El
+.
+.Sh SIGNALS
+.
+.Bl -tag -width indent
+.It Dv SIGWINCH
+Whenever the primary client resizes its terminal the server process will deliver a
+.Ev SIGWINCH
+signal to the supervised process.
+.It Dv SIGUSR1
+If for some reason the unix domain socket representing a session is deleted, sending
+.Ev SIGUSR1
+to the server process will recreate it.
+.It Dv SIGTERM
+Detaches a client.
+.El
+.
+.Sh ENVIRONMENT
+.
+.Bl -tag -width indent
+.It Ev ABDUCO_CMD
+If
+.Ic command
+is not specified, the environment variable
+.Ev $ABDUCO_CMD
+is examined, if it is not set
+.Xr dvtm 1
+is executed.
+.It Ev ABDUCO_SESSION
+The current session name available to the supervised command.
+.It Ev ABDUCO_SOCKET
+The absolute path of the session socket available to the supervised command.
+.El
+.Pp
+See the
+.Sx FILES
+section for environment variables used in determining the location
+of unix domain sockets representing sessions.
+.Sh FILES
+.
+All session related information is stored in the following directories (first
+to succeed is used):
+.Bl -bullet
+.It
+.Ev $ABDUCO_SOCKET_DIR/abduco
+.It
+.Ev $HOME/.abduco
+.It
+.Ev $TMPDIR/abduco/$USER
+.It
+.Ev /tmp/abduco/$USER
+.El
+.
+.Pp
+However, if a given session
+.Ic name
+represents either a relative or absolute path it is used unmodified.
+.
+.
+.Sh EXAMPLES
+.
+Start a new session (assuming
+.Xr dvtm 1
+is in
+.Ev $PATH )
+with
+.Pp
+.Dl $ abduco -c my-session
+.Pp
+do some work, then detach by pressing
+.Aq Ctrl+\e ,
+list existing session by running
+.Nm
+without any arguments and later reattach with
+.Pp
+.Dl $ abduco -a my-session
+.Pp
+Alternatively, we can also explicitly specify the command to run.
+.Pp
+.Dl $ abduco -c my-session /bin/sh
+.Pp
+Attach with a
+.Aq Ctrl+z
+as detach key.
+.Pp
+.Dl $ abduco -e ^z -a my-session
+.Pp
+Send a command to an existing session.
+.Pp
+.Dl $ echo make | abduco -a my-session
+.Pp
+Or in a slightly more interactive fashion.
+.Pp
+.Dl $ abduco -p my-session
+.Dl make
+.Dl ^D
+.
+.Sh SEE ALSO
+.Xr dvtm 1 ,
+.Xr dtach 1 ,
+.Xr tmux 1 ,
+.Xr screen 1
+.
+.Sh AUTHORS
+.Nm
+is written by
+.An Marc André Tanner Aq mat at brain-dump.org
diff --git a/abduco.c b/abduco.c
@@ -0,0 +1,718 @@
+/*
+ * Copyright (c) 2013-2018 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.
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.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 <pwd.h>
+#include <sys/select.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__) || defined(__DragonFly__)
+# 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"
+
+#if defined(_AIX)
+# include "forkpty-aix.c"
+#elif defined(__sun)
+# include "forkpty-sunos.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_EXIT = 4,
+ MSG_PID = 5,
+};
+
+typedef struct {
+ uint32_t type;
+ uint32_t len;
+ union {
+ char msg[4096 - 2*sizeof(uint32_t)];
+ struct {
+ uint16_t rows;
+ uint16_t cols;
+ } ws;
+ uint32_t i;
+ uint64_t l;
+ } u;
+} Packet;
+
+typedef struct Client Client;
+struct Client {
+ int socket;
+ enum {
+ STATE_CONNECTED,
+ STATE_ATTACHED,
+ STATE_DETACHED,
+ STATE_DISCONNECTED,
+ } state;
+ bool need_resize;
+ enum {
+ CLIENT_READONLY = 1 << 0,
+ CLIENT_LOWPRIORITY = 1 << 1,
+ } flags;
+ 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;
+ char host[255];
+ bool read_pty;
+} Server;
+
+static Server server = { .running = true, .exit_status = -1, .host = "@localhost" };
+static Client client;
+static struct termios orig_term, cur_term;
+static bool has_term, alternate_buffer, quiet, passthrough;
+
+static struct sockaddr_un sockaddr = {
+ .sun_family = AF_UNIX,
+};
+
+static bool set_socket_name(struct sockaddr_un *sockaddr, 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);
+ if (size > sizeof(*pkt))
+ return false;
+ 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 > sizeof(pkt->u.msg)) {
+ pkt->len = 0;
+ 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);
+ if (str && !quiet) {
+ 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(void) {
+ fprintf(stderr, "usage: abduco [-a|-A|-c|-n] [-p] [-r] [-q] [-l] [-f] [-e detachkey] name command\n");
+ exit(EXIT_FAILURE);
+}
+
+static bool xsnprintf(char *buf, size_t size, const char *fmt, ...) {
+ va_list ap;
+ if (size > INT_MAX)
+ return false;
+ va_start(ap, fmt);
+ int n = vsnprintf(buf, size, fmt, ap);
+ va_end(ap);
+ if (n == -1)
+ return false;
+ if (n >= size) {
+ errno = ENAMETOOLONG;
+ return false;
+ }
+ return true;
+}
+
+static int session_connect(const char *name) {
+ int fd;
+ struct stat sb;
+ if (!set_socket_name(&sockaddr, name) || (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
+ return -1;
+ socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1;
+ if (connect(fd, (struct sockaddr*)&sockaddr, socklen) == -1) {
+ if (errno == ECONNREFUSED && stat(sockaddr.sun_path, &sb) == 0 && S_ISSOCK(sb.st_mode))
+ unlink(sockaddr.sun_path);
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static pid_t session_exists(const char *name) {
+ Packet pkt;
+ pid_t pid = 0;
+ if ((server.socket = session_connect(name)) == -1)
+ return pid;
+ if (client_recv_packet(&pkt) && pkt.type == MSG_PID)
+ pid = pkt.u.l;
+ close(server.socket);
+ return pid;
+}
+
+static bool session_alive(const char *name) {
+ struct stat sb;
+ return session_exists(name) &&
+ stat(sockaddr.sun_path, &sb) == 0 &&
+ S_ISSOCK(sb.st_mode) && (sb.st_mode & S_IXGRP) == 0;
+}
+
+static bool create_socket_dir(struct sockaddr_un *sockaddr) {
+ sockaddr->sun_path[0] = '\0';
+ int socketfd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (socketfd == -1)
+ return false;
+
+ const size_t maxlen = sizeof(sockaddr->sun_path);
+ uid_t uid = getuid();
+ struct passwd *pw = getpwuid(uid);
+
+ for (unsigned int i = 0; i < countof(socket_dirs); i++) {
+ struct stat sb;
+ struct Dir *dir = &socket_dirs[i];
+ bool ishome = false;
+ if (dir->env) {
+ dir->path = getenv(dir->env);
+ ishome = !strcmp(dir->env, "HOME");
+ if (ishome && (!dir->path || !dir->path[0]) && pw)
+ dir->path = pw->pw_dir;
+ }
+ if (!dir->path || !dir->path[0])
+ continue;
+ if (!xsnprintf(sockaddr->sun_path, maxlen, "%s/%s%s/", dir->path, ishome ? "." : "", server.name))
+ continue;
+ mode_t mask = umask(0);
+ int r = mkdir(sockaddr->sun_path, dir->personal ? S_IRWXU : S_IRWXU|S_IRWXG|S_IRWXO|S_ISVTX);
+ umask(mask);
+ if (r != 0 && errno != EEXIST)
+ continue;
+ if (lstat(sockaddr->sun_path, &sb) != 0)
+ continue;
+ if (!S_ISDIR(sb.st_mode)) {
+ errno = ENOTDIR;
+ continue;
+ }
+
+ size_t dirlen = strlen(sockaddr->sun_path);
+ if (!dir->personal) {
+ /* create subdirectory only accessible to user */
+ if (pw && !xsnprintf(sockaddr->sun_path+dirlen, maxlen-dirlen, "%s/", pw->pw_name))
+ continue;
+ if (!pw && !xsnprintf(sockaddr->sun_path+dirlen, maxlen-dirlen, "%d/", uid))
+ continue;
+ if (mkdir(sockaddr->sun_path, S_IRWXU) != 0 && errno != EEXIST)
+ continue;
+ if (lstat(sockaddr->sun_path, &sb) != 0)
+ continue;
+ if (!S_ISDIR(sb.st_mode)) {
+ errno = ENOTDIR;
+ continue;
+ }
+ dirlen = strlen(sockaddr->sun_path);
+ }
+
+ if (sb.st_uid != uid || sb.st_mode & (S_IRWXG|S_IRWXO)) {
+ errno = EACCES;
+ continue;
+ }
+
+ if (!xsnprintf(sockaddr->sun_path+dirlen, maxlen-dirlen, ".abduco-%d", getpid()))
+ 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[dirlen] = '\0';
+ return true;
+ }
+
+ close(socketfd);
+ return false;
+}
+
+static bool set_socket_name(struct sockaddr_un *sockaddr, const char *name) {
+ const size_t maxlen = sizeof(sockaddr->sun_path);
+ const char *session_name = NULL;
+ char buf[maxlen];
+
+ if (name[0] == '/') {
+ if (strlen(name) >= maxlen) {
+ errno = ENAMETOOLONG;
+ return false;
+ }
+ strncpy(sockaddr->sun_path, name, maxlen);
+ } else if (name[0] == '.' && (name[1] == '.' || name[1] == '/')) {
+ char *cwd = getcwd(buf, sizeof buf);
+ if (!cwd)
+ return false;
+ if (!xsnprintf(sockaddr->sun_path, maxlen, "%s/%s", cwd, name))
+ return false;
+ } else {
+ if (!create_socket_dir(sockaddr))
+ return false;
+ if (strlen(sockaddr->sun_path) + strlen(name) + strlen(server.host) >= maxlen) {
+ errno = ENAMETOOLONG;
+ return false;
+ }
+ session_name = name;
+ strncat(sockaddr->sun_path, name, maxlen - strlen(sockaddr->sun_path) - 1);
+ strncat(sockaddr->sun_path, server.host, maxlen - strlen(sockaddr->sun_path) - 1);
+ }
+
+ if (!session_name) {
+ strncpy(buf, sockaddr->sun_path, sizeof buf);
+ session_name = basename(buf);
+ }
+ setenv("ABDUCO_SESSION", session_name, 1);
+ setenv("ABDUCO_SOCKET", sockaddr->sun_path, 1);
+
+ return true;
+}
+
+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 (session_exists(name)) {
+ errno = EADDRINUSE;
+ return false;
+ }
+
+ 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]);
+ if (fcntl(client_pipe[1], F_SETFD, FD_CLOEXEC) == 0 &&
+ fcntl(server_pipe[1], F_SETFD, FD_CLOEXEC) == 0)
+ execvp(argv[0], argv);
+ snprintf(errormsg, sizeof(errormsg), "server-execvp: %s: %s\n",
+ argv[0], 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);
+ if (chdir("/") == -1)
+ _exit(EXIT_FAILURE);
+ #ifdef NDEBUG
+ int fd = open("/dev/null", O_RDWR);
+ if (fd != -1) {
+ dup2(fd, STDIN_FILENO);
+ dup2(fd, STDOUT_FILENO);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ }
+ #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]);
+ while (waitpid(pid, NULL, 0) == -1 && errno == EINTR);
+ 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, const bool terminate) {
+ if (server.socket > 0)
+ close(server.socket);
+ if ((server.socket = session_connect(name)) == -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);
+
+ client_setup_terminal();
+ 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);
+ if (terminate)
+ exit(status);
+ }
+
+ return terminate;
+}
+
+static int session_filter(const struct dirent *d) {
+ return strstr(d->d_name, server.host) != NULL;
+}
+
+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(void) {
+ if (!create_socket_dir(&sockaddr))
+ return 1;
+ if (chdir(sockaddr.sun_path) == -1)
+ die("list-session");
+ struct dirent **namelist;
+ int n = scandir(sockaddr.sun_path, &namelist, session_filter, session_comparator);
+ if (n < 0)
+ return 1;
+ printf("Active sessions (on host %s)\n", server.host+1);
+ while (n--) {
+ struct stat sb; char buf[255];
+ if (stat(namelist[n]->d_name, &sb) == 0 && S_ISSOCK(sb.st_mode)) {
+ pid_t pid = 0;
+ strftime(buf, sizeof(buf), "%a%t %F %T", localtime(&sb.st_mtime));
+ char status = ' ';
+ char *local = strstr(namelist[n]->d_name, server.host);
+ if (local) {
+ *local = '\0'; /* truncate hostname if we are local */
+ if (!(pid = session_exists(namelist[n]->d_name)))
+ continue;
+ }
+ if (sb.st_mode & S_IXUSR)
+ status = '*';
+ else if (sb.st_mode & S_IXGRP)
+ status = '+';
+ printf("%c %s\t%jd\t%s\n", status, buf, (intmax_t)pid, namelist[n]->d_name);
+ }
+ free(namelist[n]);
+ }
+ free(namelist);
+ return 0;
+}
+
+int main(int argc, char *argv[]) {
+ int opt;
+ bool force = false;
+ char **cmd = NULL, action = '\0';
+
+ char *default_cmd[4] = { "/bin/sh", "-c", getenv("ABDUCO_CMD"), NULL };
+ if (!default_cmd[2]) {
+ default_cmd[0] = ABDUCO_CMD;
+ default_cmd[1] = NULL;
+ }
+
+ server.name = basename(argv[0]);
+ gethostname(server.host+1, sizeof(server.host) - 1);
+
+ while ((opt = getopt(argc, argv, "aAclne:fpqrv")) != -1) {
+ switch (opt) {
+ case 'a':
+ case 'A':
+ case 'c':
+ case 'n':
+ action = opt;
+ break;
+ case 'e':
+ if (!optarg)
+ usage();
+ if (optarg[0] == '^' && optarg[1])
+ optarg[0] = CTRL(optarg[1]);
+ KEY_DETACH = optarg[0];
+ break;
+ case 'f':
+ force = true;
+ break;
+ case 'p':
+ passthrough = true;
+ break;
+ case 'q':
+ quiet = true;
+ break;
+ case 'r':
+ client.flags |= CLIENT_READONLY;
+ break;
+ case 'l':
+ client.flags |= CLIENT_LOWPRIORITY;
+ break;
+ case 'v':
+ puts("abduco-"VERSION" © 2013-2018 Marc André Tanner");
+ exit(EXIT_SUCCESS);
+ default:
+ usage();
+ }
+ }
+
+ /* collect the session name if trailing args */
+ if (optind < argc)
+ server.session_name = argv[optind];
+
+ /* if yet more trailing arguments, they must be the command */
+ if (optind + 1 < argc)
+ cmd = &argv[optind + 1];
+ else
+ cmd = default_cmd;
+
+ if (server.session_name && !isatty(STDIN_FILENO))
+ passthrough = true;
+
+ if (passthrough) {
+ if (!action)
+ action = 'a';
+ quiet = true;
+ client.flags |= CLIENT_LOWPRIORITY;
+ }
+
+ if (!action && !server.session_name)
+ exit(list_session());
+ if (!action || !server.session_name)
+ usage();
+
+ if (!passthrough && 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');
+
+ redo:
+ switch (action) {
+ case 'n':
+ case 'c':
+ if (force) {
+ if (session_alive(server.session_name)) {
+ info("session exists and has not yet terminated");
+ return 1;
+ }
+ if (session_exists(server.session_name))
+ attach_session(server.session_name, false);
+ }
+ if (!create_session(server.session_name, cmd))
+ die("create-session");
+ if (action == 'n')
+ break;
+ /* fall through */
+ case 'a':
+ if (!attach_session(server.session_name, true))
+ die("attach-session");
+ break;
+ case 'A':
+ if (session_alive(server.session_name)) {
+ if (!attach_session(server.session_name, true))
+ die("attach-session");
+ } else if (!attach_session(server.session_name, !force)) {
+ force = false;
+ action = 'c';
+ goto redo;
+ }
+ break;
+ }
+
+ return 0;
+}
diff --git a/client.c b/client.c
@@ -0,0 +1,144 @@
+static 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_restore_terminal(void) {
+ if (!has_term)
+ return;
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_term);
+ if (alternate_buffer) {
+ printf("\033[?25h\033[?1049l");
+ fflush(stdout);
+ alternate_buffer = false;
+ }
+}
+
+static void client_setup_terminal(void) {
+ if (!has_term)
+ return;
+ 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, TCSANOW, &cur_term);
+
+ if (!alternate_buffer) {
+ printf("\033[?1049h\033[H");
+ fflush(stdout);
+ alternate_buffer = true;
+ }
+}
+
+static int client_mainloop(void) {
+ sigset_t emptyset, blockset;
+ sigemptyset(&emptyset);
+ sigemptyset(&blockset);
+ sigaddset(&blockset, SIGWINCH);
+ sigprocmask(SIG_BLOCK, &blockset, NULL);
+
+ client.need_resize = true;
+ Packet pkt = {
+ .type = MSG_ATTACH,
+ .u.i = client.flags,
+ .len = sizeof(pkt.u.i),
+ };
+ 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 = { .rows = ws.ws_row, .cols = ws.ws_col } },
+ .len = sizeof(pkt.u.ws),
+ };
+ if (client_send_packet(&pkt))
+ client.need_resize = false;
+ }
+ }
+
+ if (pselect(server.socket+1, &fds, NULL, NULL, NULL, &emptyset) == -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:
+ if (!passthrough)
+ 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 (KEY_REDRAW && 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.flags & CLIENT_READONLY)) {
+ client_send_packet(&pkt);
+ }
+ } else if (len == 0) {
+ debug("client-stdin: EOF\n");
+ return -1;
+ }
+ }
+ }
+
+ return -EIO;
+}
diff --git a/config.def.h b/config.def.h
@@ -0,0 +1,19 @@
+/* default command to execute if non is given and $ABDUCO_CMD is unset */
+#define ABDUCO_CMD "dvtm"
+/* default detach key, can be overriden at run time using -e option */
+static char KEY_DETACH = CTRL('\\');
+/* redraw key to send a SIGWINCH signal to underlying process
+ * (set to 0 to disable the redraw key) */
+static char KEY_REDRAW = 0;
+/* Where to place the "abduco" directory storing all session socket files.
+ * The first directory to succeed is used. */
+static struct Dir {
+ char *path; /* fixed (absolute) path to a directory */
+ char *env; /* environment variable to use if (set) */
+ bool personal; /* if false a user owned sub directory will be created */
+} socket_dirs[] = {
+ { .env = "ABDUCO_SOCKET_DIR", false },
+ { .env = "HOME", true },
+ { .env = "TMPDIR", false },
+ { .path = "/tmp", false },
+};
diff --git a/configure b/configure
@@ -0,0 +1,244 @@
+#!/bin/sh
+# Based on the configure script from musl libc, MIT licensed
+
+usage () {
+cat <<EOF
+Usage: $0 [OPTION]... [VAR=VALUE]...
+
+To assign environment variables (e.g., CC, CFLAGS...), specify them as
+VAR=VALUE. See below for descriptions of some of the useful variables.
+
+Defaults for the options are specified in brackets.
+
+Configuration:
+ --srcdir=DIR source directory [detected]
+
+Installation directories:
+ --prefix=PREFIX main installation prefix [/usr/local]
+ --exec-prefix=EPREFIX installation prefix for executable files [PREFIX]
+
+Fine tuning of the installation directories:
+ --bindir=DIR user executables [EPREFIX/bin]
+ --sharedir=DIR share directories [PREFIX/share]
+ --docdir=DIR misc. documentation [PREFIX/share/doc]
+ --mandir=DIR man pages [PREFIX/share/man]
+
+Some influential environment variables:
+ CC C compiler command [detected]
+ CFLAGS C compiler flags [-Os -pipe ...]
+ LDFLAGS Linker flags
+
+Use these variables to override the choices made by configure.
+
+EOF
+exit 0
+}
+
+# Helper functions
+
+quote () {
+tr '\n' ' ' <<EOF | grep '^[-[:alnum:]_=,./:]* $' >/dev/null 2>&1 && { echo "$1" ; return 0 ; }
+$1
+EOF
+printf %s\\n "$1" | sed -e "s/'/'\\\\''/g" -e "1s/^/'/" -e "\$s/\$/'/" -e "s#^'\([-[:alnum:]_,./:]*\)=\(.*\)\$#\1='\2#"
+}
+echo () { printf "%s\n" "$*" ; }
+fail () { echo "$*" ; exit 1 ; }
+fnmatch () { eval "case \"\$2\" in $1) return 0 ;; *) return 1 ;; esac" ; }
+cmdexists () { type "$1" >/dev/null 2>&1 ; }
+trycc () { test -z "$CC" && cmdexists "$1" && CC=$1 ; }
+
+stripdir () {
+while eval "fnmatch '*/' \"\${$1}\"" ; do eval "$1=\${$1%/}" ; done
+}
+
+trycppif () {
+printf "checking preprocessor condition %s... " "$1"
+echo "typedef int x;" > "$tmpc"
+echo "#if $1" >> "$tmpc"
+echo "#error yes" >> "$tmpc"
+echo "#endif" >> "$tmpc"
+if $CC $2 -c -o "$tmpo" "$tmpc" >/dev/null 2>&1 ; then
+printf "false\n"
+return 1
+else
+printf "true\n"
+return 0
+fi
+}
+
+tryflag () {
+printf "checking whether compiler accepts %s... " "$2"
+echo "typedef int x;" > "$tmpc"
+if $CC $CFLAGS_TRY $2 -c -o "$tmpo" "$tmpc" >/dev/null 2>&1 ; then
+printf "yes\n"
+eval "$1=\"\${$1} \$2\""
+eval "$1=\${$1# }"
+return 0
+else
+printf "no\n"
+return 1
+fi
+}
+
+tryldflag () {
+printf "checking whether linker accepts %s... " "$2"
+echo "typedef int x;" > "$tmpc"
+if $CC $LDFLAGS_TRY -nostdlib -shared "$2" -o "$tmpo" "$tmpc" >/dev/null 2>&1 ; then
+printf "yes\n"
+eval "$1=\"\${$1} \$2\""
+eval "$1=\${$1# }"
+return 0
+else
+printf "no\n"
+return 1
+fi
+}
+
+# Beginning of actual script
+
+CFLAGS_AUTO=
+CFLAGS_TRY=
+LDFLAGS_AUTO=
+LDFLAGS_TRY=
+SRCDIR=
+PREFIX=/usr/local
+EXEC_PREFIX='$(PREFIX)'
+BINDIR='$(EXEC_PREFIX)/bin'
+MANDIR='$(PREFIX)/share/man'
+
+for arg ; do
+case "$arg" in
+--help|-h) usage ;;
+--srcdir=*) SRCDIR=${arg#*=} ;;
+--prefix=*) PREFIX=${arg#*=} ;;
+--exec-prefix=*) EXEC_PREFIX=${arg#*=} ;;
+--bindir=*) BINDIR=${arg#*=} ;;
+--sharedir=*) SHAREDIR=${arg#*=} ;;
+--docdir=*) DOCDIR=${arg#*=} ;;
+--mandir=*) MANDIR=${arg#*=} ;;
+--enable-*|--disable-*|--with-*|--without-*|--*dir=*|--build=*) ;;
+-* ) echo "$0: unknown option $arg" ;;
+CC=*) CC=${arg#*=} ;;
+CFLAGS=*) CFLAGS=${arg#*=} ;;
+CPPFLAGS=*) CPPFLAGS=${arg#*=} ;;
+LDFLAGS=*) LDFLAGS=${arg#*=} ;;
+*=*) ;;
+*) ;;
+esac
+done
+
+for i in SRCDIR PREFIX EXEC_PREFIX BINDIR MANDIR ; do
+stripdir $i
+done
+
+#
+# Get the source dir for out-of-tree builds
+#
+if test -z "$SRCDIR" ; then
+SRCDIR="${0%/configure}"
+stripdir SRCDIR
+fi
+abs_builddir="$(pwd)" || fail "$0: cannot determine working directory"
+abs_srcdir="$(cd $SRCDIR && pwd)" || fail "$0: invalid source directory $SRCDIR"
+test "$abs_srcdir" = "$abs_builddir" && SRCDIR=.
+test "$SRCDIR" != "." -a -f Makefile -a ! -h Makefile && fail "$0: Makefile already exists in the working directory"
+
+#
+# Get a temp filename we can use
+#
+i=0
+set -C
+while : ; do i=$(($i+1))
+tmpc="./conf$$-$PPID-$i.c"
+tmpo="./conf$$-$PPID-$i.o"
+2>|/dev/null > "$tmpc" && break
+test "$i" -gt 50 && fail "$0: cannot create temporary file $tmpc"
+done
+set +C
+trap 'rm -f "$tmpc" "$tmpo"' EXIT INT QUIT TERM HUP
+
+#
+# Find a C compiler to use
+#
+printf "checking for C compiler... "
+trycc cc
+trycc gcc
+trycc clang
+printf "%s\n" "$CC"
+test -n "$CC" || { echo "$0: cannot find a C compiler" ; exit 1 ; }
+
+printf "checking whether C compiler works... "
+echo "typedef int x;" > "$tmpc"
+if output=$($CC $CPPFLAGS $CFLAGS -c -o "$tmpo" "$tmpc" 2>&1) ; then
+printf "yes\n"
+else
+printf "no; compiler output follows:\n%s\n" "$output"
+exit 1
+fi
+
+#
+# Figure out options to force errors on unknown flags.
+#
+tryflag CFLAGS_TRY -Werror=unknown-warning-option
+tryflag CFLAGS_TRY -Werror=unused-command-line-argument
+tryldflag LDFLAGS_TRY -Werror=unknown-warning-option
+tryldflag LDFLAGS_TRY -Werror=unused-command-line-argument
+
+CFLAGS_STD="-std=c99 -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -DNDEBUG -D_FORTIFY_SOURCE=2"
+LDFLAGS_STD="-lc -lutil"
+
+OS=$(uname)
+
+case "$OS" in
+FreeBSD) CFLAGS_STD="$CFLAGS_STD -D_BSD_SOURCE -D__BSD_VISIBLE=1" ;;
+*BSD) CFLAGS_STD="$CFLAGS_STD -D_BSD_SOURCE" ;;
+Darwin) CFLAGS_STD="$CFLAGS_STD -D_DARWIN_C_SOURCE" ;;
+AIX) CFLAGS_STD="$CFLAGS_STD -D_ALL_SOURCE" ;;
+esac
+
+tryflag CFLAGS -pipe
+
+# Try flags to optimize binary size
+tryflag CFLAGS -Os
+tryflag CFLAGS -ffunction-sections
+tryflag CFLAGS -fdata-sections
+tryldflag LDFLAGS_AUTO -Wl,--gc-sections
+
+# Try hardening flags
+tryflag CFLAGS -fPIE
+tryflag CFLAGS_AUTO -fstack-protector-all
+tryldflag LDFLAGS -Wl,-z,now
+tryldflag LDFLAGS -Wl,-z,relro
+tryldflag LDFLAGS_AUTO -pie
+
+printf "creating config.mk... "
+
+cmdline=$(quote "$0")
+for i ; do cmdline="$cmdline $(quote "$i")" ; done
+
+exec 3>&1 1>config.mk
+
+cat << EOF
+# This version of config.mk was generated by:
+# $cmdline
+# Any changes made here will be lost if configure is re-run
+SRCDIR = $SRCDIR
+PREFIX = $PREFIX
+EXEC_PREFIX = $EXEC_PREFIX
+BINDIR = $BINDIR
+MANPREFIX = $MANDIR
+CC = $CC
+CFLAGS = $CFLAGS
+LDFLAGS = $LDFLAGS
+CFLAGS_STD = $CFLAGS_STD
+LDFLAGS_STD = $LDFLAGS_STD
+CFLAGS_AUTO = $CFLAGS_AUTO
+LDFLAGS_AUTO = $LDFLAGS_AUTO
+CFLAGS_DEBUG = -U_FORTIFY_SOURCE -UNDEBUG -O0 -g -ggdb -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-sign-compare
+EOF
+exec 1>&3 3>&-
+
+printf "done\n"
+
+test "$SRCDIR" = "." || ln -sf $SRCDIR/Makefile .
diff --git a/contrib/abduco.zsh b/contrib/abduco.zsh
@@ -0,0 +1,35 @@
+#compdef abduco
+
+typeset -A opt_args
+
+_abduco_sessions() {
+ declare -a sessions
+ sessions=( $(abduco | sed '1d;s/.*\t[0-9][0-9]*\t//') )
+ _describe -t session 'session' sessions
+}
+
+_abduco_firstarg() {
+ if (( $+opt_args[-a] || $+opt_args[-A] )); then
+ _abduco_sessions
+ elif (( $+opt_args[-c] || $+opt_args[-n] )); then
+ _guard "^-*" 'session name'
+ elif [[ -z $words[CURRENT] ]]; then
+ compadd "$@" -S '' -- -
+ fi
+}
+
+_arguments -s \
+ '(-a -A -c -n -f)-a[attach to an existing session]' \
+ '(-a -A -c -n)-A[attach to a session, create if does not exist]' \
+ '(-a -A -c -n -l)-c[create a new session and attach to it]' \
+ '(-a -A -c -n -l)-n[create a new session but do not attach to it]' \
+ '-e[set the detachkey (default: ^\\)]:detachkey' \
+ '(-a)-f[force create the session]' \
+ '(-q)-p[pass-through mode]' \
+ '-q[be quiet]' \
+ '-r[read-only session, ignore user input]' \
+ '(-c -n)-l[attach with the lowest priority]' \
+ '(-)-v[show version information and exit]' \
+ '1: :_abduco_firstarg' \
+ '2:command:_path_commands' \
+ '*:: :{ shift $((CURRENT-3)) words; _precommand; }'
diff --git a/debug.c b/debug.c
@@ -0,0 +1,52 @@
+#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_EXIT] = "EXIT",
+ [MSG_PID] = "PID",
+ };
+ 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:
+ fwrite(pkt->u.msg, pkt->len, 1, stderr);
+ break;
+ case MSG_RESIZE:
+ fprintf(stderr, "%"PRIu16"x%"PRIu16, pkt->u.ws.cols, pkt->u.ws.rows);
+ break;
+ case MSG_ATTACH:
+ fprintf(stderr, "readonly: %d low-priority: %d",
+ pkt->u.i & CLIENT_READONLY,
+ pkt->u.i & CLIENT_LOWPRIORITY);
+ break;
+ case MSG_EXIT:
+ fprintf(stderr, "status: %"PRIu32, pkt->u.i);
+ break;
+ case MSG_PID:
+ fprintf(stderr, "pid: %"PRIu32, pkt->u.i);
+ break;
+ default:
+ fprintf(stderr, "len: %"PRIu32, pkt->len);
+ break;
+ }
+ fprintf(stderr, "\n");
+}
+
+#endif /* NDEBUG */
diff --git a/forkpty-aix.c b/forkpty-aix.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
+ * Copyright (c) 2012 Ross Palmer Mohn <rpmohn@waxandwane.org>
+ *
+ * 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 <sys/types.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stropts.h>
+#include <unistd.h>
+#include <paths.h>
+
+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);
+ }
+
+ setsid();
+
+ fd = open(_PATH_TTY, O_RDWR|O_NOCTTY);
+ if (fd >= 0)
+ return -1;
+
+ fd = open(path, O_RDWR);
+ if (fd < 0)
+ return -1;
+ close(fd);
+
+ fd = open("/dev/tty", O_WRONLY);
+ if (fd < 0)
+ return -1;
+ close(fd);
+
+ if (tcgetattr(slave, &tio2) != 0)
+ return -1;
+ 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)
+ return -1;
+ if (ioctl(slave, TIOCSWINSZ, ws) == -1)
+ return -1;
+
+ 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;
+}
diff --git a/forkpty-sunos.c b/forkpty-sunos.c
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2008 Nicholas Marriott <nicm@users.sourceforge.net>
+ *
+ * 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 <sys/types.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <strings.h>
+#include <stropts.h>
+#include <unistd.h>
+
+#ifndef TTY_NAME_MAX
+#define TTY_NAME_MAX TTYNAME_MAX
+#endif
+
+pid_t forkpty(int *master, char *name, struct termios *tio, struct winsize *ws)
+{
+ int slave;
+ char *path;
+ pid_t pid;
+
+ if ((*master = open("/dev/ptmx", O_RDWR|O_NOCTTY)) == -1)
+ return -1;
+ if (grantpt(*master) != 0)
+ goto out;
+ if (unlockpt(*master) != 0)
+ goto out;
+
+ if ((path = ptsname(*master)) == NULL)
+ goto out;
+ if (name != NULL)
+ strlcpy(name, path, TTY_NAME_MAX);
+ if ((slave = open(path, O_RDWR|O_NOCTTY)) == -1)
+ goto out;
+
+ switch (pid = fork()) {
+ case -1:
+ goto out;
+ case 0:
+ close(*master);
+
+ setsid();
+#ifdef TIOCSCTTY
+ if (ioctl(slave, TIOCSCTTY, NULL) == -1)
+ return -1;
+#endif
+
+ if (ioctl(slave, I_PUSH, "ptem") == -1)
+ return -1;
+ if (ioctl(slave, I_PUSH, "ldterm") == -1)
+ return -1;
+
+ if (tio != NULL && tcsetattr(slave, TCSAFLUSH, tio) == -1)
+ return -1;
+ if (ioctl(slave, TIOCSWINSZ, ws) == -1)
+ return -1;
+
+ 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;
+}
diff --git a/server.c b/server.c
@@ -0,0 +1,295 @@
+#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 void server_sink_client() {
+ if (!server.clients || !server.clients->next)
+ return;
+ Client *target = server.clients;
+ server.clients = target->next;
+ Client *dst = server.clients;
+ while (dst->next)
+ dst = dst->next;
+ target->next = NULL;
+ dst->next = target;
+}
+
+static void server_mark_socket_exec(bool exec, bool usr) {
+ struct stat sb;
+ if (stat(sockaddr.sun_path, &sb) == -1)
+ return;
+ mode_t mode = sb.st_mode;
+ mode_t flag = usr ? S_IXUSR : S_IXGRP;
+ if (exec)
+ mode |= flag;
+ else
+ mode &= ~flag;
+ chmod(sockaddr.sun_path, mode);
+}
+
+static int server_create_socket(const char *name) {
+ if (!set_socket_name(&sockaddr, name))
+ return -1;
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1)
+ return -1;
+ socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1;
+ mode_t mask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
+ int r = bind(fd, (struct sockaddr*)&sockaddr, socklen);
+ umask(mask);
+
+ if (r == -1) {
+ close(fd);
+ return -1;
+ }
+
+ if (listen(fd, 5) == -1) {
+ unlink(sockaddr.sun_path);
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+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 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 > 0)
+ pkt->len = len;
+ else if (len == 0)
+ server.running = false;
+ else if (len == -1 && errno != EAGAIN && errno != EINTR && errno != EWOULDBLOCK)
+ 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 Client *server_accept_client(void) {
+ int newfd = accept(server.socket, NULL, NULL);
+ if (newfd == -1 || server_set_socket_non_blocking(newfd) == -1)
+ goto error;
+ Client *c = client_malloc(newfd);
+ if (!c)
+ goto error;
+ if (!server.clients)
+ server_mark_socket_exec(true, true);
+ c->socket = newfd;
+ c->state = STATE_CONNECTED;
+ c->next = server.clients;
+ server.clients = c;
+ server.read_pty = true;
+
+ Packet pkt = {
+ .type = MSG_PID,
+ .len = sizeof pkt.u.l,
+ .u.l = getpid(),
+ };
+ server_send_packet(c, &pkt);
+
+ return c;
+error:
+ if (newfd != -1)
+ close(newfd);
+ return NULL;
+}
+
+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(void) {
+ unlink(sockaddr.sun_path);
+}
+
+static void server_mainloop(void) {
+ 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->flags = client_packet.u.i;
+ if (c->flags & CLIENT_LOWPRIORITY)
+ server_sink_client();
+ break;
+ case MSG_RESIZE:
+ c->state = STATE_ATTACHED;
+ if (!(c->flags & CLIENT_READONLY) && c == server.clients) {
+ debug("server-ioct: TIOCSWINSZ\n");
+ struct winsize ws = { 0 };
+ ws.ws_row = client_packet.u.ws.rows;
+ ws.ws_col = client_packet.u.ws.cols;
+ ioctl(server.pty, TIOCSWINSZ, &ws);
+ }
+ kill(-server.pid, SIGWINCH);
+ break;
+ case MSG_EXIT:
+ exit_packet_delivered = true;
+ /* fall through */
+ case MSG_DETACH:
+ 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))
+ 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);
+}
diff --git a/testsuite.sh b/testsuite.sh
@@ -0,0 +1,215 @@
+#!/bin/sh
+
+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
+
+TESTS_OK=0
+TESTS_RUN=0
+
+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 'qq'
+}
+
+expected_abduco_prolog() {
+ printf "[?1049h[H"
+}
+
+# $1 => session-name, $2 => exit status
+expected_abduco_epilog() {
+ echo "[?25h[?1049labduco: $1: session terminated with exit status $2"
+}
+
+# $1 => session-name, $2 => cmd to run
+expected_abduco_attached_output() {
+ expected_abduco_prolog
+ $2
+ expected_abduco_epilog "$1" $?
+}
+
+# $1 => session-name, $2 => cmd to run
+expected_abduco_detached_output() {
+ expected_abduco_prolog
+ $2 >/dev/null 2>&1
+ expected_abduco_epilog "$1" $?
+}
+
+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 2>&1
+ 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"
+
+ TESTS_RUN=$((TESTS_RUN + 1))
+ echo -n "Running test attached: $name "
+ expected_abduco_attached_output "$name" "$cmd" > "$output_expected" 2>&1
+
+ if $ABDUCO -c "$name" $cmd 2>&1 | sed 's/.$//' > "$output" && sleep 1 &&
+ diff -u "$output_expected" "$output" && check_environment; then
+ rm "$output" "$output_expected"
+ TESTS_OK=$((TESTS_OK + 1))
+ 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"
+
+ TESTS_RUN=$((TESTS_RUN + 1))
+ echo -n "Running test detached: $name "
+ expected_abduco_detached_output "$name" "$cmd" > "$output_expected" 2>&1
+
+ if $ABDUCO -n "$name" $cmd >/dev/null 2>&1 && sleep 1 &&
+ $ABDUCO -a "$name" 2>&1 | sed 's/.$//' > "$output" &&
+ diff -u "$output_expected" "$output" && check_environment; then
+ rm "$output" "$output_expected"
+ TESTS_OK=$((TESTS_OK + 1))
+ 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"
+
+ TESTS_RUN=$((TESTS_RUN + 1))
+ echo -n "Running test: $name "
+ $cmd >/dev/null 2>&1
+ expected_abduco_epilog "$name" $? > "$output_expected" 2>&1
+
+ if detach | $ABDUCO $ABDUCO_OPTS -c "$name" $cmd >/dev/null 2>&1 && sleep 3 &&
+ $ABDUCO -a "$name" 2>&1 | tail -1 | sed 's/.$//' > "$output" &&
+ diff -u "$output_expected" "$output" && check_environment; then
+ rm "$output" "$output_expected"
+ TESTS_OK=$((TESTS_OK + 1))
+ echo "OK"
+ return 0
+ else
+ echo "FAIL"
+ return 1
+ fi
+}
+
+run_test_dvtm() {
+ echo -n "Running dvtm test: "
+ if ! which dvtm >/dev/null 2>&1; then
+ echo "SKIPPED"
+ return 0;
+ fi
+
+ TESTS_RUN=$((TESTS_RUN + 1))
+ local name="dvtm"
+ local output="$name.out"
+ local output_expected="$name.expected"
+
+ : > "$output_expected"
+ if dvtm_session | $ABDUCO -c "$name" > "$output" 2>&1 &&
+ diff -u "$output_expected" "$output" && check_environment; then
+ rm "$output" "$output_expected"
+ TESTS_OK=$((TESTS_OK + 1))
+ echo "OK"
+ return 0
+ else
+ echo "FAIL"
+ return 1
+ fi
+}
+
+test_non_existing_command || echo "Execution of non existing command FAILED"
+
+run_test_attached "awk" "awk 'BEGIN {for(i=1;i<=1000;i++) print i}'"
+run_test_detached "awk" "awk 'BEGIN {for(i=1;i<=1000;i++) print i}'"
+
+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
+
+[ $TESTS_OK -eq $TESTS_RUN ]