chiark / gitweb /
WIP before rethink reading-two-files-at-once
[innduct.git] / expire / expire.c
1 /*  $Id: expire.c 6135 2003-01-19 01:15:40Z rra $
2 **
3 **  Expire news articles.
4 */
5
6 #include "config.h"
7 #include "clibrary.h"
8 #include <ctype.h>
9 #include <errno.h>
10 #include <pwd.h>
11 #include <sys/stat.h>
12 #include <syslog.h>
13 #include <time.h>
14
15 #include "inn/history.h"
16 #include "inn/innconf.h"
17 #include "inn/messages.h"
18 #include "inndcomm.h"
19 #include "libinn.h"
20 #include "paths.h"
21 #include "storage.h"
22
23
24 typedef struct _EXPIRECLASS {
25     time_t              Keep;
26     time_t              Default;
27     time_t              Purge;
28     bool                Missing;
29     bool                ReportedMissing;
30 } EXPIRECLASS;
31
32 /*
33 **  Expire-specific stuff.
34 */
35 #define MAGIC_TIME      49710.
36
37 static bool             EXPtracing;
38 static bool             EXPusepost;
39 static bool             Ignoreselfexpire = false;
40 static FILE             *EXPunlinkfile;
41 static EXPIRECLASS      EXPclasses[NUM_STORAGE_CLASSES+1];
42 static char             *EXPreason;
43 static time_t           EXPremember;
44 static time_t           Now;
45 static time_t           RealNow;
46
47 /* Statistics; for -v flag. */
48 static char             *EXPgraph;
49 static int              EXPverbose;
50 static long             EXPprocessed;
51 static long             EXPunlinked;
52 static long             EXPallgone;
53 static long             EXPstillhere;
54 static struct history   *History;
55 static char             *NHistory;
56
57 static void CleanupAndExit(bool Server, bool Paused, int x);
58
59 static int EXPsplit(char *p, char sep, char **argv, int count);
60
61 enum KR {Keep, Remove};
62
63 \f
64
65 /*
66 **  Open a file or give up.
67 */
68 static FILE *
69 EXPfopen(bool Unlink, const char *Name, const char *Mode, bool Needclean,
70          bool Server, bool Paused)
71 {
72     FILE *F;
73
74     if (Unlink && unlink(Name) < 0 && errno != ENOENT)
75         syswarn("cannot remove %s", Name);
76     if ((F = fopen(Name, Mode)) == NULL) {
77         syswarn("cannot open %s in %s mode", Name, Mode);
78         if (Needclean)
79             CleanupAndExit(Server, Paused, 1);
80         else
81             exit(1);
82     }
83     return F;
84 }
85
86
87 /*
88 **  Split a line at a specified field separator into a vector and return
89 **  the number of fields found, or -1 on error.
90 */
91 static int EXPsplit(char *p, char sep, char **argv, int count)
92 {
93     int                 i;
94
95     if (!p)
96       return 0;
97
98     while (*p == sep)
99       ++p;
100
101     if (!*p)
102       return 0;
103
104     if (!p)
105       return 0;
106
107     while (*p == sep)
108       ++p;
109
110     if (!*p)
111       return 0;
112
113     for (i = 1, *argv++ = p; *p; )
114         if (*p++ == sep) {
115             p[-1] = '\0';
116             for (; *p == sep; p++)
117                 ;
118             if (!*p)
119                 return i;
120             if (++i == count)
121                 /* Overflow. */
122                 return -1;
123             *argv++ = p;
124         }
125     return i;
126 }
127
128
129 /*
130 **  Parse a number field converting it into a "when did this start?".
131 **  This makes the "keep it" tests fast, but inverts the logic of
132 **  just about everything you expect.  Print a message and return false
133 **  on error.
134 */
135 static bool EXPgetnum(int line, char *word, time_t *v, const char *name)
136 {
137     char                *p;
138     bool                SawDot;
139     double              d;
140
141     if (strcasecmp(word, "never") == 0) {
142         *v = (time_t)0;
143         return true;
144     }
145
146     /* Check the number.  We don't have strtod yet. */
147     for (p = word; ISWHITE(*p); p++)
148         ;
149     if (*p == '+' || *p == '-')
150         p++;
151     for (SawDot = false; *p; p++)
152         if (*p == '.') {
153             if (SawDot)
154                 break;
155             SawDot = true;
156         }
157         else if (!CTYPE(isdigit, (int)*p))
158             break;
159     if (*p) {
160         warn("bad '%c' character in %s field on line %d", *p, name, line);
161         return false;
162     }
163     d = atof(word);
164     if (d > MAGIC_TIME)
165         *v = (time_t)0;
166     else
167         *v = Now - (time_t)(d * 86400.);
168     return true;
169 }
170
171
172 /*
173 **  Parse the expiration control file.  Return true if okay.
174 */
175 static bool EXPreadfile(FILE *F)
176 {
177     char                *p;
178     int                 i;
179     int                 j;
180     bool                SawDefault;
181     char                buff[BUFSIZ];
182     char                *fields[7];
183
184     /* Scan all lines. */
185     EXPremember = -1;
186     SawDefault = false;
187
188     for (i = 0; i <= NUM_STORAGE_CLASSES; i++) {
189         EXPclasses[i].ReportedMissing = false;
190         EXPclasses[i].Missing = true;
191     }
192     
193     for (i = 1; fgets(buff, sizeof buff, F) != NULL; i++) {
194         if ((p = strchr(buff, '\n')) == NULL) {
195             warn("line %d too long", i);
196             return false;
197         }
198         *p = '\0';
199         p = strchr(buff, '#');
200         if (p)
201             *p = '\0';
202         else
203             p = buff + strlen(buff);
204         while (--p >= buff) {
205             if (isspace((int)*p))
206                 *p = '\0';
207             else
208                 break;
209         }
210         if (buff[0] == '\0')
211             continue;
212         if ((j = EXPsplit(buff, ':', fields, ARRAY_SIZE(fields))) == -1) {
213             warn("too many fields on line %d", i);
214             return false;
215         }
216
217         /* Expired-article remember line? */
218         if (strcmp(fields[0], "/remember/") == 0) {
219             if (j != 2) {
220                 warn("invalid format on line %d", i);
221                 return false;
222             }
223             if (EXPremember != -1) {
224                 warn("duplicate /remember/ on line %d", i);
225                 return false;
226             }
227             if (!EXPgetnum(i, fields[1], &EXPremember, "remember"))
228                 return false;
229             continue;
230         }
231
232         /* Storage class line? */
233         if (j == 4) {
234             /* Is this the default line? */
235             if (fields[0][0] == '*' && fields[0][1] == '\0') {
236                 if (SawDefault) {
237                     warn("duplicate default on line %d", i);
238                     return false;
239                 }
240                 j = NUM_STORAGE_CLASSES;
241                 SawDefault = true;
242             } else {
243                 j = atoi(fields[0]);
244                 if ((j < 0) || (j >= NUM_STORAGE_CLASSES))
245                     warn("bad storage class %d on line %d", j, i);
246             }
247         
248             if (!EXPgetnum(i, fields[1], &EXPclasses[j].Keep,    "keep")
249                 || !EXPgetnum(i, fields[2], &EXPclasses[j].Default, "default")
250                 || !EXPgetnum(i, fields[3], &EXPclasses[j].Purge,   "purge"))
251                 return false;
252             /* These were turned into offsets, so the test is the opposite
253              * of what you think it should be.  If Purge isn't forever,
254              * make sure it's greater then the other two fields. */
255             if (EXPclasses[j].Purge) {
256                 /* Some value not forever; make sure other values are in range. */
257                 if (EXPclasses[j].Keep && EXPclasses[j].Keep < EXPclasses[j].Purge) {
258                     warn("keep time longer than purge time on line %d", i);
259                     return false;
260                 }
261                 if (EXPclasses[j].Default && EXPclasses[j].Default < EXPclasses[j].Purge) {
262                     warn("default time longer than purge time on line %d", i);
263                     return false;
264                 }
265             }
266             EXPclasses[j].Missing = false;
267             continue;
268         }
269
270         /* Regular expiration line -- right number of fields? */
271         if (j != 5) {
272             warn("bad format on line %d", i);
273             return false;
274         }
275         continue; /* don't process this line--per-group expiry is done by expireover */
276     }
277
278     return true;
279 }
280
281 /*
282 **  Should we keep the specified article?
283 */
284 static enum KR EXPkeepit(const TOKEN *token, time_t when, time_t Expires)
285 {
286     EXPIRECLASS         class;
287
288     class = EXPclasses[token->class];
289     if (class.Missing) {
290         if (EXPclasses[NUM_STORAGE_CLASSES].Missing) {
291             /* no default */
292             if (!class.ReportedMissing) {
293                 warn("class definition for %d missing from control file,"
294                      " assuming it should never expire", token->class);
295                 EXPclasses[token->class].ReportedMissing = true;
296             }
297             return Keep;
298         } else {
299             /* use the default */
300             class = EXPclasses[NUM_STORAGE_CLASSES];
301             EXPclasses[token->class] = class;
302         }
303     }
304     /* Bad posting date? */
305     if (when > (RealNow + 86400)) {
306         /* Yes -- force the article to go to right now */
307         when = Expires ? class.Purge : class.Default;
308     }
309     if (EXPverbose > 2) {
310         if (EXPverbose > 3)
311             printf("%s age = %0.2f\n", TokenToText(*token), (Now - when) / 86400.);
312         if (Expires == 0) {
313             if (when <= class.Default)
314                 printf("%s too old (no exp)\n", TokenToText(*token));
315         } else {
316             if (when <= class.Purge)
317                 printf("%s later than purge\n", TokenToText(*token));
318             if (when >= class.Keep)
319                 printf("%s earlier than min\n", TokenToText(*token));
320             if (Now >= Expires)
321                 printf("%s later than header\n", TokenToText(*token));
322         }
323     }
324     
325     /* If no expiration, make sure it wasn't posted before the default. */
326     if (Expires == 0) {
327         if (when >= class.Default)
328             return Keep;
329         
330         /* Make sure it's not posted before the purge cut-off and
331          * that it's not due to expire. */
332     } else {
333         if (when >= class.Purge && (Expires >= Now || when >= class.Keep))
334             return Keep;
335     }
336     return Remove;
337
338 }
339
340
341 /*
342 **  An article can be removed.  Either print a note, or actually remove it.
343 **  Also fill in the article size.
344 */
345 static void
346 EXPremove(const TOKEN *token)
347 {
348     /* Turn into a filename and get the size if we need it. */
349     if (EXPverbose > 1)
350         printf("\tunlink %s\n", TokenToText(*token));
351
352     if (EXPtracing) {
353         EXPunlinked++;
354         printf("%s\n", TokenToText(*token));
355         return;
356     }
357     
358     EXPunlinked++;
359     if (EXPunlinkfile) {
360         fprintf(EXPunlinkfile, "%s\n", TokenToText(*token));
361         if (!ferror(EXPunlinkfile))
362             return;
363         syswarn("cannot write to -z file (will ignore it for rest of run)");
364         fclose(EXPunlinkfile);
365         EXPunlinkfile = NULL;
366     }
367     if (!SMcancel(*token) && SMerrno != SMERR_NOENT && SMerrno != SMERR_UNINIT)
368         warn("cannot unlink %s", TokenToText(*token));
369 }
370
371 /*
372 **  Do the work of expiring one line.
373 */
374 static bool
375 EXPdoline(void *cookie UNUSED, time_t arrived, time_t posted, time_t expires,
376           TOKEN *token)
377 {
378     time_t              when;
379     bool                HasSelfexpire = false;
380     bool                Selfexpired = false;
381     ARTHANDLE           *article;
382     enum KR             kr;
383     bool                r;
384
385     if (innconf->groupbaseexpiry || SMprobe(SELFEXPIRE, token, NULL)) {
386         if ((article = SMretrieve(*token, RETR_STAT)) == (ARTHANDLE *)NULL) {
387             HasSelfexpire = true;
388             Selfexpired = true;
389         } else {
390             /* the article is still alive */
391             SMfreearticle(article);
392             if (innconf->groupbaseexpiry || !Ignoreselfexpire)
393                 HasSelfexpire = true;
394         }
395     }
396     if (EXPusepost && posted != 0)
397         when = posted;
398     else
399         when = arrived;
400     EXPprocessed++;
401         
402     if (HasSelfexpire) {
403         if (Selfexpired || token->type == TOKEN_EMPTY) {
404             EXPallgone++;
405             r = false;
406         } else {
407             EXPstillhere++;
408             r = true;
409         }
410     } else  {
411         kr = EXPkeepit(token, when, expires);
412         if (kr == Remove) {
413             EXPremove(token);
414             EXPallgone++;
415             r = false;
416         } else {
417             EXPstillhere++;
418             r = true;
419         }
420     }
421
422     return r;
423 }
424
425 \f
426
427 /*
428 **  Clean up link with the server and exit.
429 */
430 static void
431 CleanupAndExit(bool Server, bool Paused, int x)
432 {
433     FILE        *F;
434
435     if (Server) {
436         ICCreserve("");
437         if (Paused && ICCgo(EXPreason) != 0) {
438             syswarn("cannot unpause server");
439             x = 1;
440         }
441     }
442     if (Server && ICCclose() < 0) {
443         syswarn("cannot close communication link to server");
444         x = 1;
445     }
446     if (EXPunlinkfile && fclose(EXPunlinkfile) == EOF) {
447         syswarn("cannot close -z file");
448         x = 1;
449     }
450
451     /* Report stats. */
452     if (EXPverbose) {
453         printf("Article lines processed %8ld\n", EXPprocessed);
454         printf("Articles retained       %8ld\n", EXPstillhere);
455         printf("Entries expired         %8ld\n", EXPallgone);
456         if (!innconf->groupbaseexpiry)
457             printf("Articles dropped        %8ld\n", EXPunlinked);
458     }
459
460     /* Append statistics to a summary file */
461     if (EXPgraph) {
462         F = EXPfopen(false, EXPgraph, "a", false, false, false);
463         fprintf(F, "%ld %ld %ld %ld %ld\n",
464                       (long)Now, EXPprocessed, EXPstillhere, EXPallgone,
465                       EXPunlinked);
466         fclose(F);
467     }
468
469     SMshutdown();
470     HISclose(History);
471     if (EXPreason != NULL)
472         free(EXPreason);
473         
474     if (NHistory != NULL)
475         free(NHistory);
476     closelog();
477     exit(x);
478 }
479
480 /*
481 **  Print a usage message and exit.
482 */
483 static void
484 Usage(void)
485 {
486     fprintf(stderr, "Usage: expire [flags] [expire.ctl]\n");
487     exit(1);
488 }
489
490
491 /*
492 **  Change to the news user if possible, and if not, die.  Used for operations
493 **  that may create new database files so as not to mess up the ownership.
494 */
495 static void
496 setuid_news(void)
497 {
498     struct passwd *pwd;
499
500     pwd = getpwnam(NEWSUSER);
501     if (pwd == NULL)
502         die("can't resolve %s to a UID (account doesn't exist?)", NEWSUSER);
503     if (getuid() == 0)
504         setuid(pwd->pw_uid);
505     if (getuid() != pwd->pw_uid)
506         die("must be run as %s", NEWSUSER);
507 }
508
509
510 int
511 main(int ac, char *av[])
512 {
513     int                 i;
514     char                *p;
515     FILE                *F;
516     char                *HistoryText;
517     const char          *NHistoryPath = NULL;
518     const char          *NHistoryText = NULL;
519     char                *EXPhistdir;
520     char                buff[SMBUF];
521     bool                Server;
522     bool                Bad;
523     bool                IgnoreOld;
524     bool                Writing;
525     bool                UnlinkFile;
526     bool                val;
527     time_t              TimeWarp;
528     size_t              Size = 0;
529
530     /* First thing, set up logging and our identity. */
531     openlog("expire", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);
532     message_program_name = "expire";
533
534     /* Set defaults. */
535     Server = true;
536     IgnoreOld = false;
537     Writing = true;
538     TimeWarp = 0;
539     UnlinkFile = false;
540
541     if (!innconf_read(NULL))
542         exit(1);
543
544     HistoryText = concatpath(innconf->pathdb, _PATH_HISTORY);
545
546     umask(NEWSUMASK);
547
548     /* find the default history file directory */
549     EXPhistdir = xstrdup(HistoryText);
550     p = strrchr(EXPhistdir, '/');
551     if (p != NULL) {
552         *p = '\0';
553     }
554
555     /* Parse JCL. */
556     while ((i = getopt(ac, av, "f:h:d:g:iNnpr:s:tv:w:xz:")) != EOF)
557         switch (i) {
558         default:
559             Usage();
560             /* NOTREACHED */
561         case 'd':
562             NHistoryPath = optarg;
563             break;
564         case 'f':
565             NHistoryText = optarg;
566             break;
567         case 'g':
568             EXPgraph = optarg;
569             break;
570         case 'h':
571             HistoryText = optarg;
572             break;
573         case 'i':
574             IgnoreOld = true;
575             break;
576         case 'N':
577             Ignoreselfexpire = true;
578             break;
579         case 'n':
580             Server = false;
581             break;
582         case 'p':
583             EXPusepost = true;
584             break;
585         case 'r':
586             EXPreason = xstrdup(optarg);
587             break;
588         case 's':
589             Size = atoi(optarg);
590             break;
591         case 't':
592             EXPtracing = true;
593             break;
594         case 'v':
595             EXPverbose = atoi(optarg);
596             break;
597         case 'w':
598             TimeWarp = (time_t)(atof(optarg) * 86400.);
599             break;
600         case 'x':
601             Writing = false;
602             break;
603         case 'z':
604             EXPunlinkfile = EXPfopen(true, optarg, "a", false, false, false);
605             UnlinkFile = true;
606             break;
607         }
608     ac -= optind;
609     av += optind;
610     if ((ac != 0 && ac != 1))
611         Usage();
612
613     /* if EXPtracing is set, then pass in a path, this ensures we
614      * don't replace the existing history files */
615     if (EXPtracing || NHistoryText || NHistoryPath) {
616         if (NHistoryPath == NULL)
617             NHistoryPath = innconf->pathdb;
618         if (NHistoryText == NULL)
619             NHistoryText = _PATH_HISTORY;
620         NHistory = concatpath(NHistoryPath, NHistoryText);
621     }
622     else {
623         NHistory = NULL;
624     }
625
626     time(&Now);
627     RealNow = Now;
628     Now += TimeWarp;
629
630     /* Change users if necessary. */
631     setuid_news();
632
633     /* Parse the control file. */
634     if (av[0]) {
635         if (strcmp(av[0], "-") == 0)
636             F = stdin;
637         else
638             F = EXPfopen(false, av[0], "r", false, false, false);
639     } else {
640         char *path;
641
642         path = concatpath(innconf->pathetc, _PATH_EXPIRECTL);
643         F = EXPfopen(false, path, "r", false, false, false);
644         free(path);
645     }
646     if (!EXPreadfile(F)) {
647         fclose(F);
648         die("format error in expire.ctl");
649     }
650     fclose(F);
651
652     /* Set up the link, reserve the lock. */
653     if (Server) {
654         if (EXPreason == NULL) {
655             snprintf(buff, sizeof(buff), "Expiring process %ld",
656                      (long) getpid());
657             EXPreason = xstrdup(buff);
658         }
659     }
660     else {
661         EXPreason = NULL;
662     }
663
664     if (Server) {
665         /* If we fail, leave evidence behind. */
666         if (ICCopen() < 0) {
667             syswarn("cannot open channel to server");
668             CleanupAndExit(false, false, 1);
669         }
670         if (ICCreserve((char *)EXPreason) != 0) {
671             warn("cannot reserve server");
672             CleanupAndExit(false, false, 1);
673         }
674     }
675
676     History = HISopen(HistoryText, innconf->hismethod, HIS_RDONLY);
677     if (!History) {
678         warn("cannot open history");
679         CleanupAndExit(Server, false, 1);
680     }
681
682     /* Ignore failure on the HISctl()s, if the underlying history
683      * manager doesn't implement them its not a disaster */
684     HISctl(History, HISCTLS_IGNOREOLD, &IgnoreOld);
685     if (Size != 0) {
686         HISctl(History, HISCTLS_NPAIRS, &Size);
687     }
688
689     val = true;
690     if (!SMsetup(SM_RDWR, (void *)&val) || !SMsetup(SM_PREOPEN, (void *)&val)) {
691         warn("cannot set up storage manager");
692         CleanupAndExit(Server, false, 1);
693     }
694     if (!SMinit()) {
695         warn("cannot initialize storage manager: %s", SMerrorstr);
696         CleanupAndExit(Server, false, 1);
697     }
698     if (chdir(EXPhistdir) < 0) {
699         syswarn("cannot chdir to %s", EXPhistdir);
700         CleanupAndExit(Server, false, 1);
701     }
702
703     Bad = HISexpire(History, NHistory, EXPreason, Writing, NULL,
704                     EXPremember, EXPdoline) == false;
705
706     if (UnlinkFile && EXPunlinkfile == NULL)
707         /* Got -z but file was closed; oops. */
708         Bad = true;
709
710     /* If we're done okay, and we're not tracing, slip in the new files. */
711     if (EXPverbose) {
712         if (Bad)
713             printf("Expire errors: history files not updated.\n");
714         if (EXPtracing)
715             printf("Expire tracing: history files not updated.\n");
716     }
717
718     if (!Bad && NHistory != NULL) {
719         snprintf(buff, sizeof(buff), "%s.n.done", NHistory);
720         fclose(EXPfopen(false, buff, "w", true, Server, false));
721         CleanupAndExit(Server, false, Bad ? 1 : 0);
722     }
723
724     CleanupAndExit(Server, !Bad, Bad ? 1 : 0);
725     /* NOTREACHED */
726     abort();
727 }