Use abreviated weekday to preserve alignment
[abduco.git] / abduco.c
blob32c47957350d49518f3b531032510150564b2a73
1 /*
2 * Copyright (c) 2013-2014 Marc André Tanner <mat at brain-dump.org>
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.
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.
16 #define _GNU_SOURCE
17 #include <errno.h>
18 #include <fcntl.h>
19 #include <stdio.h>
20 #include <stdarg.h>
21 #include <stdlib.h>
22 #include <stdbool.h>
23 #include <stddef.h>
24 #include <signal.h>
25 #include <libgen.h>
26 #include <string.h>
27 #include <limits.h>
28 #include <dirent.h>
29 #include <termios.h>
30 #include <time.h>
31 #include <unistd.h>
32 #include <sys/stat.h>
33 #include <sys/ioctl.h>
34 #include <sys/types.h>
35 #include <sys/wait.h>
36 #include <sys/socket.h>
37 #include <sys/un.h>
38 #if defined(__linux__) || defined(__CYGWIN__)
39 # include <pty.h>
40 #elif defined(__FreeBSD__)
41 # include <libutil.h>
42 #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
43 # include <util.h>
44 #endif
46 #if defined CTRL && defined _AIX
47 #undef CTRL
48 #endif
49 #ifndef CTRL
50 #define CTRL(k) ((k) & 0x1F)
51 #endif
53 #include "config.h"
55 #ifdef _AIX
56 # include "forkpty-aix.c"
57 #endif
58 #define CLIENT_TIMEOUT 100
59 #define countof(arr) (sizeof(arr) / sizeof((arr)[0]))
61 enum PacketType {
62 MSG_CONTENT = 0,
63 MSG_ATTACH = 1,
64 MSG_DETACH = 2,
65 MSG_RESIZE = 3,
66 MSG_REDRAW = 4,
69 /* packet sent from client to server */
70 typedef struct {
71 unsigned char type;
72 unsigned char len;
73 union {
74 char msg[sizeof(struct winsize)];
75 struct winsize ws;
76 int i;
77 } u;
78 } ClientPacket;
80 /* packet sent from server to all clients */
81 typedef struct {
82 char buf[BUFSIZ];
83 ssize_t len;
84 } ServerPacket;
86 typedef struct {
87 ClientPacket pkt;
88 size_t off;
89 } ClientPacketState;
91 typedef struct {
92 ServerPacket *pkt;
93 size_t off;
94 } ServerPacketState;
96 typedef struct Client Client;
97 struct Client {
98 ServerPacketState output; /* display output as received from server */
99 ClientPacketState input; /* input as sent to the server */
100 int socket;
101 enum {
102 STATE_CONNECTED,
103 STATE_ATTACHED,
104 STATE_DETACHED,
105 STATE_DISCONNECTED,
106 } state;
107 time_t last_activity;
108 bool need_resize;
109 Client *next;
112 typedef struct {
113 Client *clients;
114 int client_count;
115 int socket;
116 ServerPacket pty_output;
117 ClientPacketState pty_input;
118 ClientPacket queue[10];
119 int queue_count;
120 int queue_insert;
121 int queue_remove;
122 int pty;
123 int exit_status;
124 struct termios term;
125 pid_t pid;
126 volatile sig_atomic_t running;
127 const char *name;
128 } Server;
130 static Server server = { .running = true, .exit_status = -1 };
131 static struct termios orig_term, cur_term;
132 bool has_term;
134 static struct sockaddr_un sockaddr = {
135 .sun_family = AF_UNIX,
138 static int create_socket(const char *name);
139 static void die(const char *s);
140 static void info(const char *str, ...);
142 static bool is_client_packet_complete(ClientPacketState *pkt) {
143 return pkt->off == sizeof pkt->pkt;
146 static bool is_server_packet_complete(ServerPacketState *pkt) {
147 return pkt->pkt && pkt->off == pkt->pkt->len;
150 static bool is_server_packet_nonempty(ServerPacketState *pkt) {
151 return pkt->pkt && pkt->pkt->len > 0;
154 #include "debug.c"
155 #include "client.c"
156 #include "server.c"
158 static void info(const char *str, ...) {
159 va_list ap;
160 va_start(ap, str);
161 fprintf(stdout, "\e[999H\r\n");
162 if (str) {
163 fprintf(stdout, "%s: ", server.name);
164 vfprintf(stdout, str, ap);
165 fprintf(stdout, "\r\n");
167 fflush(stdout);
168 va_end(ap);
171 static void die(const char *s) {
172 perror(s);
173 exit(EXIT_FAILURE);
176 static void usage() {
177 fprintf(stderr, "usage: abduco [-a|-A|-c|-n] [-e detachkey] name command\n");
178 exit(EXIT_FAILURE);
181 static int create_socket_dir() {
182 size_t maxlen = sizeof(sockaddr.sun_path);
183 char *dir = getenv("HOME");
184 if (!dir)
185 dir = "/tmp";
186 int len = snprintf(sockaddr.sun_path, maxlen, "%s/.%s/", dir, server.name);
187 if (len >= maxlen)
188 return -1;
189 if (mkdir(sockaddr.sun_path, 0750) == -1 && errno != EEXIST)
190 return -1;
191 return len;
194 static int create_socket(const char *name) {
195 size_t maxlen = sizeof(sockaddr.sun_path);
196 if (name[0] == '.' || name[0] == '/') {
197 strncpy(sockaddr.sun_path, name, maxlen);
198 if (sockaddr.sun_path[maxlen-1])
199 return -1;
200 } else {
201 int len = create_socket_dir(), rem = strlen(name);
202 if (len == -1 || maxlen - len - rem <= 0)
203 return -1;
204 strncat(sockaddr.sun_path, name, maxlen - len - 1);
206 return socket(AF_LOCAL, SOCK_STREAM, 0);
209 static bool create_session(const char *name, char * const argv[]) {
210 int pipefds[2];
211 if (pipe(pipefds) == -1)
212 return false;
213 if ((server.socket = create_socket(name)) == -1)
214 return false;
215 socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1;
216 mode_t mode = S_IRUSR|S_IWUSR;
217 fchmod(server.socket, mode);
218 if (bind(server.socket, (struct sockaddr*)&sockaddr, socklen) == -1)
219 return false;
220 if (listen(server.socket, 5) == -1)
221 goto error;
222 if (fchmod(server.socket, mode) == -1 && chmod(sockaddr.sun_path, mode) == -1)
223 goto error;
225 pid_t pid;
226 char errormsg[255];
227 struct sigaction sa;
229 switch ((pid = fork())) {
230 case 0: /* child process */
231 setsid();
232 close(pipefds[0]);
233 switch ((pid = fork())) {
234 case 0: /* child process */
235 sa.sa_flags = 0;
236 sigemptyset(&sa.sa_mask);
237 sa.sa_handler = server_pty_died_handler;
238 sigaction(SIGCHLD, &sa, NULL);
239 switch (server.pid = forkpty(&server.pty, NULL, has_term ? &server.term : NULL, NULL)) {
240 case 0: /* child process */
241 fcntl(pipefds[1], F_SETFD, FD_CLOEXEC);
242 close(server.socket);
243 execvp(argv[0], argv);
244 snprintf(errormsg, sizeof(errormsg), "server-execvp: %s\n", strerror(errno));
245 write_all(pipefds[1], errormsg, strlen(errormsg));
246 close(pipefds[1]);
247 exit(EXIT_FAILURE);
248 break;
249 case -1:
250 die("server-forkpty");
251 break;
252 default:
253 /* SIGTTIN, SIGTTU */
254 sigaction(SIGTERM, &sa, NULL);
255 sigaction(SIGINT, &sa, NULL);
256 sa.sa_handler = SIG_IGN;
257 sigaction(SIGPIPE, &sa, NULL);
258 sigaction(SIGHUP, &sa, NULL);
259 chdir("/");
260 #ifdef NDEBUG
261 int fd = open("/dev/null", O_RDWR);
262 dup2(fd, 0);
263 dup2(fd, 1);
264 dup2(fd, 2);
265 #endif /* NDEBUG */
266 close(pipefds[1]);
267 server_mainloop();
268 break;
270 break;
271 default:
272 close(pipefds[1]);
273 exit(EXIT_SUCCESS);
274 break;
276 break;
277 case -1: /* fork failed */
278 return false;
279 default: /* parent */
280 close(pipefds[1]);
281 int status;
282 wait(&status); /* wait for first fork */
283 if ((status = read_all(pipefds[0], errormsg, sizeof(errormsg))) > 0) {
284 write_all(STDERR_FILENO, errormsg, status);
285 unlink(sockaddr.sun_path);
286 exit(EXIT_FAILURE);
288 close(pipefds[0]);
290 return true;
291 error:
292 unlink(sockaddr.sun_path);
293 return false;
296 static bool attach_session(const char *name) {
297 if (server.socket > 0)
298 close(server.socket);
299 if ((server.socket = create_socket(name)) == -1)
300 return false;
301 socklen_t socklen = offsetof(struct sockaddr_un, sun_path) + strlen(sockaddr.sun_path) + 1;
302 if (connect(server.socket, (struct sockaddr*)&sockaddr, socklen) == -1)
303 return false;
304 if (server_set_socket_non_blocking(server.socket) == -1)
305 return false;
307 struct sigaction sa;
308 sa.sa_flags = 0;
309 sigemptyset(&sa.sa_mask);
310 sa.sa_handler = client_sigwinch_handler;
311 sigaction(SIGWINCH, &sa, NULL);
312 sa.sa_handler = SIG_IGN;
313 sigaction(SIGPIPE, &sa, NULL);
314 atexit(client_restore_terminal);
316 cur_term = orig_term;
317 cur_term.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IXOFF);
318 cur_term.c_oflag &= ~(OPOST);
319 cur_term.c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
320 cur_term.c_cflag &= ~(CSIZE|PARENB);
321 cur_term.c_cflag |= CS8;
322 cur_term.c_cc[VLNEXT] = _POSIX_VDISABLE;
323 cur_term.c_cc[VMIN] = 1;
324 cur_term.c_cc[VTIME] = 0;
325 tcsetattr(0, TCSADRAIN, &cur_term);
327 client_clear_screen();
328 switch (client_mainloop()) {
329 case -1:
330 info("detached");
331 break;
332 case EIO:
333 info("exited due to I/O errors: %s", strerror(errno));
334 break;
337 return true;
340 static int list_session() {
341 if (create_socket_dir() == -1)
342 return 1;
343 chdir(sockaddr.sun_path);
344 DIR *d = opendir(sockaddr.sun_path);
345 if (!d)
346 return 1;
347 puts("Active sessions");
348 struct dirent *e;
349 while ((e = readdir(d))) {
350 if (e->d_name[0] != '.') {
351 struct stat sb; char buf[255];
352 if (stat(e->d_name, &sb) == 0) {
353 strftime(buf, sizeof(buf), "%a%t %d.%m.%Y %T", localtime(&sb.st_atime));
354 printf(" %s\t%s\n", buf, e->d_name);
358 closedir(d);
359 return 0;
362 int main(int argc, char *argv[]) {
363 char *session = NULL, **cmd = NULL, action = '\0';
364 server.name = basename(argv[0]);
365 if (argc == 1)
366 exit(list_session());
367 for (int arg = 1; arg < argc; arg++) {
368 if (argv[arg][0] != '-') {
369 if (!session) {
370 session = argv[arg];
371 continue;
372 } else if (!cmd) {
373 cmd = &argv[arg];
374 break;
377 if (session)
378 usage();
379 switch (argv[arg][1]) {
380 case 'a':
381 case 'A':
382 case 'c':
383 case 'n':
384 action = argv[arg][1];
385 break;
386 case 'e':
387 if (arg + 1 >= argc)
388 usage();
389 char *esc = argv[++arg];
390 if (esc[0] == '^' && esc[1])
391 *esc = CTRL(esc[1]);
392 KEY_DETACH = *esc;
393 break;
394 case 'v':
395 puts("abduco-"VERSION" © 2013-2014 Marc André Tanner");
396 exit(EXIT_SUCCESS);
397 default:
398 usage();
402 if (!action || !session || (action != 'a' && !cmd))
403 usage();
405 if (tcgetattr(STDIN_FILENO, &orig_term) != -1) {
406 server.term = orig_term;
407 has_term = true;
410 switch (action) {
411 redo:
412 case 'n':
413 case 'c':
414 if (!create_session(session, cmd))
415 die("create-session");
416 if (action == 'n')
417 break;
418 case 'a':
419 case 'A':
420 if (!attach_session(session)) {
421 if (action == 'A') {
422 action = 'c';
423 goto redo;
425 die("attach-session");
429 return 0;