]> ruderich.org/simon Gitweb - wall-notify/wall-notify.git/blob - src/wall-notify.c
cleanup argv passing to notification program
[wall-notify/wall-notify.git] / src / wall-notify.c
1 /*
2  * Receive wall messages and pass them to a notification program via stdin.
3  *
4  * Copyright (C) 2014  Simon Ruderich
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20
21 #include <config.h>
22
23 #include <assert.h>
24 #include <fcntl.h>
25 #include <limits.h>
26 #include <pwd.h>
27 #include <signal.h>
28 #include <stdint.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <sys/select.h>
33 #include <sys/stat.h>
34 #include <sys/time.h>
35 #include <sys/types.h>
36 #include <sys/wait.h>
37 #include <unistd.h>
38
39 #ifdef USE_UTEMPTER
40 # include <utempter.h>
41 #endif
42 #ifdef USE_UTMPX
43 # include <utmpx.h>
44 #endif
45
46
47 static void sig_handler(int signal) {
48     (void)signal;
49 }
50 static void setup_signals(void) {
51     struct sigaction action;
52
53     memset(&action, 0, sizeof(action));
54     sigemptyset(&action.sa_mask);
55     action.sa_handler = sig_handler;
56
57     /* Handle all important signals which might be sent to us so we break out
58      * of the read()-loop below and can perform our cleanup. */
59     sigaction(SIGHUP,  &action, NULL);
60     sigaction(SIGINT,  &action, NULL);
61     sigaction(SIGQUIT, &action, NULL);
62     sigaction(SIGUSR1, &action, NULL);
63     sigaction(SIGUSR2, &action, NULL);
64 }
65
66 static int open_tty(void) {
67     int ptm;
68     const char *name;
69
70     ptm = posix_openpt(O_RDWR);
71     if (ptm < 0) {
72         return -1;
73     }
74     if (grantpt(ptm) != 0) {
75         return -1;
76     }
77
78     /* Prevent write access for other users so they can't use wall to send
79      * messages to this program. */
80     name = ptsname(ptm);
81     if (!name) {
82         return -1;
83     }
84     if (chmod(name, S_IRUSR | S_IWUSR) != 0) {
85         return -1;
86     }
87
88     if (unlockpt(ptm) != 0) {
89         return -1;
90     }
91
92     return ptm;
93 }
94
95 #ifdef USE_UTMPX
96 static const char *skip_prefix(const char *string, const char *prefix) {
97     size_t length = strlen(prefix);
98
99     if (!strncmp(string, prefix, length)) {
100         return string + length;
101     } else {
102         return string;
103     }
104 }
105 static int set_utmpx(short type, int ptm) {
106     struct utmpx entry;
107
108     const char *tty, *user, *id, *line;
109     struct timeval now;
110
111     user = getpwuid(getuid())->pw_name;
112     gettimeofday(&now, NULL);
113
114     tty = ptsname(ptm);
115     if (!tty) {
116         return 0;
117     }
118
119     id   = skip_prefix(tty, "/dev/pts/");
120     line = skip_prefix(tty, "/dev/");
121
122     /* Create utmp entry for the given terminal. */
123     memset(&entry, 0, sizeof(entry));
124
125     snprintf(entry.ut_user, sizeof(entry.ut_user), "%s", user);
126     snprintf(entry.ut_id,   sizeof(entry.ut_id),   "%s", id);
127     snprintf(entry.ut_line, sizeof(entry.ut_line), "%s", line);
128
129     entry.ut_pid  = getpid();
130     entry.ut_type = type;
131     entry.ut_tv.tv_sec  = now.tv_sec;
132     entry.ut_tv.tv_usec = now.tv_usec;
133
134     /* Write the entry to the utmp file. */
135     setutxent();
136     if (!pututxline(&entry)) {
137         return 0;
138     }
139     endutxent();
140
141     return 1;
142 }
143 #endif
144 static int login(int ptm) {
145 #if defined(USE_UTEMPTER)
146     int result = utempter_add_record(ptm, NULL);
147     /* Exit value of utempter_*() is not correct on all systems, e.g.
148      * FreeBSD() always returns 0. Checking the utmpx database manually is
149      * difficult because we don't know the exact values for ut_id and ut_line,
150      * therefore we only check the return value on systems known to return a
151      * useful value. */
152 # ifndef __linux
153     result = 1;
154 # endif
155     return result;
156 #elif defined(USE_UTMPX)
157     return set_utmpx(USER_PROCESS, ptm);
158 #else
159 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
160 #endif
161 }
162 static int logout(int ptm) {
163 #if defined(USE_UTEMPTER)
164     int result = utempter_remove_record(ptm);
165     /* See above. */
166 # ifndef __linux
167     result = 1;
168 # endif
169     return result;
170 #elif defined(USE_UTMPX)
171     return set_utmpx(DEAD_PROCESS, ptm);
172 #else
173 # error "neither USE_UTEMPTER nor USE_UTMPX defined"
174 #endif
175 }
176
177 static int wait_for_write(int fd, int timeout) {
178     fd_set rfds;
179     struct timeval tv;
180     int result;
181
182     FD_ZERO(&rfds);
183     FD_SET(fd, &rfds);
184
185     tv.tv_sec  = timeout;
186     tv.tv_usec = 0;
187
188     result = select(fd + 1, &rfds, NULL, NULL, &tv);
189     if (result < 0) {
190         perror("select");
191         return 0;
192     }
193     if (result == 0) {
194         /* Timeout. */
195         return 0;
196     }
197
198     /* Got more data to read. */
199     return 1;
200 }
201
202 static void pass_buffer_to_program(const char *buffer, size_t length, char **argv) {
203     int fds[2];
204     FILE *fh;
205
206     pid_t pid;
207
208     if (pipe(fds) != 0) {
209         perror("pipe");
210         return;
211     }
212
213     fh = fdopen(fds[1] /* write side */, "w");
214     if (!fh) {
215         perror("fdopen");
216         close(fds[0]);
217         close(fds[1]);
218         return;
219     }
220
221     pid = fork();
222     if (pid < 0) {
223         perror("fork");
224         goto out;
225     } else if (pid == 0) {
226         /* child */
227
228         close(fds[1]); /* write side */
229
230         /* Pass read side as stdin to the program. */
231         if (dup2(fds[0], STDIN_FILENO) < 0) {
232             perror("dup2");
233             exit(EXIT_FAILURE);
234         }
235         close(fds[0]);
236
237         /* Double fork so `wall-notify` doesn't have to wait for the
238          * notification process to terminate. We can't just use
239          * signal(SIGCHLD, SIG_IGN); because utempter on at least FreeBSD
240          * doesn't work if SIGCHLD is ignored. */
241         pid = fork();
242         if (pid < 0) {
243             perror("fork");
244             exit(EXIT_FAILURE);
245         } else if (pid == 0) {
246             execvp(argv[0], argv);
247             perror("execvp");
248             exit(EXIT_FAILURE);
249         }
250
251         exit(EXIT_SUCCESS);
252     }
253     /* father */
254
255     if (fwrite(buffer, 1, length, fh) != length) {
256         perror("fwrite");
257         /* continue to perform cleanup */
258     }
259
260 out:
261     close(fds[0]); /* read side */
262     fclose(fh);
263
264     if (waitpid(pid, NULL, 0) < 0) {
265         perror("waitpid");
266     }
267 }
268 static void handle_wall(int fd, char **argv) {
269     char buffer[4096];
270     ssize_t r;
271
272     assert(SSIZE_MAX <= SIZE_MAX);
273     while ((r = read(fd, buffer, sizeof(buffer))) > 0) {
274         size_t space;
275         ssize_t r2;
276
277         /* To prevent partial messages (sometimes it takes multiple reads to
278          * get the complete message) wait for a short time to get the rest of
279          * the message. */
280         space = sizeof(buffer) - (size_t)r;
281         while (space > 0 && wait_for_write(fd, 1 /* second */)) {
282             r2 = read(fd, buffer + r, space);
283             if (r2 <= 0) {
284                 break;
285             }
286             r += r2;
287             space -= (size_t)r2;
288         }
289
290         pass_buffer_to_program(buffer, (size_t)r, argv);
291     }
292 }
293
294 static void usage(const char *argv0) {
295     fprintf(stderr, "usage: %s <cmd args..>\n", argv0);
296     exit(EXIT_FAILURE);
297 }
298
299
300 int main(int argc, char **argv) {
301     int ptm, pts;
302     char *name;
303     const char *argv0;
304
305     argv0 = argv[0];
306     /* Don't pass our argv[0] to the notification program. */
307     argv++;
308
309     if (argc < 2) {
310         usage(argv0);
311     }
312
313     ptm = open_tty();
314     if (ptm < 0) {
315         perror("open_tty");
316         exit(EXIT_FAILURE);
317     }
318     name = ptsname(ptm);
319     if (!name) {
320         perror("ptsname");
321         exit(EXIT_FAILURE);
322     }
323
324 #ifdef DEBUG
325     printf("%s\n", name);
326 #endif
327
328     /* We need to open the slave or reading from the master yields EOF after
329      * the first wall write to it. */
330     pts = open(name, O_RDWR);
331     if (pts < 0) {
332         perror(name);
333         exit(EXIT_FAILURE);
334     }
335
336     /* Cleanup on signals. Necessary before login(). */
337     setup_signals();
338
339     if (!login(ptm)) {
340         perror("login");
341         exit(EXIT_FAILURE);
342     }
343
344     /* Main loop. Handle all wall messages sent to our PTY. */
345     handle_wall(ptm, argv);
346
347     if (!logout(ptm)) {
348         perror("logout");
349         exit(EXIT_FAILURE);
350     }
351
352     close(ptm);
353     close(pts);
354
355     return EXIT_SUCCESS;
356 }