Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

23 changed files with 0 additions and 2566 deletions

View File

@ -1,11 +0,0 @@
image: alpine/edge
sources:
- https://github.com/martanne/abduco
tasks:
- build: |
cd abduco
./configure
make
- test: |
cd abduco
./testsuite.sh

View File

@ -1,11 +0,0 @@
image: debian/stable
sources:
- https://github.com/martanne/abduco
tasks:
- build: |
cd abduco
./configure
make
- test: |
cd abduco
./testsuite.sh

View File

@ -1,11 +0,0 @@
image: freebsd/latest
sources:
- https://github.com/martanne/abduco
tasks:
- build: |
cd abduco
./configure
make
- test: |
cd abduco
./testsuite.sh

View File

@ -1,11 +0,0 @@
image: openbsd/latest
sources:
- https://github.com/martanne/abduco
tasks:
- build: |
cd abduco
./configure
make
- test: |
cd abduco
./testsuite.sh

View File

@ -1,46 +0,0 @@
name: Coverity Scan
env:
PROJECT: abduco
on:
schedule:
- cron: '0 0 * * 0' # once a week
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download Coverity Build Tool
run: |
wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=$TOKEN&project=martanne/${PROJECT}" -O cov-analysis-linux64.tar.gz
mkdir cov-analysis-linux64
tar xzf cov-analysis-linux64.tar.gz --strip 1 -C cov-analysis-linux64
env:
TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}
- name: Configure
run: ./configure
- name: Build with cov-build
run: |
export PATH=$(pwd)/cov-analysis-linux64/bin:$PATH
cov-build --dir cov-int make
- name: Submit the result to Coverity Scan
run: |
tar czvf ${PROJECT}.tgz cov-int
curl \
--form project=martanne/${PROJECT} \
--form token=$TOKEN \
--form email=mat@brain-dump.org \
--form file=@${PROJECT}.tgz \
--form version=trunk \
--form description="`./${PROJECT} -v`" \
https://scan.coverity.com/builds?project=martanne/${PROJECT}
env:
TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}

View File

@ -1,24 +0,0 @@
name: macOS
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os:
- macos-10.15
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: |
./configure
make
- name: Test
run: |
./testsuite.sh

View File

@ -1,44 +0,0 @@
name: Manual
env:
PROJECT: abduco
on:
push:
paths:
- '*.1'
jobs:
man:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Dependency
run: sudo apt install mandoc
- name: Manual generation
run: |
mkdir man
sed -e "s/VERSION/$(git describe --always)/" ${PROJECT}.1 | \
mandoc -W warning -T utf8 -T html -O man=%N.%S.html -O style=mandoc.css 1> \
"man/${PROJECT}.1.html" || true
wget 'https://cvsweb.bsd.lv/~checkout~/mandoc/mandoc.css?rev=1.46&content-type=text/plain' -O man/mandoc.css
ln -sf "${PROJECT}.1.html" man/index.html
- name: Upload
env:
DEPLOY_TOKEN: ${{ secrets.GIT_DEPLOY_TOKEN }}
run: |
git clone --depth=1 --single-branch --branch gh-pages "https://x-access-token:${DEPLOY_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" /tmp/gh-pages
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
mkdir -p /tmp/gh-pages/man
rm -f /tmp/gh-pages/man/*
cp -av man/*.html /tmp/gh-pages/man/
cp -av man/*.css /tmp/gh-pages/man/
cd /tmp/gh-pages
git add -A && git commit --allow-empty -am "Publishing from ${GITHUB_REPOSITORY} ${GITHUB_SHA}"
git push origin gh-pages

9
.gitignore vendored
View File

@ -1,9 +0,0 @@
/config.h
/config.mk
/abduco
*.css
*.gcda
*.gcno
*.gcov
*.html
*.o

15
LICENSE
View File

@ -1,15 +0,0 @@
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.

View File

@ -1,67 +0,0 @@
-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

185
README.md
View File

@ -1,185 +0,0 @@
# 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)

226
abduco.1
View File

@ -1,226 +0,0 @@
.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

718
abduco.c
View File

@ -1,718 +0,0 @@
/*
* 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;
}

144
client.c
View File

@ -1,144 +0,0 @@
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;
}

View File

@ -1,19 +0,0 @@
/* 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 },
};

244
configure vendored
View File

@ -1,244 +0,0 @@
#!/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 .

View File

@ -1,35 +0,0 @@
#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; }'

52
debug.c
View File

@ -1,52 +0,0 @@
#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 */

View File

@ -1,96 +0,0 @@
/*
* 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;
}

View File

@ -1,88 +0,0 @@
/*
* 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;
}

BIN
screencast.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

295
server.c
View File

@ -1,295 +0,0 @@
#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);
}

View File

@ -1,215 +0,0 @@
#!/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 ]