chiark / gitweb /
store and print reason for idle timeouts ("quitting")
[innduct.git] / frontends / scanspool.in
1 #! /usr/bin/perl
2 # fixscript will replace this line with require innshellvars.pl
3
4 # @(#)scanspool.pl      1.20 4/6/92 00:47:35
5 #
6 # Written by:  Landon Curt Noll         (chongo was here  /\../\)
7 #
8 # This code is placed in the public domain.
9 #
10 # scanspool - perform a big scan over all articles in /usr/spool/news
11 #
12 # usage:
13 #    scanspool [-a active_file] [-s spool_dir] [-v] [-c] [-n]
14 #
15 #    -a active_file     active file to use (default /usr/lib/news/active)
16 #    -s spool_dir       spool tree (default /usr/spool/news)
17 #    -v                 verbose mode
18 #                       verbose messages begin with a tab
19 #                       show articles found in non-active directories
20 #    -c                 check article filenames, don't scan the articles
21 #    -n                 don't throttle innd
22 #
23 # NOTE: This take a while, -v is a good thing if you want to know
24 #       how far this program has progressed.
25 #
26 # This program will scan first the active file, noting problems such as:
27 #
28 #       malformed line
29 #       group aliased to a non-existent group
30 #       group aliased to a group tat is also aliased
31 #
32 # Then it will examine all articles under your news spool directory,
33 # looking for articles that:
34 #
35 #       basename that starts with a leading 0
36 #       basename that is out of range with the active file
37 #       does not contain a Newsgroups: line
38 #       article that is all header and no text
39 #       is in a directory for which there is no active group
40 #       article that is in a group to which it does not belong
41 #
42 # Scanspool understands aliased groups.  Thus, if an article is posted
43 # to foo.old.name that is aliases to foo.bar, it will be expected to
44 # be found under foo.bar and not foo.old.name.
45 #
46 # Any group that is of type 'j' or 'x' (4th field of the active file)
47 # will be allowed to show up under the junk group.
48 #
49 # Scanspool assumes that the path of a valid newsgroup's directory
50 # from the top of the spool tree will not contain any "." character.
51 # Thus, directories such as out.going, tmp.dir, in.coming and
52 # news.archive will not be searched.  This program also assumes that
53 # article basenames contain only decimal digits.  Last, files under
54 # the top level directory "lost+found" are not scanned.
55 #
56 # The output of scanspool will start with one of 4 forms:
57 #
58 #    FATAL:         fatal or internal error                     (to stderr)
59 #
60 #    WARN:          active or article format problem,           (to stderr)
61 #                   group alias problem, find error,
62 #                   article open error
63 #
64 #    path/123:      basename starts with 0,                     (to stdout)
65 #                   article number out of range,
66 #                   article in the wrong directory,
67 #                   article in directory not related to
68 #                       an active non-aliases newsgroup
69 #
70 #    \t ...         verbose message starting with a tab         (to stdout)
71
72
73 # Data structures
74 #
75 # $gname2type{$name}
76 #    $name      - newsgroup name in foo.dot.form
77 #    produces  => 4th active field  (y, n, x, ...)
78 #                 alias type is "=", not "=foo.bar"
79 #
80 # $realgname{$name}
81 #    $name      - newsgroup name in foo.dot.form
82 #    produces  => newsgroup name in foo.dot.form
83 #                 if type is =, this will be a.b, not $name
84 #
85 # $lowart{$name}
86 #    $name      - newsgroup name in foo.dot.form
87 #    produces  => lowest article allowed in the group
88 #                 if type is =, this is not valid
89 #
90 # $highart{$name}
91 #    $name      - newsgroup name in foo.dot.form
92 #    produces  => highest article allowed in the group
93 #                 if type is =, this is not valid
94 #                 If $highart{$name} < $lowart{$name},
95 #                 then the group should be empty
96
97 # perl requirements
98 #
99 require "getopts.pl";
100
101 # setup non-buffered stdout and stderr
102 #
103 select(STDERR);
104 $|=1;
105 select(STDOUT);
106 $|=1;
107
108 # global constants
109 #
110 $prog = $0;                             # our name
111 $spool = "$inn::patharticles";
112 $active = "$inn::pathdb/active";
113 $ctlinnd = "$inn::pathbin/ctlinnd";
114 $reason = "running scanspool";          # throttle reason
115
116 # parse args
117 #
118 &Getopts("a:s:vcn");
119 $active = $opt_a if (defined($opt_a));
120 $spool = $opt_s if (defined($opt_s));
121
122 # throttle innd unless -n
123 #
124 if (! defined($opt_n)) {
125     system("$ctlinnd throttle '$reason' >/dev/null 2>&1");
126 }
127
128 # process the active file
129 #
130 &parse_active($active);
131
132 # check the spool directory
133 #
134 &check_spool($spool);
135
136 # unthrottle innd unless -n
137 #
138 if (! defined($opt_n)) {
139     system("$ctlinnd go '$reason' >/dev/null 2>&1");
140 }
141
142 # all done
143 exit(0);
144
145
146 # parse_active - parse the active file
147 #
148 # From the active file, fill out the @gname2type (type of newsgroup)
149 # and @realgname (real/non-aliased name of group), @lowart & @highart
150 # (low and high article numbers).  This routine will also check for
151 # aliases to missing groups or groups that are also aliases.
152 #
153 sub parse_active
154 {
155     local ($active) = $_[0];    # the name of the active file to use
156     local (*ACTIVE);            # active file handle
157     local ($line);              # active file line
158     local ($name);              # name of newsgroup
159     local ($low);               # low article number
160     local ($high);              # high article number
161     local ($type);              # type of newsgroup (4th active field)
162     local ($field5);            # 5th active field (should not exist)
163     local ($dir);               # directory path of group from $spool
164     local ($alias);             # realname of an aliased group
165     local ($linenum);           # active file line number
166
167     # if verbose (-v), say what we are doing
168     print "\tscanning $active\n" if defined($opt_v);
169
170     # open the active file
171     open (ACTIVE, $active) || &fatal(1, "cannot open $active");
172
173     # parse each line
174     $linenum = 0;
175     while ($line = <ACTIVE>) {
176
177         # count the line
178         ++$linenum;
179
180         # verify that we have a correct number of tokens
181         if ($line !~ /^\S+ 0*(\d+) 0*(\d+) \S+$/o) {
182             &problem("WARNING: active line is mal-formed at line $linenum");
183             next;
184         }
185         ($name, $high, $low, $type) = $line =~ /^(\S+) 0*(\d+) 0*(\d+) (\S+)$/o;
186
187         # watch for duplicate entries
188         if (defined($realgname{$name})) {
189             &problem("WARNING: ignoring dup group: $name, at line $linenum");
190             next;
191         }
192
193         # record which type it is
194         $gname2type{$name} = $type;
195
196         # record the low and high article numbers
197         $lowart{$name} = $low;
198         $highart{$name} = $high;
199
200         # determine the directory and real group name
201         if ($type eq "j" || $type eq "x") {
202             $dir = "junk";
203             $alias = $name;
204         } elsif ($type =~ /^=(.+)/o) {
205             $alias = $1;
206             ($dir = $alias) =~ s#\.#/#go;
207             $gname2type{$name} = "=";   # rename type to be just =
208         } else {
209             $dir = $name;
210             $dir =~ s#\.#/#go;
211             $alias = $name;
212         }
213         $realgname{$name} = $alias;
214     }
215
216     # close the active file
217     close (ACTIVE);
218
219     # be sure that any alias type is aliased to a real group
220     foreach $name (keys %realgname) {
221
222         # skip if not an alias type
223         next if $gname2type{$name} ne "=";
224
225         # be sure that the alias exists
226         $alias = $realgname{$name};
227         if (! defined($realgname{$alias})) {
228             &problem("WARNING: alias for $name: $alias, is not a group");
229             next;
230         }
231
232         # be sure that the alias is not an alias of something else
233         if ($gname2type{$alias} eq "=") {
234             &problem("WARNING: alias for $name: $alias, is also an alias");
235             next;
236         }
237     }
238 }
239
240
241 # problem - report a problem to stdout
242 #
243 # Print a message to stdout.  Parameters are space separated.
244 # A final newline is appended to it.
245 #
246 # usage:
247 #       &problem(arg, arg2, ...)
248 #
249 sub problem
250 {
251     local ($line);              # the line to write
252
253     # print the line with the header and newline
254     $line = join(" ", @_);
255     print STDERR $line, "\n";
256 }
257
258
259 # fatal - report a fatal error to stderr and exit
260 #
261 # Print a message to stderr.  The message has the program name prepended
262 # to it.  Parameters are space separated.  A final newline is appended
263 # to it.  This function exists with the code of exitval.
264 #
265 # usage:
266 #       &fatal(exitval, arg, arg2, ...)
267 #
268 sub fatal
269 {
270     local ($exitval) = $_[0];   # what to exit with
271
272     # firewall
273     if ($#_ < 1) {
274         print STDERR "FATAL: fatal called with only ", $#_-1, " arguments\n";
275         if ($#_ < 0) {
276             $exitval = -1;
277         }
278     }
279
280     # print the error message
281     shift(@_);
282     $line = join(" ", @_);
283     print STDERR "$prog: ", $line, "\n";
284
285     # unthrottle innd unless -n
286     #
287     if (! defined($opt_n)) {
288         system("$ctlinnd go '$reason' >/dev/null 2>&1");
289     }
290
291     # exit
292     exit($exitval);
293 }
294
295
296 # check_spool - check the articles found in the spool directory
297 #
298 # This subroutine will check all articles found under the $spool directory.
299 # It will examine only file path that do not contain any "." or whitespace
300 # character, and whose basename is completely numeric.  Files under
301 # lost+found will also be ignored.
302 #
303 # given:
304 #       $spooldir  - top of /usr/spool/news article tree
305 #
306 sub check_spool
307 {
308     local ($spooldir) = $_[0];  # top of article tree
309     local (*FILEFILE);          # pipe from the find files process
310     local ($filename);          # article pathname under $spool
311     local ($artgrp);            # group of an article
312     local ($artnum);            # article number in a group
313     local ($prevgrp);           # previous different value of $artgrp
314     local ($preverrgrp);        # previous non-active $artgrp
315     local (*ARTICLE);           # article handle
316     local ($aline);             # header line from an article
317     local (@group);             # array of groups from the Newsgroup header
318     local ($j);
319
320     # if verbose, say what we are doing
321     print "\tfinding articles under $spooldir\n" if defined($opt_v);
322
323     # move to the $spool directory
324     chdir $spooldir || &fatal(2, "cannot chdir to $spool");
325
326     # start finding files
327     #
328     if (!open (FINDFILE,
329           "find . \\( -type f -o -type l \\) -name '[0-9]*' -print 2>&1 |")) {
330         &fatal(3, "cannot start find in $spool");
331     }
332
333     # process each history line
334     #
335     while ($filename = <FINDFILE>) {
336
337         # if the line contains find:, assume it is a find error and print it
338         chop($filename);
339         if ($filename =~ /find:\s/o) {
340             &problem("WARNING:", $filename);
341             next;
342         }
343
344         # remove the \n and ./ that find put in our path
345         $filename =~ s#^\./##o;
346
347         # skip is this path has a . in it (beyond a leading ./)
348         next if ($filename =~ /\./o);
349
350         # skip if lost+found
351         next if ($filename =~ m:^lost+found/:o);
352
353         # skip if not a numeric basename
354         next if ($filename !~ m:/\d+$:o);
355
356         # get the article's newsgroup name (based on its path from $spool)
357         $artgrp = $filename;
358         $artgrp =~ s#/\d+$##o;
359         $artgrp =~ s#/#.#go;
360
361         # if verbose (-v), then note if our group changed
362         if (defined($opt_v) && $artgrp ne $prevgrp) {
363             print "\t$artgrp\n";
364             $prevgrp = $artgrp;
365         }
366
367         # note if the article is not in a directory that is used by
368         # a real (non-aliased) group in the active file
369         #
370         # If we complained about this dgroup before, don't complain again.
371         # If verbose, note files that could be removed.
372         #
373         if (!defined($gname2type{$artgrp}) || $gname2type{$artgrp} =~ /[=jx]/o){
374             if ($preverrgrp ne $artgrp) {
375                 &problem("$artgrp: not an active group directory");
376                 $preverrgrp = $artgrp;
377             }
378             if (defined($opt_v)) {
379                 &problem("$filename: article found in non-active directory");
380             }
381             next;
382         }
383
384         # check on the article number
385         $artnum = $filename;
386         $artnum =~ s#^.+/##o;
387         if ($artnum =~ m/^0/o) {
388             &problem("$filename: article basename starts with a 0");
389         }
390         if (defined($gname2type{$artgrp})) {
391             if ($lowart{$artgrp} > $highart{$artgrp}) {
392                 &problem("$filename: active indicates group should be empty");
393             } elsif ($artnum < $lowart{$artgrp}) {
394                 &problem("$filename: article number is too low");
395             } elsif ($artnum > $highart{$artgrp}) {
396                 &problem("$filename: article number is too high");
397             }
398         }
399
400         # if check filenames only (-c), then do nothing else with the file
401         next if (defined($opt_c));
402
403         # don't open a control or junk, they can be from anywhere
404         next if ($artgrp eq "control" || $artgrp eq "junk");
405
406         # try open the file
407         if (!open(ARTICLE, $filename)) {
408
409             # the find is now gone (expired?), give up on it
410             &problem("WARNING: cannot open $filename");
411             next;
412         }
413
414         # read until the Newsgroup header line is found
415         AREADLINE:
416         while ($aline = <ARTICLE>) {
417
418             # catch the newsgroup: header
419             if ($aline =~ /^Newsgroups:\w*\W/io) {
420
421                 # convert $aline into a comma separated list of groups
422                 $aline =~ s/^Newsgroups://io;
423                 $aline =~ tr/ \t\n//d;
424
425                 # form an array of news groups
426                 @group = split(",", $aline);
427
428                 # see if any groups in the Newsgroup list are our group
429                 for ($j=0; $j <= $#group; ++$j) {
430
431                     # look at the group
432                     if ($realgname{$group[$j]} eq $artgrp) {
433                         # this article was posted to this group
434                         last AREADLINE;
435                     }
436                 }
437
438                 # no group or group alias was found
439                 &problem("$filename: does not belong in $artgrp");
440                 last;
441
442             # else watch for the end of the header
443             } elsif ($aline =~ /^\s*$/o) {
444
445                 # no Newsgroup: header found
446                 &problem("WARNING: $filename: no Newsgroup header");
447                 last;
448             }
449             if (eof(ARTICLE)) {
450                 &problem("WARNING: $filename: EOF found while reading header");
451             }
452         }
453
454         # close the article
455         close(ARTICLE);
456     }
457
458     # all done with the find
459     close(FINDFILE);
460 }