2 * Receive wall messages and pass them to a notification program via stdin.
4 * Copyright (C) 2014-2015 Simon Ruderich
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
32 #include <sys/select.h>
35 #include <sys/types.h>
40 # include <utempter.h>
47 # include <X11/Xlib.h>
51 static sig_atomic_t signaled = 0;
54 static void sig_handler(int signal) {
59 static void setup_signals(void) {
60 struct sigaction action;
62 memset(&action, 0, sizeof(action));
63 sigemptyset(&action.sa_mask);
64 action.sa_handler = sig_handler;
66 /* Handle all important signals which might be sent to us so we break out
67 * of the read()-loop below and can perform our cleanup. */
68 sigaction(SIGHUP, &action, NULL);
69 sigaction(SIGINT, &action, NULL);
70 sigaction(SIGQUIT, &action, NULL);
71 sigaction(SIGUSR1, &action, NULL);
72 sigaction(SIGUSR2, &action, NULL);
75 static int set_cloexec(int fd) {
76 int flags = fcntl(fd, F_GETFD);
80 if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) {
86 static int open_tty(int public) {
90 ptm = posix_openpt(O_RDWR);
94 if (grantpt(ptm) != 0) {
97 if (!set_cloexec(ptm)) {
101 /* Prevent write access for other users so they can't use wall to send
102 * messages to this program. */
108 if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
113 if (unlockpt(ptm) != 0) {
121 static const char *skip_prefix(const char *string, const char *prefix) {
122 size_t length = strlen(prefix);
124 if (!strncmp(string, prefix, length)) {
125 return string + length;
130 static int set_utmpx(short type, int ptm) {
133 const char *tty, *user, *id, *line;
136 user = getpwuid(getuid())->pw_name;
137 gettimeofday(&now, NULL);
144 id = skip_prefix(tty, "/dev/pts/");
145 line = skip_prefix(tty, "/dev/");
147 /* Create utmp entry for the given terminal. */
148 memset(&entry, 0, sizeof(entry));
150 snprintf(entry.ut_user, sizeof(entry.ut_user), "%s", user);
151 snprintf(entry.ut_id, sizeof(entry.ut_id), "%s", id);
152 snprintf(entry.ut_line, sizeof(entry.ut_line), "%s", line);
154 entry.ut_pid = getpid();
155 entry.ut_type = type;
156 entry.ut_tv.tv_sec = now.tv_sec;
157 entry.ut_tv.tv_usec = now.tv_usec;
159 /* Write the entry to the utmp file. */
161 if (!pututxline(&entry)) {
169 static int login(int ptm) {
170 #if defined(USE_UTEMPTER)
171 int result = utempter_add_record(ptm, NULL);
172 /* Exit value of utempter_*() is not correct on all systems, e.g. FreeBSD
173 * always returns 0. Checking the utmpx database manually is difficult
174 * because we don't know the exact values for ut_id and ut_line, therefore
175 * we only check the return value on systems known to return a useful
181 #elif defined(USE_UTMPX)
182 return set_utmpx(USER_PROCESS, ptm);
184 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
187 static int logout(int ptm) {
188 #if defined(USE_UTEMPTER)
189 int result = utempter_remove_record(ptm);
195 #elif defined(USE_UTMPX)
196 return set_utmpx(DEAD_PROCESS, ptm);
198 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
202 static int wait_for_write(int fd, int timeout) {
213 result = select(fd + 1, &rfds, NULL, NULL, &tv);
223 /* Got more data to read. */
228 static void drop_privileges(void) {
229 uid_t uid, ruid, euid, suid;
230 gid_t gid, rgid, egid, sgid;
235 /* Drop all privileges. */
236 if (setresgid(gid, gid, gid) != 0) {
240 if (setresuid(uid, uid, uid) != 0) {
245 /* Verify all privileges were dropped. */
246 if (getresuid(&ruid, &euid, &suid) != 0) {
250 if (getresgid(&rgid, &egid, &sgid) != 0) {
254 if (uid == ruid && uid == euid && uid == suid
255 && gid == rgid && gid == egid && gid == sgid) {
256 /* Everything fine. */
260 fprintf(stderr, "failed to drop privileges, aborting\n");
265 static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
271 if (pipe(fds) != 0) {
276 fh = fdopen(fds[1] /* write side */, "w");
288 } else if (pid == 0) {
295 close(fds[1]); /* write side */
297 /* Pass read side as stdin to the program. */
298 if (dup2(fds[0], STDIN_FILENO) < 0) {
304 /* Double fork so `wall-notify` doesn't have to wait for the
305 * notification process to terminate. We can't just use
306 * signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
307 * doesn't work if SIGCHLD is ignored. */
312 } else if (pid == 0) {
313 execvp(argv[0], argv);
322 if (fwrite(buffer, 1, length, fh) != length) {
324 /* continue to perform cleanup */
328 close(fds[0]); /* read side */
331 if (waitpid(pid, NULL, 0) < 0) {
335 static void handle_wall(int fd, char **argv) {
339 assert(SSIZE_MAX <= SIZE_MAX);
340 while (!signaled && (r = read(fd, buffer, sizeof(buffer))) > 0) {
344 /* To prevent partial messages (sometimes it takes multiple reads to
345 * get the complete message) wait for a short time to get the rest of
347 space = sizeof(buffer) - (size_t)r;
348 while (space > 0 && wait_for_write(fd, 1 /* second */)) {
349 r2 = read(fd, buffer + r, space);
357 pass_buffer_to_program(buffer, (size_t)r, argv);
361 static void *x11_event_loop_thread(void *unused) {
367 pthread_detach(pthread_self());
369 display = XOpenDisplay(NULL);
371 fprintf(stderr, "failed to connect to X server\n");
375 /* Do nothing. We just want to die if the X11 session is closed. */
377 XNextEvent(display, &event);
382 static void usage(const char *argv0) {
383 fprintf(stderr, "usage: %s [-X] [-m] [-v] <cmd args..>\n", argv0);
384 fprintf(stderr, "Pass wall messages to <cmd args..>.\n");
385 fprintf(stderr, "\n");
386 fprintf(stderr, "-X quit when the current X session terminates\n");
387 fprintf(stderr, "-m allow messages from normal users (like `mesg y`)\n");
388 fprintf(stderr, "-v display version\n");
390 fprintf(stderr, "\n");
391 fprintf(stderr, "compiled without X11 support, -X disabled\n");
397 int main(int argc, char **argv) {
398 int option, enable_x11, mesg_yes;
403 /* Don't display error messages for unknown options. */
412 * Glibc violates POSIX by default and skips over non-option arguments and
413 * parses later arguments which look like options as well. But we want to
414 * pass everything except the options unmodified to execvp(). Prefixing
415 * the optstring with "+" fixes this behaver. This is not POSIX
416 * compatible, but the option should be ignored on other systems.
418 while ((option = getopt(argc, argv, "+Xmhv")) != -1) {
432 strlen(GIT_VERSION) ? " (Git " GIT_VERSION ")" : "");
436 fprintf(stderr, "%s: unknown option '%s'!\n\n",
437 argv0, argv[optind - 1]);
443 /* No arguments remaining, abort. */
447 /* Arguments for notification program. */
450 ptm = open_tty(mesg_yes);
462 printf("%s\n", name);
465 /* We need to open the slave or reading from the master yields EOF after
466 * the first wall write to it. */
467 pts = open(name, O_RDWR | O_CLOEXEC);
473 /* Start a thread which connects to X11. This way we die if the user logs
474 * out of its X11 session. */
477 fprintf(stderr, "%s: option -X is disabled!\n\n", argv0);
482 if (pthread_create(&tid, NULL, x11_event_loop_thread, NULL) != 0) {
483 perror("pthread_create");
489 /* Cleanup on signals. Necessary before login(). */
497 /* Main loop. Handle all wall messages sent to our PTY. */
498 handle_wall(ptm, argv);