├── .builds ├── alpine.yml ├── debian.yml ├── freebsd.yml └── openbsd.yml ├── .github └── workflows │ ├── coverity-scan.yml │ ├── macos.yml │ └── man.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── abduco.1 ├── abduco.c ├── client.c ├── config.def.h ├── configure ├── contrib └── abduco.zsh ├── debug.c ├── forkpty-aix.c ├── forkpty-sunos.c ├── server.c └── testsuite.sh /.builds/alpine.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | sources: 3 | - https://github.com/martanne/abduco 4 | tasks: 5 | - build: | 6 | cd abduco 7 | ./configure 8 | make 9 | - test: | 10 | cd abduco 11 | ./testsuite.sh 12 | -------------------------------------------------------------------------------- /.builds/debian.yml: -------------------------------------------------------------------------------- 1 | image: debian/stable 2 | sources: 3 | - https://github.com/martanne/abduco 4 | tasks: 5 | - build: | 6 | cd abduco 7 | ./configure 8 | make 9 | - test: | 10 | cd abduco 11 | ./testsuite.sh 12 | -------------------------------------------------------------------------------- /.builds/freebsd.yml: -------------------------------------------------------------------------------- 1 | image: freebsd/latest 2 | sources: 3 | - https://github.com/martanne/abduco 4 | tasks: 5 | - build: | 6 | cd abduco 7 | ./configure 8 | make 9 | - test: | 10 | cd abduco 11 | ./testsuite.sh 12 | -------------------------------------------------------------------------------- /.builds/openbsd.yml: -------------------------------------------------------------------------------- 1 | image: openbsd/latest 2 | sources: 3 | - https://github.com/martanne/abduco 4 | tasks: 5 | - build: | 6 | cd abduco 7 | ./configure 8 | make 9 | - test: | 10 | cd abduco 11 | ./testsuite.sh 12 | -------------------------------------------------------------------------------- /.github/workflows/coverity-scan.yml: -------------------------------------------------------------------------------- 1 | name: Coverity Scan 2 | 3 | env: 4 | PROJECT: abduco 5 | 6 | on: 7 | schedule: 8 | - cron: '0 0 * * 0' # once a week 9 | 10 | jobs: 11 | scan: 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Download Coverity Build Tool 19 | run: | 20 | wget -q https://scan.coverity.com/download/cxx/linux64 --post-data "token=$TOKEN&project=martanne/${PROJECT}" -O cov-analysis-linux64.tar.gz 21 | mkdir cov-analysis-linux64 22 | tar xzf cov-analysis-linux64.tar.gz --strip 1 -C cov-analysis-linux64 23 | env: 24 | TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} 25 | 26 | - name: Configure 27 | run: ./configure 28 | 29 | - name: Build with cov-build 30 | run: | 31 | export PATH=$(pwd)/cov-analysis-linux64/bin:$PATH 32 | cov-build --dir cov-int make 33 | 34 | - name: Submit the result to Coverity Scan 35 | run: | 36 | tar czvf ${PROJECT}.tgz cov-int 37 | curl \ 38 | --form project=martanne/${PROJECT} \ 39 | --form token=$TOKEN \ 40 | --form email=mat@brain-dump.org \ 41 | --form file=@${PROJECT}.tgz \ 42 | --form version=trunk \ 43 | --form description="`./${PROJECT} -v`" \ 44 | https://scan.coverity.com/builds?project=martanne/${PROJECT} 45 | env: 46 | TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: 10 | - macos-10.15 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Build 18 | run: | 19 | ./configure 20 | make 21 | 22 | - name: Test 23 | run: | 24 | ./testsuite.sh 25 | -------------------------------------------------------------------------------- /.github/workflows/man.yml: -------------------------------------------------------------------------------- 1 | name: Manual 2 | 3 | env: 4 | PROJECT: abduco 5 | 6 | on: 7 | push: 8 | paths: 9 | - '*.1' 10 | 11 | jobs: 12 | man: 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Dependency 20 | run: sudo apt install mandoc 21 | 22 | - name: Manual generation 23 | run: | 24 | mkdir man 25 | sed -e "s/VERSION/$(git describe --always)/" ${PROJECT}.1 | \ 26 | mandoc -W warning -T utf8 -T html -O man=%N.%S.html -O style=mandoc.css 1> \ 27 | "man/${PROJECT}.1.html" || true 28 | wget 'https://cvsweb.bsd.lv/~checkout~/mandoc/mandoc.css?rev=1.46&content-type=text/plain' -O man/mandoc.css 29 | ln -sf "${PROJECT}.1.html" man/index.html 30 | 31 | - name: Upload 32 | env: 33 | DEPLOY_TOKEN: ${{ secrets.GIT_DEPLOY_TOKEN }} 34 | run: | 35 | git clone --depth=1 --single-branch --branch gh-pages "https://x-access-token:${DEPLOY_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" /tmp/gh-pages 36 | git config --global user.name "${GITHUB_ACTOR}" 37 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 38 | mkdir -p /tmp/gh-pages/man 39 | rm -f /tmp/gh-pages/man/* 40 | cp -av man/*.html /tmp/gh-pages/man/ 41 | cp -av man/*.css /tmp/gh-pages/man/ 42 | cd /tmp/gh-pages 43 | git add -A && git commit --allow-empty -am "Publishing from ${GITHUB_REPOSITORY} ${GITHUB_SHA}" 44 | git push origin gh-pages 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config.h 2 | /config.mk 3 | /abduco 4 | *.css 5 | *.gcda 6 | *.gcno 7 | *.gcov 8 | *.html 9 | *.o 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2013-2018 Marc André Tanner 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include config.mk 2 | 3 | VERSION = 0.6 4 | 5 | CFLAGS_STD ?= -std=c99 -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -DNDEBUG 6 | CFLAGS_STD += -DVERSION=\"${VERSION}\" 7 | 8 | LDFLAGS_STD ?= -lc -lutil 9 | 10 | STRIP ?= strip 11 | INSTALL ?= install 12 | 13 | PREFIX ?= /usr/local 14 | SHAREDIR ?= ${PREFIX}/share 15 | 16 | SRC = abduco.c 17 | 18 | all: abduco 19 | 20 | config.h: 21 | cp config.def.h config.h 22 | 23 | config.mk: 24 | @touch $@ 25 | 26 | abduco: config.h config.mk *.c 27 | ${CC} ${CFLAGS} ${CFLAGS_STD} ${CFLAGS_AUTO} ${CFLAGS_EXTRA} ${SRC} ${LDFLAGS} ${LDFLAGS_STD} ${LDFLAGS_AUTO} -o $@ 28 | 29 | debug: clean 30 | make CFLAGS_EXTRA='${CFLAGS_DEBUG}' 31 | 32 | clean: 33 | @echo cleaning 34 | @rm -f abduco abduco-*.tar.gz 35 | 36 | dist: clean 37 | @echo creating dist tarball 38 | @git archive --prefix=abduco-${VERSION}/ -o abduco-${VERSION}.tar.gz HEAD 39 | 40 | installdirs: 41 | @${INSTALL} -d ${DESTDIR}${PREFIX}/bin \ 42 | ${DESTDIR}${MANPREFIX}/man1 43 | 44 | install: abduco installdirs 45 | @echo installing executable file to ${DESTDIR}${PREFIX}/bin 46 | @${INSTALL} -m 0755 abduco ${DESTDIR}${PREFIX}/bin 47 | @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 48 | @mkdir -p ${DESTDIR}${MANPREFIX}/man1 49 | @sed "s/VERSION/${VERSION}/g" < abduco.1 > ${DESTDIR}${MANPREFIX}/man1/abduco.1 50 | @chmod 644 ${DESTDIR}${MANPREFIX}/man1/abduco.1 51 | 52 | install-strip: install 53 | ${STRIP} ${DESTDIR}${PREFIX}/bin/abduco 54 | 55 | install-completion: 56 | @echo installing zsh completion file to ${DESTDIR}${SHAREDIR}/zsh/site-functions 57 | @install -Dm644 contrib/abduco.zsh ${DESTDIR}${SHAREDIR}/zsh/site-functions/_abduco 58 | 59 | uninstall: 60 | @echo removing executable file from ${DESTDIR}${PREFIX}/bin 61 | @rm -f ${DESTDIR}${PREFIX}/bin/abduco 62 | @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 63 | @rm -f ${DESTDIR}${MANPREFIX}/man1/abduco.1 64 | @echo removing zsh completion file from ${DESTDIR}${SHAREDIR}/zsh/site-functions 65 | @rm -f ${DESTDIR}${SHAREDIR}/zsh/site-functions/_abduco 66 | 67 | .PHONY: all clean dist install installdirs install-strip install-completion uninstall debug 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abduco a tool for session {at,de}tach support 2 | 3 | [abduco](https://www.brain-dump.org/projects/abduco) provides 4 | session management i.e. it allows programs to be run independently 5 | from their controlling terminal. That is programs can be detached - 6 | run in the background - and then later reattached. Together with 7 | [dvtm](https://www.brain-dump.org/projects/dvtm) it provides a 8 | simpler and cleaner alternative to tmux or screen. 9 | 10 | ![abduco+dvtm demo](https://raw.githubusercontent.com/martanne/abduco/gh-pages/screencast.gif#center) 11 | 12 | abduco is in many ways very similar to [dtach](http://dtach.sf.net) 13 | but is a completely independent implementation which is actively maintained, 14 | contains no legacy code, provides a few additional features, has a 15 | cleaner, more robust implementation and is distributed under the 16 | [ISC license](https://raw.githubusercontent.com/martanne/abduco/master/LICENSE) 17 | 18 | ## News 19 | 20 | * [abduco-0.6](https://www.brain-dump.org/projects/abduco/abduco-0.6.tar.gz) 21 | [released](https://lists.suckless.org/dev/1603/28589.html) (24.03.2016) 22 | * [abduco-0.5](https://www.brain-dump.org/projects/abduco/abduco-0.5.tar.gz) 23 | [released](https://lists.suckless.org/dev/1601/28094.html) (09.01.2016) 24 | * [abduco-0.4](https://www.brain-dump.org/projects/abduco/abduco-0.4.tar.gz) 25 | [released](https://lists.suckless.org/dev/1503/26027.html) (18.03.2015) 26 | * [abduco-0.3](https://www.brain-dump.org/projects/abduco/abduco-0.3.tar.gz) 27 | [released](https://lists.suckless.org/dev/1502/25557.html) (19.02.2015) 28 | * [abduco-0.2](https://www.brain-dump.org/projects/abduco/abduco-0.2.tar.gz) 29 | [released](https://lists.suckless.org/dev/1411/24447.html) (15.11.2014) 30 | * [abduco-0.1](https://www.brain-dump.org/projects/abduco/abduco-0.1.tar.gz) 31 | [released](https://lists.suckless.org/dev/1407/22703.html) (05.07.2014) 32 | * [Initial announcement](https://lists.suckless.org/dev/1403/20372.html) 33 | on the suckless development mailing list (08.03.2014) 34 | 35 | ## Download 36 | 37 | Either download the latest [source tarball](https://github.com/martanne/abduco/releases), 38 | compile and install it 39 | 40 | ./configure && make && sudo make install 41 | 42 | or use one of the distribution provided 43 | [binary packages](https://repology.org/project/abduco/packages). 44 | 45 | ## Quickstart 46 | 47 | In order to create a new session `abduco` requires a session name 48 | as well as an command which will be run. If no command is given 49 | the environment variable `$ABDUCO_CMD` is examined and if not set 50 | `dvtm` is executed. Therefore assuming `dvtm` is located somewhere 51 | in `$PATH` a new session named *demo* is created with: 52 | 53 | $ abduco -c demo 54 | 55 | An arbitrary application can be started as follows: 56 | 57 | $ abduco -c session-name your-application 58 | 59 | `CTRL-\` detaches from the active session. This detach key can be 60 | changed by means of the `-e` command line option, `-e ^q` would 61 | for example set it to `CTRL-q`. 62 | 63 | To get an overview of existing session run `abduco` without any 64 | arguments. 65 | 66 | $ abduco 67 | Active sessions (on host debbook) 68 | * Thu 2015-03-12 12:05:20 demo-active 69 | + Thu 2015-03-12 12:04:50 demo-finished 70 | Thu 2015-03-12 12:03:30 demo 71 | 72 | A leading asterisk `*` indicates that at least one client is 73 | connected. A leading plus `+` denotes that the session terminated, 74 | attaching to it will print its exit status. 75 | 76 | A session can be reattached by using the `-a` command line option 77 | in combination with the session name which was used during session 78 | creation. 79 | 80 | $ abduco -a demo 81 | 82 | If you encounter problems with incomplete redraws or other 83 | incompatibilities it is recommended to run your applications 84 | within [dvtm](https://github.com/martanne/dvtm) under abduco: 85 | 86 | $ abduco -c demo dvtm your-application 87 | 88 | Check out the manual page for further information and all available 89 | command line options. 90 | 91 | ## Improvements over dtach 92 | 93 | * **session list**, available by executing `abduco` without any arguments, 94 | indicating whether clients are connected or the command has already 95 | terminated. 96 | 97 | * the **session exit status** of the command being run is always kept and 98 | reported either upon command termination or on reconnection 99 | e.g. the following works: 100 | 101 | $ abduco -n demo true && abduco -a demo 102 | abduco: demo: session terminated with exit status 0 103 | 104 | * **read only sessions** if the `-r` command line argument is used when 105 | attaching to a session, then all keyboard input is ignored and the 106 | client is a passive observer only. 107 | 108 | Note that this is not a security feature, but only a convenient way to 109 | avoid accidental keyboard input. 110 | 111 | If you want to make your abduco session available to another user 112 | in a read only fashion, use [socat](http://www.dest-unreach.org/socat/) 113 | to proxy the abduco socket in a unidirectional (from the abduco server 114 | to the client, but not vice versa) way. 115 | 116 | Start your to be shared session, make sure only you have access to 117 | the `private` directory: 118 | 119 | $ abduco -c /tmp/abduco/private/session 120 | 121 | Then proxy the socket in unidirectional mode `-u` to a directory 122 | where the desired observers have sufficient access rights: 123 | 124 | $ socat -u unix-connect:/tmp/abduco/private/session unix-listen:/tmp/abduco/public/read-only & 125 | 126 | Now the observers can connect to the read-only side of the socket: 127 | 128 | $ abduco -a /tmp/abduco/public/read-only 129 | 130 | communication in the other direction will not be possible and keyboard 131 | input will hence be discarded. 132 | 133 | * **better resize handling** on shared sessions, resize request are only 134 | processed if they are initiated by the most recently connected, non 135 | read only client. 136 | 137 | * **socket recreation** by sending the `SIGUSR1` signal to the server 138 | process. In case the unix domain socket was removed by accident it 139 | can be recreated. The simplest way to find out the server process 140 | id is to look for abduco processes which are reparented to the init 141 | process. 142 | 143 | $ pgrep -P 1 abduco 144 | 145 | After finding the correct PID the socket can be recreated with 146 | 147 | $ kill -USR1 $PID 148 | 149 | If the abduco binary itself has also been deleted, but a session is 150 | still running, use the following command to bring back the session: 151 | 152 | $ /proc/$PID/exe 153 | 154 | * **improved socket permissions** the session sockets are by default either 155 | stored in `$HOME/.abduco` or `/tmp/abduco/$USER` in both cases it is 156 | made sure that only the owner has access to the respective directory. 157 | 158 | ## Development 159 | 160 | You can always fetch the current code base from the git repository 161 | located at [Github](https://github.com/martanne/abduco/) or 162 | [Sourcehut](https://git.sr.ht/~martanne/abduco). 163 | 164 | If you have comments, suggestions, ideas, a bug report, a patch or something 165 | else related to abduco then write to the 166 | [suckless developer mailing list](https://suckless.org/community) 167 | or contact me directly. 168 | 169 | ### Debugging 170 | 171 | The protocol content exchanged between client and server can be dumped 172 | to temporary files as follows: 173 | 174 | $ make debug 175 | $ ./abduco -n debug [command-to-debug] 2> server-log 176 | $ ./abduco -a debug 2> client-log 177 | 178 | If you want to run client and server with one command (e.g. using the `-c` 179 | option) then within `gdb` the option `set follow-fork-mode {child,parent}` 180 | might be useful. Similarly to get a syscall trace `strace -o abduco -ff 181 | [abduco-cmd]` proved to be handy. 182 | 183 | ## License 184 | 185 | abduco is licensed under the [ISC license](https://raw.githubusercontent.com/martanne/abduco/master/LICENSE) 186 | -------------------------------------------------------------------------------- /abduco.1: -------------------------------------------------------------------------------- 1 | .Dd March 18, 2018 2 | .Dt ABDUCO 1 3 | .Os abduco VERSION 4 | . 5 | .Sh NAME 6 | .Nm abduco 7 | .Nd terminal session manager 8 | . 9 | .Sh SYNOPSIS 10 | .Nm 11 | .Fl a 12 | .Op options ... 13 | .Cm name 14 | . 15 | .Nm 16 | .Fl A 17 | .Op options ... 18 | .Cm name 19 | .Cm command Op args ... 20 | . 21 | .Nm 22 | .Fl c 23 | .Op options ... 24 | .Cm name 25 | .Cm command Op args ... 26 | . 27 | .Nm 28 | .Fl n 29 | .Op options ... 30 | .Cm name 31 | .Cm command Op args ... 32 | . 33 | .Sh DESCRIPTION 34 | . 35 | .Nm 36 | disassociates a given application from its controlling 37 | terminal, thereby providing roughly the same session attach/detach support as 38 | .Xr screen 1 , 39 | .Xr tmux 1 , 40 | or 41 | .Xr dtach 1 . 42 | .Pp 43 | A session comprises of an 44 | .Nm 45 | server process which spawns a user 46 | command in its own pseudo terminal 47 | .Pq see Xr pty 7 . 48 | Each session is given a name represented by a unix domain socket 49 | .Pq see Xr unix 7 50 | stored in the local file system. 51 | .Nm 52 | clients can connect to it and their standard input output streams 53 | are relayed to the command supervised by the server. 54 | .Pp 55 | .Nm 56 | operates on the raw I/O byte stream without interpreting any terminal 57 | escape sequences. 58 | As a consequence the terminal state is not preserved across sessions. 59 | If this functionality is desired, it should be provided by another 60 | utility such as 61 | .Xr dvtm 1 . 62 | . 63 | .Ss ACTIONS 64 | . 65 | If no command line arguments are given, all currently active sessions are 66 | listed sorted by their respective creation date. 67 | Lines starting with an asterisk 68 | .Pq * 69 | indicate that at least one client is currently connected. 70 | A plus sign 71 | .Pq + 72 | signals that the command terminated while no client was connected. 73 | Attaching to the session will print its exit status. 74 | The next column shows the PID of the server process, followed by the session 75 | .Ic name . 76 | .Pp 77 | .Nm 78 | provides different actions of which one must be provided. 79 | . 80 | .Bl -tag -width indent 81 | .It Fl a 82 | Attach to an existing session. 83 | .It Fl A 84 | Try to connect to an existing session, upon failure create said session and attach immediately to it. 85 | .It Fl c 86 | Create a new session and attach immediately to it. 87 | .It Fl n 88 | Create a new session but do not attach to it. 89 | .El 90 | . 91 | .Ss OPTIONS 92 | . 93 | Additionally the following options can be provided to further tweak 94 | the behavior. 95 | .Bl -tag -width indent 96 | .It Fl e Ar detachkey 97 | Set the key to detach. 98 | Defaults to 99 | .Aq Ctrl+\e 100 | which is specified as ^\\ i.e. Ctrl is represented as a caret 101 | .Pq ^ . 102 | .It Fl f 103 | Force creation of session when there is an already terminated session of the same name, 104 | after showing its exit status. 105 | .It Fl l 106 | Attach with the lowest priority, meaning this client will be the last to control the size. 107 | .It Fl p 108 | Pass through content of standard input to the session. 109 | Implies the 110 | .Fl q 111 | and 112 | .Fl l 113 | options. 114 | .It Fl q 115 | Be quiet, do not print informative messages. 116 | .It Fl r 117 | Read-only session, user input is ignored. 118 | .It Fl v 119 | Print version information and exit. 120 | .El 121 | . 122 | .Sh SIGNALS 123 | . 124 | .Bl -tag -width indent 125 | .It Dv SIGWINCH 126 | Whenever the primary client resizes its terminal the server process will deliver a 127 | .Ev SIGWINCH 128 | signal to the supervised process. 129 | .It Dv SIGUSR1 130 | If for some reason the unix domain socket representing a session is deleted, sending 131 | .Ev SIGUSR1 132 | to the server process will recreate it. 133 | .It Dv SIGTERM 134 | Detaches a client. 135 | .El 136 | . 137 | .Sh ENVIRONMENT 138 | . 139 | .Bl -tag -width indent 140 | .It Ev ABDUCO_CMD 141 | If 142 | .Ic command 143 | is not specified, the environment variable 144 | .Ev $ABDUCO_CMD 145 | is examined, if it is not set 146 | .Xr dvtm 1 147 | is executed. 148 | .It Ev ABDUCO_SESSION 149 | The current session name available to the supervised command. 150 | .It Ev ABDUCO_SOCKET 151 | The absolute path of the session socket available to the supervised command. 152 | .El 153 | .Pp 154 | See the 155 | .Sx FILES 156 | section for environment variables used in determining the location 157 | of unix domain sockets representing sessions. 158 | .Sh FILES 159 | . 160 | All session related information is stored in the following directories (first 161 | to succeed is used): 162 | .Bl -bullet 163 | .It 164 | .Ev $ABDUCO_SOCKET_DIR/abduco 165 | .It 166 | .Ev $HOME/.abduco 167 | .It 168 | .Ev $TMPDIR/abduco/$USER 169 | .It 170 | .Ev /tmp/abduco/$USER 171 | .El 172 | . 173 | .Pp 174 | However, if a given session 175 | .Ic name 176 | represents either a relative or absolute path it is used unmodified. 177 | . 178 | . 179 | .Sh EXAMPLES 180 | . 181 | Start a new session (assuming 182 | .Xr dvtm 1 183 | is in 184 | .Ev $PATH ) 185 | with 186 | .Pp 187 | .Dl $ abduco -c my-session 188 | .Pp 189 | do some work, then detach by pressing 190 | .Aq Ctrl+\e , 191 | list existing session by running 192 | .Nm 193 | without any arguments and later reattach with 194 | .Pp 195 | .Dl $ abduco -a my-session 196 | .Pp 197 | Alternatively, we can also explicitly specify the command to run. 198 | .Pp 199 | .Dl $ abduco -c my-session /bin/sh 200 | .Pp 201 | Attach with a 202 | .Aq Ctrl+z 203 | as detach key. 204 | .Pp 205 | .Dl $ abduco -e ^z -a my-session 206 | .Pp 207 | Send a command to an existing session. 208 | .Pp 209 | .Dl $ echo make | abduco -a my-session 210 | .Pp 211 | Or in a slightly more interactive fashion. 212 | .Pp 213 | .Dl $ abduco -p my-session 214 | .Dl make 215 | .Dl ^D 216 | . 217 | .Sh SEE ALSO 218 | .Xr dvtm 1 , 219 | .Xr dtach 1 , 220 | .Xr tmux 1 , 221 | .Xr screen 1 222 | . 223 | .Sh AUTHORS 224 | .Nm 225 | is written by 226 | .An Marc André Tanner Aq mat at brain-dump.org 227 | -------------------------------------------------------------------------------- /abduco.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013-2018 Marc André Tanner 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #if defined(__linux__) || defined(__CYGWIN__) 41 | # include 42 | #elif defined(__FreeBSD__) || defined(__DragonFly__) 43 | # include 44 | #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) 45 | # include 46 | #endif 47 | 48 | #if defined CTRL && defined _AIX 49 | #undef CTRL 50 | #endif 51 | #ifndef CTRL 52 | #define CTRL(k) ((k) & 0x1F) 53 | #endif 54 | 55 | #include "config.h" 56 | 57 | #if defined(_AIX) 58 | # include "forkpty-aix.c" 59 | #elif defined(__sun) 60 | # include "forkpty-sunos.c" 61 | #endif 62 | 63 | #define countof(arr) (sizeof(arr) / sizeof((arr)[0])) 64 | 65 | enum PacketType { 66 | MSG_CONTENT = 0, 67 | MSG_ATTACH = 1, 68 | MSG_DETACH = 2, 69 | MSG_RESIZE = 3, 70 | MSG_EXIT = 4, 71 | MSG_PID = 5, 72 | }; 73 | 74 | typedef struct { 75 | uint32_t type; 76 | uint32_t len; 77 | union { 78 | char msg[4096 - 2*sizeof(uint32_t)]; 79 | struct { 80 | uint16_t rows; 81 | uint16_t cols; 82 | } ws; 83 | uint32_t i; 84 | uint64_t l; 85 | } u; 86 | } Packet; 87 | 88 | typedef struct Client Client; 89 | struct Client { 90 | int socket; 91 | enum { 92 | STATE_CONNECTED, 93 | STATE_ATTACHED, 94 | STATE_DETACHED, 95 | STATE_DISCONNECTED, 96 | } state; 97 | bool need_resize; 98 | enum { 99 | CLIENT_READONLY = 1 << 0, 100 | CLIENT_LOWPRIORITY = 1 << 1, 101 | } flags; 102 | Client *next; 103 | }; 104 | 105 | typedef struct { 106 | Client *clients; 107 | int socket; 108 | Packet pty_output; 109 | int pty; 110 | int exit_status; 111 | struct termios term; 112 | struct winsize winsize; 113 | pid_t pid; 114 | volatile sig_atomic_t running; 115 | const char *name; 116 | const char *session_name; 117 | char host[255]; 118 | bool read_pty; 119 | } Server; 120 | 121 | static Server server = { .running = true, .exit_status = -1, .host = "@localhost" }; 122 | static Client client; 123 | static struct termios orig_term, cur_term; 124 | static bool has_term, alternate_buffer, quiet, passthrough; 125 | 126 | static struct sockaddr_un sockaddr = { 127 | .sun_family = AF_UNIX, 128 | }; 129 | 130 | static bool set_socket_name(struct sockaddr_un *sockaddr, const char *name); 131 | static void die(const char *s); 132 | static void info(const char *str, ...); 133 | 134 | #include "debug.c" 135 | 136 | static inline size_t packet_header_size() { 137 | return offsetof(Packet, u); 138 | } 139 | 140 | static size_t packet_size(Packet *pkt) { 141 | return packet_header_size() + pkt->len; 142 | } 143 | 144 | static ssize_t write_all(int fd, const char *buf, size_t len) { 145 | debug("write_all(%d)\n", len); 146 | ssize_t ret = len; 147 | while (len > 0) { 148 | ssize_t res = write(fd, buf, len); 149 | if (res < 0) { 150 | if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) 151 | continue; 152 | return -1; 153 | } 154 | if (res == 0) 155 | return ret - len; 156 | buf += res; 157 | len -= res; 158 | } 159 | return ret; 160 | } 161 | 162 | static ssize_t read_all(int fd, char *buf, size_t len) { 163 | debug("read_all(%d)\n", len); 164 | ssize_t ret = len; 165 | while (len > 0) { 166 | ssize_t res = read(fd, buf, len); 167 | if (res < 0) { 168 | if (errno == EWOULDBLOCK) 169 | return ret - len; 170 | if (errno == EAGAIN || errno == EINTR) 171 | continue; 172 | return -1; 173 | } 174 | if (res == 0) 175 | return ret - len; 176 | buf += res; 177 | len -= res; 178 | } 179 | return ret; 180 | } 181 | 182 | static bool send_packet(int socket, Packet *pkt) { 183 | size_t size = packet_size(pkt); 184 | if (size > sizeof(*pkt)) 185 | return false; 186 | return write_all(socket, (char *)pkt, size) == size; 187 | } 188 | 189 | static bool recv_packet(int socket, Packet *pkt) { 190 | ssize_t len = read_all(socket, (char*)pkt, packet_header_size()); 191 | if (len <= 0 || len != packet_header_size()) 192 | return false; 193 | if (pkt->len > sizeof(pkt->u.msg)) { 194 | pkt->len = 0; 195 | return false; 196 | } 197 | if (pkt->len > 0) { 198 | len = read_all(socket, pkt->u.msg, pkt->len); 199 | if (len <= 0 || len != pkt->len) 200 | return false; 201 | } 202 | return true; 203 | } 204 | 205 | #include "client.c" 206 | #include "server.c" 207 | 208 | static void info(const char *str, ...) { 209 | va_list ap; 210 | va_start(ap, str); 211 | if (str && !quiet) { 212 | fprintf(stderr, "%s: %s: ", server.name, server.session_name); 213 | vfprintf(stderr, str, ap); 214 | fprintf(stderr, "\r\n"); 215 | fflush(stderr); 216 | } 217 | va_end(ap); 218 | } 219 | 220 | static void die(const char *s) { 221 | perror(s); 222 | exit(EXIT_FAILURE); 223 | } 224 | 225 | static void usage(void) { 226 | fprintf(stderr, "usage: abduco [-a|-A|-c|-n] [-p] [-r] [-q] [-l] [-f] [-e detachkey] name command\n"); 227 | exit(EXIT_FAILURE); 228 | } 229 | 230 | static bool xsnprintf(char *buf, size_t size, const char *fmt, ...) { 231 | va_list ap; 232 | if (size > INT_MAX) 233 | return false; 234 | va_start(ap, fmt); 235 | int n = vsnprintf(buf, size, fmt, ap); 236 | va_end(ap); 237 | if (n == -1) 238 | return false; 239 | if (n >= size) { 240 | errno = ENAMETOOLONG; 241 | return false; 242 | } 243 | return true; 244 | } 245 | 246 | static int session_connect(const char *name) { 247 | int fd; 248 | struct stat sb; 249 | if (!set_socket_name(&sockaddr, name) || (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) 250 | return -1; 251 | socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1; 252 | if (connect(fd, (struct sockaddr*)&sockaddr, socklen) == -1) { 253 | if (errno == ECONNREFUSED && stat(sockaddr.sun_path, &sb) == 0 && S_ISSOCK(sb.st_mode)) 254 | unlink(sockaddr.sun_path); 255 | close(fd); 256 | return -1; 257 | } 258 | return fd; 259 | } 260 | 261 | static pid_t session_exists(const char *name) { 262 | Packet pkt; 263 | pid_t pid = 0; 264 | if ((server.socket = session_connect(name)) == -1) 265 | return pid; 266 | if (client_recv_packet(&pkt) && pkt.type == MSG_PID) 267 | pid = pkt.u.l; 268 | close(server.socket); 269 | return pid; 270 | } 271 | 272 | static bool session_alive(const char *name) { 273 | struct stat sb; 274 | return session_exists(name) && 275 | stat(sockaddr.sun_path, &sb) == 0 && 276 | S_ISSOCK(sb.st_mode) && (sb.st_mode & S_IXGRP) == 0; 277 | } 278 | 279 | static bool create_socket_dir(struct sockaddr_un *sockaddr) { 280 | sockaddr->sun_path[0] = '\0'; 281 | int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); 282 | if (socketfd == -1) 283 | return false; 284 | 285 | const size_t maxlen = sizeof(sockaddr->sun_path); 286 | uid_t uid = getuid(); 287 | struct passwd *pw = getpwuid(uid); 288 | 289 | for (unsigned int i = 0; i < countof(socket_dirs); i++) { 290 | struct stat sb; 291 | struct Dir *dir = &socket_dirs[i]; 292 | bool ishome = false; 293 | if (dir->env) { 294 | dir->path = getenv(dir->env); 295 | ishome = !strcmp(dir->env, "HOME"); 296 | if (ishome && (!dir->path || !dir->path[0]) && pw) 297 | dir->path = pw->pw_dir; 298 | } 299 | if (!dir->path || !dir->path[0]) 300 | continue; 301 | if (!xsnprintf(sockaddr->sun_path, maxlen, "%s/%s%s/", dir->path, ishome ? "." : "", server.name)) 302 | continue; 303 | mode_t mask = umask(0); 304 | int r = mkdir(sockaddr->sun_path, dir->personal ? S_IRWXU : S_IRWXU|S_IRWXG|S_IRWXO|S_ISVTX); 305 | umask(mask); 306 | if (r != 0 && errno != EEXIST) 307 | continue; 308 | if (lstat(sockaddr->sun_path, &sb) != 0) 309 | continue; 310 | if (!S_ISDIR(sb.st_mode)) { 311 | errno = ENOTDIR; 312 | continue; 313 | } 314 | 315 | size_t dirlen = strlen(sockaddr->sun_path); 316 | if (!dir->personal) { 317 | /* create subdirectory only accessible to user */ 318 | if (pw && !xsnprintf(sockaddr->sun_path+dirlen, maxlen-dirlen, "%s/", pw->pw_name)) 319 | continue; 320 | if (!pw && !xsnprintf(sockaddr->sun_path+dirlen, maxlen-dirlen, "%d/", uid)) 321 | continue; 322 | if (mkdir(sockaddr->sun_path, S_IRWXU) != 0 && errno != EEXIST) 323 | continue; 324 | if (lstat(sockaddr->sun_path, &sb) != 0) 325 | continue; 326 | if (!S_ISDIR(sb.st_mode)) { 327 | errno = ENOTDIR; 328 | continue; 329 | } 330 | dirlen = strlen(sockaddr->sun_path); 331 | } 332 | 333 | if (sb.st_uid != uid || sb.st_mode & (S_IRWXG|S_IRWXO)) { 334 | errno = EACCES; 335 | continue; 336 | } 337 | 338 | if (!xsnprintf(sockaddr->sun_path+dirlen, maxlen-dirlen, ".abduco-%d", getpid())) 339 | continue; 340 | 341 | socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr->sun_path) + 1; 342 | if (bind(socketfd, (struct sockaddr*)sockaddr, socklen) == -1) 343 | continue; 344 | unlink(sockaddr->sun_path); 345 | close(socketfd); 346 | sockaddr->sun_path[dirlen] = '\0'; 347 | return true; 348 | } 349 | 350 | close(socketfd); 351 | return false; 352 | } 353 | 354 | static bool set_socket_name(struct sockaddr_un *sockaddr, const char *name) { 355 | const size_t maxlen = sizeof(sockaddr->sun_path); 356 | const char *session_name = NULL; 357 | char buf[maxlen]; 358 | 359 | if (name[0] == '/') { 360 | if (strlen(name) >= maxlen) { 361 | errno = ENAMETOOLONG; 362 | return false; 363 | } 364 | strncpy(sockaddr->sun_path, name, maxlen); 365 | } else if (name[0] == '.' && (name[1] == '.' || name[1] == '/')) { 366 | char *cwd = getcwd(buf, sizeof buf); 367 | if (!cwd) 368 | return false; 369 | if (!xsnprintf(sockaddr->sun_path, maxlen, "%s/%s", cwd, name)) 370 | return false; 371 | } else { 372 | if (!create_socket_dir(sockaddr)) 373 | return false; 374 | if (strlen(sockaddr->sun_path) + strlen(name) + strlen(server.host) >= maxlen) { 375 | errno = ENAMETOOLONG; 376 | return false; 377 | } 378 | session_name = name; 379 | strncat(sockaddr->sun_path, name, maxlen - strlen(sockaddr->sun_path) - 1); 380 | strncat(sockaddr->sun_path, server.host, maxlen - strlen(sockaddr->sun_path) - 1); 381 | } 382 | 383 | if (!session_name) { 384 | strncpy(buf, sockaddr->sun_path, sizeof buf); 385 | session_name = basename(buf); 386 | } 387 | setenv("ABDUCO_SESSION", session_name, 1); 388 | setenv("ABDUCO_SOCKET", sockaddr->sun_path, 1); 389 | 390 | return true; 391 | } 392 | 393 | static bool create_session(const char *name, char * const argv[]) { 394 | /* this uses the well known double fork strategy as described in section 1.7 of 395 | * 396 | * http://www.faqs.org/faqs/unix-faq/programmer/faq/ 397 | * 398 | * pipes are used for synchronization and error reporting i.e. the child sets 399 | * the close on exec flag before calling execvp(3) the parent blocks on a read(2) 400 | * in case of failure the error message is written to the pipe, success is 401 | * indicated by EOF on the pipe. 402 | */ 403 | int client_pipe[2], server_pipe[2]; 404 | pid_t pid; 405 | char errormsg[255]; 406 | struct sigaction sa; 407 | 408 | if (session_exists(name)) { 409 | errno = EADDRINUSE; 410 | return false; 411 | } 412 | 413 | if (pipe(client_pipe) == -1) 414 | return false; 415 | if ((server.socket = server_create_socket(name)) == -1) 416 | return false; 417 | 418 | switch ((pid = fork())) { 419 | case 0: /* child process */ 420 | setsid(); 421 | close(client_pipe[0]); 422 | switch ((pid = fork())) { 423 | case 0: /* child process */ 424 | if (pipe(server_pipe) == -1) { 425 | snprintf(errormsg, sizeof(errormsg), "server-pipe: %s\n", strerror(errno)); 426 | write_all(client_pipe[1], errormsg, strlen(errormsg)); 427 | close(client_pipe[1]); 428 | _exit(EXIT_FAILURE); 429 | } 430 | sa.sa_flags = 0; 431 | sigemptyset(&sa.sa_mask); 432 | sa.sa_handler = server_pty_died_handler; 433 | sigaction(SIGCHLD, &sa, NULL); 434 | switch (server.pid = forkpty(&server.pty, NULL, has_term ? &server.term : NULL, &server.winsize)) { 435 | case 0: /* child = user application process */ 436 | close(server.socket); 437 | close(server_pipe[0]); 438 | if (fcntl(client_pipe[1], F_SETFD, FD_CLOEXEC) == 0 && 439 | fcntl(server_pipe[1], F_SETFD, FD_CLOEXEC) == 0) 440 | execvp(argv[0], argv); 441 | snprintf(errormsg, sizeof(errormsg), "server-execvp: %s: %s\n", 442 | argv[0], strerror(errno)); 443 | write_all(client_pipe[1], errormsg, strlen(errormsg)); 444 | write_all(server_pipe[1], errormsg, strlen(errormsg)); 445 | close(client_pipe[1]); 446 | close(server_pipe[1]); 447 | _exit(EXIT_FAILURE); 448 | break; 449 | case -1: /* forkpty failed */ 450 | snprintf(errormsg, sizeof(errormsg), "server-forkpty: %s\n", strerror(errno)); 451 | write_all(client_pipe[1], errormsg, strlen(errormsg)); 452 | close(client_pipe[1]); 453 | close(server_pipe[0]); 454 | close(server_pipe[1]); 455 | _exit(EXIT_FAILURE); 456 | break; 457 | default: /* parent = server process */ 458 | sa.sa_handler = server_sigterm_handler; 459 | sigaction(SIGTERM, &sa, NULL); 460 | sigaction(SIGINT, &sa, NULL); 461 | sa.sa_handler = server_sigusr1_handler; 462 | sigaction(SIGUSR1, &sa, NULL); 463 | sa.sa_handler = SIG_IGN; 464 | sigaction(SIGPIPE, &sa, NULL); 465 | sigaction(SIGHUP, &sa, NULL); 466 | if (chdir("/") == -1) 467 | _exit(EXIT_FAILURE); 468 | #ifdef NDEBUG 469 | int fd = open("/dev/null", O_RDWR); 470 | if (fd != -1) { 471 | dup2(fd, STDIN_FILENO); 472 | dup2(fd, STDOUT_FILENO); 473 | dup2(fd, STDERR_FILENO); 474 | close(fd); 475 | } 476 | #endif /* NDEBUG */ 477 | close(client_pipe[1]); 478 | close(server_pipe[1]); 479 | if (read_all(server_pipe[0], errormsg, sizeof(errormsg)) > 0) 480 | _exit(EXIT_FAILURE); 481 | close(server_pipe[0]); 482 | server_mainloop(); 483 | break; 484 | } 485 | break; 486 | case -1: /* fork failed */ 487 | snprintf(errormsg, sizeof(errormsg), "server-fork: %s\n", strerror(errno)); 488 | write_all(client_pipe[1], errormsg, strlen(errormsg)); 489 | close(client_pipe[1]); 490 | _exit(EXIT_FAILURE); 491 | break; 492 | default: /* parent = intermediate process */ 493 | close(client_pipe[1]); 494 | _exit(EXIT_SUCCESS); 495 | break; 496 | } 497 | break; 498 | case -1: /* fork failed */ 499 | close(client_pipe[0]); 500 | close(client_pipe[1]); 501 | return false; 502 | default: /* parent = client process */ 503 | close(client_pipe[1]); 504 | while (waitpid(pid, NULL, 0) == -1 && errno == EINTR); 505 | ssize_t len = read_all(client_pipe[0], errormsg, sizeof(errormsg)); 506 | if (len > 0) { 507 | write_all(STDERR_FILENO, errormsg, len); 508 | unlink(sockaddr.sun_path); 509 | exit(EXIT_FAILURE); 510 | } 511 | close(client_pipe[0]); 512 | } 513 | return true; 514 | } 515 | 516 | static bool attach_session(const char *name, const bool terminate) { 517 | if (server.socket > 0) 518 | close(server.socket); 519 | if ((server.socket = session_connect(name)) == -1) 520 | return false; 521 | if (server_set_socket_non_blocking(server.socket) == -1) 522 | return false; 523 | 524 | struct sigaction sa; 525 | sa.sa_flags = 0; 526 | sigemptyset(&sa.sa_mask); 527 | sa.sa_handler = client_sigwinch_handler; 528 | sigaction(SIGWINCH, &sa, NULL); 529 | sa.sa_handler = SIG_IGN; 530 | sigaction(SIGPIPE, &sa, NULL); 531 | 532 | client_setup_terminal(); 533 | int status = client_mainloop(); 534 | client_restore_terminal(); 535 | if (status == -1) { 536 | info("detached"); 537 | } else if (status == -EIO) { 538 | info("exited due to I/O errors"); 539 | } else { 540 | info("session terminated with exit status %d", status); 541 | if (terminate) 542 | exit(status); 543 | } 544 | 545 | return terminate; 546 | } 547 | 548 | static int session_filter(const struct dirent *d) { 549 | return strstr(d->d_name, server.host) != NULL; 550 | } 551 | 552 | static int session_comparator(const struct dirent **a, const struct dirent **b) { 553 | struct stat sa, sb; 554 | if (stat((*a)->d_name, &sa) != 0) 555 | return -1; 556 | if (stat((*b)->d_name, &sb) != 0) 557 | return 1; 558 | return sa.st_atime < sb.st_atime ? -1 : 1; 559 | } 560 | 561 | static int list_session(void) { 562 | if (!create_socket_dir(&sockaddr)) 563 | return 1; 564 | if (chdir(sockaddr.sun_path) == -1) 565 | die("list-session"); 566 | struct dirent **namelist; 567 | int n = scandir(sockaddr.sun_path, &namelist, session_filter, session_comparator); 568 | if (n < 0) 569 | return 1; 570 | printf("Active sessions (on host %s)\n", server.host+1); 571 | while (n--) { 572 | struct stat sb; char buf[255]; 573 | if (stat(namelist[n]->d_name, &sb) == 0 && S_ISSOCK(sb.st_mode)) { 574 | pid_t pid = 0; 575 | strftime(buf, sizeof(buf), "%a%t %F %T", localtime(&sb.st_mtime)); 576 | char status = ' '; 577 | char *local = strstr(namelist[n]->d_name, server.host); 578 | if (local) { 579 | *local = '\0'; /* truncate hostname if we are local */ 580 | if (!(pid = session_exists(namelist[n]->d_name))) 581 | continue; 582 | } 583 | if (sb.st_mode & S_IXUSR) 584 | status = '*'; 585 | else if (sb.st_mode & S_IXGRP) 586 | status = '+'; 587 | printf("%c %s\t%jd\t%s\n", status, buf, (intmax_t)pid, namelist[n]->d_name); 588 | } 589 | free(namelist[n]); 590 | } 591 | free(namelist); 592 | return 0; 593 | } 594 | 595 | int main(int argc, char *argv[]) { 596 | int opt; 597 | bool force = false; 598 | char **cmd = NULL, action = '\0'; 599 | 600 | char *default_cmd[4] = { "/bin/sh", "-c", getenv("ABDUCO_CMD"), NULL }; 601 | if (!default_cmd[2]) { 602 | default_cmd[0] = ABDUCO_CMD; 603 | default_cmd[1] = NULL; 604 | } 605 | 606 | server.name = basename(argv[0]); 607 | gethostname(server.host+1, sizeof(server.host) - 1); 608 | 609 | while ((opt = getopt(argc, argv, "aAclne:fpqrv")) != -1) { 610 | switch (opt) { 611 | case 'a': 612 | case 'A': 613 | case 'c': 614 | case 'n': 615 | action = opt; 616 | break; 617 | case 'e': 618 | if (!optarg) 619 | usage(); 620 | if (optarg[0] == '^' && optarg[1]) 621 | optarg[0] = CTRL(optarg[1]); 622 | KEY_DETACH = optarg[0]; 623 | break; 624 | case 'f': 625 | force = true; 626 | break; 627 | case 'p': 628 | passthrough = true; 629 | break; 630 | case 'q': 631 | quiet = true; 632 | break; 633 | case 'r': 634 | client.flags |= CLIENT_READONLY; 635 | break; 636 | case 'l': 637 | client.flags |= CLIENT_LOWPRIORITY; 638 | break; 639 | case 'v': 640 | puts("abduco-"VERSION" © 2013-2018 Marc André Tanner"); 641 | exit(EXIT_SUCCESS); 642 | default: 643 | usage(); 644 | } 645 | } 646 | 647 | /* collect the session name if trailing args */ 648 | if (optind < argc) 649 | server.session_name = argv[optind]; 650 | 651 | /* if yet more trailing arguments, they must be the command */ 652 | if (optind + 1 < argc) 653 | cmd = &argv[optind + 1]; 654 | else 655 | cmd = default_cmd; 656 | 657 | if (server.session_name && !isatty(STDIN_FILENO)) 658 | passthrough = true; 659 | 660 | if (passthrough) { 661 | if (!action) 662 | action = 'a'; 663 | quiet = true; 664 | client.flags |= CLIENT_LOWPRIORITY; 665 | } 666 | 667 | if (!action && !server.session_name) 668 | exit(list_session()); 669 | if (!action || !server.session_name) 670 | usage(); 671 | 672 | if (!passthrough && tcgetattr(STDIN_FILENO, &orig_term) != -1) { 673 | server.term = orig_term; 674 | has_term = true; 675 | } 676 | 677 | if (ioctl(STDIN_FILENO, TIOCGWINSZ, &server.winsize) == -1) { 678 | server.winsize.ws_col = 80; 679 | server.winsize.ws_row = 25; 680 | } 681 | 682 | server.read_pty = (action == 'n'); 683 | 684 | redo: 685 | switch (action) { 686 | case 'n': 687 | case 'c': 688 | if (force) { 689 | if (session_alive(server.session_name)) { 690 | info("session exists and has not yet terminated"); 691 | return 1; 692 | } 693 | if (session_exists(server.session_name)) 694 | attach_session(server.session_name, false); 695 | } 696 | if (!create_session(server.session_name, cmd)) 697 | die("create-session"); 698 | if (action == 'n') 699 | break; 700 | /* fall through */ 701 | case 'a': 702 | if (!attach_session(server.session_name, true)) 703 | die("attach-session"); 704 | break; 705 | case 'A': 706 | if (session_alive(server.session_name)) { 707 | if (!attach_session(server.session_name, true)) 708 | die("attach-session"); 709 | } else if (!attach_session(server.session_name, !force)) { 710 | force = false; 711 | action = 'c'; 712 | goto redo; 713 | } 714 | break; 715 | } 716 | 717 | return 0; 718 | } 719 | -------------------------------------------------------------------------------- /client.c: -------------------------------------------------------------------------------- 1 | static void client_sigwinch_handler(int sig) { 2 | client.need_resize = true; 3 | } 4 | 5 | static bool client_send_packet(Packet *pkt) { 6 | print_packet("client-send:", pkt); 7 | if (send_packet(server.socket, pkt)) 8 | return true; 9 | debug("FAILED\n"); 10 | server.running = false; 11 | return false; 12 | } 13 | 14 | static bool client_recv_packet(Packet *pkt) { 15 | if (recv_packet(server.socket, pkt)) { 16 | print_packet("client-recv:", pkt); 17 | return true; 18 | } 19 | debug("client-recv: FAILED\n"); 20 | server.running = false; 21 | return false; 22 | } 23 | 24 | static void client_restore_terminal(void) { 25 | if (!has_term) 26 | return; 27 | tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_term); 28 | if (alternate_buffer) { 29 | printf("\033[?25h\033[?1049l"); 30 | fflush(stdout); 31 | alternate_buffer = false; 32 | } 33 | } 34 | 35 | static void client_setup_terminal(void) { 36 | if (!has_term) 37 | return; 38 | atexit(client_restore_terminal); 39 | 40 | cur_term = orig_term; 41 | cur_term.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXOFF); 42 | cur_term.c_oflag &= ~(OPOST); 43 | cur_term.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); 44 | cur_term.c_cflag &= ~(CSIZE|PARENB); 45 | cur_term.c_cflag |= CS8; 46 | cur_term.c_cc[VLNEXT] = _POSIX_VDISABLE; 47 | cur_term.c_cc[VMIN] = 1; 48 | cur_term.c_cc[VTIME] = 0; 49 | tcsetattr(STDIN_FILENO, TCSANOW, &cur_term); 50 | 51 | if (!alternate_buffer) { 52 | printf("\033[?1049h\033[H"); 53 | fflush(stdout); 54 | alternate_buffer = true; 55 | } 56 | } 57 | 58 | static int client_mainloop(void) { 59 | sigset_t emptyset, blockset; 60 | sigemptyset(&emptyset); 61 | sigemptyset(&blockset); 62 | sigaddset(&blockset, SIGWINCH); 63 | sigprocmask(SIG_BLOCK, &blockset, NULL); 64 | 65 | client.need_resize = true; 66 | Packet pkt = { 67 | .type = MSG_ATTACH, 68 | .u.i = client.flags, 69 | .len = sizeof(pkt.u.i), 70 | }; 71 | client_send_packet(&pkt); 72 | 73 | while (server.running) { 74 | fd_set fds; 75 | FD_ZERO(&fds); 76 | FD_SET(STDIN_FILENO, &fds); 77 | FD_SET(server.socket, &fds); 78 | 79 | if (client.need_resize) { 80 | struct winsize ws; 81 | if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) != -1) { 82 | Packet pkt = { 83 | .type = MSG_RESIZE, 84 | .u = { .ws = { .rows = ws.ws_row, .cols = ws.ws_col } }, 85 | .len = sizeof(pkt.u.ws), 86 | }; 87 | if (client_send_packet(&pkt)) 88 | client.need_resize = false; 89 | } 90 | } 91 | 92 | if (pselect(server.socket+1, &fds, NULL, NULL, NULL, &emptyset) == -1) { 93 | if (errno == EINTR) 94 | continue; 95 | die("client-mainloop"); 96 | } 97 | 98 | if (FD_ISSET(server.socket, &fds)) { 99 | Packet pkt; 100 | if (client_recv_packet(&pkt)) { 101 | switch (pkt.type) { 102 | case MSG_CONTENT: 103 | if (!passthrough) 104 | write_all(STDOUT_FILENO, pkt.u.msg, pkt.len); 105 | break; 106 | case MSG_RESIZE: 107 | client.need_resize = true; 108 | break; 109 | case MSG_EXIT: 110 | client_send_packet(&pkt); 111 | close(server.socket); 112 | return pkt.u.i; 113 | } 114 | } 115 | } 116 | 117 | if (FD_ISSET(STDIN_FILENO, &fds)) { 118 | Packet pkt = { .type = MSG_CONTENT }; 119 | ssize_t len = read(STDIN_FILENO, pkt.u.msg, sizeof(pkt.u.msg)); 120 | if (len == -1 && errno != EAGAIN && errno != EINTR) 121 | die("client-stdin"); 122 | if (len > 0) { 123 | debug("client-stdin: %c\n", pkt.u.msg[0]); 124 | pkt.len = len; 125 | if (KEY_REDRAW && pkt.u.msg[0] == KEY_REDRAW) { 126 | client.need_resize = true; 127 | } else if (pkt.u.msg[0] == KEY_DETACH) { 128 | pkt.type = MSG_DETACH; 129 | pkt.len = 0; 130 | client_send_packet(&pkt); 131 | close(server.socket); 132 | return -1; 133 | } else if (!(client.flags & CLIENT_READONLY)) { 134 | client_send_packet(&pkt); 135 | } 136 | } else if (len == 0) { 137 | debug("client-stdin: EOF\n"); 138 | return -1; 139 | } 140 | } 141 | } 142 | 143 | return -EIO; 144 | } 145 | -------------------------------------------------------------------------------- /config.def.h: -------------------------------------------------------------------------------- 1 | /* default command to execute if non is given and $ABDUCO_CMD is unset */ 2 | #define ABDUCO_CMD "dvtm" 3 | /* default detach key, can be overriden at run time using -e option */ 4 | static char KEY_DETACH = CTRL('\\'); 5 | /* redraw key to send a SIGWINCH signal to underlying process 6 | * (set to 0 to disable the redraw key) */ 7 | static char KEY_REDRAW = 0; 8 | /* Where to place the "abduco" directory storing all session socket files. 9 | * The first directory to succeed is used. */ 10 | static struct Dir { 11 | char *path; /* fixed (absolute) path to a directory */ 12 | char *env; /* environment variable to use if (set) */ 13 | bool personal; /* if false a user owned sub directory will be created */ 14 | } socket_dirs[] = { 15 | { .env = "ABDUCO_SOCKET_DIR", false }, 16 | { .env = "HOME", true }, 17 | { .env = "TMPDIR", false }, 18 | { .path = "/tmp", false }, 19 | }; 20 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Based on the configure script from musl libc, MIT licensed 3 | 4 | usage () { 5 | cat </dev/null 2>&1 && { echo "$1" ; return 0 ; } 41 | $1 42 | EOF 43 | printf %s\\n "$1" | sed -e "s/'/'\\\\''/g" -e "1s/^/'/" -e "\$s/\$/'/" -e "s#^'\([-[:alnum:]_,./:]*\)=\(.*\)\$#\1='\2#" 44 | } 45 | echo () { printf "%s\n" "$*" ; } 46 | fail () { echo "$*" ; exit 1 ; } 47 | fnmatch () { eval "case \"\$2\" in $1) return 0 ;; *) return 1 ;; esac" ; } 48 | cmdexists () { type "$1" >/dev/null 2>&1 ; } 49 | trycc () { test -z "$CC" && cmdexists "$1" && CC=$1 ; } 50 | 51 | stripdir () { 52 | while eval "fnmatch '*/' \"\${$1}\"" ; do eval "$1=\${$1%/}" ; done 53 | } 54 | 55 | trycppif () { 56 | printf "checking preprocessor condition %s... " "$1" 57 | echo "typedef int x;" > "$tmpc" 58 | echo "#if $1" >> "$tmpc" 59 | echo "#error yes" >> "$tmpc" 60 | echo "#endif" >> "$tmpc" 61 | if $CC $2 -c -o "$tmpo" "$tmpc" >/dev/null 2>&1 ; then 62 | printf "false\n" 63 | return 1 64 | else 65 | printf "true\n" 66 | return 0 67 | fi 68 | } 69 | 70 | tryflag () { 71 | printf "checking whether compiler accepts %s... " "$2" 72 | echo "typedef int x;" > "$tmpc" 73 | if $CC $CFLAGS_TRY $2 -c -o "$tmpo" "$tmpc" >/dev/null 2>&1 ; then 74 | printf "yes\n" 75 | eval "$1=\"\${$1} \$2\"" 76 | eval "$1=\${$1# }" 77 | return 0 78 | else 79 | printf "no\n" 80 | return 1 81 | fi 82 | } 83 | 84 | tryldflag () { 85 | printf "checking whether linker accepts %s... " "$2" 86 | echo "typedef int x;" > "$tmpc" 87 | if $CC $LDFLAGS_TRY -nostdlib -shared "$2" -o "$tmpo" "$tmpc" >/dev/null 2>&1 ; then 88 | printf "yes\n" 89 | eval "$1=\"\${$1} \$2\"" 90 | eval "$1=\${$1# }" 91 | return 0 92 | else 93 | printf "no\n" 94 | return 1 95 | fi 96 | } 97 | 98 | # Beginning of actual script 99 | 100 | CFLAGS_AUTO= 101 | CFLAGS_TRY= 102 | LDFLAGS_AUTO= 103 | LDFLAGS_TRY= 104 | SRCDIR= 105 | PREFIX=/usr/local 106 | EXEC_PREFIX='$(PREFIX)' 107 | BINDIR='$(EXEC_PREFIX)/bin' 108 | MANDIR='$(PREFIX)/share/man' 109 | 110 | for arg ; do 111 | case "$arg" in 112 | --help|-h) usage ;; 113 | --srcdir=*) SRCDIR=${arg#*=} ;; 114 | --prefix=*) PREFIX=${arg#*=} ;; 115 | --exec-prefix=*) EXEC_PREFIX=${arg#*=} ;; 116 | --bindir=*) BINDIR=${arg#*=} ;; 117 | --sharedir=*) SHAREDIR=${arg#*=} ;; 118 | --docdir=*) DOCDIR=${arg#*=} ;; 119 | --mandir=*) MANDIR=${arg#*=} ;; 120 | --enable-*|--disable-*|--with-*|--without-*|--*dir=*|--build=*) ;; 121 | -* ) echo "$0: unknown option $arg" ;; 122 | CC=*) CC=${arg#*=} ;; 123 | CFLAGS=*) CFLAGS=${arg#*=} ;; 124 | CPPFLAGS=*) CPPFLAGS=${arg#*=} ;; 125 | LDFLAGS=*) LDFLAGS=${arg#*=} ;; 126 | *=*) ;; 127 | *) ;; 128 | esac 129 | done 130 | 131 | for i in SRCDIR PREFIX EXEC_PREFIX BINDIR MANDIR ; do 132 | stripdir $i 133 | done 134 | 135 | # 136 | # Get the source dir for out-of-tree builds 137 | # 138 | if test -z "$SRCDIR" ; then 139 | SRCDIR="${0%/configure}" 140 | stripdir SRCDIR 141 | fi 142 | abs_builddir="$(pwd)" || fail "$0: cannot determine working directory" 143 | abs_srcdir="$(cd $SRCDIR && pwd)" || fail "$0: invalid source directory $SRCDIR" 144 | test "$abs_srcdir" = "$abs_builddir" && SRCDIR=. 145 | test "$SRCDIR" != "." -a -f Makefile -a ! -h Makefile && fail "$0: Makefile already exists in the working directory" 146 | 147 | # 148 | # Get a temp filename we can use 149 | # 150 | i=0 151 | set -C 152 | while : ; do i=$(($i+1)) 153 | tmpc="./conf$$-$PPID-$i.c" 154 | tmpo="./conf$$-$PPID-$i.o" 155 | 2>|/dev/null > "$tmpc" && break 156 | test "$i" -gt 50 && fail "$0: cannot create temporary file $tmpc" 157 | done 158 | set +C 159 | trap 'rm -f "$tmpc" "$tmpo"' EXIT INT QUIT TERM HUP 160 | 161 | # 162 | # Find a C compiler to use 163 | # 164 | printf "checking for C compiler... " 165 | trycc cc 166 | trycc gcc 167 | trycc clang 168 | printf "%s\n" "$CC" 169 | test -n "$CC" || { echo "$0: cannot find a C compiler" ; exit 1 ; } 170 | 171 | printf "checking whether C compiler works... " 172 | echo "typedef int x;" > "$tmpc" 173 | if output=$($CC $CPPFLAGS $CFLAGS -c -o "$tmpo" "$tmpc" 2>&1) ; then 174 | printf "yes\n" 175 | else 176 | printf "no; compiler output follows:\n%s\n" "$output" 177 | exit 1 178 | fi 179 | 180 | # 181 | # Figure out options to force errors on unknown flags. 182 | # 183 | tryflag CFLAGS_TRY -Werror=unknown-warning-option 184 | tryflag CFLAGS_TRY -Werror=unused-command-line-argument 185 | tryldflag LDFLAGS_TRY -Werror=unknown-warning-option 186 | tryldflag LDFLAGS_TRY -Werror=unused-command-line-argument 187 | 188 | CFLAGS_STD="-std=c99 -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -DNDEBUG -D_FORTIFY_SOURCE=2" 189 | LDFLAGS_STD="-lc -lutil" 190 | 191 | OS=$(uname) 192 | 193 | case "$OS" in 194 | FreeBSD) CFLAGS_STD="$CFLAGS_STD -D_BSD_SOURCE -D__BSD_VISIBLE=1" ;; 195 | *BSD) CFLAGS_STD="$CFLAGS_STD -D_BSD_SOURCE" ;; 196 | Darwin) CFLAGS_STD="$CFLAGS_STD -D_DARWIN_C_SOURCE" ;; 197 | AIX) CFLAGS_STD="$CFLAGS_STD -D_ALL_SOURCE" ;; 198 | esac 199 | 200 | tryflag CFLAGS -pipe 201 | 202 | # Try flags to optimize binary size 203 | tryflag CFLAGS -Os 204 | tryflag CFLAGS -ffunction-sections 205 | tryflag CFLAGS -fdata-sections 206 | tryldflag LDFLAGS_AUTO -Wl,--gc-sections 207 | 208 | # Try hardening flags 209 | tryflag CFLAGS -fPIE 210 | tryflag CFLAGS_AUTO -fstack-protector-all 211 | tryldflag LDFLAGS -Wl,-z,now 212 | tryldflag LDFLAGS -Wl,-z,relro 213 | tryldflag LDFLAGS_AUTO -pie 214 | 215 | printf "creating config.mk... " 216 | 217 | cmdline=$(quote "$0") 218 | for i ; do cmdline="$cmdline $(quote "$i")" ; done 219 | 220 | exec 3>&1 1>config.mk 221 | 222 | cat << EOF 223 | # This version of config.mk was generated by: 224 | # $cmdline 225 | # Any changes made here will be lost if configure is re-run 226 | SRCDIR = $SRCDIR 227 | PREFIX = $PREFIX 228 | EXEC_PREFIX = $EXEC_PREFIX 229 | BINDIR = $BINDIR 230 | MANPREFIX = $MANDIR 231 | CC = $CC 232 | CFLAGS = $CFLAGS 233 | LDFLAGS = $LDFLAGS 234 | CFLAGS_STD = $CFLAGS_STD 235 | LDFLAGS_STD = $LDFLAGS_STD 236 | CFLAGS_AUTO = $CFLAGS_AUTO 237 | LDFLAGS_AUTO = $LDFLAGS_AUTO 238 | CFLAGS_DEBUG = -U_FORTIFY_SOURCE -UNDEBUG -O0 -g -ggdb -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-sign-compare 239 | EOF 240 | exec 1>&3 3>&- 241 | 242 | printf "done\n" 243 | 244 | test "$SRCDIR" = "." || ln -sf $SRCDIR/Makefile . 245 | -------------------------------------------------------------------------------- /contrib/abduco.zsh: -------------------------------------------------------------------------------- 1 | #compdef abduco 2 | 3 | typeset -A opt_args 4 | 5 | _abduco_sessions() { 6 | declare -a sessions 7 | sessions=( $(abduco | sed '1d;s/.*\t[0-9][0-9]*\t//') ) 8 | _describe -t session 'session' sessions 9 | } 10 | 11 | _abduco_firstarg() { 12 | if (( $+opt_args[-a] || $+opt_args[-A] )); then 13 | _abduco_sessions 14 | elif (( $+opt_args[-c] || $+opt_args[-n] )); then 15 | _guard "^-*" 'session name' 16 | elif [[ -z $words[CURRENT] ]]; then 17 | compadd "$@" -S '' -- - 18 | fi 19 | } 20 | 21 | _arguments -s \ 22 | '(-a -A -c -n -f)-a[attach to an existing session]' \ 23 | '(-a -A -c -n)-A[attach to a session, create if does not exist]' \ 24 | '(-a -A -c -n -l)-c[create a new session and attach to it]' \ 25 | '(-a -A -c -n -l)-n[create a new session but do not attach to it]' \ 26 | '-e[set the detachkey (default: ^\\)]:detachkey' \ 27 | '(-a)-f[force create the session]' \ 28 | '(-q)-p[pass-through mode]' \ 29 | '-q[be quiet]' \ 30 | '-r[read-only session, ignore user input]' \ 31 | '(-c -n)-l[attach with the lowest priority]' \ 32 | '(-)-v[show version information and exit]' \ 33 | '1: :_abduco_firstarg' \ 34 | '2:command:_path_commands' \ 35 | '*:: :{ shift $((CURRENT-3)) words; _precommand; }' 36 | -------------------------------------------------------------------------------- /debug.c: -------------------------------------------------------------------------------- 1 | #ifdef NDEBUG 2 | static void debug(const char *errstr, ...) { } 3 | static void print_packet(const char *prefix, Packet *pkt) { } 4 | #else 5 | 6 | static void debug(const char *errstr, ...) { 7 | va_list ap; 8 | va_start(ap, errstr); 9 | vfprintf(stderr, errstr, ap); 10 | va_end(ap); 11 | } 12 | 13 | static void print_packet(const char *prefix, Packet *pkt) { 14 | static const char *msgtype[] = { 15 | [MSG_CONTENT] = "CONTENT", 16 | [MSG_ATTACH] = "ATTACH", 17 | [MSG_DETACH] = "DETACH", 18 | [MSG_RESIZE] = "RESIZE", 19 | [MSG_EXIT] = "EXIT", 20 | [MSG_PID] = "PID", 21 | }; 22 | const char *type = "UNKNOWN"; 23 | if (pkt->type < countof(msgtype) && msgtype[pkt->type]) 24 | type = msgtype[pkt->type]; 25 | 26 | fprintf(stderr, "%s: %s ", prefix, type); 27 | switch (pkt->type) { 28 | case MSG_CONTENT: 29 | fwrite(pkt->u.msg, pkt->len, 1, stderr); 30 | break; 31 | case MSG_RESIZE: 32 | fprintf(stderr, "%"PRIu16"x%"PRIu16, pkt->u.ws.cols, pkt->u.ws.rows); 33 | break; 34 | case MSG_ATTACH: 35 | fprintf(stderr, "readonly: %d low-priority: %d", 36 | pkt->u.i & CLIENT_READONLY, 37 | pkt->u.i & CLIENT_LOWPRIORITY); 38 | break; 39 | case MSG_EXIT: 40 | fprintf(stderr, "status: %"PRIu32, pkt->u.i); 41 | break; 42 | case MSG_PID: 43 | fprintf(stderr, "pid: %"PRIu32, pkt->u.i); 44 | break; 45 | default: 46 | fprintf(stderr, "len: %"PRIu32, pkt->len); 47 | break; 48 | } 49 | fprintf(stderr, "\n"); 50 | } 51 | 52 | #endif /* NDEBUG */ 53 | -------------------------------------------------------------------------------- /forkpty-aix.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Nicholas Marriott 3 | * Copyright (c) 2012 Ross Palmer Mohn 4 | * 5 | * Permission to use, copy, modify, and distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER 14 | * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING 15 | * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | pid_t forkpty(int *master, char *name, struct termios *tio, struct winsize *ws) 27 | { 28 | int slave, fd; 29 | char *path; 30 | pid_t pid; 31 | struct termios tio2; 32 | 33 | if ((*master = open("/dev/ptc", O_RDWR|O_NOCTTY)) == -1) 34 | return -1; 35 | 36 | if ((path = ttyname(*master)) == NULL) 37 | goto out; 38 | if ((slave = open(path, O_RDWR|O_NOCTTY)) == -1) 39 | goto out; 40 | 41 | switch (pid = fork()) { 42 | case -1: 43 | goto out; 44 | case 0: 45 | close(*master); 46 | 47 | fd = open(_PATH_TTY, O_RDWR|O_NOCTTY); 48 | if (fd >= 0) { 49 | ioctl(fd, TIOCNOTTY, NULL); 50 | close(fd); 51 | } 52 | 53 | setsid(); 54 | 55 | fd = open(_PATH_TTY, O_RDWR|O_NOCTTY); 56 | if (fd >= 0) 57 | return -1; 58 | 59 | fd = open(path, O_RDWR); 60 | if (fd < 0) 61 | return -1; 62 | close(fd); 63 | 64 | fd = open("/dev/tty", O_WRONLY); 65 | if (fd < 0) 66 | return -1; 67 | close(fd); 68 | 69 | if (tcgetattr(slave, &tio2) != 0) 70 | return -1; 71 | if (tio != NULL) 72 | memcpy(tio2.c_cc, tio->c_cc, sizeof tio2.c_cc); 73 | tio2.c_cc[VERASE] = '\177'; 74 | if (tcsetattr(slave, TCSAFLUSH, &tio2) == -1) 75 | return -1; 76 | if (ioctl(slave, TIOCSWINSZ, ws) == -1) 77 | return -1; 78 | 79 | dup2(slave, 0); 80 | dup2(slave, 1); 81 | dup2(slave, 2); 82 | if (slave > 2) 83 | close(slave); 84 | return 0; 85 | } 86 | 87 | close(slave); 88 | return pid; 89 | 90 | out: 91 | if (*master != -1) 92 | close(*master); 93 | if (slave != -1) 94 | close(slave); 95 | return -1; 96 | } 97 | -------------------------------------------------------------------------------- /forkpty-sunos.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008 Nicholas Marriott 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER 13 | * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING 14 | * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #ifndef TTY_NAME_MAX 26 | #define TTY_NAME_MAX TTYNAME_MAX 27 | #endif 28 | 29 | pid_t forkpty(int *master, char *name, struct termios *tio, struct winsize *ws) 30 | { 31 | int slave; 32 | char *path; 33 | pid_t pid; 34 | 35 | if ((*master = open("/dev/ptmx", O_RDWR|O_NOCTTY)) == -1) 36 | return -1; 37 | if (grantpt(*master) != 0) 38 | goto out; 39 | if (unlockpt(*master) != 0) 40 | goto out; 41 | 42 | if ((path = ptsname(*master)) == NULL) 43 | goto out; 44 | if (name != NULL) 45 | strlcpy(name, path, TTY_NAME_MAX); 46 | if ((slave = open(path, O_RDWR|O_NOCTTY)) == -1) 47 | goto out; 48 | 49 | switch (pid = fork()) { 50 | case -1: 51 | goto out; 52 | case 0: 53 | close(*master); 54 | 55 | setsid(); 56 | #ifdef TIOCSCTTY 57 | if (ioctl(slave, TIOCSCTTY, NULL) == -1) 58 | return -1; 59 | #endif 60 | 61 | if (ioctl(slave, I_PUSH, "ptem") == -1) 62 | return -1; 63 | if (ioctl(slave, I_PUSH, "ldterm") == -1) 64 | return -1; 65 | 66 | if (tio != NULL && tcsetattr(slave, TCSAFLUSH, tio) == -1) 67 | return -1; 68 | if (ioctl(slave, TIOCSWINSZ, ws) == -1) 69 | return -1; 70 | 71 | dup2(slave, 0); 72 | dup2(slave, 1); 73 | dup2(slave, 2); 74 | if (slave > 2) 75 | close(slave); 76 | return 0; 77 | } 78 | 79 | close(slave); 80 | return pid; 81 | 82 | out: 83 | if (*master != -1) 84 | close(*master); 85 | if (slave != -1) 86 | close(slave); 87 | return -1; 88 | } 89 | -------------------------------------------------------------------------------- /server.c: -------------------------------------------------------------------------------- 1 | #define FD_SET_MAX(fd, set, maxfd) do { \ 2 | FD_SET(fd, set); \ 3 | if (fd > maxfd) \ 4 | maxfd = fd; \ 5 | } while (0) 6 | 7 | static Client *client_malloc(int socket) { 8 | Client *c = calloc(1, sizeof(Client)); 9 | if (!c) 10 | return NULL; 11 | c->socket = socket; 12 | return c; 13 | } 14 | 15 | static void client_free(Client *c) { 16 | if (c && c->socket > 0) 17 | close(c->socket); 18 | free(c); 19 | } 20 | 21 | static void server_sink_client() { 22 | if (!server.clients || !server.clients->next) 23 | return; 24 | Client *target = server.clients; 25 | server.clients = target->next; 26 | Client *dst = server.clients; 27 | while (dst->next) 28 | dst = dst->next; 29 | target->next = NULL; 30 | dst->next = target; 31 | } 32 | 33 | static void server_mark_socket_exec(bool exec, bool usr) { 34 | struct stat sb; 35 | if (stat(sockaddr.sun_path, &sb) == -1) 36 | return; 37 | mode_t mode = sb.st_mode; 38 | mode_t flag = usr ? S_IXUSR : S_IXGRP; 39 | if (exec) 40 | mode |= flag; 41 | else 42 | mode &= ~flag; 43 | chmod(sockaddr.sun_path, mode); 44 | } 45 | 46 | static int server_create_socket(const char *name) { 47 | if (!set_socket_name(&sockaddr, name)) 48 | return -1; 49 | int fd = socket(AF_UNIX, SOCK_STREAM, 0); 50 | if (fd == -1) 51 | return -1; 52 | socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1; 53 | mode_t mask = umask(S_IXUSR|S_IRWXG|S_IRWXO); 54 | int r = bind(fd, (struct sockaddr*)&sockaddr, socklen); 55 | umask(mask); 56 | 57 | if (r == -1) { 58 | close(fd); 59 | return -1; 60 | } 61 | 62 | if (listen(fd, 5) == -1) { 63 | unlink(sockaddr.sun_path); 64 | close(fd); 65 | return -1; 66 | } 67 | 68 | return fd; 69 | } 70 | 71 | static int server_set_socket_non_blocking(int sock) { 72 | int flags; 73 | if ((flags = fcntl(sock, F_GETFL, 0)) == -1) 74 | flags = 0; 75 | return fcntl(sock, F_SETFL, flags | O_NONBLOCK); 76 | } 77 | 78 | static bool server_read_pty(Packet *pkt) { 79 | pkt->type = MSG_CONTENT; 80 | ssize_t len = read(server.pty, pkt->u.msg, sizeof(pkt->u.msg)); 81 | if (len > 0) 82 | pkt->len = len; 83 | else if (len == 0) 84 | server.running = false; 85 | else if (len == -1 && errno != EAGAIN && errno != EINTR && errno != EWOULDBLOCK) 86 | server.running = false; 87 | print_packet("server-read-pty:", pkt); 88 | return len > 0; 89 | } 90 | 91 | static bool server_write_pty(Packet *pkt) { 92 | print_packet("server-write-pty:", pkt); 93 | size_t size = pkt->len; 94 | if (write_all(server.pty, pkt->u.msg, size) == size) 95 | return true; 96 | debug("FAILED\n"); 97 | server.running = false; 98 | return false; 99 | } 100 | 101 | static bool server_recv_packet(Client *c, Packet *pkt) { 102 | if (recv_packet(c->socket, pkt)) { 103 | print_packet("server-recv:", pkt); 104 | return true; 105 | } 106 | debug("server-recv: FAILED\n"); 107 | c->state = STATE_DISCONNECTED; 108 | return false; 109 | } 110 | 111 | static bool server_send_packet(Client *c, Packet *pkt) { 112 | print_packet("server-send:", pkt); 113 | if (send_packet(c->socket, pkt)) 114 | return true; 115 | debug("FAILED\n"); 116 | c->state = STATE_DISCONNECTED; 117 | return false; 118 | } 119 | 120 | static void server_pty_died_handler(int sig) { 121 | int errsv = errno; 122 | pid_t pid; 123 | 124 | while ((pid = waitpid(-1, &server.exit_status, WNOHANG)) != 0) { 125 | if (pid == -1) 126 | break; 127 | server.exit_status = WEXITSTATUS(server.exit_status); 128 | server_mark_socket_exec(true, false); 129 | } 130 | 131 | debug("server pty died: %d\n", server.exit_status); 132 | errno = errsv; 133 | } 134 | 135 | static void server_sigterm_handler(int sig) { 136 | exit(EXIT_FAILURE); /* invoke atexit handler */ 137 | } 138 | 139 | static Client *server_accept_client(void) { 140 | int newfd = accept(server.socket, NULL, NULL); 141 | if (newfd == -1 || server_set_socket_non_blocking(newfd) == -1) 142 | goto error; 143 | Client *c = client_malloc(newfd); 144 | if (!c) 145 | goto error; 146 | if (!server.clients) 147 | server_mark_socket_exec(true, true); 148 | c->socket = newfd; 149 | c->state = STATE_CONNECTED; 150 | c->next = server.clients; 151 | server.clients = c; 152 | server.read_pty = true; 153 | 154 | Packet pkt = { 155 | .type = MSG_PID, 156 | .len = sizeof pkt.u.l, 157 | .u.l = getpid(), 158 | }; 159 | server_send_packet(c, &pkt); 160 | 161 | return c; 162 | error: 163 | if (newfd != -1) 164 | close(newfd); 165 | return NULL; 166 | } 167 | 168 | static void server_sigusr1_handler(int sig) { 169 | int socket = server_create_socket(server.session_name); 170 | if (socket != -1) { 171 | if (server.socket) 172 | close(server.socket); 173 | server.socket = socket; 174 | } 175 | } 176 | 177 | static void server_atexit_handler(void) { 178 | unlink(sockaddr.sun_path); 179 | } 180 | 181 | static void server_mainloop(void) { 182 | atexit(server_atexit_handler); 183 | fd_set new_readfds, new_writefds; 184 | FD_ZERO(&new_readfds); 185 | FD_ZERO(&new_writefds); 186 | FD_SET(server.socket, &new_readfds); 187 | int new_fdmax = server.socket; 188 | bool exit_packet_delivered = false; 189 | 190 | if (server.read_pty) 191 | FD_SET_MAX(server.pty, &new_readfds, new_fdmax); 192 | 193 | while (server.clients || !exit_packet_delivered) { 194 | int fdmax = new_fdmax; 195 | fd_set readfds = new_readfds; 196 | fd_set writefds = new_writefds; 197 | FD_SET_MAX(server.socket, &readfds, fdmax); 198 | 199 | if (select(fdmax+1, &readfds, &writefds, NULL, NULL) == -1) { 200 | if (errno == EINTR) 201 | continue; 202 | die("server-mainloop"); 203 | } 204 | 205 | FD_ZERO(&new_readfds); 206 | FD_ZERO(&new_writefds); 207 | new_fdmax = server.socket; 208 | 209 | bool pty_data = false; 210 | 211 | Packet server_packet, client_packet; 212 | 213 | if (FD_ISSET(server.socket, &readfds)) 214 | server_accept_client(); 215 | 216 | if (FD_ISSET(server.pty, &readfds)) 217 | pty_data = server_read_pty(&server_packet); 218 | 219 | for (Client **prev_next = &server.clients, *c = server.clients; c;) { 220 | if (FD_ISSET(c->socket, &readfds) && server_recv_packet(c, &client_packet)) { 221 | switch (client_packet.type) { 222 | case MSG_CONTENT: 223 | server_write_pty(&client_packet); 224 | break; 225 | case MSG_ATTACH: 226 | c->flags = client_packet.u.i; 227 | if (c->flags & CLIENT_LOWPRIORITY) 228 | server_sink_client(); 229 | break; 230 | case MSG_RESIZE: 231 | c->state = STATE_ATTACHED; 232 | if (!(c->flags & CLIENT_READONLY) && c == server.clients) { 233 | debug("server-ioct: TIOCSWINSZ\n"); 234 | struct winsize ws = { 0 }; 235 | ws.ws_row = client_packet.u.ws.rows; 236 | ws.ws_col = client_packet.u.ws.cols; 237 | ioctl(server.pty, TIOCSWINSZ, &ws); 238 | } 239 | kill(-server.pid, SIGWINCH); 240 | break; 241 | case MSG_EXIT: 242 | exit_packet_delivered = true; 243 | /* fall through */ 244 | case MSG_DETACH: 245 | c->state = STATE_DISCONNECTED; 246 | break; 247 | default: /* ignore package */ 248 | break; 249 | } 250 | } 251 | 252 | if (c->state == STATE_DISCONNECTED) { 253 | bool first = (c == server.clients); 254 | Client *t = c->next; 255 | client_free(c); 256 | *prev_next = c = t; 257 | if (first && server.clients) { 258 | Packet pkt = { 259 | .type = MSG_RESIZE, 260 | .len = 0, 261 | }; 262 | server_send_packet(server.clients, &pkt); 263 | } else if (!server.clients) { 264 | server_mark_socket_exec(false, true); 265 | } 266 | continue; 267 | } 268 | 269 | FD_SET_MAX(c->socket, &new_readfds, new_fdmax); 270 | 271 | if (pty_data) 272 | server_send_packet(c, &server_packet); 273 | if (!server.running) { 274 | if (server.exit_status != -1) { 275 | Packet pkt = { 276 | .type = MSG_EXIT, 277 | .u.i = server.exit_status, 278 | .len = sizeof(pkt.u.i), 279 | }; 280 | if (!server_send_packet(c, &pkt)) 281 | FD_SET_MAX(c->socket, &new_writefds, new_fdmax); 282 | } else { 283 | FD_SET_MAX(c->socket, &new_writefds, new_fdmax); 284 | } 285 | } 286 | prev_next = &c->next; 287 | c = c->next; 288 | } 289 | 290 | if (server.running && server.read_pty) 291 | FD_SET_MAX(server.pty, &new_readfds, new_fdmax); 292 | } 293 | 294 | exit(EXIT_SUCCESS); 295 | } 296 | -------------------------------------------------------------------------------- /testsuite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ABDUCO="./abduco" 4 | # set detach key explicitly in case it was changed in config.h 5 | ABDUCO_OPTS="-e ^\\" 6 | 7 | [ ! -z "$1" ] && ABDUCO="$1" 8 | [ ! -x "$ABDUCO" ] && echo "usage: $0 /path/to/abduco" && exit 1 9 | 10 | TESTS_OK=0 11 | TESTS_RUN=0 12 | 13 | detach() { 14 | sleep 1 15 | printf "" 16 | } 17 | 18 | dvtm_cmd() { 19 | printf "$1\n" 20 | sleep 1 21 | } 22 | 23 | dvtm_session() { 24 | sleep 1 25 | dvtm_cmd 'c' 26 | dvtm_cmd 'c' 27 | dvtm_cmd 'c' 28 | sleep 1 29 | dvtm_cmd ' ' 30 | dvtm_cmd ' ' 31 | dvtm_cmd ' ' 32 | sleep 1 33 | dvtm_cmd 'qq' 34 | } 35 | 36 | expected_abduco_prolog() { 37 | printf "[?1049h" 38 | } 39 | 40 | # $1 => session-name, $2 => exit status 41 | expected_abduco_epilog() { 42 | echo "[?25h[?1049labduco: $1: session terminated with exit status $2" 43 | } 44 | 45 | # $1 => session-name, $2 => cmd to run 46 | expected_abduco_attached_output() { 47 | expected_abduco_prolog 48 | $2 49 | expected_abduco_epilog "$1" $? 50 | } 51 | 52 | # $1 => session-name, $2 => cmd to run 53 | expected_abduco_detached_output() { 54 | expected_abduco_prolog 55 | $2 >/dev/null 2>&1 56 | expected_abduco_epilog "$1" $? 57 | } 58 | 59 | check_environment() { 60 | [ "`$ABDUCO | wc -l`" -gt 1 ] && echo Abduco session exists && exit 1; 61 | pgrep abduco && echo Abduco process exists && exit 1; 62 | return 0; 63 | } 64 | 65 | test_non_existing_command() { 66 | check_environment || return 1; 67 | $ABDUCO -c test ./non-existing-command >/dev/null 2>&1 68 | check_environment || return 1; 69 | } 70 | 71 | # $1 => session-name, $2 => command to execute 72 | run_test_attached() { 73 | check_environment || return 1; 74 | 75 | local name="$1" 76 | local cmd="$2" 77 | local output="$name.out" 78 | local output_expected="$name.expected" 79 | 80 | TESTS_RUN=$((TESTS_RUN + 1)) 81 | echo -n "Running test attached: $name " 82 | expected_abduco_attached_output "$name" "$cmd" > "$output_expected" 2>&1 83 | 84 | if $ABDUCO -c "$name" $cmd 2>&1 | sed 's/.$//' > "$output" && sleep 1 && 85 | diff -u "$output_expected" "$output" && check_environment; then 86 | rm "$output" "$output_expected" 87 | TESTS_OK=$((TESTS_OK + 1)) 88 | echo "OK" 89 | return 0 90 | else 91 | echo "FAIL" 92 | return 1 93 | fi 94 | } 95 | 96 | # $1 => session-name, $2 => command to execute 97 | run_test_detached() { 98 | check_environment || return 1; 99 | 100 | local name="$1" 101 | local cmd="$2" 102 | local output="$name.out" 103 | local output_expected="$name.expected" 104 | 105 | TESTS_RUN=$((TESTS_RUN + 1)) 106 | echo -n "Running test detached: $name " 107 | expected_abduco_detached_output "$name" "$cmd" > "$output_expected" 2>&1 108 | 109 | if $ABDUCO -n "$name" $cmd >/dev/null 2>&1 && sleep 1 && 110 | $ABDUCO -a "$name" 2>&1 | sed 's/.$//' > "$output" && 111 | diff -u "$output_expected" "$output" && check_environment; then 112 | rm "$output" "$output_expected" 113 | TESTS_OK=$((TESTS_OK + 1)) 114 | echo "OK" 115 | return 0 116 | else 117 | echo "FAIL" 118 | return 1 119 | fi 120 | } 121 | 122 | # $1 => session-name, $2 => command to execute 123 | run_test_attached_detached() { 124 | check_environment || return 1; 125 | 126 | local name="$1" 127 | local cmd="$2" 128 | local output="$name.out" 129 | local output_expected="$name.expected" 130 | 131 | TESTS_RUN=$((TESTS_RUN + 1)) 132 | echo -n "Running test: $name " 133 | $cmd >/dev/null 2>&1 134 | expected_abduco_epilog "$name" $? > "$output_expected" 2>&1 135 | 136 | if detach | $ABDUCO $ABDUCO_OPTS -c "$name" $cmd >/dev/null 2>&1 && sleep 3 && 137 | $ABDUCO -a "$name" 2>&1 | tail -1 | sed 's/.$//' > "$output" && 138 | diff -u "$output_expected" "$output" && check_environment; then 139 | rm "$output" "$output_expected" 140 | TESTS_OK=$((TESTS_OK + 1)) 141 | echo "OK" 142 | return 0 143 | else 144 | echo "FAIL" 145 | return 1 146 | fi 147 | } 148 | 149 | run_test_dvtm() { 150 | echo -n "Running dvtm test: " 151 | if ! which dvtm >/dev/null 2>&1; then 152 | echo "SKIPPED" 153 | return 0; 154 | fi 155 | 156 | TESTS_RUN=$((TESTS_RUN + 1)) 157 | local name="dvtm" 158 | local output="$name.out" 159 | local output_expected="$name.expected" 160 | 161 | : > "$output_expected" 162 | if dvtm_session | $ABDUCO -c "$name" > "$output" 2>&1 && 163 | diff -u "$output_expected" "$output" && check_environment; then 164 | rm "$output" "$output_expected" 165 | TESTS_OK=$((TESTS_OK + 1)) 166 | echo "OK" 167 | return 0 168 | else 169 | echo "FAIL" 170 | return 1 171 | fi 172 | } 173 | 174 | test_non_existing_command || echo "Execution of non existing command FAILED" 175 | 176 | run_test_attached "awk" "awk 'BEGIN {for(i=1;i<=1000;i++) print i}'" 177 | run_test_detached "awk" "awk 'BEGIN {for(i=1;i<=1000;i++) print i}'" 178 | 179 | run_test_attached "false" "false" 180 | run_test_detached "false" "false" 181 | 182 | run_test_attached "true" "true" 183 | run_test_detached "true" "true" 184 | 185 | cat > exit-status.sh <<-EOT 186 | #!/bin/sh 187 | exit 42 188 | EOT 189 | chmod +x exit-status.sh 190 | 191 | run_test_attached "exit-status" "./exit-status.sh" 192 | run_test_detached "exit-status" "./exit-status.sh" 193 | 194 | rm ./exit-status.sh 195 | 196 | cat > long-running.sh <<-EOT 197 | #!/bin/sh 198 | echo Start 199 | date 200 | sleep 3 201 | echo Hello World 202 | sleep 3 203 | echo End 204 | date 205 | exit 1 206 | EOT 207 | chmod +x long-running.sh 208 | 209 | run_test_attached_detached "attach-detach" "./long-running.sh" 210 | 211 | rm ./long-running.sh 212 | 213 | run_test_dvtm 214 | 215 | [ $TESTS_OK -eq $TESTS_RUN ] 216 | --------------------------------------------------------------------------------