abduco

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

commit 23074d3425d3fbcefa2bf6b94bdfc2c46ca4c5e7
Author: AndrewLockVI <andrew@laack.co>
Date:   Fri, 21 Feb 2025 05:16:08 -0600

Init commit of abduco

Diffstat:
A.gitignore | 9+++++++++
ALICENSE | 15+++++++++++++++
AMakefile | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aabduco.1 | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aabduco.c | 718+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclient.c | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 19+++++++++++++++++++
Aconfigure | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontrib/abduco.zsh | 35+++++++++++++++++++++++++++++++++++
Adebug.c | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Aforkpty-aix.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aforkpty-sunos.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserver.c | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atestsuite.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+dvtm demo](https://raw.githubusercontent.com/martanne/abduco/gh-pages/screencast.gif#center) + +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" +} + +# $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 ]