1 /*  $Id: run_with_lock.c 625755 2021-02-18 19:34:22Z ivanov $
2 * ===========================================================================
3 *
4 *                            PUBLIC DOMAIN NOTICE
5 *               National Center for Biotechnology Information
6 *
7 *  This software/database is a "United States Government Work" under the
8 *  terms of the United States Copyright Act.  It was written as part of
9 *  the author's official duties as a United States Government employee and
10 *  thus cannot be copyrighted.  This software/database is freely available
11 *  to the public for use. The National Library of Medicine and the U.S.
12 *  Government have not placed any restriction on its use or reproduction.
13 *
14 *  Although all reasonable efforts have been taken to ensure the accuracy
15 *  and reliability of the software and data, the NLM and the U.S.
16 *  Government do not and cannot warrant the performance or results that
17 *  may be obtained by using this software or data. The NLM and the U.S.
18 *  Government disclaim all warranties, express or implied, including
19 *  warranties of performance, merchantability or fitness for any particular
20 *  purpose.
21 *
22 *  Please cite the author in any work or product based on this material.
23 *
24 * ===========================================================================
25 *
26 * Author:  Aaron Ucko
27 *
28 * File Description:
29 *   Run a portion of the build under a filesystem-based lock,
30 *   optionally logging any resulting output (both stdout and stderr).
31 *
32 * ===========================================================================
33 */
34 
35 
36 #ifndef _GNU_SOURCE
37 #  define _GNU_SOURCE 1
38 #endif
39 
40 #include <errno.h>
41 #include <fcntl.h>
42 #include <signal.h>
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <string.h>
46 #include <sys/select.h>
47 #include <sys/wait.h>
48 #include <termios.h>
49 #include <unistd.h>
50 
51 #include <assert.h>
52 
53 #ifndef PATH_MAX
54 #  define PATH_MAX 4096
55 #endif
56 
57 #ifndef W_EXITCODE
58 #  define W_EXITCODE(x, y) (((x) << 8) | (y))
59 #endif
60 
61 static const char* s_AppName;
62 static char        s_LockName[PATH_MAX];
63 static int         s_MainChildDone;
64 static int         s_FinishingInBackground;
65 static int         s_CaughtSignal;
66 static sig_t       s_OrigHandlers[NSIG + 1];
67 
68 
69 typedef struct {
70     const char* base;
71     const char* getter;
72     const char* logfile;
73     const char* map;
74     const char* reviewer;
75     int         error_status;
76 } SOptions;
77 
78 typedef enum {
79     eFS_off,
80     eFS_on,
81     eFS_esc,
82     eFS_csi,
83 } EFilterState;
84 
85 
86 static
s_ApplyMap(SOptions * options)87 void s_ApplyMap(SOptions* options)
88 {
89     char line[1024];
90     FILE* map_file;
91 
92     assert(options->map != NULL);
93     map_file = fopen(options->map, "r");
94     if (map_file == NULL) {
95         if (errno != ENOENT) {
96             fprintf(stderr,
97                     "%s: Unable to open lock map %s: %s; leaving base as %s.\n",
98                     s_AppName, options->map, strerror(errno), options->base);
99         }
100         return;
101     }
102     while (fgets(line, sizeof(line), map_file) != NULL) {
103         char key[1024], value[1024];
104         if (sscanf(line, " %s %s", key, value) == 2
105             &&  strcmp(key, options->base + 5) == 0) {
106             char *new_base = malloc(strlen(value) + 6);
107             sprintf(new_base, "make_%s", value);
108             fprintf(stderr, "%s: Adjusting base from %s to %s per %s.\n",
109                     s_AppName, options->base, new_base, options->map);
110             options->base = new_base;
111             break;
112         }
113     }
114 
115     fclose(map_file);
116 }
117 
118 
119 static
s_ParseOptions(SOptions * options,int * argc,const char * const ** argv)120 void s_ParseOptions(SOptions* options, int* argc, const char* const** argv)
121 {
122     options->base         = NULL;
123     options->getter       = NULL;
124     options->logfile      = NULL;
125     options->map          = NULL;
126     options->reviewer     = NULL;
127     options->error_status = 1;
128 
129     while (*argc > 1) {
130         if (strcmp((*argv)[1], "-base") == 0  &&  *argc > 2) {
131             options->base = (*argv)[2];
132             *argv += 2;
133             *argc -= 2;
134         } else if (strcmp((*argv)[1], "-getter") == 0  &&  *argc > 2) {
135             options->getter = (*argv)[2];
136             *argv += 2;
137             *argc -= 2;
138         } else if (strcmp((*argv)[1], "-log") == 0  &&  *argc > 2) {
139             options->logfile = (*argv)[2];
140             *argv += 2;
141             *argc -= 2;
142         } else if (strcmp((*argv)[1], "-map") == 0  &&  *argc > 2) {
143             options->map = (*argv)[2];
144             *argv += 2;
145             *argc -= 2;
146         } else if (strcmp((*argv)[1], "-reviewer") == 0  &&  *argc > 2) {
147             options->reviewer = (*argv)[2];
148             *argv += 2;
149             *argc -= 2;
150         } else if ((*argv)[1][0] == '-') {
151             fprintf(stderr, "%s: Unsupported option %s.\n",
152                     s_AppName, (*argv)[1]);
153             exit(options->error_status);
154         } else if (strcmp((*argv)[1], "!") == 0) {
155             options->error_status = 0;
156             ++*argv;
157             --*argc;
158         } else {
159             break;
160         }
161     }
162 
163     if (options->base == NULL  &&  *argc > 1) {
164         const char* p = strrchr((*argv)[1], '/');
165         if (p != NULL) {
166             options->base = p + 1;
167         } else {
168             options->base = (*argv)[1];
169         }
170     }
171 }
172 
173 
174 static
s_Tee(int in,FILE * out1,FILE * out2,EFilterState * state)175 int s_Tee(int in, FILE* out1, FILE* out2, EFilterState* state)
176 {
177     char    buffer[1024];
178     ssize_t n;
179     while ((n = read(in, buffer, sizeof(buffer))) > 0) {
180         char* p = buffer;
181         if (fwrite(buffer, 1, n, out1) < n) {
182             fprintf(stderr, "%s: Error propagating process output: %s.\n",
183                     s_AppName, strerror(errno));
184         }
185         if (*state == eFS_esc) {
186             if (*p == '[') {
187                 ++p;
188                 /* --n; */ /* overadjusts when the final m turns up */
189                 *state = eFS_csi;
190             } else {
191                 fputc('\033', out2);
192                 *state = eFS_on;
193             }
194         }
195         if (*state == eFS_csi) {
196             p = memchr(buffer, 'm', n);
197             if (p == NULL) {
198                 continue;
199             } else {
200                 ++p;
201                 n -= (p - buffer);
202                 *state = eFS_on;
203             }
204         }
205         if (*state == eFS_on) {
206             const char* start   = p;
207             const char* src     = p;
208             char*       dest    = p;
209             while (src - p < n
210                    &&  (start = memchr(src, '\033', n - (src - p))) != NULL
211                    &&  (start == p + n - 1  ||  start[1] == '[')) {
212                 if (src > dest  &&  start > src) {
213                     memmove(dest, src, start - src);
214                 }
215                 dest += start - src;
216                 if (start == p + n - 1) {
217                     *state = eFS_esc;
218                     n = dest - p;
219                     break;
220                 }
221                 const char* end = memchr(start + 2, 'm', n - 2 - (start - p));
222                 if (end == NULL) {
223                     *state = eFS_csi;
224                     n = dest - p;
225                     break;
226                 } else {
227                     src = end + 1;
228                 }
229             }
230             if (*state == eFS_on) {
231                 memmove(dest, src, n - (src - p));
232                 n -= src - dest;
233             }
234         }
235         if (fwrite(p, 1, n, out2) < n) {
236             fprintf(stderr, "%s: Error logging process output: %s.\n",
237                     s_AppName, strerror(errno));
238         }
239     }
240     if (n < 0  &&  errno != EAGAIN  &&  errno != EWOULDBLOCK) {
241         if (*state == eFS_off  ||  errno != EIO) {
242             fprintf(stderr, "%s: Error reading from process: %s.\n",
243                     s_AppName, strerror(errno));
244         }
245         return 1;
246     }
247     return n == 0;
248 }
249 
250 
251 static
s_OnSIGCHLD(int n)252 void s_OnSIGCHLD(int n)
253 {
254     s_MainChildDone = 1;
255 }
256 
257 
258 static
s_OpenPipeOrPty(int fd_to_mimic,const char * label,int fds[2],EFilterState * state)259 int s_OpenPipeOrPty(int fd_to_mimic, const char* label, int fds[2],
260                     EFilterState* state)
261 {
262     const char* term = getenv("TERM");
263     if (term != NULL  &&  strcmp(term, "dumb")  &&  isatty(fd_to_mimic)) {
264         struct termios attr;
265         const char*    name;
266         *state = eFS_on;
267         if ((fds[0] = posix_openpt(O_RDONLY | O_NOCTTY)) < 0) {
268             fprintf(stderr, "%s: Error allocating pty master for %s: %s.\n",
269                     s_AppName, label, strerror(errno));
270             return -1;
271         } else if (grantpt(fds[0]) < 0  ||  unlockpt(fds[0]) < 0
272                    ||  (name = ptsname(fds[0])) == NULL
273                    ||  (fds[1] = open(name, O_WRONLY)) < 0) {
274             fprintf(stderr, "%s: Error opening pty slave for %s: %s.\n",
275                     s_AppName, label, strerror(errno));
276             close(fds[0]);
277             return -1;
278         }
279         if (tcgetattr(fds[1], &attr) < 0) {
280             fprintf(stderr,
281                     "%s: Warning: unable to get attributes for %s: %s.\n",
282                     s_AppName, label, strerror(errno));
283         } else {
284             attr.c_oflag |= ONLRET;
285 #ifdef ONLCR
286             attr.c_oflag &= ~ONLCR;
287 #endif
288             /* XXX -- propagate anything from fd_to_mimic? */
289             if (tcsetattr(fds[1], TCSADRAIN, &attr) < 0) {
290                 fprintf(stderr,
291                     "%s: Warning: unable to set attributes for %s: %s.\n",
292                         s_AppName, label, strerror(errno));
293             }
294         }
295     } else {
296         if (pipe(fds) < 0) {
297             fprintf(stderr, "%s: Error creating pipe for %s: %s.n",
298                     s_AppName, label, strerror(errno));
299             return -1;
300         }
301     }
302 
303     return 0;
304 }
305 
306 
307 static
s_Run(const char * const * args,FILE * log)308 int s_Run(const char* const* args, FILE* log)
309 {
310     int   stdout_fds[2], stderr_fds[2];
311     int   status = W_EXITCODE(126, 0);
312     pid_t pid;
313     EFilterState stdout_state = eFS_off, stderr_state = eFS_off;
314     if (log != NULL) {
315         if (s_OpenPipeOrPty(STDOUT_FILENO, "stdout", stdout_fds, &stdout_state)
316             < 0) {
317             return status;
318         }
319         if (s_OpenPipeOrPty(STDERR_FILENO, "stderr", stderr_fds, &stderr_state)
320             < 0) {
321             close(stdout_fds[0]);
322             close(stdout_fds[1]);
323             return status;
324         }
325         s_MainChildDone = 0;
326         signal(SIGCHLD, s_OnSIGCHLD);
327     }
328 
329     pid = fork();
330     if (pid < 0) { /* error */
331         fprintf(stderr, "%s: Fork failed: %s.\n", s_AppName, strerror(errno));
332     } else if (pid == 0) { /* child */
333         int n;
334         if (log != NULL) {
335             close(stdout_fds[0]);
336             close(stderr_fds[0]);
337             dup2(stdout_fds[1], STDOUT_FILENO);
338             dup2(stderr_fds[1], STDERR_FILENO);
339         }
340         for (n = 1;  n <= NSIG;  ++n) {
341             if (n != SIGKILL  &&  n != SIGSTOP) {
342                 signal(n, s_OrigHandlers[n]);
343             }
344         }
345         execvp(args[0], (char* const*) args);
346         fprintf(stderr, "%s: Unable to exec %s: %s.\n",
347                 s_AppName, args[0], strerror(errno));
348         _exit(127);
349     } else { /* parent */
350         if (log != NULL) {
351             int stdout_done = 0, stderr_done = 0;
352             close(stdout_fds[1]);
353             close(stderr_fds[1]);
354             fcntl(stdout_fds[0], F_SETFL,
355                   fcntl(stdout_fds[0], F_GETFL) | O_NONBLOCK);
356             fcntl(stderr_fds[0], F_SETFL,
357                   fcntl(stderr_fds[0], F_GETFL) | O_NONBLOCK);
358             while ( !stdout_done  ||  !stderr_done ) {
359                 fd_set rfds, efds;
360                 unsigned int nfds = 0;
361                 FD_ZERO(&rfds);
362                 FD_ZERO(&efds);
363                 if ( !stderr_done ) {
364                     FD_SET(stderr_fds[0], &rfds);
365                     FD_SET(stderr_fds[0], &efds);
366                     nfds = stderr_fds[0] + 1;
367                 }
368                 if ( !stdout_done ) {
369                     FD_SET(stdout_fds[0], &rfds);
370                     FD_SET(stdout_fds[0], &efds);
371                     if (stdout_fds[0] >= nfds) {
372                         nfds = stdout_fds[0] + 1;
373                     }
374                 }
375                 if (select(nfds, &rfds, NULL, &efds, NULL) < 0
376                     &&  errno != EINTR  &&  errno != EAGAIN) {
377                     fprintf(stderr,
378                             "%s: Error checking for output to log: %s.\n",
379                             s_AppName, strerror(errno));
380                     break;
381                 }
382                 if (FD_ISSET(stdout_fds[0], &rfds)
383                     ||  FD_ISSET(stdout_fds[0], &efds)) {
384                     stdout_done = s_Tee(stdout_fds[0], stdout, log,
385                                         &stdout_state);
386                 }
387                 if (FD_ISSET(stderr_fds[0], &rfds)
388                     ||  FD_ISSET(stderr_fds[0], &efds)) {
389                     stderr_done = s_Tee(stderr_fds[0], stderr, log,
390                                         &stderr_state);
391                 }
392                 if (s_MainChildDone  &&  ( !stdout_done  ||  !stderr_done )
393                     &&  waitpid(pid, &status, WNOHANG) != 0) {
394                     s_MainChildDone = 0;
395                     fflush(stdout);
396                     fflush(stderr);
397                     fflush(log);
398                     if (fork() > 0) {
399                         s_FinishingInBackground = 1;
400                         break;
401                     }
402                 }
403             }
404             signal(SIGCHLD, s_OrigHandlers[SIGCHLD]);
405         }
406         if (log == NULL  ||  !s_FinishingInBackground ) {
407             waitpid(pid, &status, 0);
408         }
409     }
410 
411     if (log != NULL) {
412         close(stdout_fds[0]);
413         close(stderr_fds[0]);
414     }
415     return status;
416 }
417 
418 
419 static
s_CleanUp(void)420 void s_CleanUp(void)
421 {
422     const char* args[] = { "/bin/rm", "-rf", s_LockName, NULL };
423     if ( !s_FinishingInBackground ) {
424         s_Run(args, NULL);
425     }
426 }
427 
428 
429 static
s_OnSignal(int n)430 void s_OnSignal(int n)
431 {
432     /* Propagate to child?  The shell version doesn't. */
433     fprintf(stderr, "%s: Caught signal %d\n", s_AppName, n);
434     /*
435     s_CleanUp();
436     _exit(n | 0x80);
437     */
438     s_CaughtSignal = n;
439     signal(n, s_OnSignal);
440 }
441 
442 
443 typedef enum {
444     eOldLogMissing,
445     eNewLogBoring,
446     eNewLogInteresting
447 } ELogStatus;
448 
449 
main(int argc,const char * const * argv)450 int main(int argc, const char* const* argv)
451 {
452     SOptions    options;
453     char        pid_str[sizeof(pid_t) * 3 + 1], new_log[PATH_MAX];
454     const char* getter_args[] = { "get_lock", "BASE", pid_str, NULL };
455     FILE*       log = NULL;
456     int         status = 0, n;
457 
458     s_AppName = argv[0];
459     s_ParseOptions(&options, &argc, &argv);
460 
461     if (options.map != NULL  &&  strncmp(options.base, "make_", 5) == 0) {
462         s_ApplyMap(&options);
463     }
464 
465     if (options.getter != NULL) {
466         getter_args[0] = options.getter;
467     }
468     getter_args[1] = options.base;
469     sprintf(pid_str, "%ld", (long)getpid());
470     sprintf(s_LockName, "%s.lock", options.base);
471     for (n = 1;  n <= NSIG;  ++n) {
472         switch (n) {
473             /* Blacklist some signals for various reasons. */
474         case SIGQUIT: case SIGILL: case SIGABRT: case SIGFPE: case SIGSEGV:
475         case SIGBUS: case SIGSYS: case SIGTRAP: case SIGXCPU: case SIGXFSZ:
476             /* Don't touch anything severe enough to cause a core dump. */
477         case SIGPIPE:
478             /* Should react immediately here too. */
479         case SIGKILL: case SIGSTOP:
480             /* Uncatchable. */
481         case SIGTSTP: case SIGTTIN: case SIGTTOU:
482             /* Harmless temporary suspension; don't overreact. */
483         case SIGCHLD: case SIGCONT: case SIGURG: case SIGWINCH:
484             /* Totally harmless; don't overreact. */
485             break;
486         default:
487             s_OrigHandlers[n] = signal(n, s_OnSignal);
488         }
489     }
490     s_Run(getter_args, NULL);
491     if (s_CaughtSignal) {
492         goto cleanup;
493     }
494 
495     if (options.logfile != NULL) {
496         sprintf(new_log, "%s.new", options.logfile);
497         log = fopen(new_log, "w");
498         if (log == NULL) {
499             fprintf(stderr, "%s: Couldn't open log file %s: %s.\n",
500                     s_AppName, options.logfile, strerror(errno));
501             return options.error_status;
502         }
503     }
504 
505     if ( !s_CaughtSignal ) {
506         status = s_Run(argv + 1, log);
507     }
508 
509     if (log != NULL  &&  !s_CaughtSignal  &&  access(new_log, F_OK) == 0) {
510         ELogStatus log_status = eNewLogInteresting;
511         fclose(log);
512         if (access(options.logfile, F_OK) != 0) {
513             log_status = eOldLogMissing;
514         } else if (options.reviewer != NULL) {
515             const char* reviewer_args[] = { options.reviewer, new_log, NULL };
516             if (s_Run(reviewer_args, NULL) != 0) {
517                 log_status = eNewLogBoring;
518             }
519         }
520         if (log_status != eNewLogBoring) {
521             if (rename(new_log, options.logfile) < 0) {
522                 fprintf(stderr, "%s: Unable to rename log file %s: %s.\n",
523                         s_AppName, new_log, strerror(errno));
524             }
525         }
526     }
527 
528  cleanup:
529     s_CleanUp();
530 
531     if (s_CaughtSignal) {
532         status = s_CaughtSignal | 0x80;
533     } else if (WIFSIGNALED(status)) {
534         status = WTERMSIG(status) | 0x80;
535     } else {
536         status = WEXITSTATUS(status);
537     }
538     if (options.error_status == 0) {
539         status = !status;
540     }
541 
542     return status;
543 }
544